From d273077b92fc23701303277657ffe11a25ff7bb1 Mon Sep 17 00:00:00 2001 From: ddc Date: Thu, 29 Jan 2026 13:32:55 -0300 Subject: [PATCH 01/18] v3.0.9 --- .github/workflows/workflow.yml | 7 ++----- ddcDatabases/core/constants.py | 3 ++- pyproject.toml | 2 +- uv.lock | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 62529c8..d8ad2cc 100755 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -9,15 +9,12 @@ name: CI/CD Pipeline jobs: test: name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} - runs-on: ${{ matrix.runs-on || matrix.os }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: ['ubuntu-latest', 'macos-latest', 'macos-14-arm64', 'windows-latest'] + os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] python-version: ['3.12', '3.13', '3.14'] - include: - - os: 'macos-14-arm64' - runs-on: 'macos-14' steps: - uses: actions/checkout@v6 diff --git a/ddcDatabases/core/constants.py b/ddcDatabases/core/constants.py index 4c79863..00ee7a0 100644 --- a/ddcDatabases/core/constants.py +++ b/ddcDatabases/core/constants.py @@ -56,7 +56,8 @@ class SettingsMessages: - # Field description strings for settings + """Field description strings for settings""" + ECHO_DESCRIPTION = "Enable SQLAlchemy query logging" AUTOFLUSH_DESCRIPTION = "Enable autoflush" EXPIRE_ON_COMMIT_DESCRIPTION = "Enable expire on commit" diff --git a/pyproject.toml b/pyproject.toml index 00ecabe..6be4221 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ddcDatabases" -version = "3.0.8" +version = "3.0.9" description = "Simplified database ORM connections with support for multiple database engines" license = {text = "MIT"} readme = "README.md" diff --git a/uv.lock b/uv.lock index 904fa10..aceaad7 100644 --- a/uv.lock +++ b/uv.lock @@ -336,7 +336,7 @@ wheels = [ [[package]] name = "ddcdatabases" -version = "3.0.8" +version = "3.0.9" source = { editable = "." } dependencies = [ { name = "pydantic-settings" }, From 789e3f991eb893e973f5f2fddd466e2fad1eaca2 Mon Sep 17 00:00:00 2001 From: ddc Date: Thu, 29 Jan 2026 13:49:29 -0300 Subject: [PATCH 02/18] v3.0.9 --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 38d305f..7010309 100755 --- a/.gitignore +++ b/.gitignore @@ -155,6 +155,9 @@ cython_debug/ # PyCharm .idea/ +# ds store +**/.DS_Store + # Custom -/junit.xml *.prof +junit.xml From ec5cd553d700172d42d11cc09244d2dbfd654771 Mon Sep 17 00:00:00 2001 From: ddc Date: Thu, 29 Jan 2026 14:44:22 -0300 Subject: [PATCH 03/18] v3.0.9 --- README.md | 13 +++-- pyproject.toml | 7 ++- uv.lock | 142 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 150 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 50d9ce4..ee6c4e3 100755 --- a/README.md +++ b/README.md @@ -638,10 +638,7 @@ logging.getLogger("ddcDatabases").addHandler(logging.StreamHandler()) # Development -Must have [UV](https://uv.run/docs/getting-started/installation), -[Black](https://black.readthedocs.io/en/stable/getting_started.html), -[Ruff](https://docs.astral.sh/ruff/installation/), and -[Poe the Poet](https://poethepoet.naber.dev/installation) installed. +Must have [UV](https://docs.astral.sh/uv/getting-started/installation/) installed. ## Create DEV Environment, Running Tests and Building Wheel @@ -656,9 +653,11 @@ poe build ## Optionals ```shell -poe profile (create a cprofile_unit.prof file from unit tests) -poe profile-integration (create a cprofile_integration.prof file from integration tests) -``` +# create a cprofile_unit.prof file from unit tests +poe profile +# create a cprofile_integration.prof file from integration tests +poe profile-integration +````` # License diff --git a/pyproject.toml b/pyproject.toml index 6be4221..98aa5fc 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,10 +58,15 @@ all = [ "ddcDatabases[pgsql]", ] test = [ - "poethepoet>=0.40.0", "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", ] +dev = [ + "ddcDatabases[test]", + "black>=26.1.0", + "poethepoet>=0.40.0", + "ruff>=0.14.14", +] integration = [ "ddcDatabases[all]", "ddcDatabases[test]", diff --git a/uv.lock b/uv.lock index aceaad7..90ceaba 100644 --- a/uv.lock +++ b/uv.lock @@ -75,6 +75,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "black" +version = "26.1.0" +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/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -198,6 +230,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -354,6 +398,13 @@ all = [ { name = "psycopg", extra = ["binary"] }, { name = "pyodbc" }, ] +dev = [ + { name = "black" }, + { name = "poethepoet" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] integration = [ { name = "aiomysql" }, { name = "aioodbc" }, @@ -361,7 +412,6 @@ integration = [ { name = "motor" }, { name = "mysqlclient" }, { name = "oracledb" }, - { name = "poethepoet" }, { name = "psycopg", extra = ["binary"] }, { name = "pyodbc" }, { name = "pytest-asyncio" }, @@ -387,7 +437,6 @@ pgsql = [ { name = "psycopg", extra = ["binary"] }, ] test = [ - { name = "poethepoet" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, ] @@ -397,26 +446,29 @@ requires-dist = [ { name = "aiomysql", marker = "extra == 'mysql'", specifier = ">=0.3.2" }, { name = "aioodbc", marker = "extra == 'mssql'", specifier = ">=0.5.0" }, { name = "asyncpg", marker = "extra == 'pgsql'", specifier = ">=0.31.0" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=26.1.0" }, { name = "ddcdatabases", extras = ["all"], marker = "extra == 'integration'" }, { name = "ddcdatabases", extras = ["mongodb"], marker = "extra == 'all'" }, { name = "ddcdatabases", extras = ["mssql"], marker = "extra == 'all'" }, { name = "ddcdatabases", extras = ["mysql"], marker = "extra == 'all'" }, { name = "ddcdatabases", extras = ["oracle"], marker = "extra == 'all'" }, { name = "ddcdatabases", extras = ["pgsql"], marker = "extra == 'all'" }, + { name = "ddcdatabases", extras = ["test"], marker = "extra == 'dev'" }, { name = "ddcdatabases", extras = ["test"], marker = "extra == 'integration'" }, { name = "motor", marker = "extra == 'mongodb'", specifier = ">=3.7.1" }, { name = "mysqlclient", marker = "extra == 'mysql'", specifier = ">=2.2.7" }, { name = "oracledb", marker = "extra == 'oracle'", specifier = ">=3.4.2" }, - { name = "poethepoet", marker = "extra == 'test'", specifier = ">=0.40.0" }, + { name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.40.0" }, { name = "psycopg", extras = ["binary"], marker = "extra == 'pgsql'", specifier = ">=3.3.2" }, { name = "pydantic-settings", specifier = ">=2.11.0" }, { name = "pyodbc", marker = "extra == 'mssql'", specifier = ">=5.3.0" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1.3.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=7.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.14" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.46" }, { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], marker = "extra == 'integration'", specifier = ">=4.14.0" }, ] -provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql", "all", "test", "integration"] +provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql", "all", "test", "dev", "integration"] [[package]] name = "dnspython" @@ -514,6 +566,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/9a/35e053d4f442addf751ed20e0e922476508ee580786546d699b0567c4c67/motor-3.7.1-py3-none-any.whl", hash = "sha256:8a63b9049e38eeeb56b4fdd57c3312a6d1f25d01db717fe7d82222393c410298", size = 74996, upload-time = "2025-05-14T18:56:31.665Z" }, ] +[[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 = "mysqlclient" version = "2.2.7" @@ -569,6 +630,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -960,6 +1039,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" }, + { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" }, + { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" }, + { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" }, + { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/d07e6209f18ef378fc2ae9dee8d1dfe91fd2447c2e2dbfa32867b6dd30cf/pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016", size = 159968, upload-time = "2026-01-19T07:59:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/0a/73/0eb111400abd382a04f253b269819db9fcc748aa40748441cebdcb6d068f/pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b", size = 253373, upload-time = "2026-01-19T07:59:24.381Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8d/9e4e2fdb5bcaba679e54afcc304e9f13f488eb4d626e6b613f9553e03dbd/pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1", size = 267024, upload-time = "2026-01-19T07:59:25.74Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b7/e0a370321af2deb772cff14ff337e1140d1eac2c29a8876bfee995f486f0/pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7", size = 270912, upload-time = "2026-01-19T07:59:27.072Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/4348f916c440d4c3e68b53b4ed0e66b292d119e799fa07afa159566dcc86/pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a", size = 103836, upload-time = "2026-01-19T07:59:28.112Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/a693c0cfa9c783a2a8c4500b7b2a8bab420f8ca4f2d496153226bf1c12e3/pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8", size = 167643, upload-time = "2026-01-19T07:59:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/c0/dd/a64eb1e9f3ec277b69b33ef1b40ffbcc8f0a3bafcde120997efc7bdefebf/pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63", size = 289553, upload-time = "2026-01-19T07:59:30.537Z" }, + { url = "https://files.pythonhosted.org/packages/df/22/06c1079d93dbc3bca5d013e1795f3d8b9ed6c87290acd6913c1c526a6bb2/pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4", size = 302490, upload-time = "2026-01-19T07:59:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/a6f5e43115b4fbf4b93aa87d6c83c79932cdb084f9711daae04549e1e4ad/pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e", size = 305652, upload-time = "2026-01-19T07:59:33.685Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3d/c136e057cb622e36e0c3ff7a8aaa19ff9720050c4078235691da885fe6ee/pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551", size = 115472, upload-time = "2026-01-19T07:59:34.734Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -1037,6 +1145,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "ruff" +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.46" From 74262705336fbc802ab5de3a3e32bd71f3355124 Mon Sep 17 00:00:00 2001 From: ddc Date: Mon, 2 Feb 2026 17:18:47 -0300 Subject: [PATCH 04/18] v3.0.9 --- .github/workflows/workflow.yml | 6 +- pyproject.toml | 15 +- uv.lock | 257 ++++++++++++++++----------------- 3 files changed, 133 insertions(+), 145 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index d8ad2cc..36fa826 100755 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -40,9 +40,9 @@ jobs: uv pip install -e .[mongodb,mssql,oracle,pgsql,test] elif [[ '${{ matrix.os }}' == macos* ]]; then export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig" - uv pip install -e .[all,test] + uv sync --group test else - uv pip install -e .[all,test] + uv sync --group test fi shell: bash @@ -81,7 +81,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev pkg-config - name: Install dependencies - run: uv sync --all-extras + run: uv sync --group test shell: bash - name: Install ODBC driver for MSSQL diff --git a/pyproject.toml b/pyproject.toml index 98aa5fc..21682ad 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ requires-python = ">=3.12" dependencies = [ "pydantic-settings>=2.11.0", - "sqlalchemy[asyncio]>=2.0.46" + "sqlalchemy[asyncio]>=2.0.46", ] [project.urls] @@ -50,6 +50,8 @@ mssql = ["pyodbc>=5.3.0", "aioodbc>=0.5.0"] mysql = ["mysqlclient>=2.2.7", "aiomysql>=0.3.2"] oracle = ["oracledb>=3.4.2"] pgsql = ["psycopg[binary]>=3.3.2", "asyncpg>=0.31.0"] + +[dependency-groups] all = [ "ddcDatabases[mongodb]", "ddcDatabases[mssql]", @@ -58,20 +60,17 @@ all = [ "ddcDatabases[pgsql]", ] test = [ + {include-group = "all"}, "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", + "testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.1", ] dev = [ - "ddcDatabases[test]", + {include-group = "test"}, "black>=26.1.0", "poethepoet>=0.40.0", "ruff>=0.14.14", ] -integration = [ - "ddcDatabases[all]", - "ddcDatabases[test]", - "testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.0", -] [tool.hatch.build] include = ["ddcDatabases/**/*"] @@ -81,7 +80,7 @@ packages = ["ddcDatabases"] [tool.poe.tasks] build = "uv build --wheel" -updatedev.shell = "uv lock && uv sync --all-extras" +updatedev.shell = "uv lock && uv sync --all-groups" linter.shell = "uv run ruff check --fix . && uv run black ." profile = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov" profile-integration = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov" diff --git a/uv.lock b/uv.lock index 90ceaba..8f56ada 100644 --- a/uv.lock +++ b/uv.lock @@ -388,36 +388,6 @@ dependencies = [ ] [package.optional-dependencies] -all = [ - { name = "aiomysql" }, - { name = "aioodbc" }, - { name = "asyncpg" }, - { name = "motor" }, - { name = "mysqlclient" }, - { name = "oracledb" }, - { name = "psycopg", extra = ["binary"] }, - { name = "pyodbc" }, -] -dev = [ - { name = "black" }, - { name = "poethepoet" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] -integration = [ - { name = "aiomysql" }, - { name = "aioodbc" }, - { name = "asyncpg" }, - { name = "motor" }, - { name = "mysqlclient" }, - { name = "oracledb" }, - { name = "psycopg", extra = ["binary"] }, - { name = "pyodbc" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "testcontainers", extra = ["mongodb", "mssql", "mysql", "oracle"] }, -] mongodb = [ { name = "motor" }, ] @@ -436,9 +406,25 @@ pgsql = [ { name = "asyncpg" }, { name = "psycopg", extra = ["binary"] }, ] + +[package.dev-dependencies] +all = [ + { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, +] +dev = [ + { name = "black" }, + { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, + { name = "poethepoet" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "testcontainers", extra = ["mongodb", "mssql", "mysql", "oracle"] }, +] test = [ + { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "testcontainers", extra = ["mongodb", "mssql", "mysql", "oracle"] }, ] [package.metadata] @@ -446,29 +432,47 @@ requires-dist = [ { name = "aiomysql", marker = "extra == 'mysql'", specifier = ">=0.3.2" }, { name = "aioodbc", marker = "extra == 'mssql'", specifier = ">=0.5.0" }, { name = "asyncpg", marker = "extra == 'pgsql'", specifier = ">=0.31.0" }, - { name = "black", marker = "extra == 'dev'", specifier = ">=26.1.0" }, - { name = "ddcdatabases", extras = ["all"], marker = "extra == 'integration'" }, - { name = "ddcdatabases", extras = ["mongodb"], marker = "extra == 'all'" }, - { name = "ddcdatabases", extras = ["mssql"], marker = "extra == 'all'" }, - { name = "ddcdatabases", extras = ["mysql"], marker = "extra == 'all'" }, - { name = "ddcdatabases", extras = ["oracle"], marker = "extra == 'all'" }, - { name = "ddcdatabases", extras = ["pgsql"], marker = "extra == 'all'" }, - { name = "ddcdatabases", extras = ["test"], marker = "extra == 'dev'" }, - { name = "ddcdatabases", extras = ["test"], marker = "extra == 'integration'" }, { name = "motor", marker = "extra == 'mongodb'", specifier = ">=3.7.1" }, { name = "mysqlclient", marker = "extra == 'mysql'", specifier = ">=2.2.7" }, { name = "oracledb", marker = "extra == 'oracle'", specifier = ">=3.4.2" }, - { name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.40.0" }, { name = "psycopg", extras = ["binary"], marker = "extra == 'pgsql'", specifier = ">=3.3.2" }, { name = "pydantic-settings", specifier = ">=2.11.0" }, { name = "pyodbc", marker = "extra == 'mssql'", specifier = ">=5.3.0" }, - { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1.3.0" }, - { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=7.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.14" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.46" }, - { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], marker = "extra == 'integration'", specifier = ">=4.14.0" }, ] -provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql", "all", "test", "dev", "integration"] +provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] + +[package.metadata.requires-dev] +all = [ + { name = "ddcdatabases", extras = ["mongodb"] }, + { name = "ddcdatabases", extras = ["mssql"] }, + { name = "ddcdatabases", extras = ["mysql"] }, + { name = "ddcdatabases", extras = ["oracle"] }, + { name = "ddcdatabases", extras = ["pgsql"] }, +] +dev = [ + { name = "black", specifier = ">=26.1.0" }, + { name = "ddcdatabases", extras = ["mongodb"] }, + { name = "ddcdatabases", extras = ["mssql"] }, + { name = "ddcdatabases", extras = ["mysql"] }, + { name = "ddcdatabases", extras = ["oracle"] }, + { name = "ddcdatabases", extras = ["pgsql"] }, + { name = "poethepoet", specifier = ">=0.40.0" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "ruff", specifier = ">=0.14.14" }, + { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], specifier = ">=4.14.1" }, +] +test = [ + { name = "ddcdatabases", extras = ["mongodb"] }, + { name = "ddcdatabases", extras = ["mssql"] }, + { name = "ddcdatabases", extras = ["mysql"] }, + { name = "ddcdatabases", extras = ["oracle"] }, + { name = "ddcdatabases", extras = ["pgsql"] }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], specifier = ">=4.14.1" }, +] [[package]] name = "dnspython" @@ -1041,31 +1045,31 @@ wheels = [ [[package]] name = "pytokens" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" } +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/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" }, - { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" }, - { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" }, - { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" }, - { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" }, - { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" }, - { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f1/d07e6209f18ef378fc2ae9dee8d1dfe91fd2447c2e2dbfa32867b6dd30cf/pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016", size = 159968, upload-time = "2026-01-19T07:59:23.07Z" }, - { url = "https://files.pythonhosted.org/packages/0a/73/0eb111400abd382a04f253b269819db9fcc748aa40748441cebdcb6d068f/pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b", size = 253373, upload-time = "2026-01-19T07:59:24.381Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8d/9e4e2fdb5bcaba679e54afcc304e9f13f488eb4d626e6b613f9553e03dbd/pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1", size = 267024, upload-time = "2026-01-19T07:59:25.74Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b7/e0a370321af2deb772cff14ff337e1140d1eac2c29a8876bfee995f486f0/pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7", size = 270912, upload-time = "2026-01-19T07:59:27.072Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/4348f916c440d4c3e68b53b4ed0e66b292d119e799fa07afa159566dcc86/pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a", size = 103836, upload-time = "2026-01-19T07:59:28.112Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f8/a693c0cfa9c783a2a8c4500b7b2a8bab420f8ca4f2d496153226bf1c12e3/pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8", size = 167643, upload-time = "2026-01-19T07:59:29.292Z" }, - { url = "https://files.pythonhosted.org/packages/c0/dd/a64eb1e9f3ec277b69b33ef1b40ffbcc8f0a3bafcde120997efc7bdefebf/pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63", size = 289553, upload-time = "2026-01-19T07:59:30.537Z" }, - { url = "https://files.pythonhosted.org/packages/df/22/06c1079d93dbc3bca5d013e1795f3d8b9ed6c87290acd6913c1c526a6bb2/pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4", size = 302490, upload-time = "2026-01-19T07:59:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/8d/de/a6f5e43115b4fbf4b93aa87d6c83c79932cdb084f9711daae04549e1e4ad/pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e", size = 305652, upload-time = "2026-01-19T07:59:33.685Z" }, - { url = "https://files.pythonhosted.org/packages/ab/3d/c136e057cb622e36e0c3ff7a8aaa19ff9720050c4078235691da885fe6ee/pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551", size = 115472, upload-time = "2026-01-19T07:59:34.734Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { 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]] @@ -1220,7 +1224,7 @@ asyncio = [ [[package]] name = "testcontainers" -version = "4.14.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docker" }, @@ -1229,9 +1233,9 @@ dependencies = [ { name = "urllib3" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/5a/d24f5c7ef787fc152b1e4e4cfb84ef9364dbf165b3c7f7817e2f2583f749/testcontainers-4.14.0.tar.gz", hash = "sha256:3b2d4fa487af23024f00fcaa2d1cf4a5c6ad0c22e638a49799813cb49b3176c7", size = 79885, upload-time = "2026-01-07T23:35:22.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/02/ef62dec9e4f804189c44df23f0b86897c738d38e9c48282fcd410308632f/testcontainers-4.14.1.tar.gz", hash = "sha256:316f1bb178d829c003acd650233e3ff3c59a833a08d8661c074f58a4fbd42a64", size = 80148, upload-time = "2026-01-31T23:13:46.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/c4/53efc88d890d7dd38337424a83bbff32007d9d3390a79a4b53bfddaa64e8/testcontainers-4.14.0-py3-none-any.whl", hash = "sha256:64e79b6b1e6d2b9b9e125539d35056caab4be739f7b7158c816d717f3596fa59", size = 125385, upload-time = "2026-01-07T23:35:21.343Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl", hash = "sha256:03dfef4797b31c82e7b762a454b6afec61a2a512ad54af47ab41e4fa5415f891", size = 125640, upload-time = "2026-01-31T23:13:45.464Z" }, ] [package.optional-dependencies] @@ -1292,69 +1296,54 @@ wheels = [ [[package]] name = "wrapt" -version = "2.0.1" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/31/afb4cf08b9892430ec419a3f0f469fb978cb013f4432e0edb9c2cf06f081/wrapt-2.1.0.tar.gz", hash = "sha256:757ff1de7e1d8db1839846672aaecf4978af433cc57e808255b83980e9651914", size = 80924, upload-time = "2026-01-31T23:25:58.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, - { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, - { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, - { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, - { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, - { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, - { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, - { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, - { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, - { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, - { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, - { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, - { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, - { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, - { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, - { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, - { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, - { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, - { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, - { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, - { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6f/c731b1fbbcdf9bd202809c6fa354c4237b663dd82a95035a7cbe899cfd25/wrapt-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a64c0fb29c89810973f312a04c067b63523e7303b9a2653820cbf16474c2e5cf", size = 61149, upload-time = "2026-01-31T23:25:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/da/7022458a1d99f0c59720a0b0fd4b1966f8df6d41e741aadfe43bc5350547/wrapt-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5509d9150ed01c4149e40020fa68e917d5c4bb77d311e79535565c2a0418afcb", size = 61743, upload-time = "2026-01-31T23:26:14.338Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f4/57cc12c3fc6f4fe6ccfc15567cc1ac8aeb53a9946a675adc3df7a1ee4e6a/wrapt-2.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:52bb58b3207ace156b6134235fd43140994597704fd07d148cbcfb474ee084ea", size = 121331, upload-time = "2026-01-31T23:25:37.294Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a4/a96ea114298f81f02c07313da85fd46a2a57bbe12389d0619ac3371f691c/wrapt-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7112cbf72fc4035afe1e3314a311654c41dd92c2932021ef76f5ca87583917b3", size = 122907, upload-time = "2026-01-31T23:26:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/df73362b6e47f92aaff0fc3fc459314025c795f75d61724c83232dee199c/wrapt-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e90656b433808a0ab68e95aaf9f588aea5c8c7a514e180849dfc638ba00ec449", size = 121337, upload-time = "2026-01-31T23:26:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/51/4f/8147e3b9a7887cee4eeb3a3414265ad4649a156832a08063f55aa7842af0/wrapt-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e45f54903da38fc4f6f66397fd550fc0dac6164b4c5e721c1b4eb05664181821", size = 120461, upload-time = "2026-01-31T23:26:43.055Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/eea720fcca8a05dec848a6d11a47c20f59bdabdcc444ba3be0589350eb7a/wrapt-2.1.0-cp312-cp312-win32.whl", hash = "sha256:6653bf30dbbafd55cb4553195cc60b94920b6711a8835866c0e02aa9f22c5598", size = 58089, upload-time = "2026-01-31T23:26:47.773Z" }, + { url = "https://files.pythonhosted.org/packages/af/79/8a8f3f8c71ee3379191b69e47f32115fa25cdb6d5b581d74c64d5c897fa7/wrapt-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d61238a072501ed071a9f4b9567d10c2eb3d2f1a0258ae79b47160871d8f29c3", size = 60330, upload-time = "2026-01-31T23:26:12.518Z" }, + { url = "https://files.pythonhosted.org/packages/08/4e/e992d05c3d2f7163883a65ead2620ff5fe7b3d44d7c2136ce981e40e453d/wrapt-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:9e971000347f61271725e801ef44fa5d01b52720e59737f0d96280bffb98c5d1", size = 58727, upload-time = "2026-01-31T23:26:53.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/b414826a5aaf2fdcfe73c2e649cbeb2e098fef4820d1217554ee64f45666/wrapt-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:875a10a6f3b667f90a39010af26acf684ba831d9b18a86b242899d57c74550fa", size = 61155, upload-time = "2026-01-31T23:26:24.462Z" }, + { url = "https://files.pythonhosted.org/packages/58/9e/8b21ea776bf2a3c858e3377ecde4b348893ec44dc1726baaf583ca22c56e/wrapt-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e00f8559ceac0fb45091daad5f15d37f2c22bdc28ed71521d47ff01aad8fff3d", size = 61747, upload-time = "2026-01-31T23:25:53.987Z" }, + { url = "https://files.pythonhosted.org/packages/da/ec/48cd2470ad09557dfe6fccfe9de98698cc0df3786a6d4d97e8edd574d67a/wrapt-2.1.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ce0cf4c79c19904aaf2e822af280d7b3c23ad902f57e31c5a19433bc86e5d36d", size = 121342, upload-time = "2026-01-31T23:26:32.156Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4e/e8447b31be27b6057cdfc904a38632a765c3407fb4d10d11e5c1d0c203d5/wrapt-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3dd4f8c2256fcde1a85037a1837afc52e8d32d086fd669ae469455fd9a988d6", size = 122951, upload-time = "2026-01-31T23:25:08.936Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b6/73a6c9277e844ffe11f3002ad27a84ff5418248def33af9435d24dfe6c5b/wrapt-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:737e1e491473047cb66944b8b8fd23f3f542019afd6cf0569d1356d18a7ea6d5", size = 121373, upload-time = "2026-01-31T23:26:18.322Z" }, + { url = "https://files.pythonhosted.org/packages/85/04/869384435fecf829dc05621ffa02dab0f2f830be5d42fa8d8ac7b0b4c9fa/wrapt-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38de19e30e266c15d542ceb0603e657db4e82c53e7f47fd70674ae5da2b41180", size = 120468, upload-time = "2026-01-31T23:25:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/80/ac/42a5378d9b5b486122ae0572c46ae8d69ab6486b9f13961e6b9706297ff5/wrapt-2.1.0-cp313-cp313-win32.whl", hash = "sha256:bc7d496b6e16bd2f77e37e8969b21a7b58d6954e46c6689986fb67b9078100e5", size = 58095, upload-time = "2026-01-31T23:26:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/86/de/538fcef30f70a1aaadab4cab7d0396037518d7ec2b064557171147ce297f/wrapt-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:57df799e67b011847ef7ac64b05ed4633e56b64e7e7cab5eb83dc9689dbe0acf", size = 60344, upload-time = "2026-01-31T23:25:10.615Z" }, + { url = "https://files.pythonhosted.org/packages/08/13/27884668b21e9f0a625c13ebd6a8d70ad8371250ec8519881858404686bf/wrapt-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:01559d2961c29edc6263849fd9d32b29a20737da67648c7fd752a67bd96208c7", size = 58734, upload-time = "2026-01-31T23:26:00.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a3/e558c5b8f3a097aa1e942e2d75923adebfdfafb5a51ec425d1d062e49ab0/wrapt-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:66f588c8b3a44863156cfaccb516f946a64b3b03a6880822ab0b878135ca1f5c", size = 62972, upload-time = "2026-01-31T23:26:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/93/b6/7157e98107099fad846f1e79308cc0954e26b25b01c03f1624ba7f57ec54/wrapt-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:355779ff720c11a2a5cffd03332dbce1005cb4747dca65b0fc8cdd5f8bf1037e", size = 63610, upload-time = "2026-01-31T23:26:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8e/b8992671e4b4d3ce2a53af930588c204bf37b66eb212bd1722f2a5a8cf62/wrapt-2.1.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a0471df3fb4e85a9ff62f7142cdb169e31172467cdb79a713f9b1319c555903", size = 152538, upload-time = "2026-01-31T23:26:27.696Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f6/79f9fd4b3c0a8715e651fff1cc1182a983fd971376d5688a06fa94e31acd/wrapt-2.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bacf063143fa86f15b00a21259a81c95c527a18d504b8c820835366d361c879", size = 158702, upload-time = "2026-01-31T23:25:11.848Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/f88b52beb813eeb830d9134bc6eaf3e53cde4e3cfa1804e383754d4104fe/wrapt-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c87cd4f61a3b7cd65113e74006e1cd6352b74807fcc65d440e8342f001f8de5e", size = 155564, upload-time = "2026-01-31T23:25:15.033Z" }, + { url = "https://files.pythonhosted.org/packages/93/31/97145ea71e3e5a1b419af5c410b07b258155dc7cc1a6302791a93e991c83/wrapt-2.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2893498fe898719ac8fb6b4fe36ca86892bec1e2480d94e3bd1bc592c00527ad", size = 150165, upload-time = "2026-01-31T23:26:09.848Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/f33551d5bfbb0ddab81296cffc15570570039a973c0f99bba474be0fadf2/wrapt-2.1.0-cp313-cp313t-win32.whl", hash = "sha256:cbc07f101f5f1e7c23ec06a07e45715f459de992108eeb381b21b76d94dbaf4f", size = 59785, upload-time = "2026-01-31T23:25:52.23Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3a/9a76be7a36442f43841bb6336e262e09a915b2fb5dfc2822ffce1fb903d2/wrapt-2.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2ccc89cd504fc29c32f0b24046e8edf3ef0fcbc5d5efe8c91b303c099863d2c8", size = 63085, upload-time = "2026-01-31T23:26:05.363Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/65a13c2df008d189ebca5fec534011c5dd69ab4f47e6923b403321816fbf/wrapt-2.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:0b660be1c9cdfb4c711baab4ccbd0e9d1b65a0480d38729ec8cdbf3b29cb7f15", size = 60254, upload-time = "2026-01-31T23:25:06.052Z" }, + { url = "https://files.pythonhosted.org/packages/6f/eb/7c9eb1ea9b10ea98d9983a147c877a2ae927acb4a86e2dc4a0b548f05ad1/wrapt-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f7bf95bae7ac5f2bbcb307464b3b0ff70569dd3b036a87b1cf7efb2c76e66e5", size = 61316, upload-time = "2026-01-31T23:25:20.739Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/1c3d16d6b644f688913a00e2dc10f59adca817b5b3ee034ce4e9a692ab63/wrapt-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:be2f541a242818829526e5d08c716b6730970ed0dc1b76ba962a546947d0f005", size = 61813, upload-time = "2026-01-31T23:25:49.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/51/b6170084b6b771cc62374d924e328df2e81f687399a835f003497cad1110/wrapt-2.1.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad3aa174d06a14b4758d5a1678b9adde8b8e657c6695de9a3d4c223f4fcbbcce", size = 120309, upload-time = "2026-01-31T23:25:16.866Z" }, + { url = "https://files.pythonhosted.org/packages/f8/34/467829f0dd79f50878b2e67b67c67c816a6326a27d252d4192ef815b4a09/wrapt-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bffa584240d41bc3127510e07a752f94223d73bb1283ac2e99ac44235762efd2", size = 122690, upload-time = "2026-01-31T23:26:16.914Z" }, + { url = "https://files.pythonhosted.org/packages/df/5b/244c61a65e0bc9d4a18cfa2a2b3b05f8065290284fc60436a7ea5047ee10/wrapt-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9b2da9c8f1723994b335dbf9f496fbfabc76bcdd001f73772b8eb2118a714cea", size = 121115, upload-time = "2026-01-31T23:26:44.518Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/f9b5e103d3caf23a72c04a1baf2b61c4a14d1feb440d3c98c26725b4503a/wrapt-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:eabe95ea5fbe1524a53c0f3fc535c99f2aa376ec1451b0b79d943d2240d80e36", size = 119487, upload-time = "2026-01-31T23:25:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/b61fdc4680dd5cd6828977341b9fd729e2c623338bfe65647f5c0ff8195e/wrapt-2.1.0-cp314-cp314-win32.whl", hash = "sha256:2cd647097df1df78f027ac7d5d663f05daa1a117b69cf7f476cb299f90557747", size = 58519, upload-time = "2026-01-31T23:25:04.426Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4f/42ab43e496d0d19caed9f69366d0f28f7f08c139297e78b17dab6ecbb6d5/wrapt-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0fc3e388a14ef8101c685dc80b4d2932924a639a03e5c44b5ffabbda2f1f2dc", size = 60767, upload-time = "2026-01-31T23:25:21.954Z" }, + { url = "https://files.pythonhosted.org/packages/ef/15/0337768ac97a8758bc0fc1afdf5f656075a7facf198f62bbe8a22b789277/wrapt-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7c06653908a23a85c4b2455b9d37c085f9756c09058df87b4a2fce2b2f8d58c2", size = 59056, upload-time = "2026-01-31T23:26:25.814Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f1/58f4674d1db44912003a51b34e8d9823a832fbbb39162e9dbe06e5f6424e/wrapt-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c70b4829c6f2f4af4cdaa16442032fcaf882063304160555e4a19b43fd2c6c9d", size = 63061, upload-time = "2026-01-31T23:26:06.601Z" }, + { url = "https://files.pythonhosted.org/packages/02/c1/07f6bf6619285f39cd616314217170c6160da99a46ad6ae4a60044f6ab5a/wrapt-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7fd4c4ee51ebdf245549d54a7c2181a4f39caac97c9dc8a050b5ba814067a29", size = 63620, upload-time = "2026-01-31T23:25:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/46/82/f7df1648762260f60c4e22c066a17d95f20267c94bfe653fab4f08e2c297/wrapt-2.1.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b158558438874e5fd5cb505b5a635bd08c84857bc937973d9e12e1166cdf3b", size = 152546, upload-time = "2026-01-31T23:25:02.102Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/d953336e09bac13a9ffa9073e167c5dec8aaa4a717a8551bf64cb4683590/wrapt-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e2e156fe2d41700b837be9b1d8d80ebab44e9891589bc7c41578ef110184e29", size = 158704, upload-time = "2026-01-31T23:25:43.269Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/2ed57e46b30af2a5a750c85a9dd30d2244ef10e2f8db150560126d8cbd24/wrapt-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f1e9bac6a6c1ba65e0ac50e32c575266734a07b6c17e718c4babd91e2faa69b", size = 155563, upload-time = "2026-01-31T23:25:39.17Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8c/4f54f7ea5addf208be44459393185aaa193bd2d0b8ecf4683b159fcc5238/wrapt-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12687e6271df7ae5706bee44cc1f77fecb7805976ec9f14f58381b30ae2aceb5", size = 150189, upload-time = "2026-01-31T23:25:44.654Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cc/e8290a1cd94297fbc1e9fbad06481b5a7c918f2db6645c550f05ee47f359/wrapt-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:38bbe336ee32f67eb99f886bd4f040d91310b7e660061bb03b9083d26e8cf915", size = 60431, upload-time = "2026-01-31T23:25:48.34Z" }, + { url = "https://files.pythonhosted.org/packages/d0/df/af5d244938853e3adb1251ca1397e9fa78d3e92adc808a0af0a8547585d3/wrapt-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0fa64a9a07df7f85b352adc42b43e7f44085fb11191b8f5b9b77219f7aaf7e17", size = 63859, upload-time = "2026-01-31T23:26:23.2Z" }, + { url = "https://files.pythonhosted.org/packages/39/c4/28b6f2804e8bc05d17114dfed03a80bce5b83ca2113fd44eecbef12275d1/wrapt-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:da379cbdf3b7d97ace33a69a391b7a7e2130b1aca94dc447246217994233974c", size = 60446, upload-time = "2026-01-31T23:25:41.001Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/70983b75d4abd6f85cffc6df79c623220ec5a579ceaacabac35c904b7b52/wrapt-2.1.0-py3-none-any.whl", hash = "sha256:e035693a0d25ea5bf5826df3e203dff7d091b0d5442aaefec9ca8f2bab38417f", size = 43886, upload-time = "2026-01-31T23:25:07.22Z" }, ] From afc71b0525777b785a670c8e73bab028238de76b Mon Sep 17 00:00:00 2001 From: ddc Date: Mon, 2 Feb 2026 17:49:23 -0300 Subject: [PATCH 05/18] v3.0.9 --- ddcDatabases/core/base.py | 4 ++-- ddcDatabases/core/persistent.py | 8 ++++---- ddcDatabases/core/retry.py | 8 ++++---- ddcDatabases/mongodb.py | 2 +- ddcDatabases/mssql.py | 4 ++-- ddcDatabases/mysql.py | 2 +- ddcDatabases/oracle.py | 4 ++-- ddcDatabases/postgresql.py | 4 ++-- ddcDatabases/sqlite.py | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/ddcDatabases/core/base.py b/ddcDatabases/core/base.py index 0e33677..8ff0889 100644 --- a/ddcDatabases/core/base.py +++ b/ddcDatabases/core/base.py @@ -42,7 +42,7 @@ def __init__( async_driver: str | None, conn_retry_config: BaseRetryConfig | None = None, op_retry_config: BaseOperationRetryConfig | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: self.connection_url = connection_url self.engine_args = engine_args @@ -155,7 +155,7 @@ def __init__( sync_session: Session | None = None, async_session: AsyncSession | None = None, host_url: URL | str = "", - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: self.sync_session = sync_session self.async_session = async_session diff --git a/ddcDatabases/core/persistent.py b/ddcDatabases/core/persistent.py index c47013c..77f813c 100644 --- a/ddcDatabases/core/persistent.py +++ b/ddcDatabases/core/persistent.py @@ -133,7 +133,7 @@ def __init__( connection_key: str, config: PersistentConnectionConfig | None = None, retry_config: BaseRetryConfig | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: self._connection_key = connection_key self._config = config or PersistentConnectionConfig() @@ -213,7 +213,7 @@ def __init__( expire_on_commit: bool = False, config: PersistentConnectionConfig | None = None, retry_config: BaseRetryConfig | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: super().__init__(connection_key, config, retry_config, logger) self._connection_url = connection_url @@ -335,7 +335,7 @@ def __init__( expire_on_commit: bool = False, config: PersistentConnectionConfig | None = None, retry_config: BaseRetryConfig | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: super().__init__(connection_key, config, retry_config, logger) self._connection_url = connection_url @@ -486,7 +486,7 @@ def __init__( database: str, config: PersistentConnectionConfig | None = None, retry_config: BaseRetryConfig | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: self._connection_key = connection_key self._connection_url = connection_url diff --git a/ddcDatabases/core/retry.py b/ddcDatabases/core/retry.py index 7f5931b..60ea980 100644 --- a/ddcDatabases/core/retry.py +++ b/ddcDatabases/core/retry.py @@ -6,7 +6,7 @@ import time from .configs import BaseRetryConfig from .constants import CONNECTION_ERROR_KEYWORDS -from typing import Awaitable, Callable, TypeVar +from typing import Any, Awaitable, Callable, TypeVar # Type variable for generic return types T = TypeVar('T') @@ -66,7 +66,7 @@ def _handle_retry_exception( attempt: int, config: BaseRetryConfig, operation_name: str, - logger: logging.Logger | None = None, + logger: Any = None, ) -> float: """ Handle an exception during retry operation. @@ -107,7 +107,7 @@ def retry_operation( operation: Callable[[], T], config: BaseRetryConfig, operation_name: str = "operation", - logger: logging.Logger | None = None, + logger: Any = None, ) -> T: """ Execute an operation with retry logic (synchronous). @@ -147,7 +147,7 @@ async def retry_operation_async( operation: Callable[[], Awaitable[T]], config: BaseRetryConfig, operation_name: str = "operation", - logger: logging.Logger | None = None, + logger: Any = None, ) -> T: """ Execute an operation with retry logic (asynchronous). diff --git a/ddcDatabases/mongodb.py b/ddcDatabases/mongodb.py index 3720aff..7a44f9c 100644 --- a/ddcDatabases/mongodb.py +++ b/ddcDatabases/mongodb.py @@ -64,7 +64,7 @@ def __init__( conn_retry_config: MongoDBConnRetryConfig | None = None, op_retry_config: MongoDBOpRetryConfig | None = None, tls_config: MongoDBTLSConfig | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: _settings = get_mongodb_settings() diff --git a/ddcDatabases/mssql.py b/ddcDatabases/mssql.py index 597e392..f175f8d 100755 --- a/ddcDatabases/mssql.py +++ b/ddcDatabases/mssql.py @@ -13,7 +13,7 @@ from sqlalchemy.engine import URL, Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import Session -from typing import AsyncGenerator, Generator +from typing import Any, AsyncGenerator, Generator _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) @@ -72,7 +72,7 @@ def __init__( op_retry_config: MSSQLOpRetryConfig | None = None, ssl_config: MSSQLSSLConfig | None = None, extra_engine_args: dict | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: _settings = get_mssql_settings() diff --git a/ddcDatabases/mysql.py b/ddcDatabases/mysql.py index 868e6f1..698287a 100755 --- a/ddcDatabases/mysql.py +++ b/ddcDatabases/mysql.py @@ -70,7 +70,7 @@ def __init__( op_retry_config: MySQLOpRetryConfig | None = None, ssl_config: MySQLSSLConfig | None = None, extra_engine_args: dict | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: _settings = get_mysql_settings() diff --git a/ddcDatabases/oracle.py b/ddcDatabases/oracle.py index 30414f5..e973dd0 100644 --- a/ddcDatabases/oracle.py +++ b/ddcDatabases/oracle.py @@ -12,7 +12,7 @@ from dataclasses import dataclass from sqlalchemy.engine import URL, Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine -from typing import AsyncGenerator, Generator +from typing import Any, AsyncGenerator, Generator _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) @@ -67,7 +67,7 @@ def __init__( op_retry_config: OracleOpRetryConfig | None = None, ssl_config: OracleSSLConfig | None = None, extra_engine_args: dict | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: _settings = get_oracle_settings() diff --git a/ddcDatabases/postgresql.py b/ddcDatabases/postgresql.py index d2f5612..30130f7 100755 --- a/ddcDatabases/postgresql.py +++ b/ddcDatabases/postgresql.py @@ -15,7 +15,7 @@ from sqlalchemy import URL from sqlalchemy.engine import Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine -from typing import AsyncGenerator, Generator +from typing import Any, AsyncGenerator, Generator _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) @@ -73,7 +73,7 @@ def __init__( op_retry_config: PostgreSQLOpRetryConfig | None = None, ssl_config: PostgreSQLSSLConfig | None = None, extra_engine_args: dict | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: _settings = get_postgresql_settings() diff --git a/ddcDatabases/sqlite.py b/ddcDatabases/sqlite.py index eecebdb..f22b382 100755 --- a/ddcDatabases/sqlite.py +++ b/ddcDatabases/sqlite.py @@ -39,7 +39,7 @@ def __init__( conn_retry_config: SqliteConnRetryConfig | None = None, op_retry_config: SqliteOpRetryConfig | None = None, extra_engine_args: dict[str, Any] | None = None, - logger: logging.Logger | None = None, + logger: Any = None, ) -> None: _settings = get_sqlite_settings() From e3ab7b64460b46b00bea1d26e70664d4c6a92044 Mon Sep 17 00:00:00 2001 From: ddc Date: Tue, 3 Feb 2026 17:56:03 -0300 Subject: [PATCH 06/18] v3.0.9 --- README.md | 59 ++- ddcDatabases/.env.example | 143 +++---- ddcDatabases/__init__.py | 61 +-- ddcDatabases/core/base.py | 16 +- ddcDatabases/core/constants.py | 5 + ddcDatabases/core/persistent.py | 358 ++++++++++++++++-- ddcDatabases/core/settings.py | 153 +++++--- ddcDatabases/mongodb.py | 52 +-- ddcDatabases/mssql.py | 52 +-- ddcDatabases/mysql.py | 52 +-- ddcDatabases/oracle.py | 52 +-- ddcDatabases/postgresql.py | 52 +-- ddcDatabases/sqlite.py | 50 +-- pyproject.toml | 6 +- .../core/test_persistent_multiple.py | 74 ++++ .../integration/{ => mariadb}/test_mariadb.py | 0 .../mariadb/test_mariadb_persistent.py | 126 ++++++ tests/integration/mariadb/test_mariadb_ssl.py | 93 +++++ .../integration/{ => mongodb}/test_mongodb.py | 0 .../mongodb/test_mongodb_persistent.py | 110 ++++++ tests/integration/mongodb/test_mongodb_tls.py | 82 ++++ tests/integration/{ => mssql}/test_mssql.py | 0 .../mssql/test_mssql_persistent.py | 97 +++++ tests/integration/mssql/test_mssql_ssl.py | 90 +++++ tests/integration/{ => mysql}/test_mysql.py | 0 .../mysql/test_mysql_persistent.py | 124 ++++++ tests/integration/mysql/test_mysql_ssl.py | 109 ++++++ tests/integration/{ => oracle}/test_oracle.py | 0 .../oracle/test_oracle_persistent.py | 92 +++++ tests/integration/oracle/test_oracle_ssl.py | 71 ++++ .../{ => postgresql}/test_postgresql.py | 0 .../postgresql/test_postgresql_persistent.py | 185 +++++++++ .../postgresql/test_postgresql_ssl.py | 128 +++++++ tests/integration/{ => sqlite}/test_sqlite.py | 0 tests/unit/core/test_db_utils.py | 10 +- tests/unit/core/test_init_module.py | 12 +- tests/unit/core/test_persistent.py | 109 +++--- tests/unit/core/test_retry_logic.py | 18 +- tests/unit/core/test_ssl_configs.py | 33 ++ tests/unit/mariadb/__init__.py | 0 tests/unit/mariadb/test_mariadb_persistent.py | 113 ++++++ tests/unit/mariadb/test_mariadb_ssl.py | 67 ++++ tests/unit/{ => mongodb}/test_mongodb.py | 34 +- tests/unit/mongodb/test_mongodb_persistent.py | 158 ++++++++ tests/unit/mongodb/test_mongodb_tls.py | 86 +++++ tests/unit/{ => mssql}/test_mssql.py | 26 +- tests/unit/mssql/test_mssql_persistent.py | 104 +++++ tests/unit/mssql/test_mssql_ssl.py | 67 ++++ tests/unit/{ => mysql}/test_mysql.py | 34 +- tests/unit/mysql/test_mysql_persistent.py | 142 +++++++ tests/unit/mysql/test_mysql_ssl.py | 98 +++++ tests/unit/{ => oracle}/test_oracle.py | 34 +- tests/unit/oracle/test_oracle_persistent.py | 102 +++++ tests/unit/oracle/test_oracle_ssl.py | 57 +++ .../unit/{ => postgresql}/test_postgresql.py | 152 ++++---- .../postgresql/test_postgresql_persistent.py | 183 +++++++++ tests/unit/postgresql/test_postgresql_ssl.py | 102 +++++ tests/unit/sqlite/__init__.py | 0 tests/unit/{ => sqlite}/test_sqlite.py | 36 +- uv.lock | 41 +- 60 files changed, 3721 insertions(+), 589 deletions(-) create mode 100644 tests/integration/core/test_persistent_multiple.py rename tests/integration/{ => mariadb}/test_mariadb.py (100%) create mode 100644 tests/integration/mariadb/test_mariadb_persistent.py create mode 100644 tests/integration/mariadb/test_mariadb_ssl.py rename tests/integration/{ => mongodb}/test_mongodb.py (100%) create mode 100644 tests/integration/mongodb/test_mongodb_persistent.py create mode 100644 tests/integration/mongodb/test_mongodb_tls.py rename tests/integration/{ => mssql}/test_mssql.py (100%) create mode 100644 tests/integration/mssql/test_mssql_persistent.py create mode 100644 tests/integration/mssql/test_mssql_ssl.py rename tests/integration/{ => mysql}/test_mysql.py (100%) create mode 100644 tests/integration/mysql/test_mysql_persistent.py create mode 100644 tests/integration/mysql/test_mysql_ssl.py rename tests/integration/{ => oracle}/test_oracle.py (100%) create mode 100644 tests/integration/oracle/test_oracle_persistent.py create mode 100644 tests/integration/oracle/test_oracle_ssl.py rename tests/integration/{ => postgresql}/test_postgresql.py (100%) create mode 100644 tests/integration/postgresql/test_postgresql_persistent.py create mode 100644 tests/integration/postgresql/test_postgresql_ssl.py rename tests/integration/{ => sqlite}/test_sqlite.py (100%) create mode 100644 tests/unit/core/test_ssl_configs.py create mode 100644 tests/unit/mariadb/__init__.py create mode 100644 tests/unit/mariadb/test_mariadb_persistent.py create mode 100644 tests/unit/mariadb/test_mariadb_ssl.py rename tests/unit/{ => mongodb}/test_mongodb.py (97%) create mode 100644 tests/unit/mongodb/test_mongodb_persistent.py create mode 100644 tests/unit/mongodb/test_mongodb_tls.py rename tests/unit/{ => mssql}/test_mssql.py (96%) create mode 100644 tests/unit/mssql/test_mssql_persistent.py create mode 100644 tests/unit/mssql/test_mssql_ssl.py rename tests/unit/{ => mysql}/test_mysql.py (92%) create mode 100644 tests/unit/mysql/test_mysql_persistent.py create mode 100644 tests/unit/mysql/test_mysql_ssl.py rename tests/unit/{ => oracle}/test_oracle.py (94%) create mode 100644 tests/unit/oracle/test_oracle_persistent.py create mode 100644 tests/unit/oracle/test_oracle_ssl.py rename tests/unit/{ => postgresql}/test_postgresql.py (92%) create mode 100644 tests/unit/postgresql/test_postgresql_persistent.py create mode 100644 tests/unit/postgresql/test_postgresql_ssl.py create mode 100644 tests/unit/sqlite/__init__.py rename tests/unit/{ => sqlite}/test_sqlite.py (92%) diff --git a/README.md b/README.md index ee6c4e3..ecf5d1d 100755 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ Database classes use structured configuration dataclasses instead of flat keywor |------------------------------|---------------------------------|-------------------------------------------------------------------------------------| | `{DB}PoolConfig` | Connection pool settings | `pool_size`, `max_overflow`, `pool_recycle`, `connection_timeout` | | `{DB}SessionConfig` | SQLAlchemy session settings | `echo`, `autoflush`, `expire_on_commit`, `autocommit` | -| `{DB}ConnRetryConfig` | Connection-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay` | -| `{DB}OpRetryConfig` | Operation-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay`, `jitter` | +| `{DB}ConnectionRetryConfig` | Connection-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay` | +| `{DB}OperationRetryConfig` | Operation-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay`, `jitter` | | `PersistentConnectionConfig` | Persistent connection settings | `idle_timeout`, `health_check_interval`, `auto_reconnect` | **Note:** Replace `{DB}` with the database prefix: `PostgreSQL`, `MySQL`, `MSSQL`, `Oracle`, `MongoDB`, or `Sqlite`. @@ -120,10 +120,10 @@ Retry with exponential backoff is enabled by default at two levels: **1. Connection Level** - Retries when establishing database connections: ```python -from ddcDatabases import PostgreSQL, PostgreSQLConnRetryConfig +from ddcDatabases import PostgreSQL, PostgreSQLConnectionRetryConfig with PostgreSQL( - conn_retry_config=PostgreSQLConnRetryConfig( + connection_retry_config=PostgreSQLConnectionRetryConfig( enable_retry=True, # Enable/disable retry (default: True) max_retries=3, # Maximum retry attempts (default: 3) initial_retry_delay=1.0, # Initial delay in seconds (default: 1.0) @@ -136,10 +136,10 @@ with PostgreSQL( **2. Operation Level** - Retries individual database operations (fetchall, insert, etc.): ```python -from ddcDatabases import DBUtils, PostgreSQL, PostgreSQLOpRetryConfig +from ddcDatabases import DBUtils, PostgreSQL, PostgreSQLOperationRetryConfig with PostgreSQL( - op_retry_config=PostgreSQLOpRetryConfig( + operation_retry_config=PostgreSQLOperationRetryConfig( enable_retry=True, # Enable/disable (default: True) max_retries=3, # Max attempts (default: 3) initial_retry_delay=1.0, # Initial delay in seconds (default: 1.0) @@ -174,6 +174,8 @@ from ddcDatabases import ( MySQLPersistent, MongoDBPersistent, PersistentConnectionConfig, + PostgreSQLConnectionRetryConfig, + PostgreSQLOperationRetryConfig, close_all_persistent_connections, ) @@ -188,6 +190,19 @@ conn = PostgreSQLPersistent( health_check_interval=30, # seconds between health checks (default: 30) auto_reconnect=True, # auto-reconnect on failure (default: True) ), + connection_retry_config=PostgreSQLConnectionRetryConfig( + enable_retry=True, # enable connection retry (default: True) + max_retries=5, # max connection attempts (default: 5) + initial_retry_delay=1.0, # initial delay in seconds (default: 1.0) + max_retry_delay=30.0, # max delay in seconds (default: 30.0) + ), + operation_retry_config=PostgreSQLOperationRetryConfig( + enable_retry=True, # enable operation retry (default: True) + max_retries=3, # max operation attempts (default: 3) + initial_retry_delay=0.5, # initial delay in seconds (default: 0.5) + max_retry_delay=10.0, # max delay in seconds (default: 10.0) + jitter=0.1, # randomization factor (default: 0.1) + ), ) # Use as context manager (doesn't disconnect on exit, just updates last-used time) @@ -208,6 +223,36 @@ async with conn as session: close_all_persistent_connections() ``` +### Execute with Retry + +The `execute_with_retry` method provides automatic session management with retry logic: + +**Synchronous:** +```python +from ddcDatabases import PostgreSQLPersistent + +db = PostgreSQLPersistent(logger=logger) +result = db.execute_with_retry( + lambda session: MyDal(session).do_something() +) +``` + +**Asynchronous:** +```python +from ddcDatabases import PostgreSQLPersistent + +db = PostgreSQLPersistent(async_mode=True, logger=logger) +result = await db.execute_with_retry( + lambda session: MyDal(session).do_something() +) +``` + +The method automatically: +- Connects (or reuses existing connection) +- Executes the operation with the session +- Commits on success, rolls back on failure +- Retries with exponential backoff if `auto_reconnect` is enabled + **Available Persistent Connection Classes:** - `PostgreSQLPersistent` - PostgreSQL (sync/async) @@ -605,7 +650,7 @@ async with PostgreSQL() as session: results = await db_utils_async.fetchall(stmt) ``` -**Note:** Retry logic is configured at the database connection level using `op_retry_config` (see [Retry Logic](#retry-logic) section). +**Note:** Retry logic is configured at the database connection level using `operation_retry_config` (see [Retry Logic](#retry-logic) section). # Logging diff --git a/ddcDatabases/.env.example b/ddcDatabases/.env.example index 30ed0f7..8fe189c 100644 --- a/ddcDatabases/.env.example +++ b/ddcDatabases/.env.example @@ -5,17 +5,16 @@ SQLITE_FILE_PATH=sqlite.db SQLITE_ECHO=false # Connection Retry settings -SQLITE_CONN_ENABLE_RETRY=false -SQLITE_CONN_MAX_RETRIES=1 -SQLITE_CONN_INITIAL_RETRY_DELAY=1.0 -SQLITE_CONN_MAX_RETRY_DELAY=30.0 +SQLITE_CONNECTION_ENABLE_RETRY=false +SQLITE_CONNECTION_MAX_RETRIES=1 +SQLITE_CONNECTION_INITIAL_RETRY_DELAY=1.0 +SQLITE_CONNECTION_MAX_RETRY_DELAY=30.0 # Operation Retry settings -SQLITE_OP_ENABLE_RETRY=false -SQLITE_OP_MAX_RETRIES=1 -SQLITE_OP_INITIAL_RETRY_DELAY=0.5 -SQLITE_OP_MAX_RETRY_DELAY=10.0 -SQLITE_OP_JITTER=0.1 - +SQLITE_OPERATION_ENABLE_RETRY=false +SQLITE_OPERATION_MAX_RETRIES=1 +SQLITE_OPERATION_INITIAL_RETRY_DELAY=0.5 +SQLITE_OPERATION_MAX_RETRY_DELAY=10.0 +SQLITE_OPERATION_JITTER=0.1 # PostgreSQL Database Settings POSTGRESQL_HOST=localhost @@ -40,18 +39,21 @@ POSTGRESQL_SSL_CA_CERT_PATH= POSTGRESQL_SSL_CLIENT_CERT_PATH= POSTGRESQL_SSL_CLIENT_KEY_PATH= # Connection Retry settings -POSTGRESQL_CONN_ENABLE_RETRY=true -POSTGRESQL_CONN_MAX_RETRIES=3 -POSTGRESQL_CONN_INITIAL_RETRY_DELAY=1.0 -POSTGRESQL_CONN_MAX_RETRY_DELAY=30.0 -POSTGRESQL_CONN_DISCONNECT_IDLE_TIMEOUT=300 +POSTGRESQL_CONNECTION_ENABLE_RETRY=true +POSTGRESQL_CONNECTION_MAX_RETRIES=3 +POSTGRESQL_CONNECTION_INITIAL_RETRY_DELAY=1.0 +POSTGRESQL_CONNECTION_MAX_RETRY_DELAY=30.0 +POSTGRESQL_CONNECTION_DISCONNECT_IDLE_TIMEOUT=300 # Operation Retry settings -POSTGRESQL_OP_ENABLE_RETRY=true -POSTGRESQL_OP_MAX_RETRIES=3 -POSTGRESQL_OP_INITIAL_RETRY_DELAY=0.5 -POSTGRESQL_OP_MAX_RETRY_DELAY=10.0 -POSTGRESQL_OP_JITTER=0.1 - +POSTGRESQL_OPERATION_ENABLE_RETRY=true +POSTGRESQL_OPERATION_MAX_RETRIES=3 +POSTGRESQL_OPERATION_INITIAL_RETRY_DELAY=0.5 +POSTGRESQL_OPERATION_MAX_RETRY_DELAY=10.0 +POSTGRESQL_OPERATION_JITTER=0.1 +# Persistent connection settings +POSTGRESQL_PERSISTENT_IDLE_TIMEOUT=300 +POSTGRESQL_PERSISTENT_HEALTH_CHECK_INTERVAL=30 +POSTGRESQL_PERSISTENT_AUTO_RECONNECT=true # Microsoft SQL Server Database Settings MSSQL_HOST=localhost @@ -76,18 +78,21 @@ MSSQL_SSL_ENCRYPT=false MSSQL_SSL_TRUST_SERVER_CERTIFICATE=true MSSQL_SSL_CA_CERT_PATH= # Connection Retry settings -MSSQL_CONN_ENABLE_RETRY=true -MSSQL_CONN_MAX_RETRIES=3 -MSSQL_CONN_INITIAL_RETRY_DELAY=1.0 -MSSQL_CONN_MAX_RETRY_DELAY=30.0 -MSSQL_CONN_DISCONNECT_IDLE_TIMEOUT=300 +MSSQL_CONNECTION_ENABLE_RETRY=true +MSSQL_CONNECTION_MAX_RETRIES=3 +MSSQL_CONNECTION_INITIAL_RETRY_DELAY=1.0 +MSSQL_CONNECTION_MAX_RETRY_DELAY=30.0 +MSSQL_CONNECTION_DISCONNECT_IDLE_TIMEOUT=300 # Operation Retry settings -MSSQL_OP_ENABLE_RETRY=true -MSSQL_OP_MAX_RETRIES=3 -MSSQL_OP_INITIAL_RETRY_DELAY=0.5 -MSSQL_OP_MAX_RETRY_DELAY=10.0 -MSSQL_OP_JITTER=0.1 - +MSSQL_OPERATION_ENABLE_RETRY=true +MSSQL_OPERATION_MAX_RETRIES=3 +MSSQL_OPERATION_INITIAL_RETRY_DELAY=0.5 +MSSQL_OPERATION_MAX_RETRY_DELAY=10.0 +MSSQL_OPERATION_JITTER=0.1 +# Persistent connection settings +MSSQL_PERSISTENT_IDLE_TIMEOUT=300 +MSSQL_PERSISTENT_HEALTH_CHECK_INTERVAL=30 +MSSQL_PERSISTENT_AUTO_RECONNECT=true # MySQL Database Settings MYSQL_HOST=localhost @@ -111,18 +116,21 @@ MYSQL_SSL_CA_CERT_PATH= MYSQL_SSL_CLIENT_CERT_PATH= MYSQL_SSL_CLIENT_KEY_PATH= # Connection Retry settings -MYSQL_CONN_ENABLE_RETRY=true -MYSQL_CONN_MAX_RETRIES=3 -MYSQL_CONN_INITIAL_RETRY_DELAY=1.0 -MYSQL_CONN_MAX_RETRY_DELAY=30.0 -MYSQL_CONN_DISCONNECT_IDLE_TIMEOUT=300 +MYSQL_CONNECTION_ENABLE_RETRY=true +MYSQL_CONNECTION_MAX_RETRIES=3 +MYSQL_CONNECTION_INITIAL_RETRY_DELAY=1.0 +MYSQL_CONNECTION_MAX_RETRY_DELAY=30.0 +MYSQL_CONNECTION_DISCONNECT_IDLE_TIMEOUT=300 # Operation Retry settings -MYSQL_OP_ENABLE_RETRY=true -MYSQL_OP_MAX_RETRIES=3 -MYSQL_OP_INITIAL_RETRY_DELAY=0.5 -MYSQL_OP_MAX_RETRY_DELAY=10.0 -MYSQL_OP_JITTER=0.1 - +MYSQL_OPERATION_ENABLE_RETRY=true +MYSQL_OPERATION_MAX_RETRIES=3 +MYSQL_OPERATION_INITIAL_RETRY_DELAY=0.5 +MYSQL_OPERATION_MAX_RETRY_DELAY=10.0 +MYSQL_OPERATION_JITTER=0.1 +# Persistent connection settings +MYSQL_PERSISTENT_IDLE_TIMEOUT=300 +MYSQL_PERSISTENT_HEALTH_CHECK_INTERVAL=30 +MYSQL_PERSISTENT_AUTO_RECONNECT=true # Oracle Database Settings ORACLE_HOST=localhost @@ -144,18 +152,21 @@ ORACLE_MAX_OVERFLOW=20 ORACLE_SSL_ENABLED=false ORACLE_SSL_WALLET_PATH= # Connection Retry settings -ORACLE_CONN_ENABLE_RETRY=true -ORACLE_CONN_MAX_RETRIES=3 -ORACLE_CONN_INITIAL_RETRY_DELAY=1.0 -ORACLE_CONN_MAX_RETRY_DELAY=30.0 -ORACLE_CONN_DISCONNECT_IDLE_TIMEOUT=300 +ORACLE_CONNECTION_ENABLE_RETRY=true +ORACLE_CONNECTION_MAX_RETRIES=3 +ORACLE_CONNECTION_INITIAL_RETRY_DELAY=1.0 +ORACLE_CONNECTION_MAX_RETRY_DELAY=30.0 +ORACLE_CONNECTION_DISCONNECT_IDLE_TIMEOUT=300 # Operation Retry settings -ORACLE_OP_ENABLE_RETRY=true -ORACLE_OP_MAX_RETRIES=3 -ORACLE_OP_INITIAL_RETRY_DELAY=0.5 -ORACLE_OP_MAX_RETRY_DELAY=10.0 -ORACLE_OP_JITTER=0.1 - +ORACLE_OPERATION_ENABLE_RETRY=true +ORACLE_OPERATION_MAX_RETRIES=3 +ORACLE_OPERATION_INITIAL_RETRY_DELAY=0.5 +ORACLE_OPERATION_MAX_RETRY_DELAY=10.0 +ORACLE_OPERATION_JITTER=0.1 +# Persistent connection settings +ORACLE_PERSISTENT_IDLE_TIMEOUT=300 +ORACLE_PERSISTENT_HEALTH_CHECK_INTERVAL=30 +ORACLE_PERSISTENT_AUTO_RECONNECT=true # MongoDB Database Settings MONGODB_HOST=localhost @@ -171,14 +182,18 @@ MONGODB_TLS_CA_CERT_PATH= MONGODB_TLS_CERT_KEY_PATH= MONGODB_TLS_ALLOW_INVALID_CERTIFICATES=false # Connection Retry settings -MONGODB_CONN_ENABLE_RETRY=true -MONGODB_CONN_MAX_RETRIES=3 -MONGODB_CONN_INITIAL_RETRY_DELAY=1.0 -MONGODB_CONN_MAX_RETRY_DELAY=30.0 -MONGODB_CONN_DISCONNECT_IDLE_TIMEOUT=300 +MONGODB_CONNECTION_ENABLE_RETRY=true +MONGODB_CONNECTION_MAX_RETRIES=3 +MONGODB_CONNECTION_INITIAL_RETRY_DELAY=1.0 +MONGODB_CONNECTION_MAX_RETRY_DELAY=30.0 +MONGODB_CONNECTION_DISCONNECT_IDLE_TIMEOUT=300 # Operation Retry settings -MONGODB_OP_ENABLE_RETRY=true -MONGODB_OP_MAX_RETRIES=3 -MONGODB_OP_INITIAL_RETRY_DELAY=0.5 -MONGODB_OP_MAX_RETRY_DELAY=10.0 -MONGODB_OP_JITTER=0.1 +MONGODB_OPERATION_ENABLE_RETRY=true +MONGODB_OPERATION_MAX_RETRIES=3 +MONGODB_OPERATION_INITIAL_RETRY_DELAY=0.5 +MONGODB_OPERATION_MAX_RETRY_DELAY=10.0 +MONGODB_OPERATION_JITTER=0.1 +# Persistent connection settings +MONGODB_PERSISTENT_IDLE_TIMEOUT=300 +MONGODB_PERSISTENT_HEALTH_CHECK_INTERVAL=30 +MONGODB_PERSISTENT_AUTO_RECONNECT=true diff --git a/ddcDatabases/__init__.py b/ddcDatabases/__init__.py index 2375771..61035f4 100755 --- a/ddcDatabases/__init__.py +++ b/ddcDatabases/__init__.py @@ -1,6 +1,9 @@ import logging from .core.operations import DBUtils, DBUtilsAsync -from .core.persistent import PersistentConnectionConfig, close_all_persistent_connections +from .core.persistent import ( + PersistentConnectionConfig, + close_all_persistent_connections, +) from importlib.metadata import version __all__ = [ @@ -15,15 +18,15 @@ from .core.settings import clear_sqlite_settings_cache, get_sqlite_settings from .sqlite import ( Sqlite, - SqliteConnRetryConfig, - SqliteOpRetryConfig, + SqliteConnectionRetryConfig, + SqliteOperationRetryConfig, SqliteSessionConfig, ) __all__ += [ "Sqlite", - "SqliteConnRetryConfig", - "SqliteOpRetryConfig", + "SqliteConnectionRetryConfig", + "SqliteOperationRetryConfig", "SqliteSessionConfig", "clear_sqlite_settings_cache", "get_sqlite_settings", @@ -37,8 +40,8 @@ from .mongodb import ( MongoDB, MongoDBConnectionConfig, - MongoDBConnRetryConfig, - MongoDBOpRetryConfig, + MongoDBConnectionRetryConfig, + MongoDBOperationRetryConfig, MongoDBQueryConfig, MongoDBTLSConfig, ) @@ -46,8 +49,8 @@ __all__ += [ "MongoDB", "MongoDBConnectionConfig", - "MongoDBConnRetryConfig", - "MongoDBOpRetryConfig", + "MongoDBConnectionRetryConfig", + "MongoDBOperationRetryConfig", "MongoDBPersistent", "MongoDBQueryConfig", "MongoDBTLSConfig", @@ -63,8 +66,8 @@ from .mssql import ( MSSQL, MSSQLConnectionConfig, - MSSQLConnRetryConfig, - MSSQLOpRetryConfig, + MSSQLConnectionRetryConfig, + MSSQLOperationRetryConfig, MSSQLPoolConfig, MSSQLSessionConfig, MSSQLSSLConfig, @@ -73,8 +76,8 @@ __all__ += [ "MSSQL", "MSSQLConnectionConfig", - "MSSQLConnRetryConfig", - "MSSQLOpRetryConfig", + "MSSQLConnectionRetryConfig", + "MSSQLOperationRetryConfig", "MSSQLPersistent", "MSSQLPoolConfig", "MSSQLSessionConfig", @@ -91,8 +94,8 @@ from .mysql import ( MySQL, MySQLConnectionConfig, - MySQLConnRetryConfig, - MySQLOpRetryConfig, + MySQLConnectionRetryConfig, + MySQLOperationRetryConfig, MySQLPoolConfig, MySQLSessionConfig, MySQLSSLConfig, @@ -101,8 +104,8 @@ # MariaDB aliases (MariaDB is fully compatible with MySQL driver) MariaDB = MySQL MariaDBConnectionConfig = MySQLConnectionConfig - MariaDBConnRetryConfig = MySQLConnRetryConfig - MariaDBOpRetryConfig = MySQLOpRetryConfig + MariaDBConnectionRetryConfig = MySQLConnectionRetryConfig + MariaDBOperationRetryConfig = MySQLOperationRetryConfig MariaDBPersistent = MySQLPersistent MariaDBPoolConfig = MySQLPoolConfig MariaDBSessionConfig = MySQLSessionConfig @@ -113,8 +116,8 @@ __all__ += [ "MySQL", "MySQLConnectionConfig", - "MySQLConnRetryConfig", - "MySQLOpRetryConfig", + "MySQLConnectionRetryConfig", + "MySQLOperationRetryConfig", "MySQLPersistent", "MySQLPoolConfig", "MySQLSessionConfig", @@ -124,8 +127,8 @@ # MariaDB aliases "MariaDB", "MariaDBConnectionConfig", - "MariaDBConnRetryConfig", - "MariaDBOpRetryConfig", + "MariaDBConnectionRetryConfig", + "MariaDBOperationRetryConfig", "MariaDBPersistent", "MariaDBPoolConfig", "MariaDBSessionConfig", @@ -142,8 +145,8 @@ from .oracle import ( Oracle, OracleConnectionConfig, - OracleConnRetryConfig, - OracleOpRetryConfig, + OracleConnectionRetryConfig, + OracleOperationRetryConfig, OraclePoolConfig, OracleSessionConfig, OracleSSLConfig, @@ -152,8 +155,8 @@ __all__ += [ "Oracle", "OracleConnectionConfig", - "OracleConnRetryConfig", - "OracleOpRetryConfig", + "OracleConnectionRetryConfig", + "OracleOperationRetryConfig", "OraclePersistent", "OraclePoolConfig", "OracleSessionConfig", @@ -170,8 +173,8 @@ from .postgresql import ( PostgreSQL, PostgreSQLConnectionConfig, - PostgreSQLConnRetryConfig, - PostgreSQLOpRetryConfig, + PostgreSQLConnectionRetryConfig, + PostgreSQLOperationRetryConfig, PostgreSQLPoolConfig, PostgreSQLSessionConfig, PostgreSQLSSLConfig, @@ -180,8 +183,8 @@ __all__ += [ "PostgreSQL", "PostgreSQLConnectionConfig", - "PostgreSQLConnRetryConfig", - "PostgreSQLOpRetryConfig", + "PostgreSQLConnectionRetryConfig", + "PostgreSQLOperationRetryConfig", "PostgreSQLPersistent", "PostgreSQLPoolConfig", "PostgreSQLSessionConfig", diff --git a/ddcDatabases/core/base.py b/ddcDatabases/core/base.py index 8ff0889..7bf712e 100644 --- a/ddcDatabases/core/base.py +++ b/ddcDatabases/core/base.py @@ -27,8 +27,8 @@ class BaseConnection(ABC): "session", "is_connected", "_temp_engine", - "conn_retry_config", - "op_retry_config", + "connection_retry_config", + "operation_retry_config", "logger", ) @@ -40,8 +40,8 @@ def __init__( expire_on_commit: bool, sync_driver: str | None, async_driver: str | None, - conn_retry_config: BaseRetryConfig | None = None, - op_retry_config: BaseOperationRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, logger: Any = None, ) -> None: self.connection_url = connection_url @@ -53,8 +53,8 @@ def __init__( self.session: Session | AsyncSession | None = None self.is_connected = False self._temp_engine: Engine | AsyncEngine | None = None - self.conn_retry_config = conn_retry_config or BaseRetryConfig() - self.op_retry_config = op_retry_config or BaseOperationRetryConfig() + self.connection_retry_config = connection_retry_config or BaseRetryConfig() + self.operation_retry_config = operation_retry_config or BaseOperationRetryConfig() self.logger = logger if logger is not None else _logger def __enter__(self) -> Session: @@ -71,7 +71,7 @@ def connect() -> Session: self.is_connected = True return self.session - return retry_operation(connect, self.conn_retry_config, "sync_connect", logger=self.logger) + return retry_operation(connect, self.connection_retry_config, "sync_connect", logger=self.logger) def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: if self.session: @@ -95,7 +95,7 @@ async def connect() -> AsyncSession: self.is_connected = True return self.session - return await retry_operation_async(connect, self.conn_retry_config, "async_connect", logger=self.logger) + return await retry_operation_async(connect, self.connection_retry_config, "async_connect", logger=self.logger) async def __aexit__( self, diff --git a/ddcDatabases/core/constants.py b/ddcDatabases/core/constants.py index 00ee7a0..741fe5f 100644 --- a/ddcDatabases/core/constants.py +++ b/ddcDatabases/core/constants.py @@ -90,6 +90,11 @@ class SettingsMessages: JITTER_DESCRIPTION = "Jitter factor for retry delays (0.0-1.0)" DISCONNECT_IDLE_TIMEOUT_DESCRIPTION = "Disconnect idle timeout in seconds for persistent connections" + # Persistent connection settings descriptions + PERSISTENT_IDLE_TIMEOUT_DESCRIPTION = "Seconds before idle persistent connection is closed" + PERSISTENT_HEALTH_CHECK_INTERVAL_DESCRIPTION = "Seconds between health checks for persistent connections" + PERSISTENT_AUTO_RECONNECT_DESCRIPTION = "Enable automatic reconnection for persistent connections" + # SQLite specific SQLITE_FILE_PATH_DESCRIPTION = "Path to SQLite database file" diff --git a/ddcDatabases/core/persistent.py b/ddcDatabases/core/persistent.py index 77f813c..8f7d1f2 100644 --- a/ddcDatabases/core/persistent.py +++ b/ddcDatabases/core/persistent.py @@ -13,7 +13,7 @@ import threading import time import weakref -from .configs import BaseRetryConfig +from .configs import BaseOperationRetryConfig, BaseRetryConfig from .retry import retry_operation, retry_operation_async from .settings import ( get_mongodb_settings, @@ -29,7 +29,7 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import Session, sessionmaker -from typing import Any, Generic, TypeVar, cast +from typing import Any, Callable, Generic, Literal, TypeVar, cast, overload _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) @@ -43,9 +43,9 @@ class PersistentConnectionConfig: """Configuration for persistent connections.""" - idle_timeout: int = 300 # seconds - health_check_interval: int = 30 # seconds - auto_reconnect: bool = True + idle_timeout: int | None = None + health_check_interval: int | None = None + auto_reconnect: bool | None = None # Global registry for persistent connections (weak references to allow cleanup) @@ -120,7 +120,8 @@ class BasePersistentConnection(IdleCheckerMixin, ABC, Generic[SessionT]): '_last_used', '_lock', '_config', - '_retry_config', + '_connection_retry_config', + '_operation_retry_config', '_idle_checker_thread', '_shutdown_event', '_is_connected', @@ -132,12 +133,14 @@ def __init__( self, connection_key: str, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, logger: Any = None, ) -> None: self._connection_key = connection_key self._config = config or PersistentConnectionConfig() - self._retry_config = retry_config or BaseRetryConfig() + self._connection_retry_config = connection_retry_config or BaseRetryConfig() + self._operation_retry_config = operation_retry_config or BaseOperationRetryConfig() self._engine: Engine | AsyncEngine | None = None self._session: SessionT | None = None self._last_used = time.time() @@ -212,10 +215,11 @@ def __init__( autoflush: bool = False, expire_on_commit: bool = False, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, logger: Any = None, ) -> None: - super().__init__(connection_key, config, retry_config, logger) + super().__init__(connection_key, config, connection_retry_config, operation_retry_config, logger) self._connection_url = connection_url self._engine_args = engine_args or {} self._autoflush = autoflush @@ -284,7 +288,7 @@ def do_connect() -> Session: if self._config.auto_reconnect: return retry_operation( do_connect, - self._retry_config, + self._connection_retry_config, f"{self._connection_key}_connect", logger=self._logger, ) @@ -297,6 +301,49 @@ def disconnect(self) -> None: self._disconnect_internal() self._logger.info(f"[{self._connection_key}] Disconnected") + def execute_with_retry(self, operation: Callable[[Session], T]) -> T: + """ + Execute an operation with automatic session management and retry logic. + + Connects (or reuses existing connection), executes the operation, + commits on success, and rolls back on failure. + + Args: + operation: A callable that takes a Session and returns a result. + + Returns: + The result of the operation. + + Raises: + Exception: If the operation fails after all retries. + + Example: + conn = PostgreSQLPersistent() + result = conn.execute_with_retry( + lambda session: MyDal(session).do_something() + ) + """ + + def do_operation() -> T: + session = self.connect() + try: + result = operation(session) + session.commit() + return result + except Exception: + session.rollback() + raise + + if self._config.auto_reconnect: + return retry_operation( + do_operation, + self._operation_retry_config, + f"{self._connection_key}_execute", + logger=self._logger, + ) + else: + return do_operation() + def __enter__(self) -> Session: """Context manager entry.""" return self.connect() @@ -334,10 +381,11 @@ def __init__( autoflush: bool = False, expire_on_commit: bool = False, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, logger: Any = None, ) -> None: - super().__init__(connection_key, config, retry_config, logger) + super().__init__(connection_key, config, connection_retry_config, operation_retry_config, logger) self._connection_url = connection_url self._engine_args = engine_args or {} self._autoflush = autoflush @@ -422,7 +470,7 @@ async def do_connect() -> AsyncSession: if self._config.auto_reconnect: return await retry_operation_async( do_connect, - self._retry_config, + self._connection_retry_config, f"{self._connection_key}_async_connect", logger=self._logger, ) @@ -441,6 +489,52 @@ async def async_disconnect(self) -> None: await self._async_disconnect_internal() self._logger.info(f"[{self._connection_key}] Disconnected") + async def execute_with_retry(self, operation: Callable[[AsyncSession], T]) -> T: + """ + Execute an async operation with automatic session management and retry logic. + + Connects (or reuses existing connection), executes the operation, + commits on success, and rolls back on failure. + + Args: + operation: A callable that takes an AsyncSession and returns a result (can be sync or async). + + Returns: + The result of the operation. + + Raises: + Exception: If the operation fails after all retries. + + Example: + conn = PostgreSQLPersistent(async_mode=True) + result = await conn.execute_with_retry( + lambda session: MyDal(session).do_something() + ) + """ + + async def do_operation() -> T: + session = await self.async_connect() + try: + result = operation(session) + # Handle both sync and async operation results + if asyncio.iscoroutine(result): + result = await result + await session.commit() + return result + except Exception: + await session.rollback() + raise + + if self._config.auto_reconnect: + return await retry_operation_async( + do_operation, + self._operation_retry_config, + f"{self._connection_key}_execute", + logger=self._logger, + ) + else: + return await do_operation() + async def __aenter__(self) -> AsyncSession: """Async context manager entry.""" return await self.async_connect() @@ -471,7 +565,8 @@ class PersistentMongoDBConnection(IdleCheckerMixin): '_last_used', '_lock', '_config', - '_retry_config', + '_connection_retry_config', + '_operation_retry_config', '_idle_checker_thread', '_shutdown_event', '_is_connected', @@ -485,14 +580,16 @@ def __init__( connection_url: str, database: str, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, logger: Any = None, ) -> None: self._connection_key = connection_key self._connection_url = connection_url self._database = database self._config = config or PersistentConnectionConfig() - self._retry_config = retry_config or BaseRetryConfig() + self._connection_retry_config = connection_retry_config or BaseRetryConfig() + self._operation_retry_config = operation_retry_config or BaseOperationRetryConfig() self._client = None self._db = None self._last_used = time.time() @@ -535,7 +632,7 @@ def connect(self) -> Any: with self._lock: self._update_last_used() - if self._is_connected and self._client and self._db: + if self._is_connected and self._client is not None and self._db is not None: # Verify connection is still valid try: self._client.admin.command("ping") @@ -556,7 +653,7 @@ def do_connect() -> Any: if self._config.auto_reconnect: return retry_operation( do_connect, - self._retry_config, + self._connection_retry_config, f"{self._connection_key}_connect", logger=self._logger, ) @@ -612,6 +709,39 @@ class PostgreSQLPersistent: # use async session """ + @overload + def __new__( + cls, + host: str | None = None, + port: int | None = None, + user: str | None = None, + password: str | None = None, + database: str | None = None, + async_mode: Literal[False] = False, + config: PersistentConnectionConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, + **engine_kwargs: Any, + ) -> PersistentSQLAlchemyConnection: ... + + @overload + def __new__( + cls, + host: str | None = None, + port: int | None = None, + user: str | None = None, + password: str | None = None, + database: str | None = None, + *, + async_mode: Literal[True], + config: PersistentConnectionConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, + **engine_kwargs: Any, + ) -> PersistentSQLAlchemyAsyncConnection: ... + def __new__( cls, host: str | None = None, @@ -621,7 +751,9 @@ def __new__( database: str | None = None, async_mode: bool = False, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, **engine_kwargs: Any, ) -> PersistentSQLAlchemyConnection | PersistentSQLAlchemyAsyncConnection: """Create or return existing persistent PostgreSQL connection.""" @@ -633,6 +765,20 @@ def __new__( database = database or _settings.database connection_key = f"postgresql://{user}@{host}:{port}/{database}" # NOSONAR + # Build config from settings, allowing partial overrides + _cfg = config or PersistentConnectionConfig() + config = PersistentConnectionConfig( + idle_timeout=_cfg.idle_timeout if _cfg.idle_timeout is not None else _settings.persistent_idle_timeout, + health_check_interval=( + _cfg.health_check_interval + if _cfg.health_check_interval is not None + else _settings.persistent_health_check_interval + ), + auto_reconnect=( + _cfg.auto_reconnect if _cfg.auto_reconnect is not None else _settings.persistent_auto_reconnect + ), + ) + with _registry_lock: if connection_key in _persistent_connections: return cast( @@ -654,11 +800,13 @@ def __new__( connection_url=connection_url, engine_args=engine_kwargs, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) else: connection_url = URL.create( - drivername="postgresql+psycopg2", + drivername="postgresql+psycopg", username=user, password=password, host=host, @@ -670,7 +818,9 @@ def __new__( connection_url=connection_url, engine_args=engine_kwargs, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) _persistent_connections[connection_key] = conn @@ -696,6 +846,39 @@ class MySQLPersistent: # use async session """ + @overload + def __new__( + cls, + host: str | None = None, + port: int | None = None, + user: str | None = None, + password: str | None = None, + database: str | None = None, + async_mode: Literal[False] = False, + config: PersistentConnectionConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, + **engine_kwargs: Any, + ) -> PersistentSQLAlchemyConnection: ... + + @overload + def __new__( + cls, + host: str | None = None, + port: int | None = None, + user: str | None = None, + password: str | None = None, + database: str | None = None, + *, + async_mode: Literal[True], + config: PersistentConnectionConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, + **engine_kwargs: Any, + ) -> PersistentSQLAlchemyAsyncConnection: ... + def __new__( cls, host: str | None = None, @@ -705,7 +888,9 @@ def __new__( database: str | None = None, async_mode: bool = False, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, **engine_kwargs: Any, ) -> PersistentSQLAlchemyConnection | PersistentSQLAlchemyAsyncConnection: """Create or return existing persistent MySQL connection.""" @@ -717,6 +902,20 @@ def __new__( database = database or _settings.database connection_key = f"mysql://{user}@{host}:{port}/{database}" # NOSONAR + # Build config from settings, allowing partial overrides + _cfg = config or PersistentConnectionConfig() + config = PersistentConnectionConfig( + idle_timeout=_cfg.idle_timeout if _cfg.idle_timeout is not None else _settings.persistent_idle_timeout, + health_check_interval=( + _cfg.health_check_interval + if _cfg.health_check_interval is not None + else _settings.persistent_health_check_interval + ), + auto_reconnect=( + _cfg.auto_reconnect if _cfg.auto_reconnect is not None else _settings.persistent_auto_reconnect + ), + ) + with _registry_lock: if connection_key in _persistent_connections: return cast( @@ -738,7 +937,9 @@ def __new__( connection_url=connection_url, engine_args=engine_kwargs, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) else: connection_url = URL.create( @@ -754,7 +955,9 @@ def __new__( connection_url=connection_url, engine_args=engine_kwargs, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) _persistent_connections[connection_key] = conn @@ -780,6 +983,39 @@ class MSSQLPersistent: # use async session """ + @overload + def __new__( + cls, + host: str | None = None, + port: int | None = None, + user: str | None = None, + password: str | None = None, + database: str | None = None, + async_mode: Literal[False] = False, + config: PersistentConnectionConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, + **engine_kwargs: Any, + ) -> PersistentSQLAlchemyConnection: ... + + @overload + def __new__( + cls, + host: str | None = None, + port: int | None = None, + user: str | None = None, + password: str | None = None, + database: str | None = None, + *, + async_mode: Literal[True], + config: PersistentConnectionConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, + **engine_kwargs: Any, + ) -> PersistentSQLAlchemyAsyncConnection: ... + def __new__( cls, host: str | None = None, @@ -789,7 +1025,9 @@ def __new__( database: str | None = None, async_mode: bool = False, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, **engine_kwargs: Any, ) -> PersistentSQLAlchemyConnection | PersistentSQLAlchemyAsyncConnection: """Create or return existing persistent MSSQL connection.""" @@ -801,6 +1039,20 @@ def __new__( database = database or _settings.database connection_key = f"mssql://{user}@{host}:{port}/{database}" # NOSONAR + # Build config from settings, allowing partial overrides + _cfg = config or PersistentConnectionConfig() + config = PersistentConnectionConfig( + idle_timeout=_cfg.idle_timeout if _cfg.idle_timeout is not None else _settings.persistent_idle_timeout, + health_check_interval=( + _cfg.health_check_interval + if _cfg.health_check_interval is not None + else _settings.persistent_health_check_interval + ), + auto_reconnect=( + _cfg.auto_reconnect if _cfg.auto_reconnect is not None else _settings.persistent_auto_reconnect + ), + ) + with _registry_lock: if connection_key in _persistent_connections: return cast( @@ -823,7 +1075,9 @@ def __new__( connection_url=connection_url, engine_args=engine_kwargs, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) else: connection_url = URL.create( @@ -840,7 +1094,9 @@ def __new__( connection_url=connection_url, engine_args=engine_kwargs, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) _persistent_connections[connection_key] = conn @@ -868,7 +1124,9 @@ def __new__( password: str | None = None, servicename: str | None = None, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, **engine_kwargs: Any, ) -> PersistentSQLAlchemyConnection: """Create or return existing persistent Oracle connection.""" @@ -880,6 +1138,20 @@ def __new__( servicename = servicename or _settings.servicename connection_key = f"oracle://{user}@{host}:{port}/{servicename}" # NOSONAR + # Build config from settings, allowing partial overrides + _cfg = config or PersistentConnectionConfig() + config = PersistentConnectionConfig( + idle_timeout=_cfg.idle_timeout if _cfg.idle_timeout is not None else _settings.persistent_idle_timeout, + health_check_interval=( + _cfg.health_check_interval + if _cfg.health_check_interval is not None + else _settings.persistent_health_check_interval + ), + auto_reconnect=( + _cfg.auto_reconnect if _cfg.auto_reconnect is not None else _settings.persistent_auto_reconnect + ), + ) + with _registry_lock: if connection_key in _persistent_connections: return cast(PersistentSQLAlchemyConnection, _persistent_connections[connection_key]) @@ -897,7 +1169,9 @@ def __new__( connection_url=connection_url, engine_args=engine_kwargs, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) _persistent_connections[connection_key] = conn @@ -925,7 +1199,9 @@ def __new__( password: str | None = None, database: str | None = None, config: PersistentConnectionConfig | None = None, - retry_config: BaseRetryConfig | None = None, + connection_retry_config: BaseRetryConfig | None = None, + operation_retry_config: BaseOperationRetryConfig | None = None, + logger: Any = None, ) -> PersistentMongoDBConnection: """Create or return existing persistent MongoDB connection.""" _settings = get_mongodb_settings() @@ -936,6 +1212,20 @@ def __new__( database = database or _settings.database connection_key = f"mongodb://{user}@{host}:{port}/{database}" # NOSONAR + # Build config from settings, allowing partial overrides + _cfg = config or PersistentConnectionConfig() + config = PersistentConnectionConfig( + idle_timeout=_cfg.idle_timeout if _cfg.idle_timeout is not None else _settings.persistent_idle_timeout, + health_check_interval=( + _cfg.health_check_interval + if _cfg.health_check_interval is not None + else _settings.persistent_health_check_interval + ), + auto_reconnect=( + _cfg.auto_reconnect if _cfg.auto_reconnect is not None else _settings.persistent_auto_reconnect + ), + ) + with _registry_lock: if connection_key in _persistent_connections: return cast(PersistentMongoDBConnection, cast(object, _persistent_connections[connection_key])) @@ -946,7 +1236,9 @@ def __new__( connection_url=connection_url, database=database, config=config, - retry_config=retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, + logger=logger, ) _persistent_connections[connection_key] = conn diff --git a/ddcDatabases/core/settings.py b/ddcDatabases/core/settings.py index 6db1bc7..41d2886 100644 --- a/ddcDatabases/core/settings.py +++ b/ddcDatabases/core/settings.py @@ -44,17 +44,17 @@ class SQLiteSettings(_BaseDBSettings): echo: bool = Field(default=False, description=Msg.ECHO_DESCRIPTION) # Connection Retry settings (minimal for file-based database) - conn_enable_retry: bool = Field(default=False, description=Msg.ENABLE_RETRY_DESCRIPTION) - conn_max_retries: int = Field(default=1, description=Msg.MAX_RETRIES_DESCRIPTION) - conn_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - conn_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + connection_enable_retry: bool = Field(default=False, description=Msg.ENABLE_RETRY_DESCRIPTION) + connection_max_retries: int = Field(default=1, description=Msg.MAX_RETRIES_DESCRIPTION) + connection_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + connection_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) # Operation Retry settings - op_enable_retry: bool = Field(default=False, description=Msg.ENABLE_RETRY_DESCRIPTION) - op_max_retries: int = Field(default=1, description=Msg.MAX_RETRIES_DESCRIPTION) - op_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - op_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - op_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + operation_enable_retry: bool = Field(default=False, description=Msg.ENABLE_RETRY_DESCRIPTION) + operation_max_retries: int = Field(default=1, description=Msg.MAX_RETRIES_DESCRIPTION) + operation_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + operation_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + operation_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) model_config = SettingsConfigDict(env_prefix="SQLITE_") @@ -87,18 +87,25 @@ class PostgreSQLSettings(_BaseDBSettings): ssl_client_key_path: str | None = Field(default=None, description=Msg.SSL_CLIENT_KEY_PATH_DESCRIPTION) # Connection Retry settings - conn_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - conn_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - conn_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - conn_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - conn_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) + connection_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + connection_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + connection_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + connection_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + connection_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) # Operation Retry settings - op_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - op_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - op_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - op_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - op_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + operation_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + operation_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + operation_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + operation_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + operation_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + + # Persistent connection settings + persistent_idle_timeout: int = Field(default=300, description=Msg.PERSISTENT_IDLE_TIMEOUT_DESCRIPTION) + persistent_health_check_interval: int = Field( + default=30, description=Msg.PERSISTENT_HEALTH_CHECK_INTERVAL_DESCRIPTION + ) + persistent_auto_reconnect: bool = Field(default=True, description=Msg.PERSISTENT_AUTO_RECONNECT_DESCRIPTION) model_config = SettingsConfigDict(env_prefix="POSTGRESQL_") @@ -131,18 +138,25 @@ class MSSQLSettings(_BaseDBSettings): ssl_ca_cert_path: str | None = Field(default=None, description=Msg.SSL_CA_CERT_PATH_DESCRIPTION) # Connection Retry settings - conn_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - conn_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - conn_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - conn_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - conn_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) + connection_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + connection_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + connection_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + connection_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + connection_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) # Operation Retry settings - op_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - op_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - op_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - op_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - op_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + operation_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + operation_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + operation_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + operation_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + operation_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + + # Persistent connection settings + persistent_idle_timeout: int = Field(default=300, description=Msg.PERSISTENT_IDLE_TIMEOUT_DESCRIPTION) + persistent_health_check_interval: int = Field( + default=30, description=Msg.PERSISTENT_HEALTH_CHECK_INTERVAL_DESCRIPTION + ) + persistent_auto_reconnect: bool = Field(default=True, description=Msg.PERSISTENT_AUTO_RECONNECT_DESCRIPTION) model_config = SettingsConfigDict(env_prefix="MSSQL_") @@ -174,18 +188,25 @@ class MySQLSettings(_BaseDBSettings): ssl_client_key_path: str | None = Field(default=None, description=Msg.SSL_CLIENT_KEY_PATH_DESCRIPTION) # Connection Retry settings - conn_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - conn_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - conn_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - conn_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - conn_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) + connection_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + connection_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + connection_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + connection_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + connection_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) # Operation Retry settings - op_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - op_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - op_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - op_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - op_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + operation_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + operation_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + operation_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + operation_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + operation_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + + # Persistent connection settings + persistent_idle_timeout: int = Field(default=300, description=Msg.PERSISTENT_IDLE_TIMEOUT_DESCRIPTION) + persistent_health_check_interval: int = Field( + default=30, description=Msg.PERSISTENT_HEALTH_CHECK_INTERVAL_DESCRIPTION + ) + persistent_auto_reconnect: bool = Field(default=True, description=Msg.PERSISTENT_AUTO_RECONNECT_DESCRIPTION) model_config = SettingsConfigDict(env_prefix="MYSQL_") @@ -212,18 +233,25 @@ class MongoDBSettings(_BaseDBSettings): ) # Connection Retry settings - conn_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - conn_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - conn_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - conn_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - conn_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) + connection_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + connection_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + connection_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + connection_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + connection_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) # Operation Retry settings - op_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - op_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - op_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - op_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - op_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + operation_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + operation_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + operation_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + operation_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + operation_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + + # Persistent connection settings + persistent_idle_timeout: int = Field(default=300, description=Msg.PERSISTENT_IDLE_TIMEOUT_DESCRIPTION) + persistent_health_check_interval: int = Field( + default=30, description=Msg.PERSISTENT_HEALTH_CHECK_INTERVAL_DESCRIPTION + ) + persistent_auto_reconnect: bool = Field(default=True, description=Msg.PERSISTENT_AUTO_RECONNECT_DESCRIPTION) model_config = SettingsConfigDict(env_prefix="MONGODB_") @@ -252,18 +280,25 @@ class OracleSettings(_BaseDBSettings): ssl_wallet_path: str | None = Field(default=None, description=Msg.SSL_WALLET_PATH_DESCRIPTION) # Connection Retry settings - conn_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - conn_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - conn_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - conn_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - conn_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) + connection_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + connection_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + connection_initial_retry_delay: float = Field(default=1.0, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + connection_max_retry_delay: float = Field(default=30.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + connection_disconnect_idle_timeout: int = Field(default=300, description=Msg.DISCONNECT_IDLE_TIMEOUT_DESCRIPTION) # Operation Retry settings - op_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) - op_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) - op_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) - op_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) - op_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + operation_enable_retry: bool = Field(default=True, description=Msg.ENABLE_RETRY_DESCRIPTION) + operation_max_retries: int = Field(default=3, description=Msg.MAX_RETRIES_DESCRIPTION) + operation_initial_retry_delay: float = Field(default=0.5, description=Msg.INITIAL_RETRY_DELAY_DESCRIPTION) + operation_max_retry_delay: float = Field(default=10.0, description=Msg.MAX_RETRY_DELAY_DESCRIPTION) + operation_jitter: float = Field(default=0.1, description=Msg.JITTER_DESCRIPTION) + + # Persistent connection settings + persistent_idle_timeout: int = Field(default=300, description=Msg.PERSISTENT_IDLE_TIMEOUT_DESCRIPTION) + persistent_health_check_interval: int = Field( + default=30, description=Msg.PERSISTENT_HEALTH_CHECK_INTERVAL_DESCRIPTION + ) + persistent_auto_reconnect: bool = Field(default=True, description=Msg.PERSISTENT_AUTO_RECONNECT_DESCRIPTION) model_config = SettingsConfigDict(env_prefix="ORACLE_") diff --git a/ddcDatabases/mongodb.py b/ddcDatabases/mongodb.py index 7a44f9c..b7cc230 100644 --- a/ddcDatabases/mongodb.py +++ b/ddcDatabases/mongodb.py @@ -38,12 +38,12 @@ class MongoDBQueryConfig: @dataclass(frozen=True, slots=True) -class MongoDBConnRetryConfig(BaseRetryConfig): +class MongoDBConnectionRetryConfig(BaseRetryConfig): pass @dataclass(frozen=True, slots=True) -class MongoDBOpRetryConfig(BaseOperationRetryConfig): +class MongoDBOperationRetryConfig(BaseOperationRetryConfig): pass @@ -61,8 +61,8 @@ def __init__( database: str | None = None, collection: str | None = None, query_config: MongoDBQueryConfig | None = None, - conn_retry_config: MongoDBConnRetryConfig | None = None, - op_retry_config: MongoDBOpRetryConfig | None = None, + connection_retry_config: MongoDBConnectionRetryConfig | None = None, + operation_retry_config: MongoDBOperationRetryConfig | None = None, tls_config: MongoDBTLSConfig | None = None, logger: Any = None, ) -> None: @@ -108,28 +108,34 @@ def __init__( self.async_cursor_ref = None # Create connection retry configuration - _crc = conn_retry_config or MongoDBConnRetryConfig() - self._conn_retry_config = MongoDBConnRetryConfig( - enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.conn_enable_retry, - max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.conn_max_retries, + _crc = connection_retry_config or MongoDBConnectionRetryConfig() + self._connection_retry_config = MongoDBConnectionRetryConfig( + enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.connection_enable_retry, + max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.connection_max_retries, initial_retry_delay=( - _crc.initial_retry_delay if _crc.initial_retry_delay is not None else _settings.conn_initial_retry_delay + _crc.initial_retry_delay + if _crc.initial_retry_delay is not None + else _settings.connection_initial_retry_delay ), max_retry_delay=( - _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.conn_max_retry_delay + _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.connection_max_retry_delay ), ) # Create operation retry configuration - _orc = op_retry_config or MongoDBOpRetryConfig() - self._op_retry_config = MongoDBOpRetryConfig( - enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.op_enable_retry, - max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.op_max_retries, + _orc = operation_retry_config or MongoDBOperationRetryConfig() + self._operation_retry_config = MongoDBOperationRetryConfig( + enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.operation_enable_retry, + max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.operation_max_retries, initial_retry_delay=( - _orc.initial_retry_delay if _orc.initial_retry_delay is not None else _settings.op_initial_retry_delay + _orc.initial_retry_delay + if _orc.initial_retry_delay is not None + else _settings.operation_initial_retry_delay ), - max_retry_delay=_orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.op_max_retry_delay, - jitter=_orc.jitter if _orc.jitter is not None else _settings.op_jitter, + max_retry_delay=( + _orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.operation_max_retry_delay + ), + jitter=_orc.jitter if _orc.jitter is not None else _settings.operation_jitter, ) self.logger = logger if logger is not None else _logger @@ -162,11 +168,11 @@ def get_connection_info(self) -> MongoDBConnectionConfig: def get_query_info(self) -> MongoDBQueryConfig: return self._query_config - def get_conn_retry_info(self) -> MongoDBConnRetryConfig: - return self._conn_retry_config + def get_connection_retry_info(self) -> MongoDBConnectionRetryConfig: + return self._connection_retry_config - def get_op_retry_info(self) -> MongoDBOpRetryConfig: - return self._op_retry_config + def get_operation_retry_info(self) -> MongoDBOperationRetryConfig: + return self._operation_retry_config def get_tls_info(self) -> MongoDBTLSConfig: return self._tls_config @@ -206,7 +212,7 @@ def connect() -> Cursor: raise try: - return retry_operation(connect, self._conn_retry_config, "mongodb_connect", logger=self.logger) + return retry_operation(connect, self._connection_retry_config, "mongodb_connect", logger=self.logger) except (PyMongoError, ConnectionError, RuntimeError, ValueError, TypeError): sys.exit(1) @@ -281,7 +287,7 @@ async def connect() -> AsyncIOMotorCursor: try: return await retry_operation_async( - connect, self._conn_retry_config, "mongodb_async_connect", logger=self.logger + connect, self._connection_retry_config, "mongodb_async_connect", logger=self.logger ) except (PyMongoError, ConnectionError, RuntimeError, ValueError, TypeError): sys.exit(1) diff --git a/ddcDatabases/mssql.py b/ddcDatabases/mssql.py index f175f8d..dac17ee 100755 --- a/ddcDatabases/mssql.py +++ b/ddcDatabases/mssql.py @@ -44,12 +44,12 @@ class MSSQLSessionConfig(BaseSessionConfig): @dataclass(frozen=True, slots=True) -class MSSQLConnRetryConfig(BaseRetryConfig): +class MSSQLConnectionRetryConfig(BaseRetryConfig): pass @dataclass(frozen=True, slots=True) -class MSSQLOpRetryConfig(BaseOperationRetryConfig): +class MSSQLOperationRetryConfig(BaseOperationRetryConfig): pass @@ -68,8 +68,8 @@ def __init__( schema: str | None = None, pool_config: MSSQLPoolConfig | None = None, session_config: MSSQLSessionConfig | None = None, - conn_retry_config: MSSQLConnRetryConfig | None = None, - op_retry_config: MSSQLOpRetryConfig | None = None, + connection_retry_config: MSSQLConnectionRetryConfig | None = None, + operation_retry_config: MSSQLOperationRetryConfig | None = None, ssl_config: MSSQLSSLConfig | None = None, extra_engine_args: dict | None = None, logger: Any = None, @@ -147,28 +147,34 @@ def __init__( } # Create connection retry configuration - _crc = conn_retry_config or MSSQLConnRetryConfig() - self._conn_retry_config = MSSQLConnRetryConfig( - enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.conn_enable_retry, - max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.conn_max_retries, + _crc = connection_retry_config or MSSQLConnectionRetryConfig() + self._connection_retry_config = MSSQLConnectionRetryConfig( + enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.connection_enable_retry, + max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.connection_max_retries, initial_retry_delay=( - _crc.initial_retry_delay if _crc.initial_retry_delay is not None else _settings.conn_initial_retry_delay + _crc.initial_retry_delay + if _crc.initial_retry_delay is not None + else _settings.connection_initial_retry_delay ), max_retry_delay=( - _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.conn_max_retry_delay + _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.connection_max_retry_delay ), ) # Create operation retry configuration - _orc = op_retry_config or MSSQLOpRetryConfig() - self._op_retry_config = MSSQLOpRetryConfig( - enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.op_enable_retry, - max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.op_max_retries, + _orc = operation_retry_config or MSSQLOperationRetryConfig() + self._operation_retry_config = MSSQLOperationRetryConfig( + enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.operation_enable_retry, + max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.operation_max_retries, initial_retry_delay=( - _orc.initial_retry_delay if _orc.initial_retry_delay is not None else _settings.op_initial_retry_delay + _orc.initial_retry_delay + if _orc.initial_retry_delay is not None + else _settings.operation_initial_retry_delay ), - max_retry_delay=_orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.op_max_retry_delay, - jitter=_orc.jitter if _orc.jitter is not None else _settings.op_jitter, + max_retry_delay=( + _orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.operation_max_retry_delay + ), + jitter=_orc.jitter if _orc.jitter is not None else _settings.operation_jitter, ) self.logger = logger if logger is not None else _logger @@ -180,8 +186,8 @@ def __init__( expire_on_commit=self._session_config.expire_on_commit, sync_driver=self.sync_driver, async_driver=self.async_driver, - conn_retry_config=self._conn_retry_config, - op_retry_config=self._op_retry_config, + connection_retry_config=self._connection_retry_config, + operation_retry_config=self._operation_retry_config, logger=self.logger, ) @@ -211,11 +217,11 @@ def get_pool_info(self) -> MSSQLPoolConfig: def get_session_info(self) -> MSSQLSessionConfig: return self._session_config - def get_conn_retry_info(self) -> MSSQLConnRetryConfig: - return self._conn_retry_config + def get_connection_retry_info(self) -> MSSQLConnectionRetryConfig: + return self._connection_retry_config - def get_op_retry_info(self) -> MSSQLOpRetryConfig: - return self._op_retry_config + def get_operation_retry_info(self) -> MSSQLOperationRetryConfig: + return self._operation_retry_config def get_ssl_info(self) -> MSSQLSSLConfig: return self._ssl_config diff --git a/ddcDatabases/mysql.py b/ddcDatabases/mysql.py index 698287a..51308ef 100755 --- a/ddcDatabases/mysql.py +++ b/ddcDatabases/mysql.py @@ -43,12 +43,12 @@ class MySQLSessionConfig(BaseSessionConfig): @dataclass(frozen=True, slots=True) -class MySQLConnRetryConfig(BaseRetryConfig): +class MySQLConnectionRetryConfig(BaseRetryConfig): pass @dataclass(frozen=True, slots=True) -class MySQLOpRetryConfig(BaseOperationRetryConfig): +class MySQLOperationRetryConfig(BaseOperationRetryConfig): pass @@ -66,8 +66,8 @@ def __init__( database: str | None = None, pool_config: MySQLPoolConfig | None = None, session_config: MySQLSessionConfig | None = None, - conn_retry_config: MySQLConnRetryConfig | None = None, - op_retry_config: MySQLOpRetryConfig | None = None, + connection_retry_config: MySQLConnectionRetryConfig | None = None, + operation_retry_config: MySQLOperationRetryConfig | None = None, ssl_config: MySQLSSLConfig | None = None, extra_engine_args: dict | None = None, logger: Any = None, @@ -155,28 +155,34 @@ def __init__( } # Create connection retry configuration - _crc = conn_retry_config or MySQLConnRetryConfig() - self._conn_retry_config = MySQLConnRetryConfig( - enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.conn_enable_retry, - max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.conn_max_retries, + _crc = connection_retry_config or MySQLConnectionRetryConfig() + self._connection_retry_config = MySQLConnectionRetryConfig( + enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.connection_enable_retry, + max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.connection_max_retries, initial_retry_delay=( - _crc.initial_retry_delay if _crc.initial_retry_delay is not None else _settings.conn_initial_retry_delay + _crc.initial_retry_delay + if _crc.initial_retry_delay is not None + else _settings.connection_initial_retry_delay ), max_retry_delay=( - _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.conn_max_retry_delay + _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.connection_max_retry_delay ), ) # Create operation retry configuration - _orc = op_retry_config or MySQLOpRetryConfig() - self._op_retry_config = MySQLOpRetryConfig( - enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.op_enable_retry, - max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.op_max_retries, + _orc = operation_retry_config or MySQLOperationRetryConfig() + self._operation_retry_config = MySQLOperationRetryConfig( + enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.operation_enable_retry, + max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.operation_max_retries, initial_retry_delay=( - _orc.initial_retry_delay if _orc.initial_retry_delay is not None else _settings.op_initial_retry_delay + _orc.initial_retry_delay + if _orc.initial_retry_delay is not None + else _settings.operation_initial_retry_delay ), - max_retry_delay=_orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.op_max_retry_delay, - jitter=_orc.jitter if _orc.jitter is not None else _settings.op_jitter, + max_retry_delay=( + _orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.operation_max_retry_delay + ), + jitter=_orc.jitter if _orc.jitter is not None else _settings.operation_jitter, ) self.logger = logger if logger is not None else _logger @@ -188,8 +194,8 @@ def __init__( expire_on_commit=self._session_config.expire_on_commit, sync_driver=self.sync_driver, async_driver=self.async_driver, - conn_retry_config=self._conn_retry_config, - op_retry_config=self._op_retry_config, + connection_retry_config=self._connection_retry_config, + operation_retry_config=self._operation_retry_config, logger=self.logger, ) @@ -219,11 +225,11 @@ def get_pool_info(self) -> MySQLPoolConfig: def get_session_info(self) -> MySQLSessionConfig: return self._session_config - def get_conn_retry_info(self) -> MySQLConnRetryConfig: - return self._conn_retry_config + def get_connection_retry_info(self) -> MySQLConnectionRetryConfig: + return self._connection_retry_config - def get_op_retry_info(self) -> MySQLOpRetryConfig: - return self._op_retry_config + def get_operation_retry_info(self) -> MySQLOperationRetryConfig: + return self._operation_retry_config def get_ssl_info(self) -> MySQLSSLConfig: return self._ssl_config diff --git a/ddcDatabases/oracle.py b/ddcDatabases/oracle.py index e973dd0..435d7ab 100644 --- a/ddcDatabases/oracle.py +++ b/ddcDatabases/oracle.py @@ -40,12 +40,12 @@ class OracleSessionConfig(BaseSessionConfig): @dataclass(frozen=True, slots=True) -class OracleConnRetryConfig(BaseRetryConfig): +class OracleConnectionRetryConfig(BaseRetryConfig): pass @dataclass(frozen=True, slots=True) -class OracleOpRetryConfig(BaseOperationRetryConfig): +class OracleOperationRetryConfig(BaseOperationRetryConfig): pass @@ -63,8 +63,8 @@ def __init__( servicename: str | None = None, pool_config: OraclePoolConfig | None = None, session_config: OracleSessionConfig | None = None, - conn_retry_config: OracleConnRetryConfig | None = None, - op_retry_config: OracleOpRetryConfig | None = None, + connection_retry_config: OracleConnectionRetryConfig | None = None, + operation_retry_config: OracleOperationRetryConfig | None = None, ssl_config: OracleSSLConfig | None = None, extra_engine_args: dict | None = None, logger: Any = None, @@ -130,28 +130,34 @@ def __init__( } # Create connection retry configuration - _crc = conn_retry_config or OracleConnRetryConfig() - self._conn_retry_config = OracleConnRetryConfig( - enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.conn_enable_retry, - max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.conn_max_retries, + _crc = connection_retry_config or OracleConnectionRetryConfig() + self._connection_retry_config = OracleConnectionRetryConfig( + enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.connection_enable_retry, + max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.connection_max_retries, initial_retry_delay=( - _crc.initial_retry_delay if _crc.initial_retry_delay is not None else _settings.conn_initial_retry_delay + _crc.initial_retry_delay + if _crc.initial_retry_delay is not None + else _settings.connection_initial_retry_delay ), max_retry_delay=( - _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.conn_max_retry_delay + _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.connection_max_retry_delay ), ) # Create operation retry configuration - _orc = op_retry_config or OracleOpRetryConfig() - self._op_retry_config = OracleOpRetryConfig( - enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.op_enable_retry, - max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.op_max_retries, + _orc = operation_retry_config or OracleOperationRetryConfig() + self._operation_retry_config = OracleOperationRetryConfig( + enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.operation_enable_retry, + max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.operation_max_retries, initial_retry_delay=( - _orc.initial_retry_delay if _orc.initial_retry_delay is not None else _settings.op_initial_retry_delay + _orc.initial_retry_delay + if _orc.initial_retry_delay is not None + else _settings.operation_initial_retry_delay ), - max_retry_delay=_orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.op_max_retry_delay, - jitter=_orc.jitter if _orc.jitter is not None else _settings.op_jitter, + max_retry_delay=( + _orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.operation_max_retry_delay + ), + jitter=_orc.jitter if _orc.jitter is not None else _settings.operation_jitter, ) self.logger = logger if logger is not None else _logger @@ -163,8 +169,8 @@ def __init__( expire_on_commit=self._session_config.expire_on_commit, sync_driver=self.sync_driver, async_driver=None, - conn_retry_config=self._conn_retry_config, - op_retry_config=self._op_retry_config, + connection_retry_config=self._connection_retry_config, + operation_retry_config=self._operation_retry_config, logger=self.logger, ) @@ -194,11 +200,11 @@ def get_pool_info(self) -> OraclePoolConfig: def get_session_info(self) -> OracleSessionConfig: return self._session_config - def get_conn_retry_info(self) -> OracleConnRetryConfig: - return self._conn_retry_config + def get_connection_retry_info(self) -> OracleConnectionRetryConfig: + return self._connection_retry_config - def get_op_retry_info(self) -> OracleOpRetryConfig: - return self._op_retry_config + def get_operation_retry_info(self) -> OracleOperationRetryConfig: + return self._operation_retry_config def get_ssl_info(self) -> OracleSSLConfig: return self._ssl_config diff --git a/ddcDatabases/postgresql.py b/ddcDatabases/postgresql.py index 30130f7..e0cf0b1 100755 --- a/ddcDatabases/postgresql.py +++ b/ddcDatabases/postgresql.py @@ -45,12 +45,12 @@ class PostgreSQLSessionConfig(BaseSessionConfig): @dataclass(frozen=True, slots=True) -class PostgreSQLConnRetryConfig(BaseRetryConfig): +class PostgreSQLConnectionRetryConfig(BaseRetryConfig): pass @dataclass(frozen=True, slots=True) -class PostgreSQLOpRetryConfig(BaseOperationRetryConfig): +class PostgreSQLOperationRetryConfig(BaseOperationRetryConfig): pass @@ -69,8 +69,8 @@ def __init__( schema: str | None = None, pool_config: PostgreSQLPoolConfig | None = None, session_config: PostgreSQLSessionConfig | None = None, - conn_retry_config: PostgreSQLConnRetryConfig | None = None, - op_retry_config: PostgreSQLOpRetryConfig | None = None, + connection_retry_config: PostgreSQLConnectionRetryConfig | None = None, + operation_retry_config: PostgreSQLOperationRetryConfig | None = None, ssl_config: PostgreSQLSSLConfig | None = None, extra_engine_args: dict | None = None, logger: Any = None, @@ -136,28 +136,34 @@ def __init__( } # Create connection retry configuration - _crc = conn_retry_config or PostgreSQLConnRetryConfig() - self._conn_retry_config = PostgreSQLConnRetryConfig( - enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.conn_enable_retry, - max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.conn_max_retries, + _crc = connection_retry_config or PostgreSQLConnectionRetryConfig() + self._connection_retry_config = PostgreSQLConnectionRetryConfig( + enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.connection_enable_retry, + max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.connection_max_retries, initial_retry_delay=( - _crc.initial_retry_delay if _crc.initial_retry_delay is not None else _settings.conn_initial_retry_delay + _crc.initial_retry_delay + if _crc.initial_retry_delay is not None + else _settings.connection_initial_retry_delay ), max_retry_delay=( - _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.conn_max_retry_delay + _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.connection_max_retry_delay ), ) # Create operation retry configuration - _orc = op_retry_config or PostgreSQLOpRetryConfig() - self._op_retry_config = PostgreSQLOpRetryConfig( - enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.op_enable_retry, - max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.op_max_retries, + _orc = operation_retry_config or PostgreSQLOperationRetryConfig() + self._operation_retry_config = PostgreSQLOperationRetryConfig( + enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.operation_enable_retry, + max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.operation_max_retries, initial_retry_delay=( - _orc.initial_retry_delay if _orc.initial_retry_delay is not None else _settings.op_initial_retry_delay + _orc.initial_retry_delay + if _orc.initial_retry_delay is not None + else _settings.operation_initial_retry_delay ), - max_retry_delay=_orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.op_max_retry_delay, - jitter=_orc.jitter if _orc.jitter is not None else _settings.op_jitter, + max_retry_delay=( + _orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.operation_max_retry_delay + ), + jitter=_orc.jitter if _orc.jitter is not None else _settings.operation_jitter, ) self.logger = logger if logger is not None else _logger @@ -169,8 +175,8 @@ def __init__( expire_on_commit=self._session_config.expire_on_commit, sync_driver=self.sync_driver, async_driver=self.async_driver, - conn_retry_config=self._conn_retry_config, - op_retry_config=self._op_retry_config, + connection_retry_config=self._connection_retry_config, + operation_retry_config=self._operation_retry_config, logger=self.logger, ) @@ -200,11 +206,11 @@ def get_pool_info(self) -> PostgreSQLPoolConfig: def get_session_info(self) -> PostgreSQLSessionConfig: return self._session_config - def get_conn_retry_info(self) -> PostgreSQLConnRetryConfig: - return self._conn_retry_config + def get_connection_retry_info(self) -> PostgreSQLConnectionRetryConfig: + return self._connection_retry_config - def get_op_retry_info(self) -> PostgreSQLOpRetryConfig: - return self._op_retry_config + def get_operation_retry_info(self) -> PostgreSQLOperationRetryConfig: + return self._operation_retry_config def get_ssl_info(self) -> PostgreSQLSSLConfig: return self._ssl_config diff --git a/ddcDatabases/sqlite.py b/ddcDatabases/sqlite.py index f22b382..054a692 100755 --- a/ddcDatabases/sqlite.py +++ b/ddcDatabases/sqlite.py @@ -17,12 +17,12 @@ class SqliteSessionConfig(BaseSessionConfig): @dataclass(frozen=True, slots=True) -class SqliteConnRetryConfig(BaseRetryConfig): +class SqliteConnectionRetryConfig(BaseRetryConfig): pass @dataclass(frozen=True, slots=True) -class SqliteOpRetryConfig(BaseOperationRetryConfig): +class SqliteOperationRetryConfig(BaseOperationRetryConfig): pass @@ -36,8 +36,8 @@ def __init__( filepath: str | None = None, echo: bool | None = None, session_config: SqliteSessionConfig | None = None, - conn_retry_config: SqliteConnRetryConfig | None = None, - op_retry_config: SqliteOpRetryConfig | None = None, + connection_retry_config: SqliteConnectionRetryConfig | None = None, + operation_retry_config: SqliteOperationRetryConfig | None = None, extra_engine_args: dict[str, Any] | None = None, logger: Any = None, ) -> None: @@ -60,28 +60,34 @@ def __init__( self._temp_engine: Engine | None = None # Create connection retry configuration - _crc = conn_retry_config or SqliteConnRetryConfig() - self._conn_retry_config = SqliteConnRetryConfig( - enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.conn_enable_retry, - max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.conn_max_retries, + _crc = connection_retry_config or SqliteConnectionRetryConfig() + self._connection_retry_config = SqliteConnectionRetryConfig( + enable_retry=_crc.enable_retry if _crc.enable_retry is not None else _settings.connection_enable_retry, + max_retries=_crc.max_retries if _crc.max_retries is not None else _settings.connection_max_retries, initial_retry_delay=( - _crc.initial_retry_delay if _crc.initial_retry_delay is not None else _settings.conn_initial_retry_delay + _crc.initial_retry_delay + if _crc.initial_retry_delay is not None + else _settings.connection_initial_retry_delay ), max_retry_delay=( - _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.conn_max_retry_delay + _crc.max_retry_delay if _crc.max_retry_delay is not None else _settings.connection_max_retry_delay ), ) # Create operation retry configuration - _orc = op_retry_config or SqliteOpRetryConfig() - self._op_retry_config = SqliteOpRetryConfig( - enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.op_enable_retry, - max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.op_max_retries, + _orc = operation_retry_config or SqliteOperationRetryConfig() + self._operation_retry_config = SqliteOperationRetryConfig( + enable_retry=_orc.enable_retry if _orc.enable_retry is not None else _settings.operation_enable_retry, + max_retries=_orc.max_retries if _orc.max_retries is not None else _settings.operation_max_retries, initial_retry_delay=( - _orc.initial_retry_delay if _orc.initial_retry_delay is not None else _settings.op_initial_retry_delay + _orc.initial_retry_delay + if _orc.initial_retry_delay is not None + else _settings.operation_initial_retry_delay ), - max_retry_delay=_orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.op_max_retry_delay, - jitter=_orc.jitter if _orc.jitter is not None else _settings.op_jitter, + max_retry_delay=( + _orc.max_retry_delay if _orc.max_retry_delay is not None else _settings.operation_max_retry_delay + ), + jitter=_orc.jitter if _orc.jitter is not None else _settings.operation_jitter, ) self.logger = logger if logger is not None else _logger @@ -91,13 +97,13 @@ def get_session_info(self) -> SqliteSessionConfig: """Get immutable session configuration.""" return self._session_config - def get_conn_retry_info(self) -> SqliteConnRetryConfig: + def get_connection_retry_info(self) -> SqliteConnectionRetryConfig: """Get immutable connection retry configuration.""" - return self._conn_retry_config + return self._connection_retry_config - def get_op_retry_info(self) -> SqliteOpRetryConfig: + def get_operation_retry_info(self) -> SqliteOperationRetryConfig: """Get immutable operation retry configuration.""" - return self._op_retry_config + return self._operation_retry_config def __enter__(self) -> Session: def connect() -> Session: @@ -123,7 +129,7 @@ def connect() -> Session: self.logger.info(f"Connected to {self.filepath}") return self.session - return retry_operation(connect, self._conn_retry_config, "sqlite_connect", logger=self.logger) + return retry_operation(connect, self._connection_retry_config, "sqlite_connect", logger=self.logger) def __exit__( self, diff --git a/pyproject.toml b/pyproject.toml index 21682ad..aca954e 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ dev = [ {include-group = "test"}, "black>=26.1.0", "poethepoet>=0.40.0", - "ruff>=0.14.14", + "ruff>=0.15.0", ] [tool.hatch.build] @@ -91,6 +91,10 @@ test-integration = "uv run pytest tests/integration --no-cov" addopts = "-v --cov --cov-report=term --cov-report=xml --junitxml=junit.xml" junit_family = "legacy" testpaths = ["tests/unit"] +pythonpath = ["."] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" markers = [ diff --git a/tests/integration/core/test_persistent_multiple.py b/tests/integration/core/test_persistent_multiple.py new file mode 100644 index 0000000..2aebed3 --- /dev/null +++ b/tests/integration/core/test_persistent_multiple.py @@ -0,0 +1,74 @@ +"""Integration tests for close_all_persistent_connections with multiple database types.""" + +import pytest +import sqlalchemy as sa + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + MySQLPersistent, + PostgreSQLPersistent, + close_all_persistent_connections, +) + +pytestmark = pytest.mark.integration + + +class TestCloseAllPersistentConnectionsIntegration: + """Integration tests for close_all_persistent_connections.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_close_all_with_active_connections(self, postgres_container, mysql_container): + """Test close_all properly closes active connections.""" + pg_port = postgres_container.get_exposed_port(5432) + pg_host = postgres_container.get_container_host_ip() + mysql_port = mysql_container.get_exposed_port(3306) + mysql_host = mysql_container.get_container_host_ip() + + pg_conn = PostgreSQLPersistent( + host=pg_host, + port=int(pg_port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=False, + ) + mysql_conn = MySQLPersistent( + host=mysql_host, + port=int(mysql_port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + async_mode=False, + ) + + # Establish connections + with pg_conn as pg_session: + pg_session.execute(sa.text("SELECT 1")) + with mysql_conn as mysql_session: + mysql_session.execute(sa.text("SELECT 1")) + + # Close all + close_all_persistent_connections() + + # Connections should no longer be active + # Getting new instances should create fresh connections + pg_conn_new = PostgreSQLPersistent( + host=pg_host, + port=int(pg_port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=False, + ) + + # Should be a new instance after close_all + assert not pg_conn_new.is_connected + + pg_conn_new.shutdown() diff --git a/tests/integration/test_mariadb.py b/tests/integration/mariadb/test_mariadb.py similarity index 100% rename from tests/integration/test_mariadb.py rename to tests/integration/mariadb/test_mariadb.py diff --git a/tests/integration/mariadb/test_mariadb_persistent.py b/tests/integration/mariadb/test_mariadb_persistent.py new file mode 100644 index 0000000..4465803 --- /dev/null +++ b/tests/integration/mariadb/test_mariadb_persistent.py @@ -0,0 +1,126 @@ +"""Integration tests for MariaDB persistent connections. + +Note: MariaDB uses MySQL driver and persistent connections, these tests verify +the MariaDB aliases work correctly. +""" + +import pytest +import sqlalchemy as sa + +# noinspection PyProtectedMember +from ddcDatabases import MariaDBPersistent +from ddcDatabases.core.persistent import close_all_persistent_connections +from tests.integration.conftest import Base, IntegrationModel + +pytestmark = pytest.mark.integration + + +class TestMariaDBPersistentIntegration: + """Integration tests for MariaDB persistent connections.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_persistent_connection(self, mariadb_container): + """Test synchronous MariaDB persistent connection.""" + port = mariadb_container.get_exposed_port(3306) + host = mariadb_container.get_container_host_ip() + + conn = MariaDBPersistent( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + async_mode=False, + ) + + with conn as session: + # Create tables + Base.metadata.create_all(session.bind) + + # Insert + stmt = sa.insert(IntegrationModel).values(name="mariadb_persistent_test", enabled=True) + session.execute(stmt) + session.commit() + + # Verify + stmt = sa.select(IntegrationModel.name).where(IntegrationModel.name == "mariadb_persistent_test") + result = session.execute(stmt).scalar() + assert result == "mariadb_persistent_test" + + # Cleanup + stmt = sa.delete(IntegrationModel).where(IntegrationModel.name == "mariadb_persistent_test") + session.execute(stmt) + session.commit() + + conn.shutdown() + + def test_persistent_connection_singleton(self, mariadb_container): + """Test MariaDB persistent connection singleton pattern.""" + port = mariadb_container.get_exposed_port(3306) + host = mariadb_container.get_container_host_ip() + + conn1 = MariaDBPersistent( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + async_mode=False, + ) + conn2 = MariaDBPersistent( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + async_mode=False, + ) + + assert conn1 is conn2 + + conn1.shutdown() + + def test_persistent_connection_multiple_operations(self, mariadb_container): + """Test multiple operations on MariaDB persistent connection.""" + port = mariadb_container.get_exposed_port(3306) + host = mariadb_container.get_container_host_ip() + + conn = MariaDBPersistent( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + async_mode=False, + ) + + # First use + with conn as session: + Base.metadata.create_all(session.bind) + session.execute(sa.insert(IntegrationModel).values(name="mariadb_op1", enabled=True)) + session.commit() + + # Second use + with conn as session: + session.execute(sa.insert(IntegrationModel).values(name="mariadb_op2", enabled=False)) + session.commit() + + # Third use - verify both records exist + with conn as session: + stmt = sa.select(IntegrationModel.name).order_by(IntegrationModel.name) + results = session.execute(stmt).scalars().all() + assert "mariadb_op1" in results + assert "mariadb_op2" in results + + # Cleanup + session.execute(sa.delete(IntegrationModel)) + session.commit() + + conn.shutdown() diff --git a/tests/integration/mariadb/test_mariadb_ssl.py b/tests/integration/mariadb/test_mariadb_ssl.py new file mode 100644 index 0000000..aba0006 --- /dev/null +++ b/tests/integration/mariadb/test_mariadb_ssl.py @@ -0,0 +1,93 @@ +"""Integration tests for MariaDB SSL configuration. + +Note: MariaDB uses MySQL driver and SSL configuration, these tests verify +the MariaDB aliases work correctly. +""" + +import pytest +import sqlalchemy as sa + +pytestmark = pytest.mark.integration + + +class TestMariaDBSSLIntegration: + """Integration tests for MariaDB SSL configuration.""" + + def test_connection_with_ssl_disabled(self, mariadb_container): + """Test MariaDB connection with SSL explicitly disabled.""" + from ddcDatabases import MariaDB, MariaDBSSLConfig + + port = mariadb_container.get_exposed_port(3306) + host = mariadb_container.get_container_host_ip() + + with MariaDB( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + ssl_config=MariaDBSSLConfig(ssl_mode="DISABLED"), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_connection_with_ssl_preferred(self, mariadb_container): + """Test MariaDB connection with SSL preferred mode.""" + from ddcDatabases import MariaDB, MariaDBSSLConfig + + port = mariadb_container.get_exposed_port(3306) + host = mariadb_container.get_container_host_ip() + + with MariaDB( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + ssl_config=MariaDBSSLConfig(ssl_mode="PREFERRED"), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_ssl_config_is_accessible(self, mariadb_container): + """Test that MariaDB SSL config is accessible.""" + from ddcDatabases import MariaDB, MariaDBSSLConfig + + port = mariadb_container.get_exposed_port(3306) + host = mariadb_container.get_container_host_ip() + + mariadb = MariaDB( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + ssl_config=MariaDBSSLConfig( + ssl_mode="PREFERRED", + ssl_ca_cert_path="/path/to/ca.pem", + ), + ) + + ssl_info = mariadb.get_ssl_info() + assert ssl_info.ssl_mode == "PREFERRED" + assert ssl_info.ssl_ca_cert_path == "/path/to/ca.pem" + + def test_ssl_config_immutable(self, mariadb_container): + """Test that MariaDB SSL config is immutable.""" + from ddcDatabases import MariaDB, MariaDBSSLConfig + + port = mariadb_container.get_exposed_port(3306) + host = mariadb_container.get_container_host_ip() + + mariadb = MariaDB( + host=host, + port=int(port), + user=mariadb_container.username, + password=mariadb_container.password, + database=mariadb_container.dbname, + ssl_config=MariaDBSSLConfig(ssl_mode="DISABLED"), + ) + + ssl_info = mariadb.get_ssl_info() + with pytest.raises(AttributeError): + ssl_info.ssl_mode = "REQUIRED" # noqa diff --git a/tests/integration/test_mongodb.py b/tests/integration/mongodb/test_mongodb.py similarity index 100% rename from tests/integration/test_mongodb.py rename to tests/integration/mongodb/test_mongodb.py diff --git a/tests/integration/mongodb/test_mongodb_persistent.py b/tests/integration/mongodb/test_mongodb_persistent.py new file mode 100644 index 0000000..35b59a5 --- /dev/null +++ b/tests/integration/mongodb/test_mongodb_persistent.py @@ -0,0 +1,110 @@ +"""Integration tests for MongoDB persistent connections.""" + +import pytest + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + MongoDBPersistent, + close_all_persistent_connections, +) + +pytestmark = pytest.mark.integration + + +class TestMongoDBPersistentIntegration: + """Integration tests for MongoDB persistent connections.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_persistent_connection(self, mongodb_container): + """Test MongoDB persistent connection.""" + port = mongodb_container.get_exposed_port(27017) + host = mongodb_container.get_container_host_ip() + + conn = MongoDBPersistent( + host=host, + port=int(port), + user="test", + password="test", + database="admin", + ) + + with conn as db: + assert conn.is_connected + + # Insert a document + collection = db["persistent_test"] + collection.delete_many({}) # Clean up first + collection.insert_one({"name": "test_doc", "value": 42}) + + # Query + doc = collection.find_one({"name": "test_doc"}) + assert doc["value"] == 42 + + # Cleanup + collection.delete_many({}) + + conn.shutdown() + + def test_persistent_connection_singleton(self, mongodb_container): + """Test MongoDB persistent connection singleton pattern.""" + port = mongodb_container.get_exposed_port(27017) + host = mongodb_container.get_container_host_ip() + + conn1 = MongoDBPersistent( + host=host, + port=int(port), + user="test", + password="test", + database="admin", + ) + conn2 = MongoDBPersistent( + host=host, + port=int(port), + user="test", + password="test", + database="admin", + ) + + assert conn1 is conn2 + + conn1.shutdown() + + def test_persistent_connection_multiple_uses(self, mongodb_container): + """Test MongoDB persistent connection can be used multiple times.""" + port = mongodb_container.get_exposed_port(27017) + host = mongodb_container.get_container_host_ip() + + conn = MongoDBPersistent( + host=host, + port=int(port), + user="test", + password="test", + database="admin", + ) + + # First use + with conn as db: + collection = db["multi_use_test"] + collection.delete_many({}) + collection.insert_one({"iteration": 1}) + + # Second use + with conn as db: + collection = db["multi_use_test"] + collection.insert_one({"iteration": 2}) + + # Third use - verify + with conn as db: + collection = db["multi_use_test"] + count = collection.count_documents({}) + assert count == 2 + collection.delete_many({}) + + conn.shutdown() diff --git a/tests/integration/mongodb/test_mongodb_tls.py b/tests/integration/mongodb/test_mongodb_tls.py new file mode 100644 index 0000000..cd7616c --- /dev/null +++ b/tests/integration/mongodb/test_mongodb_tls.py @@ -0,0 +1,82 @@ +"""Integration tests for MongoDB TLS configuration.""" + +import pytest + +pytestmark = pytest.mark.integration + + +class TestMongoDBTLSIntegration: + """Integration tests for MongoDB TLS configuration.""" + + def test_connection_with_tls_disabled(self, mongodb_container): + """Test MongoDB connection with TLS disabled.""" + from ddcDatabases import MongoDB + from ddcDatabases.mongodb import MongoDBTLSConfig + + port = mongodb_container.get_exposed_port(27017) + host = f"{mongodb_container.get_container_host_ip()}:{port}" + + mongo = MongoDB( + host=host, + user="test", + password="test", + database="admin", + collection="tls_test", + tls_config=MongoDBTLSConfig(tls_enabled=False), + ) + + with mongo: + assert mongo.is_connected is True + # Simple operation to verify connection + db = mongo.client["admin"] + collection = db["tls_test"] + collection.delete_many({}) + collection.insert_one({"test": "value"}) + doc = collection.find_one({"test": "value"}) + assert doc["test"] == "value" + collection.delete_many({}) + + def test_tls_config_is_accessible(self, mongodb_container): + """Test that MongoDB TLS config is accessible.""" + from ddcDatabases import MongoDB + from ddcDatabases.mongodb import MongoDBTLSConfig + + port = mongodb_container.get_exposed_port(27017) + host = f"{mongodb_container.get_container_host_ip()}:{port}" + + mongo = MongoDB( + host=host, + user="test", + password="test", + database="admin", + collection="tls_config_test", + tls_config=MongoDBTLSConfig( + tls_enabled=False, + tls_ca_cert_path="/path/to/ca.pem", + ), + ) + + tls_info = mongo.get_tls_info() + assert tls_info.tls_enabled is False + assert tls_info.tls_ca_cert_path == "/path/to/ca.pem" + + def test_tls_config_immutable(self, mongodb_container): + """Test that MongoDB TLS config is immutable.""" + from ddcDatabases import MongoDB + from ddcDatabases.mongodb import MongoDBTLSConfig + + port = mongodb_container.get_exposed_port(27017) + host = f"{mongodb_container.get_container_host_ip()}:{port}" + + mongo = MongoDB( + host=host, + user="test", + password="test", + database="admin", + collection="tls_immutable_test", + tls_config=MongoDBTLSConfig(tls_enabled=False), + ) + + tls_info = mongo.get_tls_info() + with pytest.raises(AttributeError): + tls_info.tls_enabled = True # noqa diff --git a/tests/integration/test_mssql.py b/tests/integration/mssql/test_mssql.py similarity index 100% rename from tests/integration/test_mssql.py rename to tests/integration/mssql/test_mssql.py diff --git a/tests/integration/mssql/test_mssql_persistent.py b/tests/integration/mssql/test_mssql_persistent.py new file mode 100644 index 0000000..689009a --- /dev/null +++ b/tests/integration/mssql/test_mssql_persistent.py @@ -0,0 +1,97 @@ +"""Integration tests for MSSQL persistent connections.""" + +import pytest +import sqlalchemy as sa + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + MSSQLPersistent, + close_all_persistent_connections, +) + +pytestmark = pytest.mark.integration + + +class TestMSSQLPersistentIntegration: + """Integration tests for MSSQL persistent connections.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_persistent_connection(self, mssql_container): + """Test synchronous MSSQL persistent connection.""" + port = mssql_container.get_exposed_port(1433) + host = mssql_container.get_container_host_ip() + + conn = MSSQLPersistent( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + async_mode=False, + ) + + with conn as session: + # Insert + result = session.execute(sa.text("SELECT 1")) + assert result.scalar() == 1 + + conn.shutdown() + + def test_persistent_connection_singleton(self, mssql_container): + """Test MSSQL persistent connection singleton pattern.""" + port = mssql_container.get_exposed_port(1433) + host = mssql_container.get_container_host_ip() + + conn1 = MSSQLPersistent( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + async_mode=False, + ) + conn2 = MSSQLPersistent( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + async_mode=False, + ) + + assert conn1 is conn2 + + conn1.shutdown() + + def test_persistent_connection_multiple_operations(self, mssql_container): + """Test multiple operations on MSSQL persistent connection.""" + port = mssql_container.get_exposed_port(1433) + host = mssql_container.get_container_host_ip() + + conn = MSSQLPersistent( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + async_mode=False, + ) + + # First use + with conn as session: + result1 = session.execute(sa.text("SELECT 1")).scalar() + assert result1 == 1 + + # Second use + with conn as session: + result2 = session.execute(sa.text("SELECT 2")).scalar() + assert result2 == 2 + + conn.shutdown() diff --git a/tests/integration/mssql/test_mssql_ssl.py b/tests/integration/mssql/test_mssql_ssl.py new file mode 100644 index 0000000..e9c4a06 --- /dev/null +++ b/tests/integration/mssql/test_mssql_ssl.py @@ -0,0 +1,90 @@ +"""Integration tests for MSSQL SSL configuration.""" + +import pytest +import sqlalchemy as sa + +pytestmark = pytest.mark.integration + + +class TestMSSQLSSLIntegration: + """Integration tests for MSSQL SSL configuration.""" + + def test_connection_with_ssl_disabled(self, mssql_container): + """Test MSSQL connection with SSL/encryption disabled.""" + from ddcDatabases import MSSQL + from ddcDatabases.mssql import MSSQLSSLConfig + + port = mssql_container.get_exposed_port(1433) + host = mssql_container.get_container_host_ip() + + with MSSQL( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + ssl_config=MSSQLSSLConfig(ssl_encrypt=False, ssl_trust_server_certificate=True), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_ssl_config_is_accessible(self, mssql_container): + """Test that MSSQL SSL config is accessible.""" + from ddcDatabases import MSSQL + from ddcDatabases.mssql import MSSQLSSLConfig + + port = mssql_container.get_exposed_port(1433) + host = mssql_container.get_container_host_ip() + + mssql = MSSQL( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + ssl_config=MSSQLSSLConfig(ssl_encrypt=True, ssl_trust_server_certificate=True), + ) + + ssl_info = mssql.get_ssl_info() + assert ssl_info.ssl_encrypt is True + assert ssl_info.ssl_trust_server_certificate is True + + def test_ssl_config_immutable(self, mssql_container): + """Test that MSSQL SSL config is immutable.""" + from ddcDatabases import MSSQL + from ddcDatabases.mssql import MSSQLSSLConfig + + port = mssql_container.get_exposed_port(1433) + host = mssql_container.get_container_host_ip() + + mssql = MSSQL( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + ssl_config=MSSQLSSLConfig(ssl_encrypt=False), + ) + + ssl_info = mssql.get_ssl_info() + with pytest.raises(AttributeError): + ssl_info.ssl_encrypt = True # noqa + + def test_connection_with_trust_certificate(self, mssql_container): + """Test MSSQL connection with trust server certificate enabled.""" + from ddcDatabases import MSSQL + from ddcDatabases.mssql import MSSQLSSLConfig + + port = mssql_container.get_exposed_port(1433) + host = mssql_container.get_container_host_ip() + + with MSSQL( + host=host, + port=int(port), + user="sa", + password="Strong@Pass123", + database="master", + ssl_config=MSSQLSSLConfig(ssl_encrypt=True, ssl_trust_server_certificate=True), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 diff --git a/tests/integration/test_mysql.py b/tests/integration/mysql/test_mysql.py similarity index 100% rename from tests/integration/test_mysql.py rename to tests/integration/mysql/test_mysql.py diff --git a/tests/integration/mysql/test_mysql_persistent.py b/tests/integration/mysql/test_mysql_persistent.py new file mode 100644 index 0000000..a5b45d6 --- /dev/null +++ b/tests/integration/mysql/test_mysql_persistent.py @@ -0,0 +1,124 @@ +"""Integration tests for MySQL persistent connections.""" + +import pytest +import sqlalchemy as sa + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + MySQLPersistent, + close_all_persistent_connections, +) +from tests.integration.conftest import Base, IntegrationModel + +pytestmark = pytest.mark.integration + + +class TestMySQLPersistentIntegration: + """Integration tests for MySQL persistent connections.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_persistent_connection(self, mysql_container): + """Test synchronous MySQL persistent connection.""" + port = mysql_container.get_exposed_port(3306) + host = mysql_container.get_container_host_ip() + + conn = MySQLPersistent( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + async_mode=False, + ) + + with conn as session: + # Create tables + Base.metadata.create_all(session.bind) + + # Insert + stmt = sa.insert(IntegrationModel).values(name="mysql_persistent_test", enabled=True) + session.execute(stmt) + session.commit() + + # Verify + stmt = sa.select(IntegrationModel.name).where(IntegrationModel.name == "mysql_persistent_test") + result = session.execute(stmt).scalar() + assert result == "mysql_persistent_test" + + # Cleanup + stmt = sa.delete(IntegrationModel).where(IntegrationModel.name == "mysql_persistent_test") + session.execute(stmt) + session.commit() + + conn.shutdown() + + def test_persistent_connection_singleton(self, mysql_container): + """Test MySQL persistent connection singleton pattern.""" + port = mysql_container.get_exposed_port(3306) + host = mysql_container.get_container_host_ip() + + conn1 = MySQLPersistent( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + async_mode=False, + ) + conn2 = MySQLPersistent( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + async_mode=False, + ) + + assert conn1 is conn2 + + conn1.shutdown() + + def test_persistent_connection_multiple_operations(self, mysql_container): + """Test multiple operations on MySQL persistent connection.""" + port = mysql_container.get_exposed_port(3306) + host = mysql_container.get_container_host_ip() + + conn = MySQLPersistent( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + async_mode=False, + ) + + # First use + with conn as session: + Base.metadata.create_all(session.bind) + session.execute(sa.insert(IntegrationModel).values(name="mysql_op1", enabled=True)) + session.commit() + + # Second use + with conn as session: + session.execute(sa.insert(IntegrationModel).values(name="mysql_op2", enabled=False)) + session.commit() + + # Third use - verify both records exist + with conn as session: + stmt = sa.select(IntegrationModel.name).order_by(IntegrationModel.name) + results = session.execute(stmt).scalars().all() + assert "mysql_op1" in results + assert "mysql_op2" in results + + # Cleanup + session.execute(sa.delete(IntegrationModel)) + session.commit() + + conn.shutdown() diff --git a/tests/integration/mysql/test_mysql_ssl.py b/tests/integration/mysql/test_mysql_ssl.py new file mode 100644 index 0000000..9832c06 --- /dev/null +++ b/tests/integration/mysql/test_mysql_ssl.py @@ -0,0 +1,109 @@ +"""Integration tests for MySQL SSL configuration.""" + +import pytest +import sqlalchemy as sa + +pytestmark = pytest.mark.integration + + +class TestMySQLSSLIntegration: + """Integration tests for MySQL SSL configuration.""" + + def test_connection_with_ssl_disabled(self, mysql_container): + """Test MySQL connection with SSL explicitly disabled.""" + from ddcDatabases import MySQL + from ddcDatabases.mysql import MySQLSSLConfig + + port = mysql_container.get_exposed_port(3306) + host = mysql_container.get_container_host_ip() + + with MySQL( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + ssl_config=MySQLSSLConfig(ssl_mode="DISABLED"), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_connection_with_ssl_preferred(self, mysql_container): + """Test MySQL connection with SSL preferred mode.""" + from ddcDatabases import MySQL + from ddcDatabases.mysql import MySQLSSLConfig + + port = mysql_container.get_exposed_port(3306) + host = mysql_container.get_container_host_ip() + + with MySQL( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + ssl_config=MySQLSSLConfig(ssl_mode="PREFERRED"), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_ssl_config_is_accessible(self, mysql_container): + """Test that MySQL SSL config is accessible.""" + from ddcDatabases import MySQL + from ddcDatabases.mysql import MySQLSSLConfig + + port = mysql_container.get_exposed_port(3306) + host = mysql_container.get_container_host_ip() + + mysql = MySQL( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + ssl_config=MySQLSSLConfig( + ssl_mode="PREFERRED", + ssl_ca_cert_path="/path/to/ca.pem", + ), + ) + + ssl_info = mysql.get_ssl_info() + assert ssl_info.ssl_mode == "PREFERRED" + assert ssl_info.ssl_ca_cert_path == "/path/to/ca.pem" + + def test_ssl_config_immutable(self, mysql_container): + """Test that MySQL SSL config is immutable.""" + from ddcDatabases import MySQL + from ddcDatabases.mysql import MySQLSSLConfig + + port = mysql_container.get_exposed_port(3306) + host = mysql_container.get_container_host_ip() + + mysql = MySQL( + host=host, + port=int(port), + user=mysql_container.username, + password=mysql_container.password, + database=mysql_container.dbname, + ssl_config=MySQLSSLConfig(ssl_mode="DISABLED"), + ) + + ssl_info = mysql.get_ssl_info() + with pytest.raises(AttributeError): + ssl_info.ssl_mode = "REQUIRED" # noqa + + def test_invalid_ssl_mode(self): + """Test MySQL rejects invalid SSL mode.""" + from ddcDatabases.mysql import MySQLSSLConfig + + with pytest.raises(ValueError, match="ssl_mode must be one of"): + MySQLSSLConfig(ssl_mode="invalid_mode") + + def test_valid_ssl_modes(self): + """Test MySQL accepts all valid SSL modes.""" + from ddcDatabases.mysql import MySQLSSLConfig + + valid_modes = ["DISABLED", "PREFERRED", "REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"] + for mode in valid_modes: + config = MySQLSSLConfig(ssl_mode=mode) + assert config.ssl_mode == mode diff --git a/tests/integration/test_oracle.py b/tests/integration/oracle/test_oracle.py similarity index 100% rename from tests/integration/test_oracle.py rename to tests/integration/oracle/test_oracle.py diff --git a/tests/integration/oracle/test_oracle_persistent.py b/tests/integration/oracle/test_oracle_persistent.py new file mode 100644 index 0000000..26cc7a0 --- /dev/null +++ b/tests/integration/oracle/test_oracle_persistent.py @@ -0,0 +1,92 @@ +"""Integration tests for Oracle persistent connections.""" + +import pytest +import sqlalchemy as sa + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + OraclePersistent, + close_all_persistent_connections, +) + +pytestmark = pytest.mark.integration + + +class TestOraclePersistentIntegration: + """Integration tests for Oracle persistent connections.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_persistent_connection(self, oracle_container): + """Test synchronous Oracle persistent connection.""" + port = oracle_container.get_exposed_port(1521) + host = oracle_container.get_container_host_ip() + + conn = OraclePersistent( + host=host, + port=int(port), + user="system", + password=oracle_container.oracle_password, + servicename="FREEPDB1", + ) + + with conn as session: + result = session.execute(sa.text("SELECT 1 FROM dual")) + assert result.scalar() == 1 + + conn.shutdown() + + def test_persistent_connection_singleton(self, oracle_container): + """Test Oracle persistent connection singleton pattern.""" + port = oracle_container.get_exposed_port(1521) + host = oracle_container.get_container_host_ip() + + conn1 = OraclePersistent( + host=host, + port=int(port), + user="system", + password=oracle_container.oracle_password, + servicename="FREEPDB1", + ) + conn2 = OraclePersistent( + host=host, + port=int(port), + user="system", + password=oracle_container.oracle_password, + servicename="FREEPDB1", + ) + + assert conn1 is conn2 + + conn1.shutdown() + + def test_persistent_connection_multiple_operations(self, oracle_container): + """Test multiple operations on Oracle persistent connection.""" + port = oracle_container.get_exposed_port(1521) + host = oracle_container.get_container_host_ip() + + conn = OraclePersistent( + host=host, + port=int(port), + user="system", + password=oracle_container.oracle_password, + servicename="FREEPDB1", + ) + + # First use + with conn as session: + result1 = session.execute(sa.text("SELECT 1 FROM dual")).scalar() + assert result1 == 1 + + # Second use + with conn as session: + result2 = session.execute(sa.text("SELECT 2 FROM dual")).scalar() + assert result2 == 2 + + conn.shutdown() diff --git a/tests/integration/oracle/test_oracle_ssl.py b/tests/integration/oracle/test_oracle_ssl.py new file mode 100644 index 0000000..84fd2d5 --- /dev/null +++ b/tests/integration/oracle/test_oracle_ssl.py @@ -0,0 +1,71 @@ +"""Integration tests for Oracle SSL configuration.""" + +import pytest +import sqlalchemy as sa + +pytestmark = pytest.mark.integration + + +class TestOracleSSLIntegration: + """Integration tests for Oracle SSL configuration.""" + + def test_connection_with_ssl_disabled(self, oracle_container): + """Test Oracle connection with SSL disabled.""" + from ddcDatabases import Oracle + from ddcDatabases.oracle import OracleSSLConfig + + port = oracle_container.get_exposed_port(1521) + host = oracle_container.get_container_host_ip() + + with Oracle( + host=host, + port=int(port), + user="system", + password=oracle_container.oracle_password, + servicename="FREEPDB1", + ssl_config=OracleSSLConfig(ssl_enabled=False), + ) as session: + result = session.execute(sa.text("SELECT 1 FROM dual")).scalar() + assert result == 1 + + def test_ssl_config_is_accessible(self, oracle_container): + """Test that Oracle SSL config is accessible.""" + from ddcDatabases import Oracle + from ddcDatabases.oracle import OracleSSLConfig + + port = oracle_container.get_exposed_port(1521) + host = oracle_container.get_container_host_ip() + + oracle = Oracle( + host=host, + port=int(port), + user="system", + password=oracle_container.oracle_password, + servicename="FREEPDB1", + ssl_config=OracleSSLConfig(ssl_enabled=False, ssl_wallet_path="/path/to/wallet"), + ) + + ssl_info = oracle.get_ssl_info() + assert ssl_info.ssl_enabled is False + assert ssl_info.ssl_wallet_path == "/path/to/wallet" + + def test_ssl_config_immutable(self, oracle_container): + """Test that Oracle SSL config is immutable.""" + from ddcDatabases import Oracle + from ddcDatabases.oracle import OracleSSLConfig + + port = oracle_container.get_exposed_port(1521) + host = oracle_container.get_container_host_ip() + + oracle = Oracle( + host=host, + port=int(port), + user="system", + password=oracle_container.oracle_password, + servicename="FREEPDB1", + ssl_config=OracleSSLConfig(ssl_enabled=False), + ) + + ssl_info = oracle.get_ssl_info() + with pytest.raises(AttributeError): + ssl_info.ssl_enabled = True # noqa diff --git a/tests/integration/test_postgresql.py b/tests/integration/postgresql/test_postgresql.py similarity index 100% rename from tests/integration/test_postgresql.py rename to tests/integration/postgresql/test_postgresql.py diff --git a/tests/integration/postgresql/test_postgresql_persistent.py b/tests/integration/postgresql/test_postgresql_persistent.py new file mode 100644 index 0000000..ae3e156 --- /dev/null +++ b/tests/integration/postgresql/test_postgresql_persistent.py @@ -0,0 +1,185 @@ +"""Integration tests for PostgreSQL persistent connections.""" + +import pytest +import sqlalchemy as sa + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + PostgreSQLPersistent, + close_all_persistent_connections, +) +from tests.integration.conftest import Base, IntegrationModel + +pytestmark = pytest.mark.integration + + +class TestPostgreSQLPersistentIntegration: + """Integration tests for PostgreSQL persistent connections.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_persistent_connection(self, postgres_container): + """Test synchronous PostgreSQL persistent connection.""" + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + conn = PostgreSQLPersistent( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=False, + ) + + with conn as session: + # Create tables + Base.metadata.create_all(session.bind) + + # Insert + stmt = sa.insert(IntegrationModel).values(name="persistent_test", enabled=True) + session.execute(stmt) + session.commit() + + # Verify + stmt = sa.select(IntegrationModel.name).where(IntegrationModel.name == "persistent_test") + result = session.execute(stmt).scalar() + assert result == "persistent_test" + + # Cleanup + stmt = sa.delete(IntegrationModel).where(IntegrationModel.name == "persistent_test") + session.execute(stmt) + session.commit() + + conn.shutdown() + + def test_persistent_connection_reuse(self, postgres_container): + """Test that persistent connection reuses the same connection.""" + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + conn1 = PostgreSQLPersistent( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=False, + ) + conn2 = PostgreSQLPersistent( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=False, + ) + + # Should be the same instance (singleton) + assert conn1 is conn2 + + with conn1 as session1: + Base.metadata.create_all(session1.bind) + result1 = session1.execute(sa.text("SELECT 1")).scalar() + assert result1 == 1 + + with conn2 as session2: + result2 = session2.execute(sa.text("SELECT 2")).scalar() + assert result2 == 2 + + # Sessions should be the same (persistent) + assert session1 is session2 + + conn1.shutdown() + + def test_persistent_connection_multiple_operations(self, postgres_container): + """Test multiple operations on persistent connection.""" + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + conn = PostgreSQLPersistent( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=False, + ) + + # First use + with conn as session: + Base.metadata.create_all(session.bind) + session.execute(sa.insert(IntegrationModel).values(name="op1", enabled=True)) + session.commit() + + # Second use + with conn as session: + session.execute(sa.insert(IntegrationModel).values(name="op2", enabled=False)) + session.commit() + + # Third use - verify both records exist + with conn as session: + stmt = sa.select(IntegrationModel.name).order_by(IntegrationModel.name) + results = session.execute(stmt).scalars().all() + assert "op1" in results + assert "op2" in results + + # Cleanup + session.execute(sa.delete(IntegrationModel)) + session.commit() + + conn.shutdown() + + @pytest.mark.asyncio + async def test_async_persistent_connection(self, postgres_container): + """Test asynchronous PostgreSQL persistent connection.""" + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + # Create tables with sync connection first + sync_conn = PostgreSQLPersistent( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=False, + ) + with sync_conn as session: + Base.metadata.create_all(session.bind) + sync_conn.shutdown() + + # Clear registry to get fresh async connection + close_all_persistent_connections() + + async_conn = PostgreSQLPersistent( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + async_mode=True, + ) + + async with async_conn as session: + # Insert + stmt = sa.insert(IntegrationModel).values(name="async_persistent", enabled=True) + await session.execute(stmt) + await session.commit() + + # Verify + stmt = sa.select(IntegrationModel.name).where(IntegrationModel.name == "async_persistent") + result = await session.execute(stmt) + name = result.scalar() + assert name == "async_persistent" + + # Cleanup + stmt = sa.delete(IntegrationModel).where(IntegrationModel.name == "async_persistent") + await session.execute(stmt) + await session.commit() diff --git a/tests/integration/postgresql/test_postgresql_ssl.py b/tests/integration/postgresql/test_postgresql_ssl.py new file mode 100644 index 0000000..f7337d8 --- /dev/null +++ b/tests/integration/postgresql/test_postgresql_ssl.py @@ -0,0 +1,128 @@ +"""Integration tests for PostgreSQL SSL configuration.""" + +import pytest +import sqlalchemy as sa + +pytestmark = pytest.mark.integration + + +class TestPostgreSQLSSLIntegration: + """Integration tests for PostgreSQL SSL configuration.""" + + def test_connection_with_ssl_disabled(self, postgres_container): + """Test PostgreSQL connection with SSL explicitly disabled.""" + from ddcDatabases import PostgreSQL + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + with PostgreSQL( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + ssl_config=PostgreSQLSSLConfig(ssl_mode="disable"), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_connection_with_ssl_prefer(self, postgres_container): + """Test PostgreSQL connection with SSL prefer mode (falls back to no SSL).""" + from ddcDatabases import PostgreSQL + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + with PostgreSQL( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + ssl_config=PostgreSQLSSLConfig(ssl_mode="prefer"), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_connection_with_ssl_allow(self, postgres_container): + """Test PostgreSQL connection with SSL allow mode.""" + from ddcDatabases import PostgreSQL + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + with PostgreSQL( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + ssl_config=PostgreSQLSSLConfig(ssl_mode="allow"), + ) as session: + result = session.execute(sa.text("SELECT 1")).scalar() + assert result == 1 + + def test_ssl_config_is_accessible(self, postgres_container): + """Test that SSL config is accessible through getter method.""" + from ddcDatabases import PostgreSQL + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + pg = PostgreSQL( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + ssl_config=PostgreSQLSSLConfig( + ssl_mode="prefer", + ssl_ca_cert_path="/path/to/ca.pem", + ), + ) + + ssl_info = pg.get_ssl_info() + assert ssl_info.ssl_mode == "prefer" + assert ssl_info.ssl_ca_cert_path == "/path/to/ca.pem" + + def test_ssl_config_immutable(self, postgres_container): + """Test that SSL config is immutable.""" + from ddcDatabases import PostgreSQL + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + port = postgres_container.get_exposed_port(5432) + host = postgres_container.get_container_host_ip() + + pg = PostgreSQL( + host=host, + port=int(port), + user=postgres_container.username, + password=postgres_container.password, + database=postgres_container.dbname, + ssl_config=PostgreSQLSSLConfig(ssl_mode="disable"), + ) + + ssl_info = pg.get_ssl_info() + with pytest.raises(AttributeError): + ssl_info.ssl_mode = "require" # noqa + + def test_invalid_ssl_mode(self): + """Test PostgreSQL rejects invalid SSL mode.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + with pytest.raises(ValueError, match="ssl_mode must be one of"): + PostgreSQLSSLConfig(ssl_mode="invalid_mode") + + def test_valid_ssl_modes(self): + """Test PostgreSQL accepts all valid SSL modes.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + valid_modes = ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"] + for mode in valid_modes: + config = PostgreSQLSSLConfig(ssl_mode=mode) + assert config.ssl_mode == mode diff --git a/tests/integration/test_sqlite.py b/tests/integration/sqlite/test_sqlite.py similarity index 100% rename from tests/integration/test_sqlite.py rename to tests/integration/sqlite/test_sqlite.py diff --git a/tests/unit/core/test_db_utils.py b/tests/unit/core/test_db_utils.py index 71aa2ce..a048722 100644 --- a/tests/unit/core/test_db_utils.py +++ b/tests/unit/core/test_db_utils.py @@ -35,8 +35,8 @@ def create_test_connection( expire_on_commit, sync_driver, async_driver, - conn_retry_config=None, - op_retry_config=None, + connection_retry_config=None, + operation_retry_config=None, ): """Create a concrete test implementation of BaseConnection""" from ddcDatabases.core.base import BaseConnection @@ -82,8 +82,8 @@ async def _get_async_engine(self) -> AsyncGenerator[AsyncEngine, None]: expire_on_commit=expire_on_commit, sync_driver=sync_driver, async_driver=async_driver, - conn_retry_config=conn_retry_config, - op_retry_config=op_retry_config, + connection_retry_config=connection_retry_config, + operation_retry_config=operation_retry_config, ) @@ -656,7 +656,7 @@ def test_sync_context_manager(self): expire_on_commit=False, sync_driver="postgresql+psycopg", async_driver=None, - conn_retry_config=BaseRetryConfig(enable_retry=False), + connection_retry_config=BaseRetryConfig(enable_retry=False), ) mock_session = MagicMock() diff --git a/tests/unit/core/test_init_module.py b/tests/unit/core/test_init_module.py index b7611b8..db4bb22 100644 --- a/tests/unit/core/test_init_module.py +++ b/tests/unit/core/test_init_module.py @@ -181,16 +181,16 @@ def test_mariadb_aliases(self): from ddcDatabases import ( MariaDB, MariaDBConnectionConfig, - MariaDBConnRetryConfig, - MariaDBOpRetryConfig, + MariaDBConnectionRetryConfig, + MariaDBOperationRetryConfig, MariaDBPersistent, MariaDBPoolConfig, MariaDBSessionConfig, MariaDBSSLConfig, MySQL, MySQLConnectionConfig, - MySQLConnRetryConfig, - MySQLOpRetryConfig, + MySQLConnectionRetryConfig, + MySQLOperationRetryConfig, MySQLPersistent, MySQLPoolConfig, MySQLSessionConfig, @@ -200,8 +200,8 @@ def test_mariadb_aliases(self): # Verify all MariaDB aliases point to their MySQL equivalents assert MariaDB is MySQL assert MariaDBConnectionConfig is MySQLConnectionConfig - assert MariaDBConnRetryConfig is MySQLConnRetryConfig - assert MariaDBOpRetryConfig is MySQLOpRetryConfig + assert MariaDBConnectionRetryConfig is MySQLConnectionRetryConfig + assert MariaDBOperationRetryConfig is MySQLOperationRetryConfig assert MariaDBPersistent is MySQLPersistent assert MariaDBPoolConfig is MySQLPoolConfig assert MariaDBSessionConfig is MySQLSessionConfig diff --git a/tests/unit/core/test_persistent.py b/tests/unit/core/test_persistent.py index 6c62d4e..1d8d60f 100644 --- a/tests/unit/core/test_persistent.py +++ b/tests/unit/core/test_persistent.py @@ -2,6 +2,7 @@ import pytest import time +from ddcDatabases.core.configs import BaseOperationRetryConfig as OperationRetryConfig from ddcDatabases.core.configs import BaseRetryConfig as RetryConfig # noinspection PyProtectedMember @@ -34,11 +35,11 @@ class TestPersistentConnectionConfig: """Test PersistentConnectionConfig dataclass.""" def test_default_values(self): - """Test default configuration values.""" + """Test default configuration values are None (to be filled from settings).""" config = PersistentConnectionConfig() - assert config.idle_timeout == 300 - assert config.health_check_interval == 30 - assert config.auto_reconnect is True + assert config.idle_timeout is None + assert config.health_check_interval is None + assert config.auto_reconnect is None def test_custom_values(self): """Test custom configuration values.""" @@ -73,16 +74,20 @@ def test_initialization(self): def test_custom_config(self): """Test connection with custom config.""" config = PersistentConnectionConfig(idle_timeout=100) - retry_config = RetryConfig(max_retries=5) + conn_retry = RetryConfig(max_retries=5) + op_retry = OperationRetryConfig(max_retries=3, jitter=0.2) conn = PersistentSQLAlchemyConnection( connection_key="test://localhost/db", connection_url="postgresql://user:pass@localhost/db", config=config, - retry_config=retry_config, + connection_retry_config=conn_retry, + operation_retry_config=op_retry, ) assert conn._config.idle_timeout == 100 - assert conn._retry_config.max_retries == 5 + assert conn._connection_retry_config.max_retries == 5 + assert conn._operation_retry_config.max_retries == 3 + assert conn._operation_retry_config.jitter == 0.2 def test_connect_creates_engine_and_session(self): """Test that connect creates engine and session.""" @@ -773,78 +778,78 @@ def test_postgresql_retry_settings(self): from ddcDatabases.core.settings import PostgreSQLSettings settings = PostgreSQLSettings() - assert hasattr(settings, 'conn_enable_retry') - assert hasattr(settings, 'conn_max_retries') - assert hasattr(settings, 'conn_initial_retry_delay') - assert hasattr(settings, 'conn_max_retry_delay') - assert hasattr(settings, 'op_enable_retry') - assert hasattr(settings, 'op_max_retries') - assert hasattr(settings, 'op_initial_retry_delay') - assert hasattr(settings, 'op_max_retry_delay') - assert hasattr(settings, 'op_jitter') - assert hasattr(settings, 'conn_disconnect_idle_timeout') - - assert settings.conn_enable_retry is True - assert settings.conn_max_retries == 3 - assert settings.conn_initial_retry_delay == pytest.approx(1.0) - assert settings.conn_max_retry_delay == pytest.approx(30.0) - assert settings.op_enable_retry is True - assert settings.op_max_retries == 3 - assert settings.op_initial_retry_delay == pytest.approx(0.5) - assert settings.op_max_retry_delay == pytest.approx(10.0) - assert settings.op_jitter == pytest.approx(0.1) - assert settings.conn_disconnect_idle_timeout == 300 + assert hasattr(settings, 'connection_enable_retry') + assert hasattr(settings, 'connection_max_retries') + assert hasattr(settings, 'connection_initial_retry_delay') + assert hasattr(settings, 'connection_max_retry_delay') + assert hasattr(settings, 'operation_enable_retry') + assert hasattr(settings, 'operation_max_retries') + assert hasattr(settings, 'operation_initial_retry_delay') + assert hasattr(settings, 'operation_max_retry_delay') + assert hasattr(settings, 'operation_jitter') + assert hasattr(settings, 'connection_disconnect_idle_timeout') + + assert settings.connection_enable_retry is True + assert settings.connection_max_retries == 3 + assert settings.connection_initial_retry_delay == pytest.approx(1.0) + assert settings.connection_max_retry_delay == pytest.approx(30.0) + assert settings.operation_enable_retry is True + assert settings.operation_max_retries == 3 + assert settings.operation_initial_retry_delay == pytest.approx(0.5) + assert settings.operation_max_retry_delay == pytest.approx(10.0) + assert settings.operation_jitter == pytest.approx(0.1) + assert settings.connection_disconnect_idle_timeout == 300 def test_mysql_retry_settings(self): """Test MySQL settings include retry fields.""" from ddcDatabases.core.settings import MySQLSettings settings = MySQLSettings() - assert settings.conn_enable_retry is True - assert settings.conn_max_retries == 3 - assert settings.op_enable_retry is True - assert settings.op_max_retries == 3 - assert settings.conn_disconnect_idle_timeout == 300 + assert settings.connection_enable_retry is True + assert settings.connection_max_retries == 3 + assert settings.operation_enable_retry is True + assert settings.operation_max_retries == 3 + assert settings.connection_disconnect_idle_timeout == 300 def test_mssql_retry_settings(self): """Test MSSQL settings include retry fields.""" from ddcDatabases.core.settings import MSSQLSettings settings = MSSQLSettings() - assert settings.conn_enable_retry is True - assert settings.conn_max_retries == 3 - assert settings.op_enable_retry is True - assert settings.op_max_retries == 3 - assert settings.conn_disconnect_idle_timeout == 300 + assert settings.connection_enable_retry is True + assert settings.connection_max_retries == 3 + assert settings.operation_enable_retry is True + assert settings.operation_max_retries == 3 + assert settings.connection_disconnect_idle_timeout == 300 def test_oracle_retry_settings(self): """Test Oracle settings include retry fields.""" from ddcDatabases.core.settings import OracleSettings settings = OracleSettings() - assert settings.conn_enable_retry is True - assert settings.conn_max_retries == 3 - assert settings.op_enable_retry is True - assert settings.op_max_retries == 3 - assert settings.conn_disconnect_idle_timeout == 300 + assert settings.connection_enable_retry is True + assert settings.connection_max_retries == 3 + assert settings.operation_enable_retry is True + assert settings.operation_max_retries == 3 + assert settings.connection_disconnect_idle_timeout == 300 def test_mongodb_retry_settings(self): """Test MongoDB settings include retry fields.""" from ddcDatabases.core.settings import MongoDBSettings settings = MongoDBSettings() - assert settings.conn_enable_retry is True - assert settings.conn_max_retries == 3 - assert settings.op_enable_retry is True - assert settings.op_max_retries == 3 - assert settings.conn_disconnect_idle_timeout == 300 + assert settings.connection_enable_retry is True + assert settings.connection_max_retries == 3 + assert settings.operation_enable_retry is True + assert settings.operation_max_retries == 3 + assert settings.connection_disconnect_idle_timeout == 300 def test_sqlite_retry_settings(self): """Test SQLite settings include retry fields (minimal).""" from ddcDatabases.core.settings import SQLiteSettings settings = SQLiteSettings() - assert settings.conn_enable_retry is False # SQLite disabled by default - assert settings.conn_max_retries == 1 # Minimal retries for file-based DB - assert settings.op_enable_retry is False # SQLite disabled by default - assert settings.op_max_retries == 1 # Minimal retries for file-based DB + assert settings.connection_enable_retry is False # SQLite disabled by default + assert settings.connection_max_retries == 1 # Minimal retries for file-based DB + assert settings.operation_enable_retry is False # SQLite disabled by default + assert settings.operation_max_retries == 1 # Minimal retries for file-based DB diff --git a/tests/unit/core/test_retry_logic.py b/tests/unit/core/test_retry_logic.py index 40fa7a8..6916b26 100644 --- a/tests/unit/core/test_retry_logic.py +++ b/tests/unit/core/test_retry_logic.py @@ -465,7 +465,7 @@ def test_postgresql_retry_config(self): from ddcDatabases.postgresql import PostgreSQL pg = PostgreSQL() - retry_info = pg.get_conn_retry_info() + retry_info = pg.get_connection_retry_info() assert retry_info.enable_retry is True assert retry_info.max_retries == 3 @@ -477,7 +477,7 @@ def test_mysql_retry_config(self): from ddcDatabases.mysql import MySQL mysql = MySQL() - retry_info = mysql.get_conn_retry_info() + retry_info = mysql.get_connection_retry_info() assert retry_info.enable_retry is True assert retry_info.max_retries == 3 @@ -487,7 +487,7 @@ def test_mssql_retry_config(self): from ddcDatabases.mssql import MSSQL mssql = MSSQL() - retry_info = mssql.get_conn_retry_info() + retry_info = mssql.get_connection_retry_info() assert retry_info.enable_retry is True assert retry_info.max_retries == 3 @@ -497,7 +497,7 @@ def test_oracle_retry_config(self): from ddcDatabases.oracle import Oracle oracle = Oracle() - retry_info = oracle.get_conn_retry_info() + retry_info = oracle.get_connection_retry_info() assert retry_info.enable_retry is True assert retry_info.max_retries == 3 @@ -507,7 +507,7 @@ def test_sqlite_retry_config(self): from ddcDatabases.sqlite import Sqlite sqlite = Sqlite() - retry_info = sqlite.get_conn_retry_info() + retry_info = sqlite.get_connection_retry_info() # SQLite has retry disabled by default assert retry_info.enable_retry is False @@ -518,24 +518,24 @@ def test_mongodb_retry_config(self): from ddcDatabases.mongodb import MongoDB mongodb = MongoDB(collection="test") - retry_info = mongodb.get_conn_retry_info() + retry_info = mongodb.get_connection_retry_info() assert retry_info.enable_retry is True assert retry_info.max_retries == 3 def test_custom_retry_settings(self): """Test passing custom retry settings to database class.""" - from ddcDatabases.postgresql import PostgreSQL, PostgreSQLConnRetryConfig + from ddcDatabases.postgresql import PostgreSQL, PostgreSQLConnectionRetryConfig pg = PostgreSQL( - conn_retry_config=PostgreSQLConnRetryConfig( + connection_retry_config=PostgreSQLConnectionRetryConfig( enable_retry=False, max_retries=5, initial_retry_delay=2.0, max_retry_delay=60.0, ), ) - retry_info = pg.get_conn_retry_info() + retry_info = pg.get_connection_retry_info() assert retry_info.enable_retry is False assert retry_info.max_retries == 5 diff --git a/tests/unit/core/test_ssl_configs.py b/tests/unit/core/test_ssl_configs.py new file mode 100644 index 0000000..4f44ffe --- /dev/null +++ b/tests/unit/core/test_ssl_configs.py @@ -0,0 +1,33 @@ +"""Tests for SSL/TLS constants and shared SSL functionality.""" + + +class TestSSLConstantsIntegrity: + """Test SSL constants integrity across the module.""" + + def test_postgresql_ssl_modes_frozenset(self): + """Test that PostgreSQL SSL modes is a frozenset.""" + from ddcDatabases.core.constants import POSTGRESQL_SSL_MODES + + assert isinstance(POSTGRESQL_SSL_MODES, frozenset) + assert len(POSTGRESQL_SSL_MODES) == 6 + + def test_mysql_ssl_modes_frozenset(self): + """Test that MySQL SSL modes is a frozenset.""" + from ddcDatabases.core.constants import MYSQL_SSL_MODES + + assert isinstance(MYSQL_SSL_MODES, frozenset) + assert len(MYSQL_SSL_MODES) == 5 + + def test_postgresql_modes_content(self): + """Test PostgreSQL SSL modes contain expected values.""" + from ddcDatabases.core.constants import POSTGRESQL_SSL_MODES + + expected_modes = {"disable", "allow", "prefer", "require", "verify-ca", "verify-full"} + assert POSTGRESQL_SSL_MODES == expected_modes + + def test_mysql_modes_content(self): + """Test MySQL SSL modes contain expected values.""" + from ddcDatabases.core.constants import MYSQL_SSL_MODES + + expected_modes = {"DISABLED", "PREFERRED", "REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"} + assert MYSQL_SSL_MODES == expected_modes diff --git a/tests/unit/mariadb/__init__.py b/tests/unit/mariadb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/mariadb/test_mariadb_persistent.py b/tests/unit/mariadb/test_mariadb_persistent.py new file mode 100644 index 0000000..4019479 --- /dev/null +++ b/tests/unit/mariadb/test_mariadb_persistent.py @@ -0,0 +1,113 @@ +"""Tests for MariaDB persistent connections (MySQL alias). + +Note: MariaDB uses MySQL driver and persistent connections, these tests verify +the MariaDB aliases work correctly. +""" + +# noinspection PyProtectedMember +from ddcDatabases import MariaDBPersistent +from ddcDatabases.core.persistent import ( + MySQLPersistent, + PersistentSQLAlchemyAsyncConnection, + PersistentSQLAlchemyConnection, + close_all_persistent_connections, +) + + +class TestMariaDBPersistent: + """Test MariaDB persistent connection specifics.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_mariadb_persistent_is_mysql_alias(self): + """Test that MariaDBPersistent is an alias for MySQLPersistent.""" + assert MariaDBPersistent is MySQLPersistent + + def test_sync_connection_creates_correct_type(self): + """Test MariaDB sync persistent connection returns correct type.""" + conn = MariaDBPersistent( + host="testhost", + port=3307, + user="testuser", + password="testpass", + database="testdb", + async_mode=False, + ) + assert isinstance(conn, PersistentSQLAlchemyConnection) + assert "mysql" in conn.connection_key # Uses MySQL driver + + def test_async_connection_creates_correct_type(self): + """Test MariaDB async persistent connection returns correct type.""" + conn = MariaDBPersistent( + host="testhost", + port=3306, + user="testuser", + password="testpass", + database="testdb", + async_mode=True, + ) + assert isinstance(conn, PersistentSQLAlchemyAsyncConnection) + assert "mysql" in conn.connection_key # Uses MySQL driver + + def test_default_port(self): + """Test MariaDB default port is used when not specified.""" + conn = MariaDBPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + assert "3306" in conn.connection_key + + def test_singleton_pattern(self): + """Test that same params return same instance.""" + conn1 = MariaDBPersistent( + host="localhost", + port=3306, + user="test", + password="test", + database="mariadb_singleton_test", + ) + conn2 = MariaDBPersistent( + host="localhost", + port=3306, + user="test", + password="test", + database="mariadb_singleton_test", + ) + assert conn1 is conn2 + + def test_different_databases_return_different_instances(self): + """Test that different databases return different instances.""" + conn1 = MariaDBPersistent( + host="localhost", + user="test", + password="test", + database="mariadb_db1", + ) + conn2 = MariaDBPersistent( + host="localhost", + user="test", + password="test", + database="mariadb_db2", + ) + assert conn1 is not conn2 + + def test_connection_key_format(self): + """Test that connection key has correct format.""" + conn = MariaDBPersistent( + host="myhost", + port=3306, + user="myuser", + password="mypass", + database="mydb", + ) + # MariaDB uses MySQL driver, so key contains mysql + assert conn.connection_key == "mysql://myuser@myhost:3306/mydb" diff --git a/tests/unit/mariadb/test_mariadb_ssl.py b/tests/unit/mariadb/test_mariadb_ssl.py new file mode 100644 index 0000000..9be0165 --- /dev/null +++ b/tests/unit/mariadb/test_mariadb_ssl.py @@ -0,0 +1,67 @@ +"""Tests for MariaDB SSL configuration (MySQL alias). + +Note: MariaDB uses MySQL driver and SSL configuration, these tests verify +the MariaDB aliases work correctly. +""" + +import pytest + + +class TestMariaDBSSLConfig: + """Test MariaDB SSL configuration validation (MySQL alias).""" + + def test_mariadb_ssl_config_is_mysql_alias(self): + """Test that MariaDBSSLConfig is an alias for MySQLSSLConfig.""" + from ddcDatabases import MariaDBSSLConfig + from ddcDatabases.mysql import MySQLSSLConfig + + assert MariaDBSSLConfig is MySQLSSLConfig + + def test_valid_ssl_modes(self): + """Test all valid MariaDB SSL modes.""" + from ddcDatabases import MariaDBSSLConfig + + valid_modes = ["DISABLED", "PREFERRED", "REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"] + for mode in valid_modes: + config = MariaDBSSLConfig(ssl_mode=mode) + assert config.ssl_mode == mode + + def test_invalid_ssl_mode_raises_error(self): + """Test that invalid SSL mode raises ValueError.""" + from ddcDatabases import MariaDBSSLConfig + + with pytest.raises(ValueError, match="ssl_mode must be one of"): + MariaDBSSLConfig(ssl_mode="invalid_mode") + + def test_ssl_config_with_all_paths(self): + """Test SSL config with all certificate paths.""" + from ddcDatabases import MariaDBSSLConfig + + config = MariaDBSSLConfig( + ssl_mode="VERIFY_IDENTITY", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.pem", + ssl_client_key_path="/path/to/client-key.pem", + ) + assert config.ssl_mode == "VERIFY_IDENTITY" + assert config.ssl_ca_cert_path == "/path/to/ca.pem" + assert config.ssl_client_cert_path == "/path/to/client.pem" + assert config.ssl_client_key_path == "/path/to/client-key.pem" + + def test_ssl_config_immutability(self): + """Test that SSL config is immutable (frozen).""" + from ddcDatabases import MariaDBSSLConfig + + config = MariaDBSSLConfig(ssl_mode="REQUIRED") + with pytest.raises(AttributeError): + config.ssl_mode = "DISABLED" # noqa + + def test_default_values_are_none(self): + """Test MariaDB SSL config default values are None.""" + from ddcDatabases import MariaDBSSLConfig + + config = MariaDBSSLConfig() + assert config.ssl_mode is None + assert config.ssl_ca_cert_path is None + assert config.ssl_client_cert_path is None + assert config.ssl_client_key_path is None diff --git a/tests/unit/test_mongodb.py b/tests/unit/mongodb/test_mongodb.py similarity index 97% rename from tests/unit/test_mongodb.py rename to tests/unit/mongodb/test_mongodb.py index c85f065..c4b4f64 100644 --- a/tests/unit/test_mongodb.py +++ b/tests/unit/mongodb/test_mongodb.py @@ -67,16 +67,16 @@ def _create_mock_settings(self, **overrides): mock_settings.tls_cert_key_path = overrides.get('tls_cert_key_path', None) mock_settings.tls_allow_invalid_certificates = overrides.get('tls_allow_invalid_certificates', False) # Connection retry settings - mock_settings.conn_enable_retry = overrides.get('conn_enable_retry', False) - mock_settings.conn_max_retries = overrides.get('conn_max_retries', 0) - mock_settings.conn_initial_retry_delay = overrides.get('conn_initial_retry_delay', 0.0) - mock_settings.conn_max_retry_delay = overrides.get('conn_max_retry_delay', 0.0) + mock_settings.connection_enable_retry = overrides.get('connection_enable_retry', False) + mock_settings.connection_max_retries = overrides.get('connection_max_retries', 0) + mock_settings.connection_initial_retry_delay = overrides.get('connection_initial_retry_delay', 0.0) + mock_settings.connection_max_retry_delay = overrides.get('connection_max_retry_delay', 0.0) # Operation retry settings - mock_settings.op_enable_retry = overrides.get('op_enable_retry', False) - mock_settings.op_max_retries = overrides.get('op_max_retries', 0) - mock_settings.op_initial_retry_delay = overrides.get('op_initial_retry_delay', 0.0) - mock_settings.op_max_retry_delay = overrides.get('op_max_retry_delay', 0.0) - mock_settings.op_jitter = overrides.get('op_jitter', 0.1) + mock_settings.operation_enable_retry = overrides.get('operation_enable_retry', False) + mock_settings.operation_max_retries = overrides.get('operation_max_retries', 0) + mock_settings.operation_initial_retry_delay = overrides.get('operation_initial_retry_delay', 0.0) + mock_settings.operation_max_retry_delay = overrides.get('operation_max_retry_delay', 0.0) + mock_settings.operation_jitter = overrides.get('operation_jitter', 0.1) return mock_settings # noinspection PyMethodMayBeStatic @@ -856,11 +856,11 @@ def test_enter_with_connection_error_calls_sys_exit(self, mock_get_settings): mock_client.admin.command.side_effect = PyMongoError("Connection failed") with patch('ddcDatabases.mongodb.MongoClient', return_value=mock_client): - from ddcDatabases.mongodb import MongoDBConnRetryConfig + from ddcDatabases.mongodb import MongoDBConnectionRetryConfig mongodb = MongoDB( collection="test_collection", - conn_retry_config=MongoDBConnRetryConfig(enable_retry=False), + connection_retry_config=MongoDBConnectionRetryConfig(enable_retry=False), ) with pytest.raises(SystemExit) as exc_info: @@ -982,14 +982,14 @@ def test_get_query_info(self, mock_get_settings): assert query_info.batch_size == 500 @patch('ddcDatabases.mongodb.get_mongodb_settings') - def test_get_op_retry_info(self, mock_get_settings): - """Test get_op_retry_info returns operation retry config""" + def test_get_operation_retry_info(self, mock_get_settings): + """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() mongodb = MongoDB(collection="test_collection") - op_retry_info = mongodb.get_op_retry_info() + op_retry_info = mongodb.get_operation_retry_info() - assert op_retry_info is mongodb._op_retry_config + assert op_retry_info is mongodb._operation_retry_config # Check that it has the expected attributes from BaseOperationRetryConfig assert hasattr(op_retry_info, 'enable_retry') assert hasattr(op_retry_info, 'max_retries') @@ -1235,7 +1235,7 @@ def test_create_cursor_async_with_sorting(self, mock_get_settings): @patch('ddcDatabases.mongodb.AsyncIOMotorClient') async def test_async_aenter_connection_error(self, mock_async_client, mock_get_settings): """Test async __aenter__ handles connection error""" - from ddcDatabases.mongodb import MongoDBConnRetryConfig + from ddcDatabases.mongodb import MongoDBConnectionRetryConfig from pymongo.errors import PyMongoError mock_get_settings.return_value = self._create_mock_settings() @@ -1250,7 +1250,7 @@ async def mock_command(_cmd): mongodb = MongoDB( collection="test_collection", - conn_retry_config=MongoDBConnRetryConfig(enable_retry=False), + connection_retry_config=MongoDBConnectionRetryConfig(enable_retry=False), ) with pytest.raises(SystemExit) as exc_info: diff --git a/tests/unit/mongodb/test_mongodb_persistent.py b/tests/unit/mongodb/test_mongodb_persistent.py new file mode 100644 index 0000000..d897c49 --- /dev/null +++ b/tests/unit/mongodb/test_mongodb_persistent.py @@ -0,0 +1,158 @@ +"""Tests for MongoDB persistent connections.""" + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + MongoDBPersistent, + PersistentConnectionConfig, + PersistentMongoDBConnection, + close_all_persistent_connections, +) +from unittest.mock import MagicMock, patch + +TEST_CONFIG = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=False, +) + + +class TestMongoDBPersistent: + """Test MongoDB persistent connection specifics.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_connection_creates_correct_url(self): + """Test MongoDB persistent connection URL formation.""" + conn = MongoDBPersistent( + host="testhost", + port=27018, + user="testuser", + password="testpass", + database="testdb", + ) + assert isinstance(conn, PersistentMongoDBConnection) + assert "mongodb" in conn.connection_key + assert "testhost" in conn.connection_key + assert "27018" in conn.connection_key + + def test_default_port(self): + """Test MongoDB default port is used when not specified.""" + conn = MongoDBPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + ) + assert "27017" in conn.connection_key + + def test_database_stored(self): + """Test MongoDB database is correctly stored.""" + conn = MongoDBPersistent( + host="localhost", + user="test", + password="test", + database="mydb", + ) + assert conn._database == "mydb" + + def test_singleton_pattern(self): + """Test that same params return same instance.""" + conn1 = MongoDBPersistent( + host="localhost", + port=27017, + user="test", + password="test", + database="singleton_test", + ) + conn2 = MongoDBPersistent( + host="localhost", + port=27017, + user="test", + password="test", + database="singleton_test", + ) + assert conn1 is conn2 + + def test_different_databases_return_different_instances(self): + """Test that different databases return different instances.""" + conn1 = MongoDBPersistent( + host="localhost", + user="test", + password="test", + database="db1", + ) + conn2 = MongoDBPersistent( + host="localhost", + user="test", + password="test", + database="db2", + ) + assert conn1 is not conn2 + + @patch('pymongo.MongoClient') + def test_context_manager(self, mock_client_class): + """Test MongoDB persistent connection context manager.""" + mock_client = MagicMock() + mock_db = MagicMock() + mock_client.__getitem__ = MagicMock(return_value=mock_db) + mock_client_class.return_value = mock_client + + conn = PersistentMongoDBConnection( + connection_key="mongodb://localhost/db", + connection_url="mongodb://user:pass@localhost/admin", + database="test_db", + config=TEST_CONFIG, + ) + + with conn as db: + assert conn.is_connected + mock_client.admin.command.assert_called_with("ping") + + conn.shutdown() + + @patch('pymongo.MongoClient') + def test_health_check_on_connect(self, mock_client_class): + """Test that MongoDB health check (ping) runs on connect.""" + mock_client = MagicMock() + mock_db = MagicMock() + mock_client.__getitem__ = MagicMock(return_value=mock_db) + mock_client_class.return_value = mock_client + + conn = PersistentMongoDBConnection( + connection_key="mongodb://localhost/db", + connection_url="mongodb://user:pass@localhost/admin", + database="test_db", + config=TEST_CONFIG, + ) + + conn.connect() + + mock_client.admin.command.assert_called_with("ping") + + conn.shutdown() + + def test_connection_has_lock(self): + """Test that MongoDB persistent connections have a lock.""" + conn = PersistentMongoDBConnection( + connection_key="mongodb://localhost/db", + connection_url="mongodb://user:pass@localhost/admin", + database="test_db", + ) + assert hasattr(conn, '_lock') + + def test_connection_key_format(self): + """Test that connection key has correct format.""" + conn = MongoDBPersistent( + host="myhost", + port=27017, + user="myuser", + password="mypass", + database="mydb", + ) + assert conn.connection_key == "mongodb://myuser@myhost:27017/mydb" diff --git a/tests/unit/mongodb/test_mongodb_tls.py b/tests/unit/mongodb/test_mongodb_tls.py new file mode 100644 index 0000000..862c14d --- /dev/null +++ b/tests/unit/mongodb/test_mongodb_tls.py @@ -0,0 +1,86 @@ +"""Tests for MongoDB TLS configuration.""" + +import pytest + + +class TestMongoDBTLSConfig: + """Test MongoDB TLS configuration validation.""" + + def test_default_values_are_none(self): + """Test MongoDB TLS config default values are None.""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig() + assert config.tls_enabled is None + assert config.tls_ca_cert_path is None + assert config.tls_cert_key_path is None + assert config.tls_allow_invalid_certificates is None + + def test_tls_enabled(self): + """Test MongoDB TLS enabled configuration.""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig(tls_enabled=True) + assert config.tls_enabled is True + + def test_tls_config_with_all_paths(self): + """Test MongoDB TLS config with all certificate paths.""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig( + tls_enabled=True, + tls_ca_cert_path="/path/to/ca.pem", + tls_cert_key_path="/path/to/cert.pem", + tls_allow_invalid_certificates=True, + ) + assert config.tls_enabled is True + assert config.tls_ca_cert_path == "/path/to/ca.pem" + assert config.tls_cert_key_path == "/path/to/cert.pem" + assert config.tls_allow_invalid_certificates is True + + def test_tls_config_immutability(self): + """Test that TLS config is immutable (frozen).""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig(tls_enabled=True) + with pytest.raises(AttributeError): + config.tls_enabled = False # noqa + + def test_tls_disabled(self): + """Test MongoDB TLS disabled configuration.""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig(tls_enabled=False) + assert config.tls_enabled is False + + def test_tls_allow_invalid_certificates(self): + """Test MongoDB TLS allow invalid certificates.""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig( + tls_enabled=True, + tls_allow_invalid_certificates=True, + ) + assert config.tls_allow_invalid_certificates is True + + def test_tls_ca_cert_path_only(self): + """Test MongoDB TLS config with only CA cert path.""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig( + tls_enabled=True, + tls_ca_cert_path="/path/to/ca.pem", + ) + assert config.tls_ca_cert_path == "/path/to/ca.pem" + assert config.tls_cert_key_path is None + + def test_tls_cert_key_path_only(self): + """Test MongoDB TLS config with only cert key path.""" + from ddcDatabases.mongodb import MongoDBTLSConfig + + config = MongoDBTLSConfig( + tls_enabled=True, + tls_cert_key_path="/path/to/cert.pem", + ) + assert config.tls_cert_key_path == "/path/to/cert.pem" + assert config.tls_ca_cert_path is None diff --git a/tests/unit/test_mssql.py b/tests/unit/mssql/test_mssql.py similarity index 96% rename from tests/unit/test_mssql.py rename to tests/unit/mssql/test_mssql.py index c9a6d5a..25e8222 100644 --- a/tests/unit/test_mssql.py +++ b/tests/unit/mssql/test_mssql.py @@ -30,15 +30,15 @@ def _create_mock_settings(self, **overrides): "odbcdriver_version": 17, "ssl_encrypt": False, "ssl_trust_server_certificate": True, - "conn_enable_retry": True, - "conn_max_retries": 3, - "conn_initial_retry_delay": 1.0, - "conn_max_retry_delay": 30.0, - "op_enable_retry": True, - "op_max_retries": 3, - "op_initial_retry_delay": 1.0, - "op_max_retry_delay": 30.0, - "op_jitter": 0.1, + "connection_enable_retry": True, + "connection_max_retries": 3, + "connection_initial_retry_delay": 1.0, + "connection_max_retry_delay": 30.0, + "operation_enable_retry": True, + "operation_max_retries": 3, + "operation_initial_retry_delay": 1.0, + "operation_max_retry_delay": 30.0, + "operation_jitter": 0.1, } defaults.update(overrides) for key, value in defaults.items(): @@ -467,14 +467,14 @@ def test_get_session_info(self, mock_get_settings): assert session_info.autoflush == False @patch('ddcDatabases.mssql.get_mssql_settings') - def test_get_op_retry_info(self, mock_get_settings): - """Test get_op_retry_info returns operation retry config""" + def test_get_operation_retry_info(self, mock_get_settings): + """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() mssql = MSSQL() - op_retry_info = mssql.get_op_retry_info() + op_retry_info = mssql.get_operation_retry_info() - assert op_retry_info is mssql._op_retry_config + assert op_retry_info is mssql._operation_retry_config assert hasattr(op_retry_info, 'enable_retry') assert hasattr(op_retry_info, 'max_retries') assert hasattr(op_retry_info, 'jitter') diff --git a/tests/unit/mssql/test_mssql_persistent.py b/tests/unit/mssql/test_mssql_persistent.py new file mode 100644 index 0000000..fe188be --- /dev/null +++ b/tests/unit/mssql/test_mssql_persistent.py @@ -0,0 +1,104 @@ +"""Tests for MSSQL persistent connections.""" + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + MSSQLPersistent, + PersistentSQLAlchemyAsyncConnection, + PersistentSQLAlchemyConnection, + close_all_persistent_connections, +) + + +class TestMSSQLPersistent: + """Test MSSQL persistent connection specifics.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_connection_creates_correct_url(self): + """Test MSSQL sync persistent connection URL formation.""" + conn = MSSQLPersistent( + host="testhost", + port=1434, + user="testuser", + password="testpass", + database="testdb", + async_mode=False, + ) + assert isinstance(conn, PersistentSQLAlchemyConnection) + assert "mssql" in conn.connection_key + + def test_async_connection_creates_correct_url(self): + """Test MSSQL async persistent connection URL formation.""" + conn = MSSQLPersistent( + host="testhost", + port=1433, + user="testuser", + password="testpass", + database="testdb", + async_mode=True, + ) + assert isinstance(conn, PersistentSQLAlchemyAsyncConnection) + assert "mssql" in conn.connection_key + assert "testhost" in conn.connection_key + + def test_default_port(self): + """Test MSSQL default port is used when not specified.""" + conn = MSSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + assert "1433" in conn.connection_key + + def test_singleton_pattern(self): + """Test that same params return same instance.""" + conn1 = MSSQLPersistent( + host="localhost", + port=1433, + user="test", + password="test", + database="singleton_test", + ) + conn2 = MSSQLPersistent( + host="localhost", + port=1433, + user="test", + password="test", + database="singleton_test", + ) + assert conn1 is conn2 + + def test_different_databases_return_different_instances(self): + """Test that different databases return different instances.""" + conn1 = MSSQLPersistent( + host="localhost", + user="test", + password="test", + database="db1", + ) + conn2 = MSSQLPersistent( + host="localhost", + user="test", + password="test", + database="db2", + ) + assert conn1 is not conn2 + + def test_connection_key_format(self): + """Test that connection key has correct format.""" + conn = MSSQLPersistent( + host="myhost", + port=1433, + user="myuser", + password="mypass", + database="mydb", + ) + assert conn.connection_key == "mssql://myuser@myhost:1433/mydb" diff --git a/tests/unit/mssql/test_mssql_ssl.py b/tests/unit/mssql/test_mssql_ssl.py new file mode 100644 index 0000000..abb73b0 --- /dev/null +++ b/tests/unit/mssql/test_mssql_ssl.py @@ -0,0 +1,67 @@ +"""Tests for MSSQL SSL configuration.""" + +import pytest + + +class TestMSSQLSSLConfig: + """Test MSSQL SSL configuration validation.""" + + def test_default_values_are_none(self): + """Test MSSQL SSL config default values are None.""" + from ddcDatabases.mssql import MSSQLSSLConfig + + config = MSSQLSSLConfig() + assert config.ssl_encrypt is None + assert config.ssl_trust_server_certificate is None + assert config.ssl_ca_cert_path is None + + def test_ssl_encrypt_enabled(self): + """Test MSSQL SSL encryption enabled.""" + from ddcDatabases.mssql import MSSQLSSLConfig + + config = MSSQLSSLConfig(ssl_encrypt=True, ssl_trust_server_certificate=False) + assert config.ssl_encrypt is True + assert config.ssl_trust_server_certificate is False + + def test_ssl_config_with_ca_cert(self): + """Test MSSQL SSL config with CA certificate.""" + from ddcDatabases.mssql import MSSQLSSLConfig + + config = MSSQLSSLConfig( + ssl_encrypt=True, + ssl_trust_server_certificate=False, + ssl_ca_cert_path="/path/to/ca.pem", + ) + assert config.ssl_ca_cert_path == "/path/to/ca.pem" + + def test_ssl_config_immutability(self): + """Test that SSL config is immutable (frozen).""" + from ddcDatabases.mssql import MSSQLSSLConfig + + config = MSSQLSSLConfig(ssl_encrypt=True) + with pytest.raises(AttributeError): + config.ssl_encrypt = False # noqa + + def test_ssl_encrypt_disabled(self): + """Test MSSQL SSL encryption disabled.""" + from ddcDatabases.mssql import MSSQLSSLConfig + + config = MSSQLSSLConfig(ssl_encrypt=False, ssl_trust_server_certificate=True) + assert config.ssl_encrypt is False + assert config.ssl_trust_server_certificate is True + + def test_trust_server_certificate_only(self): + """Test MSSQL SSL config with trust server certificate only.""" + from ddcDatabases.mssql import MSSQLSSLConfig + + config = MSSQLSSLConfig(ssl_trust_server_certificate=True) + assert config.ssl_trust_server_certificate is True + assert config.ssl_encrypt is None + + def test_ssl_encrypt_with_trust_cert(self): + """Test MSSQL SSL encrypt with trust certificate combination.""" + from ddcDatabases.mssql import MSSQLSSLConfig + + config = MSSQLSSLConfig(ssl_encrypt=True, ssl_trust_server_certificate=True) + assert config.ssl_encrypt is True + assert config.ssl_trust_server_certificate is True diff --git a/tests/unit/test_mysql.py b/tests/unit/mysql/test_mysql.py similarity index 92% rename from tests/unit/test_mysql.py rename to tests/unit/mysql/test_mysql.py index 94d3968..7efe349 100644 --- a/tests/unit/test_mysql.py +++ b/tests/unit/mysql/test_mysql.py @@ -31,16 +31,16 @@ def _create_mock_settings(self, **overrides): "ssl_client_cert_path": None, "ssl_client_key_path": None, # Connection retry settings - "conn_enable_retry": True, - "conn_max_retries": 3, - "conn_initial_retry_delay": 1.0, - "conn_max_retry_delay": 30.0, + "connection_enable_retry": True, + "connection_max_retries": 3, + "connection_initial_retry_delay": 1.0, + "connection_max_retry_delay": 30.0, # Operation retry settings - "op_enable_retry": True, - "op_max_retries": 3, - "op_initial_retry_delay": 1.0, - "op_max_retry_delay": 30.0, - "op_jitter": 0.1, + "operation_enable_retry": True, + "operation_max_retries": 3, + "operation_initial_retry_delay": 1.0, + "operation_max_retry_delay": 30.0, + "operation_jitter": 0.1, } defaults.update(overrides) for key, value in defaults.items(): @@ -256,26 +256,26 @@ def test_get_session_info(self, mock_get_settings): assert session_info.autoflush == False @patch('ddcDatabases.mysql.get_mysql_settings') - def test_get_conn_retry_info(self, mock_get_settings): - """Test get_conn_retry_info returns connection retry config""" + def test_get_connection_retry_info(self, mock_get_settings): + """Test get_connection_retry_info returns connection retry config""" mock_get_settings.return_value = self._create_mock_settings() mysql = MySQL() - conn_retry_info = mysql.get_conn_retry_info() + conn_retry_info = mysql.get_connection_retry_info() - assert conn_retry_info is mysql._conn_retry_config + assert conn_retry_info is mysql._connection_retry_config assert hasattr(conn_retry_info, 'enable_retry') assert hasattr(conn_retry_info, 'max_retries') @patch('ddcDatabases.mysql.get_mysql_settings') - def test_get_op_retry_info(self, mock_get_settings): - """Test get_op_retry_info returns operation retry config""" + def test_get_operation_retry_info(self, mock_get_settings): + """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() mysql = MySQL() - op_retry_info = mysql.get_op_retry_info() + op_retry_info = mysql.get_operation_retry_info() - assert op_retry_info is mysql._op_retry_config + assert op_retry_info is mysql._operation_retry_config assert hasattr(op_retry_info, 'enable_retry') assert hasattr(op_retry_info, 'max_retries') assert hasattr(op_retry_info, 'jitter') diff --git a/tests/unit/mysql/test_mysql_persistent.py b/tests/unit/mysql/test_mysql_persistent.py new file mode 100644 index 0000000..5839df5 --- /dev/null +++ b/tests/unit/mysql/test_mysql_persistent.py @@ -0,0 +1,142 @@ +"""Tests for MySQL persistent connections.""" + +from ddcDatabases.core.configs import BaseRetryConfig as RetryConfig + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + MySQLPersistent, + PersistentConnectionConfig, + PersistentSQLAlchemyAsyncConnection, + PersistentSQLAlchemyConnection, + close_all_persistent_connections, +) + +TEST_CONFIG = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=False, +) + + +class TestMySQLPersistent: + """Test MySQL persistent connection specifics.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_connection_creates_correct_url(self): + """Test MySQL sync persistent connection URL formation.""" + conn = MySQLPersistent( + host="testhost", + port=3307, + user="testuser", + password="testpass", + database="testdb", + async_mode=False, + ) + assert isinstance(conn, PersistentSQLAlchemyConnection) + assert "mysql" in conn.connection_key + assert "testhost" in conn.connection_key + assert "3307" in conn.connection_key + + def test_async_connection_creates_correct_url(self): + """Test MySQL async persistent connection URL formation.""" + conn = MySQLPersistent( + host="testhost", + port=3306, + user="testuser", + password="testpass", + database="testdb", + async_mode=True, + ) + assert isinstance(conn, PersistentSQLAlchemyAsyncConnection) + assert "mysql" in conn.connection_key + assert "testhost" in conn.connection_key + + def test_default_port(self): + """Test MySQL default port is used when not specified.""" + conn = MySQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + assert "3306" in conn.connection_key + + def test_localhost_normalization(self): + """Test that localhost is kept as-is for MySQL persistent connections.""" + conn = MySQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + assert "localhost" in conn.connection_key or "127.0.0.1" in conn.connection_key + + def test_singleton_pattern(self): + """Test that same params return same instance.""" + conn1 = MySQLPersistent( + host="localhost", + port=3306, + user="test", + password="test", + database="singleton_test", + ) + conn2 = MySQLPersistent( + host="localhost", + port=3306, + user="test", + password="test", + database="singleton_test", + ) + assert conn1 is conn2 + + def test_different_databases_return_different_instances(self): + """Test that different databases return different instances.""" + conn1 = MySQLPersistent( + host="localhost", + user="test", + password="test", + database="db1", + ) + conn2 = MySQLPersistent( + host="localhost", + user="test", + password="test", + database="db2", + ) + assert conn1 is not conn2 + + def test_custom_config(self): + """Test MySQL persistent with custom config.""" + config = PersistentConnectionConfig(idle_timeout=600) + conn_retry_config = RetryConfig(max_retries=5) + + conn = MySQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + config=config, + connection_retry_config=conn_retry_config, + ) + assert conn._config.idle_timeout == 600 + assert conn._connection_retry_config.max_retries == 5 + + def test_connection_key_format(self): + """Test that connection key has correct format.""" + conn = MySQLPersistent( + host="myhost", + port=3306, + user="myuser", + password="mypass", + database="mydb", + ) + assert conn.connection_key == "mysql://myuser@myhost:3306/mydb" diff --git a/tests/unit/mysql/test_mysql_ssl.py b/tests/unit/mysql/test_mysql_ssl.py new file mode 100644 index 0000000..19279d8 --- /dev/null +++ b/tests/unit/mysql/test_mysql_ssl.py @@ -0,0 +1,98 @@ +"""Tests for MySQL SSL configuration.""" + +import pytest + + +class TestMySQLSSLConfig: + """Test MySQL SSL configuration validation.""" + + def test_valid_ssl_modes(self): + """Test all valid MySQL SSL modes.""" + from ddcDatabases.core.constants import MYSQL_SSL_MODES + from ddcDatabases.mysql import MySQLSSLConfig + + for mode in MYSQL_SSL_MODES: + config = MySQLSSLConfig(ssl_mode=mode) + assert config.ssl_mode == mode + + def test_invalid_ssl_mode_raises_error(self): + """Test that invalid SSL mode raises ValueError.""" + from ddcDatabases.mysql import MySQLSSLConfig + + with pytest.raises(ValueError, match="ssl_mode must be one of"): + MySQLSSLConfig(ssl_mode="invalid_mode") + + def test_ssl_config_with_all_paths(self): + """Test SSL config with all certificate paths.""" + from ddcDatabases.mysql import MySQLSSLConfig + + config = MySQLSSLConfig( + ssl_mode="VERIFY_IDENTITY", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.pem", + ssl_client_key_path="/path/to/client-key.pem", + ) + assert config.ssl_mode == "VERIFY_IDENTITY" + assert config.ssl_ca_cert_path == "/path/to/ca.pem" + assert config.ssl_client_cert_path == "/path/to/client.pem" + assert config.ssl_client_key_path == "/path/to/client-key.pem" + + def test_ssl_config_immutability(self): + """Test that SSL config is immutable (frozen).""" + from ddcDatabases.mysql import MySQLSSLConfig + + config = MySQLSSLConfig(ssl_mode="REQUIRED") + with pytest.raises(AttributeError): + config.ssl_mode = "DISABLED" # noqa + + def test_ssl_mode_none_by_default(self): + """Test default SSL mode is None (not set).""" + from ddcDatabases.mysql import MySQLSSLConfig + + config = MySQLSSLConfig() + assert config.ssl_mode is None + + def test_ssl_modes_case_insensitive_validation(self): + """Test that MySQL SSL mode validation is case-insensitive.""" + from ddcDatabases.mysql import MySQLSSLConfig + + config = MySQLSSLConfig(ssl_mode="required") + assert config.ssl_mode == "required" + + def test_preferred_mode(self): + """Test PREFERRED SSL mode.""" + from ddcDatabases.mysql import MySQLSSLConfig + + config = MySQLSSLConfig(ssl_mode="PREFERRED") + assert config.ssl_mode == "PREFERRED" + + def test_verify_ca_mode(self): + """Test VERIFY_CA SSL mode.""" + from ddcDatabases.mysql import MySQLSSLConfig + + config = MySQLSSLConfig( + ssl_mode="VERIFY_CA", + ssl_ca_cert_path="/path/to/ca.pem", + ) + assert config.ssl_mode == "VERIFY_CA" + + def test_all_ssl_modes_are_valid(self): + """Test that all documented SSL modes are accepted.""" + from ddcDatabases.mysql import MySQLSSLConfig + + valid_modes = ["DISABLED", "PREFERRED", "REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"] + for mode in valid_modes: + config = MySQLSSLConfig(ssl_mode=mode) + assert config.ssl_mode == mode + + def test_ssl_ca_cert_path_only(self): + """Test SSL config with only CA certificate path.""" + from ddcDatabases.mysql import MySQLSSLConfig + + config = MySQLSSLConfig( + ssl_mode="VERIFY_CA", + ssl_ca_cert_path="/path/to/ca.pem", + ) + assert config.ssl_ca_cert_path == "/path/to/ca.pem" + assert config.ssl_client_cert_path is None + assert config.ssl_client_key_path is None diff --git a/tests/unit/test_oracle.py b/tests/unit/oracle/test_oracle.py similarity index 94% rename from tests/unit/test_oracle.py rename to tests/unit/oracle/test_oracle.py index 5c31f62..f2614fc 100644 --- a/tests/unit/test_oracle.py +++ b/tests/unit/oracle/test_oracle.py @@ -29,16 +29,16 @@ def _create_mock_settings(self, **overrides): "ssl_enabled": False, "ssl_wallet_path": None, # Connection retry settings - "conn_enable_retry": True, - "conn_max_retries": 3, - "conn_initial_retry_delay": 1.0, - "conn_max_retry_delay": 30.0, + "connection_enable_retry": True, + "connection_max_retries": 3, + "connection_initial_retry_delay": 1.0, + "connection_max_retry_delay": 30.0, # Operation retry settings - "op_enable_retry": True, - "op_max_retries": 3, - "op_initial_retry_delay": 0.5, - "op_max_retry_delay": 10.0, - "op_jitter": 0.1, + "operation_enable_retry": True, + "operation_max_retries": 3, + "operation_initial_retry_delay": 0.5, + "operation_max_retry_delay": 10.0, + "operation_jitter": 0.1, } defaults.update(overrides) for key, value in defaults.items(): @@ -342,26 +342,26 @@ def test_get_session_info(self, mock_get_settings): assert session_info.autoflush == False @patch('ddcDatabases.oracle.get_oracle_settings') - def test_get_conn_retry_info(self, mock_get_settings): - """Test get_conn_retry_info returns connection retry config""" + def test_get_connection_retry_info(self, mock_get_settings): + """Test get_connection_retry_info returns connection retry config""" mock_get_settings.return_value = self._create_mock_settings() oracle = Oracle() - conn_retry_info = oracle.get_conn_retry_info() + conn_retry_info = oracle.get_connection_retry_info() - assert conn_retry_info is oracle._conn_retry_config + assert conn_retry_info is oracle._connection_retry_config assert hasattr(conn_retry_info, 'enable_retry') assert hasattr(conn_retry_info, 'max_retries') @patch('ddcDatabases.oracle.get_oracle_settings') - def test_get_op_retry_info(self, mock_get_settings): - """Test get_op_retry_info returns operation retry config""" + def test_get_operation_retry_info(self, mock_get_settings): + """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() oracle = Oracle() - op_retry_info = oracle.get_op_retry_info() + op_retry_info = oracle.get_operation_retry_info() - assert op_retry_info is oracle._op_retry_config + assert op_retry_info is oracle._operation_retry_config assert hasattr(op_retry_info, 'enable_retry') assert hasattr(op_retry_info, 'max_retries') assert hasattr(op_retry_info, 'jitter') diff --git a/tests/unit/oracle/test_oracle_persistent.py b/tests/unit/oracle/test_oracle_persistent.py new file mode 100644 index 0000000..161882a --- /dev/null +++ b/tests/unit/oracle/test_oracle_persistent.py @@ -0,0 +1,102 @@ +"""Tests for Oracle persistent connections.""" + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + OraclePersistent, + PersistentSQLAlchemyConnection, + close_all_persistent_connections, +) + + +class TestOraclePersistent: + """Test Oracle persistent connection specifics.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_connection_creates_correct_url(self): + """Test Oracle sync persistent connection URL formation.""" + conn = OraclePersistent( + host="testhost", + port=1522, + user="testuser", + password="testpass", + servicename="testservice", + ) + assert isinstance(conn, PersistentSQLAlchemyConnection) + assert "oracle" in conn.connection_key + + def test_oracle_only_supports_sync(self): + """Test Oracle persistent connection only supports sync mode.""" + # Oracle doesn't have an async_mode parameter - it only supports sync + conn = OraclePersistent( + host="testhost", + port=1521, + user="testuser", + password="testpass", + servicename="testservice", + ) + # Always returns sync connection + assert isinstance(conn, PersistentSQLAlchemyConnection) + assert "oracle" in conn.connection_key + assert "testhost" in conn.connection_key + + def test_default_port(self): + """Test Oracle default port is used when not specified.""" + conn = OraclePersistent( + host="localhost", + user="test", + password="test", + servicename="xe", + ) + assert "1521" in conn.connection_key + + def test_singleton_pattern(self): + """Test that same params return same instance.""" + conn1 = OraclePersistent( + host="localhost", + port=1521, + user="test", + password="test", + servicename="singleton_test", + ) + conn2 = OraclePersistent( + host="localhost", + port=1521, + user="test", + password="test", + servicename="singleton_test", + ) + assert conn1 is conn2 + + def test_different_servicenames_return_different_instances(self): + """Test that different service names return different instances.""" + conn1 = OraclePersistent( + host="localhost", + user="test", + password="test", + servicename="service1", + ) + conn2 = OraclePersistent( + host="localhost", + user="test", + password="test", + servicename="service2", + ) + assert conn1 is not conn2 + + def test_connection_key_format(self): + """Test that connection key has correct format.""" + conn = OraclePersistent( + host="myhost", + port=1521, + user="myuser", + password="mypass", + servicename="myservice", + ) + assert conn.connection_key == "oracle://myuser@myhost:1521/myservice" diff --git a/tests/unit/oracle/test_oracle_ssl.py b/tests/unit/oracle/test_oracle_ssl.py new file mode 100644 index 0000000..4baeff7 --- /dev/null +++ b/tests/unit/oracle/test_oracle_ssl.py @@ -0,0 +1,57 @@ +"""Tests for Oracle SSL configuration.""" + +import pytest + + +class TestOracleSSLConfig: + """Test Oracle SSL configuration validation.""" + + def test_default_values_are_none(self): + """Test Oracle SSL config default values are None.""" + from ddcDatabases.oracle import OracleSSLConfig + + config = OracleSSLConfig() + assert config.ssl_enabled is None + assert config.ssl_wallet_path is None + + def test_ssl_enabled_with_wallet(self): + """Test Oracle SSL enabled with wallet path.""" + from ddcDatabases.oracle import OracleSSLConfig + + config = OracleSSLConfig( + ssl_enabled=True, + ssl_wallet_path="/path/to/wallet", + ) + assert config.ssl_enabled is True + assert config.ssl_wallet_path == "/path/to/wallet" + + def test_ssl_config_immutability(self): + """Test that SSL config is immutable (frozen).""" + from ddcDatabases.oracle import OracleSSLConfig + + config = OracleSSLConfig(ssl_enabled=True) + with pytest.raises(AttributeError): + config.ssl_enabled = False # noqa + + def test_ssl_disabled(self): + """Test Oracle SSL disabled configuration.""" + from ddcDatabases.oracle import OracleSSLConfig + + config = OracleSSLConfig(ssl_enabled=False) + assert config.ssl_enabled is False + + def test_ssl_enabled_without_wallet(self): + """Test Oracle SSL enabled without wallet path.""" + from ddcDatabases.oracle import OracleSSLConfig + + config = OracleSSLConfig(ssl_enabled=True) + assert config.ssl_enabled is True + assert config.ssl_wallet_path is None + + def test_wallet_path_only(self): + """Test Oracle SSL config with only wallet path.""" + from ddcDatabases.oracle import OracleSSLConfig + + config = OracleSSLConfig(ssl_wallet_path="/path/to/wallet") + assert config.ssl_wallet_path == "/path/to/wallet" + assert config.ssl_enabled is None diff --git a/tests/unit/test_postgresql.py b/tests/unit/postgresql/test_postgresql.py similarity index 92% rename from tests/unit/test_postgresql.py rename to tests/unit/postgresql/test_postgresql.py index 6edc609..9c9c1a0 100644 --- a/tests/unit/test_postgresql.py +++ b/tests/unit/postgresql/test_postgresql.py @@ -508,15 +508,15 @@ def test_enhanced_configuration_methods(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = None - mock_settings.conn_max_retries = None - mock_settings.conn_initial_retry_delay = None - mock_settings.conn_max_retry_delay = None - mock_settings.op_enable_retry = None - mock_settings.op_max_retries = None - mock_settings.op_initial_retry_delay = None - mock_settings.op_max_retry_delay = None - mock_settings.op_jitter = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None mock_get_settings.return_value = mock_settings postgresql = PostgreSQL( @@ -577,15 +577,15 @@ def test_get_engine_method_with_psycopg(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = None - mock_settings.conn_max_retries = None - mock_settings.conn_initial_retry_delay = None - mock_settings.conn_max_retry_delay = None - mock_settings.op_enable_retry = None - mock_settings.op_max_retries = None - mock_settings.op_initial_retry_delay = None - mock_settings.op_max_retry_delay = None - mock_settings.op_jitter = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=True)) @@ -623,15 +623,15 @@ def test_get_engine_method_without_autocommit(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = None - mock_settings.conn_max_retries = None - mock_settings.conn_initial_retry_delay = None - mock_settings.conn_max_retry_delay = None - mock_settings.op_enable_retry = None - mock_settings.op_max_retries = None - mock_settings.op_initial_retry_delay = None - mock_settings.op_max_retry_delay = None - mock_settings.op_jitter = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=False)) @@ -668,15 +668,15 @@ def test_get_engine_method_non_psycopg_driver(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = None - mock_settings.conn_max_retries = None - mock_settings.conn_initial_retry_delay = None - mock_settings.conn_max_retry_delay = None - mock_settings.op_enable_retry = None - mock_settings.op_max_retries = None - mock_settings.op_initial_retry_delay = None - mock_settings.op_max_retry_delay = None - mock_settings.op_jitter = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -1116,15 +1116,15 @@ def test_get_connection_info(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = True - mock_settings.conn_max_retries = 3 - mock_settings.conn_initial_retry_delay = 1.0 - mock_settings.conn_max_retry_delay = 30.0 - mock_settings.op_enable_retry = True - mock_settings.op_max_retries = 3 - mock_settings.op_initial_retry_delay = 1.0 - mock_settings.op_max_retry_delay = 30.0 - mock_settings.op_jitter = 0.1 + mock_settings.connection_enable_retry = True + mock_settings.connection_max_retries = 3 + mock_settings.connection_initial_retry_delay = 1.0 + mock_settings.connection_max_retry_delay = 30.0 + mock_settings.operation_enable_retry = True + mock_settings.operation_max_retries = 3 + mock_settings.operation_initial_retry_delay = 1.0 + mock_settings.operation_max_retry_delay = 30.0 + mock_settings.operation_jitter = 0.1 mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -1162,15 +1162,15 @@ def test_get_pool_info(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = True - mock_settings.conn_max_retries = 3 - mock_settings.conn_initial_retry_delay = 1.0 - mock_settings.conn_max_retry_delay = 30.0 - mock_settings.op_enable_retry = True - mock_settings.op_max_retries = 3 - mock_settings.op_initial_retry_delay = 1.0 - mock_settings.op_max_retry_delay = 30.0 - mock_settings.op_jitter = 0.1 + mock_settings.connection_enable_retry = True + mock_settings.connection_max_retries = 3 + mock_settings.connection_initial_retry_delay = 1.0 + mock_settings.connection_max_retry_delay = 30.0 + mock_settings.operation_enable_retry = True + mock_settings.operation_max_retries = 3 + mock_settings.operation_initial_retry_delay = 1.0 + mock_settings.operation_max_retry_delay = 30.0 + mock_settings.operation_jitter = 0.1 mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(pool_config=PostgreSQLPoolConfig(pool_size=20, max_overflow=40)) @@ -1208,15 +1208,15 @@ def test_get_session_info(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = True - mock_settings.conn_max_retries = 3 - mock_settings.conn_initial_retry_delay = 1.0 - mock_settings.conn_max_retry_delay = 30.0 - mock_settings.op_enable_retry = True - mock_settings.op_max_retries = 3 - mock_settings.op_initial_retry_delay = 1.0 - mock_settings.op_max_retry_delay = 30.0 - mock_settings.op_jitter = 0.1 + mock_settings.connection_enable_retry = True + mock_settings.connection_max_retries = 3 + mock_settings.connection_initial_retry_delay = 1.0 + mock_settings.connection_max_retry_delay = 30.0 + mock_settings.operation_enable_retry = True + mock_settings.operation_max_retries = 3 + mock_settings.operation_initial_retry_delay = 1.0 + mock_settings.operation_max_retry_delay = 30.0 + mock_settings.operation_jitter = 0.1 mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(echo=True, autoflush=False)) @@ -1229,8 +1229,8 @@ def test_get_session_info(self, mock_get_settings): assert session_info.autoflush == False @patch('ddcDatabases.postgresql.get_postgresql_settings') - def test_get_op_retry_info(self, mock_get_settings): - """Test get_op_retry_info returns operation retry config""" + def test_get_operation_retry_info(self, mock_get_settings): + """Test get_operation_retry_info returns operation retry config""" from ddcDatabases.postgresql import PostgreSQL mock_settings = MagicMock() @@ -1254,22 +1254,22 @@ def test_get_op_retry_info(self, mock_get_settings): mock_settings.ssl_ca_cert_path = None mock_settings.ssl_client_cert_path = None mock_settings.ssl_client_key_path = None - mock_settings.conn_enable_retry = True - mock_settings.conn_max_retries = 3 - mock_settings.conn_initial_retry_delay = 1.0 - mock_settings.conn_max_retry_delay = 30.0 - mock_settings.op_enable_retry = True - mock_settings.op_max_retries = 3 - mock_settings.op_initial_retry_delay = 1.0 - mock_settings.op_max_retry_delay = 30.0 - mock_settings.op_jitter = 0.1 + mock_settings.connection_enable_retry = True + mock_settings.connection_max_retries = 3 + mock_settings.connection_initial_retry_delay = 1.0 + mock_settings.connection_max_retry_delay = 30.0 + mock_settings.operation_enable_retry = True + mock_settings.operation_max_retries = 3 + mock_settings.operation_initial_retry_delay = 1.0 + mock_settings.operation_max_retry_delay = 30.0 + mock_settings.operation_jitter = 0.1 mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() - op_retry_info = postgresql.get_op_retry_info() + op_retry_info = postgresql.get_operation_retry_info() - assert op_retry_info is postgresql._op_retry_config + assert op_retry_info is postgresql._operation_retry_config assert hasattr(op_retry_info, 'enable_retry') assert hasattr(op_retry_info, 'max_retries') assert hasattr(op_retry_info, 'jitter') diff --git a/tests/unit/postgresql/test_postgresql_persistent.py b/tests/unit/postgresql/test_postgresql_persistent.py new file mode 100644 index 0000000..925900d --- /dev/null +++ b/tests/unit/postgresql/test_postgresql_persistent.py @@ -0,0 +1,183 @@ +"""Tests for PostgreSQL persistent connections.""" + +import pytest +from ddcDatabases.core.configs import BaseRetryConfig as RetryConfig + +# noinspection PyProtectedMember +from ddcDatabases.core.persistent import ( + PersistentConnectionConfig, + PersistentSQLAlchemyAsyncConnection, + PersistentSQLAlchemyConnection, + PostgreSQLPersistent, + close_all_persistent_connections, +) +from unittest.mock import AsyncMock, MagicMock, patch + +TEST_CONFIG = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=False, +) + + +class TestPostgreSQLPersistent: + """Test PostgreSQL persistent connection specifics.""" + + def setup_method(self): + """Clear persistent connections before each test.""" + close_all_persistent_connections() + + def teardown_method(self): + """Clean up after each test.""" + close_all_persistent_connections() + + def test_sync_connection_creates_correct_url(self): + """Test PostgreSQL sync persistent connection URL formation.""" + conn = PostgreSQLPersistent( + host="testhost", + port=5433, + user="testuser", + password="testpass", + database="testdb", + async_mode=False, + ) + assert isinstance(conn, PersistentSQLAlchemyConnection) + assert "postgresql" in conn.connection_key + assert "testhost" in conn.connection_key + assert "5433" in conn.connection_key + assert "testdb" in conn.connection_key + + def test_async_connection_creates_correct_url(self): + """Test PostgreSQL async persistent connection URL formation.""" + conn = PostgreSQLPersistent( + host="testhost", + port=5433, + user="testuser", + password="testpass", + database="testdb", + async_mode=True, + ) + assert isinstance(conn, PersistentSQLAlchemyAsyncConnection) + assert "postgresql" in conn.connection_key + assert "testhost" in conn.connection_key + + def test_default_port(self): + """Test PostgreSQL default port is used when not specified.""" + conn = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + assert "5432" in conn.connection_key + + def test_custom_config(self): + """Test PostgreSQL persistent with custom config.""" + config = PersistentConnectionConfig(idle_timeout=600) + conn_retry_config = RetryConfig(max_retries=5) + + conn = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + config=config, + connection_retry_config=conn_retry_config, + ) + assert conn._config.idle_timeout == 600 + assert conn._connection_retry_config.max_retries == 5 + + def test_singleton_pattern(self): + """Test that same params return same instance.""" + conn1 = PostgreSQLPersistent( + host="localhost", + port=5432, + user="test", + password="test", + database="singleton_test", + ) + conn2 = PostgreSQLPersistent( + host="localhost", + port=5432, + user="test", + password="test", + database="singleton_test", + ) + assert conn1 is conn2 + + def test_different_databases_return_different_instances(self): + """Test that different databases return different instances.""" + conn1 = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="db1", + ) + conn2 = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="db2", + ) + assert conn1 is not conn2 + + def test_different_hosts_return_different_instances(self): + """Test that different hosts return different instances.""" + conn1 = PostgreSQLPersistent( + host="host1", + user="test", + password="test", + database="testdb", + ) + conn2 = PostgreSQLPersistent( + host="host2", + user="test", + password="test", + database="testdb", + ) + assert conn1 is not conn2 + + def test_context_manager_sync(self): + """Test PostgreSQL sync persistent connection context manager.""" + conn = PersistentSQLAlchemyConnection( + connection_key="postgresql://test@localhost:5432/testdb", + connection_url="sqlite:///:memory:", + config=TEST_CONFIG, + ) + + with conn as session: + assert session is not None + assert conn.is_connected + + conn.shutdown() + + @pytest.mark.asyncio + async def test_context_manager_async(self): + """Test PostgreSQL async persistent connection context manager.""" + mock_engine = MagicMock() + mock_session = AsyncMock() + mock_session.execute = AsyncMock() + + with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_engine', return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_session', return_value=mock_session): + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="postgresql://test@localhost:5432/testdb", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + config=TEST_CONFIG, + ) + + async with conn as session: + assert session is mock_session + assert conn.is_connected + + def test_connection_key_format(self): + """Test that connection key has correct format.""" + conn = PostgreSQLPersistent( + host="myhost", + port=5432, + user="myuser", + password="mypass", + database="mydb", + ) + assert conn.connection_key == "postgresql://myuser@myhost:5432/mydb" diff --git a/tests/unit/postgresql/test_postgresql_ssl.py b/tests/unit/postgresql/test_postgresql_ssl.py new file mode 100644 index 0000000..1bd0146 --- /dev/null +++ b/tests/unit/postgresql/test_postgresql_ssl.py @@ -0,0 +1,102 @@ +"""Tests for PostgreSQL SSL configuration.""" + +import pytest + + +class TestPostgreSQLSSLConfig: + """Test PostgreSQL SSL configuration validation.""" + + def test_valid_ssl_modes(self): + """Test all valid PostgreSQL SSL modes.""" + from ddcDatabases.core.constants import POSTGRESQL_SSL_MODES + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + for mode in POSTGRESQL_SSL_MODES: + config = PostgreSQLSSLConfig(ssl_mode=mode) + assert config.ssl_mode == mode + + def test_invalid_ssl_mode_raises_error(self): + """Test that invalid SSL mode raises ValueError.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + with pytest.raises(ValueError, match="ssl_mode must be one of"): + PostgreSQLSSLConfig(ssl_mode="invalid_mode") + + def test_ssl_config_with_all_paths(self): + """Test SSL config with all certificate paths.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + config = PostgreSQLSSLConfig( + ssl_mode="verify-full", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.pem", + ssl_client_key_path="/path/to/client-key.pem", + ) + assert config.ssl_mode == "verify-full" + assert config.ssl_ca_cert_path == "/path/to/ca.pem" + assert config.ssl_client_cert_path == "/path/to/client.pem" + assert config.ssl_client_key_path == "/path/to/client-key.pem" + + def test_ssl_config_immutability(self): + """Test that SSL config is immutable (frozen).""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + config = PostgreSQLSSLConfig(ssl_mode="require") + with pytest.raises(AttributeError): + config.ssl_mode = "disable" # noqa + + def test_ssl_mode_none_by_default(self): + """Test default SSL mode is None (not set).""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + config = PostgreSQLSSLConfig() + assert config.ssl_mode is None + + def test_ssl_modes_case_insensitive_validation(self): + """Test that PostgreSQL SSL mode validation is case-insensitive.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + config = PostgreSQLSSLConfig(ssl_mode="REQUIRE") + assert config.ssl_mode == "REQUIRE" + + def test_verify_ca_mode(self): + """Test verify-ca SSL mode.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + config = PostgreSQLSSLConfig( + ssl_mode="verify-ca", + ssl_ca_cert_path="/path/to/ca.pem", + ) + assert config.ssl_mode == "verify-ca" + + def test_all_ssl_modes_are_valid(self): + """Test that all documented SSL modes are accepted.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + valid_modes = ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"] + for mode in valid_modes: + config = PostgreSQLSSLConfig(ssl_mode=mode) + assert config.ssl_mode == mode + + def test_ssl_ca_cert_path_only(self): + """Test SSL config with only CA certificate path.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + config = PostgreSQLSSLConfig( + ssl_mode="verify-ca", + ssl_ca_cert_path="/path/to/ca.pem", + ) + assert config.ssl_ca_cert_path == "/path/to/ca.pem" + assert config.ssl_client_cert_path is None + assert config.ssl_client_key_path is None + + def test_ssl_client_cert_without_key(self): + """Test SSL config with client cert but no key.""" + from ddcDatabases.postgresql import PostgreSQLSSLConfig + + config = PostgreSQLSSLConfig( + ssl_mode="verify-full", + ssl_client_cert_path="/path/to/client.pem", + ) + assert config.ssl_client_cert_path == "/path/to/client.pem" + assert config.ssl_client_key_path is None diff --git a/tests/unit/sqlite/__init__.py b/tests/unit/sqlite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_sqlite.py b/tests/unit/sqlite/test_sqlite.py similarity index 92% rename from tests/unit/test_sqlite.py rename to tests/unit/sqlite/test_sqlite.py index c66094e..581e780 100644 --- a/tests/unit/test_sqlite.py +++ b/tests/unit/sqlite/test_sqlite.py @@ -36,15 +36,15 @@ def test_init_basic(self, mock_get_settings): mock_settings = MagicMock() mock_settings.file_path = "sqlite.db" mock_settings.echo = False - mock_settings.conn_enable_retry = False - mock_settings.conn_max_retries = 1 - mock_settings.conn_initial_retry_delay = 1.0 - mock_settings.conn_max_retry_delay = 30.0 - mock_settings.op_enable_retry = False - mock_settings.op_max_retries = 1 - mock_settings.op_initial_retry_delay = 0.5 - mock_settings.op_max_retry_delay = 10.0 - mock_settings.op_jitter = 0.1 + mock_settings.connection_enable_retry = False + mock_settings.connection_max_retries = 1 + mock_settings.connection_initial_retry_delay = 1.0 + mock_settings.connection_max_retry_delay = 30.0 + mock_settings.operation_enable_retry = False + mock_settings.operation_max_retries = 1 + mock_settings.operation_initial_retry_delay = 0.5 + mock_settings.operation_max_retry_delay = 10.0 + mock_settings.operation_jitter = 0.1 mock_get_settings.return_value = mock_settings sqlite = self.Sqlite() @@ -59,15 +59,15 @@ def test_init_with_parameters(self, mock_get_settings): mock_settings = MagicMock() mock_settings.file_path = "default.db" mock_settings.echo = False - mock_settings.conn_enable_retry = False - mock_settings.conn_max_retries = 1 - mock_settings.conn_initial_retry_delay = 1.0 - mock_settings.conn_max_retry_delay = 30.0 - mock_settings.op_enable_retry = False - mock_settings.op_max_retries = 1 - mock_settings.op_initial_retry_delay = 0.5 - mock_settings.op_max_retry_delay = 10.0 - mock_settings.op_jitter = 0.1 + mock_settings.connection_enable_retry = False + mock_settings.connection_max_retries = 1 + mock_settings.connection_initial_retry_delay = 1.0 + mock_settings.connection_max_retry_delay = 30.0 + mock_settings.operation_enable_retry = False + mock_settings.operation_max_retries = 1 + mock_settings.operation_initial_retry_delay = 0.5 + mock_settings.operation_max_retry_delay = 10.0 + mock_settings.operation_jitter = 0.1 mock_get_settings.return_value = mock_settings sqlite = self.Sqlite( diff --git a/uv.lock b/uv.lock index 8f56ada..c02958d 100644 --- a/uv.lock +++ b/uv.lock @@ -460,7 +460,7 @@ dev = [ { name = "poethepoet", specifier = ">=0.40.0" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, - { name = "ruff", specifier = ">=0.14.14" }, + { name = "ruff", specifier = ">=0.15.0" }, { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], specifier = ">=4.14.1" }, ] test = [ @@ -1151,28 +1151,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.14" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] From f7bb48acdb54eb79b6749d5a6430a12fc646fe0a Mon Sep 17 00:00:00 2001 From: ddc Date: Wed, 4 Feb 2026 13:56:18 -0300 Subject: [PATCH 07/18] v3.0.9 --- pyproject.toml | 8 +-- uv.lock | 144 ++++++++++++++++++++++++++----------------------- 2 files changed, 80 insertions(+), 72 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aca954e..a57f671 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,8 @@ license = {text = "MIT"} readme = "README.md" authors = [{name = "Daniel Costa", email = "danieldcsta@gmail.com"}] maintainers = [{name = "Daniel Costa"}] +urls.Repository = "https://github.com/ddc/ddcDatabases" +urls.Homepage = "https://pypi.org/project/ddcDatabases" keywords = [ "python3", "python-3", "python", "databases", "database", "ddcDatabases", @@ -40,10 +42,6 @@ dependencies = [ "sqlalchemy[asyncio]>=2.0.46", ] -[project.urls] -Homepage = "https://pypi.org/project/ddcDatabases" -Repository = "https://github.com/ddc/ddcDatabases" - [project.optional-dependencies] mongodb = ["motor>=3.7.1"] mssql = ["pyodbc>=5.3.0", "aioodbc>=0.5.0"] @@ -61,6 +59,8 @@ all = [ ] test = [ {include-group = "all"}, + "pytest>=9.0.2", + "coverage>=7.13.3", "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", "testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.1", diff --git a/uv.lock b/uv.lock index c02958d..9346a79 100644 --- a/uv.lock +++ b/uv.lock @@ -253,76 +253,76 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.2" +version = "7.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, - { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, - { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, - { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, - { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, - { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, - { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, - { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, - { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, - { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, - { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, - { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, - { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, - { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, - { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, - { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, - { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, - { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, - { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, - { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, - { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, - { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, - { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, - { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, - { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, - { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, - { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, - { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, - { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, - { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, - { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, - { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, - { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, - { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, - { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, - { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, - { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, - { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, - { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, + { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, + { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, + { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, + { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, + { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, + { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, ] [[package]] @@ -413,15 +413,19 @@ all = [ ] dev = [ { name = "black" }, + { name = "coverage" }, { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, { name = "poethepoet" }, + { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, { name = "testcontainers", extra = ["mongodb", "mssql", "mysql", "oracle"] }, ] test = [ + { name = "coverage" }, { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, + { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "testcontainers", extra = ["mongodb", "mssql", "mysql", "oracle"] }, @@ -452,23 +456,27 @@ all = [ ] dev = [ { name = "black", specifier = ">=26.1.0" }, + { name = "coverage", specifier = ">=7.13.3" }, { name = "ddcdatabases", extras = ["mongodb"] }, { name = "ddcdatabases", extras = ["mssql"] }, { name = "ddcdatabases", extras = ["mysql"] }, { name = "ddcdatabases", extras = ["oracle"] }, { name = "ddcdatabases", extras = ["pgsql"] }, { name = "poethepoet", specifier = ">=0.40.0" }, + { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.15.0" }, { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], specifier = ">=4.14.1" }, ] test = [ + { name = "coverage", specifier = ">=7.13.3" }, { name = "ddcdatabases", extras = ["mongodb"] }, { name = "ddcdatabases", extras = ["mssql"] }, { name = "ddcdatabases", extras = ["mysql"] }, { name = "ddcdatabases", extras = ["oracle"] }, { name = "ddcdatabases", extras = ["pgsql"] }, + { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], specifier = ">=4.14.1" }, From d9302984747220e15aee0ce41b76c05a14e5f0e2 Mon Sep 17 00:00:00 2001 From: ddc Date: Wed, 4 Feb 2026 14:47:34 -0300 Subject: [PATCH 08/18] v3.0.9 --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a57f671..8919946 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,8 +60,8 @@ all = [ test = [ {include-group = "all"}, "pytest>=9.0.2", - "coverage>=7.13.3", "pytest-asyncio>=1.3.0", + "coverage>=7.13.3", "pytest-cov>=7.0.0", "testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.1", ] @@ -97,6 +97,9 @@ python_classes = ["Test*"] python_functions = ["test_*"] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "ignore:Field name \"schema\" in .* shadows an attribute in parent:UserWarning", +] markers = [ "integration: integration tests requiring Docker (deselect with '-m \"not integration\"')", ] From 34a937a5d4fdb292e965e1990d94e73db08401e4 Mon Sep 17 00:00:00 2001 From: ddc Date: Thu, 5 Feb 2026 15:54:00 -0300 Subject: [PATCH 09/18] v3.0.9 --- .github/PULL_REQUEST_TEMPLATE | 18 +- .github/workflows/workflow.yml | 48 +- ddcDatabases/__init__.py | 2 +- ddcDatabases/core/operations.py | 3 +- ddcDatabases/core/persistent.py | 5 +- ddcDatabases/core/retry.py | 3 +- ddcDatabases/core/settings.py | 3 +- ddcDatabases/mssql.py | 3 +- ddcDatabases/mysql.py | 3 +- ddcDatabases/oracle.py | 3 +- ddcDatabases/postgresql.py | 3 +- pyproject.toml | 81 ++-- tests/integration/sqlite/test_sqlite.py | 2 +- tests/unit/core/test_async_functionality.py | 8 +- tests/unit/core/test_db_utils.py | 22 +- tests/unit/core/test_settings.py | 24 +- tests/unit/mongodb/test_mongodb.py | 22 +- tests/unit/mssql/test_mssql.py | 36 +- tests/unit/mysql/test_mysql.py | 12 +- tests/unit/oracle/test_oracle.py | 30 +- tests/unit/postgresql/test_postgresql.py | 33 +- tests/unit/sqlite/test_sqlite.py | 34 +- uv.lock | 466 ++++++++++++++++++-- 23 files changed, 640 insertions(+), 224 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 0251d90..e1ae10c 100755 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -3,14 +3,14 @@ ## Checklist -- [ ] If code changes were made, then they have been tested. - - [ ] I have updated the documentation to reflect the changes. -- [ ] I have thought about how this code may affect other services. -- [ ] This PR fixes an issue. -- [ ] This PR add/remove/change unit tests. -- [ ] This PR adds something new (e.g. new method or parameters). -- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed) -- [ ] This PR is **not** a code change (e.g. documentation, README, ...) +- [ ] If code changes were made, then they have been tested +- [ ] I have updated the documentation to reflect any changes made +- [ ] I have thought about how this code may affect other services +- [ ] This PR fixes an issue +- [ ] This PR is a breaking change (e.g. method, parameters, env variables) +- [ ] This PR adds something new (e.g. method, parameters, env variables) +- [ ] This PR change unit and integration tests +- [ ] This PR is **NOT** a code change (e.g. documentation, packages) ## Reviewer -- [ ] I understand that approving this code, I am also responsible for it going into the codebase. +- [ ] I understand that approving this code, I am also responsible for it going into the codebase diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 36fa826..9b470d4 100755 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -5,8 +5,22 @@ name: CI/CD Pipeline branches: ['**'] tags: ['v*'] +env: + LATEST_PYTHON_VERSION: '3.14' jobs: + lint: + name: Lint (ruff) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/ruff-action@v3 + with: + args: "check" + - uses: astral-sh/ruff-action@v3 + with: + args: "format --check" + test: name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -14,7 +28,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] - python-version: ['3.12', '3.13', '3.14'] + python-version: ['3.10', '3.14'] steps: - uses: actions/checkout@v6 @@ -37,12 +51,13 @@ jobs: uv venv if [[ '${{ matrix.os }}' == 'windows-latest' ]]; then # Skip MySQL on Windows - mysqlclient requires C compilation with MySQL headers - uv pip install -e .[mongodb,mssql,oracle,pgsql,test] - elif [[ '${{ matrix.os }}' == macos* ]]; then + uv pip install -e .[mongodb,mssql,oracle,pgsql] + uv pip install pytest pytest-asyncio coverage pytest-cov + elif [[ '${{ matrix.os }}' == 'macos-latest' ]]; then export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig" - uv sync --group test + uv sync --group dev else - uv sync --group test + uv sync --group dev fi shell: bash @@ -55,16 +70,15 @@ jobs: shell: bash - name: Upload coverage to Codecov - if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest' + if: matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v5 - name: Upload test results to Codecov - if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest' + if: matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v5 with: report_type: test_results - integration-test: name: Integration Tests runs-on: ubuntu-latest @@ -74,14 +88,14 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 - - name: Set up Python 3.14 - run: uv python install 3.14 + - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} + run: uv python install ${{ env.LATEST_PYTHON_VERSION }} - name: Install MySQL client libraries run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev pkg-config - name: Install dependencies - run: uv sync --group test + run: uv sync --group dev shell: bash - name: Install ODBC driver for MSSQL @@ -103,9 +117,9 @@ jobs: shell: bash build: - name: Build package + name: Build Package runs-on: ubuntu-latest - needs: [test, integration-test] + needs: [lint, test, integration-test] if: startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/checkout@v6 @@ -113,8 +127,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 - - name: Set up Python 3.14 - run: uv python install 3.14 + - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} + run: uv python install ${{ env.LATEST_PYTHON_VERSION }} - name: Build package run: uv build @@ -126,11 +140,10 @@ jobs: path: dist/ retention-days: 7 - release: name: Create Release runs-on: ubuntu-latest - needs: [build] + needs: build if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write @@ -151,7 +164,6 @@ jobs: prerelease: false files: release-assets/* - publish: name: Publish to PyPI runs-on: ubuntu-latest diff --git a/ddcDatabases/__init__.py b/ddcDatabases/__init__.py index 61035f4..6d24341 100755 --- a/ddcDatabases/__init__.py +++ b/ddcDatabases/__init__.py @@ -198,7 +198,7 @@ __all__ = tuple(__all__) __title__ = "ddcDatabases" __author__ = "Daniel Costa" -__email__ = "danieldcsta@gmail.com>" +__email__ = "danieldcsta@gmail.com" __license__ = "MIT" __copyright__ = "Copyright 2024-present DDC Softwares" __version__ = version(__title__) diff --git a/ddcDatabases/core/operations.py b/ddcDatabases/core/operations.py index 2e0658b..efcc9be 100644 --- a/ddcDatabases/core/operations.py +++ b/ddcDatabases/core/operations.py @@ -12,10 +12,11 @@ DBInsertSingleException, ) from .retry import retry_operation, retry_operation_async +from collections.abc import Callable, Sequence from sqlalchemy import RowMapping from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from typing import Any, Callable, Sequence, TypeVar +from typing import Any, TypeVar _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) diff --git a/ddcDatabases/core/persistent.py b/ddcDatabases/core/persistent.py index 8f7d1f2..b80fd1e 100644 --- a/ddcDatabases/core/persistent.py +++ b/ddcDatabases/core/persistent.py @@ -23,13 +23,14 @@ get_postgresql_settings, ) from abc import ABC, abstractmethod +from collections.abc import Callable from dataclasses import dataclass from sqlalchemy import text from sqlalchemy.engine import URL, Engine, create_engine from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import Session, sessionmaker -from typing import Any, Callable, Generic, Literal, TypeVar, cast, overload +from typing import Any, Generic, Literal, TypeVar, cast, overload _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) @@ -49,7 +50,7 @@ class PersistentConnectionConfig: # Global registry for persistent connections (weak references to allow cleanup) -_persistent_connections: weakref.WeakValueDictionary[str, "BasePersistentConnection | PersistentMongoDBConnection"] = ( +_persistent_connections: weakref.WeakValueDictionary[str, BasePersistentConnection | PersistentMongoDBConnection] = ( weakref.WeakValueDictionary() ) _registry_lock = threading.Lock() diff --git a/ddcDatabases/core/retry.py b/ddcDatabases/core/retry.py index 60ea980..f65db3b 100644 --- a/ddcDatabases/core/retry.py +++ b/ddcDatabases/core/retry.py @@ -6,7 +6,8 @@ import time from .configs import BaseRetryConfig from .constants import CONNECTION_ERROR_KEYWORDS -from typing import Any, Awaitable, Callable, TypeVar +from collections.abc import Awaitable, Callable +from typing import Any, TypeVar # Type variable for generic return types T = TypeVar('T') diff --git a/ddcDatabases/core/settings.py b/ddcDatabases/core/settings.py index 41d2886..550f598 100644 --- a/ddcDatabases/core/settings.py +++ b/ddcDatabases/core/settings.py @@ -1,9 +1,10 @@ from .constants import SettingsMessages as Msg +from collections.abc import Callable from dotenv import load_dotenv from functools import lru_cache from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict -from typing import Callable, TypeVar +from typing import TypeVar # Type variable for generic settings factory T = TypeVar('T', bound=BaseSettings) diff --git a/ddcDatabases/mssql.py b/ddcDatabases/mssql.py index dac17ee..88fbe38 100755 --- a/ddcDatabases/mssql.py +++ b/ddcDatabases/mssql.py @@ -8,12 +8,13 @@ BaseSessionConfig, ) from .core.settings import get_mssql_settings +from collections.abc import AsyncGenerator, Generator from contextlib import asynccontextmanager, contextmanager from dataclasses import dataclass from sqlalchemy.engine import URL, Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import Session -from typing import Any, AsyncGenerator, Generator +from typing import Any _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) diff --git a/ddcDatabases/mysql.py b/ddcDatabases/mysql.py index 51308ef..22c88b9 100755 --- a/ddcDatabases/mysql.py +++ b/ddcDatabases/mysql.py @@ -10,11 +10,12 @@ ) from .core.constants import MYSQL_SSL_MODES from .core.settings import get_mysql_settings +from collections.abc import AsyncGenerator, Generator from contextlib import asynccontextmanager, contextmanager from dataclasses import dataclass from sqlalchemy.engine import URL, Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine -from typing import Any, AsyncGenerator, Generator +from typing import Any _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) diff --git a/ddcDatabases/oracle.py b/ddcDatabases/oracle.py index 435d7ab..cf93697 100644 --- a/ddcDatabases/oracle.py +++ b/ddcDatabases/oracle.py @@ -8,11 +8,12 @@ BaseSessionConfig, ) from .core.settings import get_oracle_settings +from collections.abc import AsyncGenerator, Generator from contextlib import asynccontextmanager, contextmanager from dataclasses import dataclass from sqlalchemy.engine import URL, Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine -from typing import Any, AsyncGenerator, Generator +from typing import Any _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) diff --git a/ddcDatabases/postgresql.py b/ddcDatabases/postgresql.py index e0cf0b1..55165f6 100755 --- a/ddcDatabases/postgresql.py +++ b/ddcDatabases/postgresql.py @@ -10,12 +10,13 @@ ) from .core.constants import POSTGRESQL_SSL_MODES from .core.settings import get_postgresql_settings +from collections.abc import AsyncGenerator, Generator from contextlib import asynccontextmanager, contextmanager from dataclasses import dataclass from sqlalchemy import URL from sqlalchemy.engine import Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine -from typing import Any, AsyncGenerator, Generator +from typing import Any _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) diff --git a/pyproject.toml b/pyproject.toml index 8919946..1b9979c 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,18 +2,31 @@ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build] +include = ["ddcDatabases/**/*"] + +[tool.hatch.build.targets.wheel] +packages = ["ddcDatabases"] + [project] name = "ddcDatabases" version = "3.0.9" description = "Simplified database ORM connections with support for multiple database engines" -license = {text = "MIT"} -readme = "README.md" -authors = [{name = "Daniel Costa", email = "danieldcsta@gmail.com"}] -maintainers = [{name = "Daniel Costa"}] urls.Repository = "https://github.com/ddc/ddcDatabases" urls.Homepage = "https://pypi.org/project/ddcDatabases" +license = {text = "MIT"} +readme = "README.md" +authors = [ + {name = "Daniel Costa", email = "danieldcsta@gmail.com"}, +] +maintainers = [ + {name = "Daniel Costa"}, +] keywords = [ - "python3", "python-3", "python", + "python", "python3", "python-3", "databases", "database", "ddcDatabases", "mssql", "mssql-database", "mysql", "mysql-database", @@ -28,6 +41,8 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", @@ -36,7 +51,7 @@ classifiers = [ "Intended Audience :: Developers", "Natural Language :: English", ] -requires-python = ">=3.12" +requires-python = ">=3.10" dependencies = [ "pydantic-settings>=2.11.0", "sqlalchemy[asyncio]>=2.0.46", @@ -50,68 +65,52 @@ oracle = ["oracledb>=3.4.2"] pgsql = ["psycopg[binary]>=3.3.2", "asyncpg>=0.31.0"] [dependency-groups] -all = [ +dev = [ "ddcDatabases[mongodb]", "ddcDatabases[mssql]", "ddcDatabases[mysql]", "ddcDatabases[oracle]", "ddcDatabases[pgsql]", -] -test = [ - {include-group = "all"}, "pytest>=9.0.2", "pytest-asyncio>=1.3.0", "coverage>=7.13.3", "pytest-cov>=7.0.0", "testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.1", -] -dev = [ - {include-group = "test"}, "black>=26.1.0", "poethepoet>=0.40.0", "ruff>=0.15.0", ] -[tool.hatch.build] -include = ["ddcDatabases/**/*"] - -[tool.hatch.build.targets.wheel] -packages = ["ddcDatabases"] - [tool.poe.tasks] -build = "uv build --wheel" -updatedev.shell = "uv lock && uv sync --all-groups" linter.shell = "uv run ruff check --fix . && uv run black ." profile = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov" profile-integration = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov" test = "uv run pytest tests/unit" test-integration = "uv run pytest tests/integration --no-cov" +updatedev.sequence = ["linter", {shell = "uv lock && uv sync --all-extras --group dev"}] +build.sequence = ["updatedev", "test", "test-integration", {shell = "uv build --wheel"}] [tool.pytest.ini_options] addopts = "-v --cov --cov-report=term --cov-report=xml --junitxml=junit.xml" junit_family = "legacy" testpaths = ["tests/unit"] -pythonpath = ["."] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" +markers = [ + "integration: marks tests as integration tests", +] filterwarnings = [ "ignore:Field name \"schema\" in .* shadows an attribute in parent:UserWarning", ] -markers = [ - "integration: integration tests requiring Docker (deselect with '-m \"not integration\"')", -] - [tool.coverage.run] omit = [ "tests/*", "*/__init__.py", - "*/_version.py", ] [tool.coverage.report] +show_missing = true +skip_covered = false exclude_lines = [ "pragma: no cover", "def __repr__", @@ -124,8 +123,6 @@ exclude_lines = [ "class .*\\bProtocol\\):", "@(abc\\.)?abstractmethod", ] -show_missing = true -skip_covered = false [tool.black] line-length = 120 @@ -133,22 +130,18 @@ skip-string-normalization = true [tool.ruff] line-length = 120 +target-version = "py310" [tool.ruff.lint] -# I - Import sorting and organization -# F401 - Detect and remove unused imports -select = ["I", "F401"] +select = ["E", "W", "F", "I", "B", "C4", "UP"] +ignore = ["E501", "E402", "UP046", "UP047"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001", "F841"] [tool.ruff.lint.isort] -known-first-party = ["pythonLogs"] +known-first-party = ["ddcDatabases"] force-sort-within-sections = false from-first = false no-sections = true - -[tool.ruff.lint.per-file-ignores] -# S101 Use of `assert` detected -# S105 Possible hardcoded password assigned to variable -# S106 Possible hardcoded password assigned to argument -# S311 Standard pseudo-random generators are not suitable for cryptographic purposes -# SLF001 Private member accessed -"tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001"] diff --git a/tests/integration/sqlite/test_sqlite.py b/tests/integration/sqlite/test_sqlite.py index 2177d04..6d3bf01 100644 --- a/tests/integration/sqlite/test_sqlite.py +++ b/tests/integration/sqlite/test_sqlite.py @@ -116,6 +116,6 @@ def test_multiple_inserts(self): assert len(results) == 5 # Filter enabled - stmt = sa.select(IntegrationModel.name).where(IntegrationModel.enabled == True) + stmt = sa.select(IntegrationModel.name).where(IntegrationModel.enabled.is_(True)) results = db_utils.fetchall(stmt, as_dict=True) assert len(results) == 3 # items 0, 2, 4 diff --git a/tests/unit/core/test_async_functionality.py b/tests/unit/core/test_async_functionality.py index ead64a4..73c0c3a 100644 --- a/tests/unit/core/test_async_functionality.py +++ b/tests/unit/core/test_async_functionality.py @@ -1,13 +1,13 @@ import asyncio import pytest import sqlalchemy as sa +from collections.abc import AsyncGenerator, Generator from contextlib import asynccontextmanager, contextmanager from importlib.util import find_spec from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy.engine import Engine from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.orm import declarative_base -from typing import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch POSTGRESQL_AVAILABLE = find_spec("asyncpg") is not None and find_spec("psycopg") is not None @@ -116,7 +116,7 @@ async def test_async_context_manager_entry(self): async with conn as session: assert session is mock_session - assert conn.is_connected == True + assert conn.is_connected mock_sessionmaker.assert_called_once() mock_test_conn.assert_called_once_with(mock_session) @@ -149,7 +149,7 @@ async def test_async_context_manager_exit(self): mock_session.close.assert_called_once() mock_engine.dispose.assert_called_once() - assert conn.is_connected == False + assert not conn.is_connected @pytest.mark.skipif(not POSTGRESQL_AVAILABLE, reason="PostgreSQL drivers not available") async def test_get_async_engine(self): @@ -521,5 +521,5 @@ def test_base_connection_async_methods(self): assert inspect.iscoroutinefunction(conn._test_connection_async) # Check async generator method - method = getattr(conn, '_get_async_engine') + method = conn._get_async_engine assert inspect.ismethod(method) or inspect.isfunction(method) diff --git a/tests/unit/core/test_db_utils.py b/tests/unit/core/test_db_utils.py index a048722..f424eb0 100644 --- a/tests/unit/core/test_db_utils.py +++ b/tests/unit/core/test_db_utils.py @@ -1,5 +1,6 @@ import pytest import sqlalchemy as sa +from collections.abc import AsyncGenerator, Generator from contextlib import asynccontextmanager, contextmanager from importlib.util import find_spec from sqlalchemy import Boolean, Column, Integer, String @@ -7,7 +8,6 @@ from sqlalchemy.engine.url import URL from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.orm import declarative_base -from typing import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch POSTGRESQL_AVAILABLE = find_spec("asyncpg") is not None and find_spec("psycopg") is not None @@ -116,11 +116,11 @@ def test_init(self): assert conn.connection_url == connection_url assert conn.engine_args == engine_args - assert conn.autoflush == True - assert conn.expire_on_commit == False + assert conn.autoflush + assert not conn.expire_on_commit assert conn.sync_driver == "postgresql+psycopg" assert conn.async_driver == "postgresql+asyncpg" - assert conn.is_connected == False + assert not conn.is_connected assert conn.session is None @pytest.mark.skipif(not POSTGRESQL_AVAILABLE, reason="PostgreSQL drivers not available") @@ -159,7 +159,7 @@ def test_test_connection_sync_non_oracle(self): test_conn = self.ConnectionTester(sync_session=mock_session) result = test_conn.test_connection_sync() - assert result == True + assert result mock_session.execute.assert_called_once() # Check that standard query was used call_args = mock_session.execute.call_args[0][0] @@ -677,11 +677,11 @@ def test_sync_context_manager(self): with conn as session: assert session is mock_session - assert conn.is_connected == True + assert conn.is_connected mock_test_conn.assert_called_once_with(mock_session) # After exiting context, connection should be cleaned up - assert conn.is_connected == False + assert not conn.is_connected mock_session.close.assert_called_once() mock_engine.dispose.assert_called_once() @@ -719,11 +719,11 @@ async def test_async_context_manager(self): async with conn as session: assert session is mock_session - assert conn.is_connected == True + assert conn.is_connected mock_test_conn.assert_called_once_with(mock_session) # After exiting context, connection should be cleaned up - assert conn.is_connected == False + assert not conn.is_connected mock_session.close.assert_called_once() # Engine dispose is called once in __aexit__ (we mocked _get_async_engine) mock_engine.dispose.assert_called_once() @@ -861,7 +861,7 @@ def test_sync_oracle_connection(self): tester = self.ConnectionTester(sync_session=mock_session) result = tester.test_connection_sync() - assert result == True + assert result mock_session.execute.assert_called_once() # Verify Oracle-specific query was used call_args = mock_session.execute.call_args[0][0] @@ -876,7 +876,7 @@ async def test_async_oracle_connection(self): tester = self.ConnectionTester(async_session=mock_session) result = await tester.test_connection_async() - assert result == True # This tests line 185 + assert result # This tests line 185 mock_session.execute.assert_called_once() # Verify Oracle-specific query was used (line 181) call_args = mock_session.execute.call_args[0][0] diff --git a/tests/unit/core/test_settings.py b/tests/unit/core/test_settings.py index 124c047..c92e647 100644 --- a/tests/unit/core/test_settings.py +++ b/tests/unit/core/test_settings.py @@ -91,7 +91,7 @@ def test_default_values(self): settings = SQLiteSettings() assert settings.file_path == "sqlite.db" - assert settings.echo == False + assert not settings.echo def test_env_override(self): """Test environment variable overrides""" @@ -104,7 +104,7 @@ def test_env_override(self): ): settings = SQLiteSettings() assert settings.file_path == 'tests/data/test.db' - assert settings.echo == True + assert settings.echo class TestPostgreSQLSettings: @@ -120,7 +120,7 @@ def test_default_values(self): assert settings.password == "postgres" assert settings.database == "postgres" assert settings.schema == "public" - assert settings.echo == False + assert not settings.echo assert settings.async_driver == "postgresql+asyncpg" assert settings.sync_driver == "postgresql+psycopg" assert settings.ssl_mode == "disable" @@ -160,7 +160,7 @@ def test_env_override(self): assert settings.user == 'testuser' assert settings.password == 'testpass' assert settings.database == 'testdb' - assert settings.echo == True + assert settings.echo assert settings.schema == 'custom_schema' assert settings.ssl_mode == 'require' @@ -178,14 +178,14 @@ def test_default_values(self): assert settings.password == "sa" assert settings.schema == "dbo" assert settings.database == "master" - assert settings.echo == False + assert not settings.echo assert settings.pool_size == 25 assert settings.max_overflow == 50 assert settings.odbcdriver_version == 18 assert settings.async_driver == "mssql+aioodbc" assert settings.sync_driver == "mssql+pyodbc" - assert settings.ssl_encrypt == False - assert settings.ssl_trust_server_certificate == True + assert not settings.ssl_encrypt + assert settings.ssl_trust_server_certificate assert settings.ssl_ca_cert_path is None def test_env_override(self): @@ -226,7 +226,7 @@ def test_default_values(self): assert settings.user == "root" assert settings.password == "root" assert settings.database == "dev" - assert settings.echo == False + assert not settings.echo assert settings.async_driver == "mysql+aiomysql" assert settings.sync_driver == "mysql+mysqldb" assert settings.ssl_mode == "DISABLED" @@ -284,10 +284,10 @@ def test_default_values(self): assert settings.batch_size == 2865 assert settings.limit == 0 assert settings.driver == "mongodb" - assert settings.tls_enabled == False + assert not settings.tls_enabled assert settings.tls_ca_cert_path is None assert settings.tls_cert_key_path is None - assert settings.tls_allow_invalid_certificates == False + assert not settings.tls_allow_invalid_certificates def test_env_override(self): """Test environment variable overrides""" @@ -325,9 +325,9 @@ def test_default_values(self): assert settings.user == "system" assert settings.password == "oracle" assert settings.servicename == "xe" - assert settings.echo == False + assert not settings.echo assert settings.sync_driver == "oracle+oracledb" - assert settings.ssl_enabled == False + assert not settings.ssl_enabled assert settings.ssl_wallet_path is None def test_env_override(self): diff --git a/tests/unit/mongodb/test_mongodb.py b/tests/unit/mongodb/test_mongodb.py index c4b4f64..dccc366 100644 --- a/tests/unit/mongodb/test_mongodb.py +++ b/tests/unit/mongodb/test_mongodb.py @@ -109,7 +109,7 @@ def test_init_with_settings(self): assert mongodb._connection_config.database == "admin" assert mongodb._query_config.batch_size == 2865 assert mongodb._query_config.limit == 0 - assert mongodb.is_connected == False + assert not mongodb.is_connected @patch('ddcDatabases.mongodb.get_mongodb_settings') def test_init_with_parameters(self, mock_get_settings): @@ -261,7 +261,7 @@ def patched_enter(mongodb_self): with mongodb as mongo_instance: assert mongo_instance is mongodb # Returns self, not client - assert mongodb.is_connected == True + assert mongodb.is_connected assert mongodb.client is not None def test_enter_context_manager_exception_handling(self): @@ -391,7 +391,7 @@ def patched_exit(mongodb_self, exc_type, exc_val, exc_tb): with mongodb: pass - assert mongodb.is_connected == False + assert not mongodb.is_connected mongodb.client.close.assert_called_once() @patch('ddcDatabases.mongodb.get_mongodb_settings') @@ -429,7 +429,7 @@ def test_exit_method_with_none_client(self, mock_get_settings): mongodb.__exit__(None, None, None) # Verify is_connected was not changed since client was None - assert mongodb.is_connected == True + assert mongodb.is_connected def test_missing_collection_runtime_error(self): """Test RuntimeError when collection is missing""" @@ -889,10 +889,10 @@ def test_tls_enabled(self, mock_get_settings): ), ) - assert mongodb._tls_config.tls_enabled == True + assert mongodb._tls_config.tls_enabled assert mongodb._tls_config.tls_ca_cert_path == "/path/to/ca.pem" assert mongodb._tls_config.tls_cert_key_path == "/path/to/cert.pem" - assert mongodb._tls_config.tls_allow_invalid_certificates == True + assert mongodb._tls_config.tls_allow_invalid_certificates @patch('ddcDatabases.mongodb.get_mongodb_settings') def test_tls_disabled(self, mock_get_settings): @@ -906,10 +906,10 @@ def test_tls_disabled(self, mock_get_settings): mongodb = MongoDB(collection="test_collection") - assert mongodb._tls_config.tls_enabled == False + assert not mongodb._tls_config.tls_enabled assert mongodb._tls_config.tls_ca_cert_path is None assert mongodb._tls_config.tls_cert_key_path is None - assert mongodb._tls_config.tls_allow_invalid_certificates == False + assert not mongodb._tls_config.tls_allow_invalid_certificates @patch('ddcDatabases.mongodb.get_mongodb_settings') @patch('ddcDatabases.mongodb.MongoClient') @@ -1010,7 +1010,7 @@ def test_get_tls_info(self, mock_get_settings): assert tls_info is mongodb._tls_config assert isinstance(tls_info, MongoDBTLSConfig) - assert tls_info.tls_enabled == True + assert tls_info.tls_enabled assert tls_info.tls_ca_cert_path == "/path/to/ca.pem" @patch('ddcDatabases.mongodb.get_mongodb_settings') @@ -1113,7 +1113,7 @@ async def mock_close(): await mongodb.__aexit__(None, None, None) assert mongodb.async_cursor_ref is None - assert mongodb.is_connected == False + assert not mongodb.is_connected mock_client.close.assert_called_once() @pytest.mark.asyncio @@ -1131,7 +1131,7 @@ async def test_async_aexit_without_cursor(self, mock_get_settings): await mongodb.__aexit__(None, None, None) - assert mongodb.is_connected == False + assert not mongodb.is_connected mock_client.close.assert_called_once() @pytest.mark.asyncio diff --git a/tests/unit/mssql/test_mssql.py b/tests/unit/mssql/test_mssql.py index 25e8222..99ff7aa 100644 --- a/tests/unit/mssql/test_mssql.py +++ b/tests/unit/mssql/test_mssql.py @@ -75,7 +75,7 @@ def test_init_with_parameters(self, mock_get_settings): assert mssql.connection_url["username"] == "customuser" assert mssql.connection_url["password"] == "custompass" assert mssql._connection_config.schema == "customschema" - assert mssql._session_config.echo == True + assert mssql._session_config.echo assert mssql._pool_config.pool_size == 30 assert mssql._pool_config.max_overflow == 20 @@ -202,7 +202,7 @@ def test_extra_engine_args(self, mock_get_settings): # Test that default args are still present assert mssql.engine_args["pool_size"] == 25 assert mssql.engine_args["max_overflow"] == 50 - assert mssql.engine_args["echo"] == False + assert not mssql.engine_args["echo"] @patch('ddcDatabases.mssql.get_mssql_settings') def test_autoflush_and_expire_on_commit(self, mock_get_settings): @@ -212,8 +212,8 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): mssql = MSSQL(session_config=MSSQLSessionConfig(autoflush=False, expire_on_commit=False)) - assert mssql._session_config.autoflush == False - assert mssql._session_config.expire_on_commit == False + assert not mssql._session_config.autoflush + assert not mssql._session_config.expire_on_commit @patch('ddcDatabases.mssql.get_mssql_settings') def test_autocommit_parameter(self, mock_get_settings): @@ -223,8 +223,8 @@ def test_autocommit_parameter(self, mock_get_settings): mssql = MSSQL(session_config=MSSQLSessionConfig(autocommit=True)) - assert mssql._session_config.autocommit == True - assert mssql.engine_args["connect_args"]["autocommit"] == True + assert mssql._session_config.autocommit + assert mssql.engine_args["connect_args"]["autocommit"] @patch('ddcDatabases.mssql.get_mssql_settings') def test_connection_timeout_parameter(self, mock_get_settings): @@ -257,12 +257,12 @@ def test_all_parameters_defaults(self, mock_get_settings): mssql = MSSQL() - assert mssql._session_config.autoflush == False - assert mssql._session_config.expire_on_commit == False - assert mssql._session_config.autocommit == False + assert not mssql._session_config.autoflush + assert not mssql._session_config.expire_on_commit + assert not mssql._session_config.autocommit assert mssql._pool_config.connection_timeout == 30 assert mssql._pool_config.pool_recycle == 3600 - assert mssql.engine_args["connect_args"]["autocommit"] == False + assert not mssql.engine_args["connect_args"]["autocommit"] assert mssql.engine_args["connect_args"]["timeout"] == 30 assert mssql.engine_args["pool_recycle"] == 3600 @@ -390,8 +390,8 @@ def test_ssl_encrypt_enabled(self, mock_get_settings): mssql = MSSQL(ssl_config=MSSQLSSLConfig(ssl_encrypt=True, ssl_trust_server_certificate=False)) - assert mssql._ssl_config.ssl_encrypt == True - assert mssql._ssl_config.ssl_trust_server_certificate == False + assert mssql._ssl_config.ssl_encrypt + assert not mssql._ssl_config.ssl_trust_server_certificate assert mssql.connection_url["query"]["Encrypt"] == "yes" assert mssql.connection_url["query"]["TrustServerCertificate"] == "no" @@ -403,8 +403,8 @@ def test_ssl_encrypt_disabled(self, mock_get_settings): mssql = MSSQL() - assert mssql._ssl_config.ssl_encrypt == False - assert mssql._ssl_config.ssl_trust_server_certificate == True + assert not mssql._ssl_config.ssl_encrypt + assert mssql._ssl_config.ssl_trust_server_certificate assert mssql.connection_url["query"]["Encrypt"] == "no" assert mssql.connection_url["query"]["TrustServerCertificate"] == "yes" @@ -423,8 +423,8 @@ def test_get_ssl_info(self, mock_get_settings): ssl_info = mssql.get_ssl_info() assert isinstance(ssl_info, MSSQLSSLConfig) - assert ssl_info.ssl_encrypt == True - assert ssl_info.ssl_trust_server_certificate == False + assert ssl_info.ssl_encrypt + assert not ssl_info.ssl_trust_server_certificate assert ssl_info.ssl_ca_cert_path == "/path/to/ca.pem" @patch('ddcDatabases.mssql.get_mssql_settings') @@ -463,8 +463,8 @@ def test_get_session_info(self, mock_get_settings): assert session_info is mssql._session_config assert isinstance(session_info, MSSQLSessionConfig) - assert session_info.echo == True - assert session_info.autoflush == False + assert session_info.echo + assert not session_info.autoflush @patch('ddcDatabases.mssql.get_mssql_settings') def test_get_operation_retry_info(self, mock_get_settings): diff --git a/tests/unit/mysql/test_mysql.py b/tests/unit/mysql/test_mysql.py index 7efe349..e6cfcce 100644 --- a/tests/unit/mysql/test_mysql.py +++ b/tests/unit/mysql/test_mysql.py @@ -105,7 +105,7 @@ def test_init_with_parameters(self, mock_get_settings): assert mysql.connection_url["database"] == "customdb" assert mysql.connection_url["username"] == "customuser" assert mysql.connection_url["password"] == "custompass" - assert mysql._session_config.echo == True + assert mysql._session_config.echo @patch('ddcDatabases.mysql.get_mysql_settings') def test_minimal_init(self, mock_get_settings): @@ -134,7 +134,7 @@ def test_extra_engine_args(self, mock_get_settings): assert mysql.engine_args["connect_timeout"] == 30 # Test that default args are still present - assert mysql.engine_args["echo"] == False + assert not mysql.engine_args["echo"] @patch('ddcDatabases.mysql.get_mysql_settings') def test_autoflush_and_expire_on_commit(self, mock_get_settings): @@ -144,8 +144,8 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): mysql = MySQL(session_config=MySQLSessionConfig(autoflush=False, expire_on_commit=False)) - assert mysql._session_config.autoflush == False - assert mysql._session_config.expire_on_commit == False + assert not mysql._session_config.autoflush + assert not mysql._session_config.expire_on_commit @patch('ddcDatabases.mysql.get_mysql_settings') def test_pool_size_parameter(self, mock_get_settings): @@ -252,8 +252,8 @@ def test_get_session_info(self, mock_get_settings): assert session_info is mysql._session_config assert isinstance(session_info, MySQLSessionConfig) - assert session_info.echo == True - assert session_info.autoflush == False + assert session_info.echo + assert not session_info.autoflush @patch('ddcDatabases.mysql.get_mysql_settings') def test_get_connection_retry_info(self, mock_get_settings): diff --git a/tests/unit/oracle/test_oracle.py b/tests/unit/oracle/test_oracle.py index f2614fc..f3984b1 100644 --- a/tests/unit/oracle/test_oracle.py +++ b/tests/unit/oracle/test_oracle.py @@ -126,7 +126,7 @@ def test_init_with_parameters(self, mock_get_settings): assert oracle.connection_url["username"] == "customuser" assert oracle.connection_url["password"] == "custompass" assert oracle.connection_url["query"]["service_name"] == "customxe" - assert oracle._session_config.echo == True + assert oracle._session_config.echo @patch('ddcDatabases.oracle.get_oracle_settings') def test_minimal_init(self, mock_get_settings): @@ -151,7 +151,7 @@ def test_test_connection_sync_oracle(self): test_conn = ConnectionTester(sync_session=mock_session) result = test_conn.test_connection_sync() - assert result == True + assert result mock_session.execute.assert_called_once() # Check that Oracle-specific query was used call_args = mock_session.execute.call_args[0][0] @@ -166,7 +166,7 @@ async def test_test_connection_async_oracle(self): test_conn = ConnectionTester(async_session=mock_session) result = await test_conn.test_connection_async() - assert result == True + assert result mock_session.execute.assert_called_once() @patch('ddcDatabases.oracle.get_oracle_settings') @@ -184,7 +184,7 @@ def test_extra_engine_args(self, mock_get_settings): assert oracle.engine_args["nencoding"] == "UTF-8" # Test that default args are still present - assert oracle.engine_args["echo"] == False + assert not oracle.engine_args["echo"] @patch('ddcDatabases.oracle.get_oracle_settings') def test_autoflush_and_expire_on_commit(self, mock_get_settings): @@ -194,8 +194,8 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): oracle = Oracle(session_config=OracleSessionConfig(autoflush=False, expire_on_commit=False)) - assert oracle._session_config.autoflush == False - assert oracle._session_config.expire_on_commit == False + assert not oracle._session_config.autoflush + assert not oracle._session_config.expire_on_commit @patch('ddcDatabases.oracle.get_oracle_settings') def test_autocommit_parameter(self, mock_get_settings): @@ -205,7 +205,7 @@ def test_autocommit_parameter(self, mock_get_settings): oracle = Oracle(session_config=OracleSessionConfig(autocommit=False)) - assert oracle._session_config.autocommit == False + assert not oracle._session_config.autocommit @patch('ddcDatabases.oracle.get_oracle_settings') def test_connection_timeout_parameter(self, mock_get_settings): @@ -225,9 +225,9 @@ def test_all_parameters_defaults(self, mock_get_settings): oracle = Oracle() - assert oracle._session_config.autoflush == False - assert oracle._session_config.expire_on_commit == False - assert oracle._session_config.autocommit == False + assert not oracle._session_config.autoflush + assert not oracle._session_config.expire_on_commit + assert not oracle._session_config.autocommit assert oracle._pool_config.connection_timeout == 30 @patch('ddcDatabases.oracle.get_oracle_settings') @@ -286,7 +286,7 @@ def test_ssl_wallet_path(self, mock_get_settings): oracle = Oracle(ssl_config=OracleSSLConfig(ssl_enabled=True, ssl_wallet_path="/path/to/wallet")) - assert oracle._ssl_config.ssl_enabled == True + assert oracle._ssl_config.ssl_enabled assert oracle._ssl_config.ssl_wallet_path == "/path/to/wallet" assert oracle.engine_args["connect_args"]["wallet_location"] == "/path/to/wallet" @@ -298,7 +298,7 @@ def test_ssl_disabled_no_wallet(self, mock_get_settings): oracle = Oracle() - assert oracle._ssl_config.ssl_enabled == False + assert not oracle._ssl_config.ssl_enabled assert oracle._ssl_config.ssl_wallet_path is None assert "wallet_location" not in oracle.engine_args["connect_args"] @@ -338,8 +338,8 @@ def test_get_session_info(self, mock_get_settings): assert session_info is oracle._session_config assert isinstance(session_info, OracleSessionConfig) - assert session_info.echo == True - assert session_info.autoflush == False + assert session_info.echo + assert not session_info.autoflush @patch('ddcDatabases.oracle.get_oracle_settings') def test_get_connection_retry_info(self, mock_get_settings): @@ -376,5 +376,5 @@ def test_get_ssl_info(self, mock_get_settings): assert ssl_info is oracle._ssl_config assert isinstance(ssl_info, OracleSSLConfig) - assert ssl_info.ssl_enabled == True + assert ssl_info.ssl_enabled assert ssl_info.ssl_wallet_path == "/path/to/wallet" diff --git a/tests/unit/postgresql/test_postgresql.py b/tests/unit/postgresql/test_postgresql.py index 9c9c1a0..f65ddf8 100644 --- a/tests/unit/postgresql/test_postgresql.py +++ b/tests/unit/postgresql/test_postgresql.py @@ -1,3 +1,4 @@ +import dataclasses import pytest from importlib.util import find_spec from unittest.mock import MagicMock, patch @@ -90,7 +91,7 @@ def test_init_with_parameters(self, mock_get_settings): assert postgresql.connection_url["database"] == "customdb" assert postgresql.connection_url["username"] == "customuser" assert postgresql.connection_url["password"] == "custompass" - assert postgresql._session_config.echo == True + assert postgresql._session_config.echo @patch('ddcDatabases.postgresql.get_postgresql_settings') def test_extra_engine_args(self, mock_get_settings): @@ -115,7 +116,7 @@ def test_extra_engine_args(self, mock_get_settings): assert postgresql.engine_args["connect_timeout"] == 30 # Test that default args are still present - assert postgresql.engine_args["echo"] == False + assert not postgresql.engine_args["echo"] @patch('ddcDatabases.postgresql.get_postgresql_settings') def test_autoflush_and_expire_on_commit(self, mock_get_settings): @@ -133,8 +134,8 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autoflush=False, expire_on_commit=False)) - assert postgresql._session_config.autoflush == False - assert postgresql._session_config.expire_on_commit == False + assert not postgresql._session_config.autoflush + assert not postgresql._session_config.expire_on_commit @patch('ddcDatabases.postgresql.get_postgresql_settings') def test_autocommit_parameter(self, mock_get_settings): @@ -153,7 +154,7 @@ def test_autocommit_parameter(self, mock_get_settings): postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=True)) - assert postgresql._session_config.autocommit == True + assert postgresql._session_config.autocommit @patch('ddcDatabases.postgresql.get_postgresql_settings') def test_connect_args_psycopg_driver(self, mock_get_settings): @@ -243,7 +244,7 @@ def test_engine_args_structure(self): postgresql = PostgreSQL(extra_engine_args=extra_args) # Test that extra args are properly included - assert postgresql.engine_args['pool_pre_ping'] == True + assert postgresql.engine_args['pool_pre_ping'] assert postgresql.engine_args['pool_recycle'] == 3600 assert postgresql.engine_args['pool_timeout'] == 60 @@ -326,7 +327,7 @@ def test_get_base_engine_args_method(self, mock_get_settings): assert result["url"] == connection_url assert result["pool_size"] == 25 assert result["max_overflow"] == 50 - assert result["pool_pre_ping"] == True + assert result["pool_pre_ping"] assert result["pool_recycle"] == 3600 assert result["query_cache_size"] == 1000 assert result["connect_args"]["connect_timeout"] == 30 @@ -548,10 +549,10 @@ def test_enhanced_configuration_methods(self, mock_get_settings): # Test get_session_info method (line 161) session_config = postgresql.get_session_info() - assert session_config.echo == False - assert session_config.autoflush == False - assert session_config.expire_on_commit == False - assert session_config.autocommit == True + assert not session_config.echo + assert not session_config.autoflush + assert not session_config.expire_on_commit + assert session_config.autocommit @patch('ddcDatabases.postgresql.get_postgresql_settings') def test_get_engine_method_with_psycopg(self, mock_get_settings): @@ -871,13 +872,13 @@ def test_configuration_immutability(self, mock_get_settings): session_config = postgresql.get_session_info() # Try to modify configurations - should raise FrozenInstanceError - with pytest.raises(Exception): # FrozenInstanceError + with pytest.raises(dataclasses.FrozenInstanceError): conn_config.host = "modified" # noqa - with pytest.raises(Exception): # FrozenInstanceError + with pytest.raises(dataclasses.FrozenInstanceError): pool_config.pool_size = 999 # noqa - with pytest.raises(Exception): # FrozenInstanceError + with pytest.raises(dataclasses.FrozenInstanceError): session_config.echo = True # noqa @patch('ddcDatabases.postgresql.get_postgresql_settings') @@ -1225,8 +1226,8 @@ def test_get_session_info(self, mock_get_settings): assert session_info is postgresql._session_config assert isinstance(session_info, PostgreSQLSessionConfig) - assert session_info.echo == True - assert session_info.autoflush == False + assert session_info.echo + assert not session_info.autoflush @patch('ddcDatabases.postgresql.get_postgresql_settings') def test_get_operation_retry_info(self, mock_get_settings): diff --git a/tests/unit/sqlite/test_sqlite.py b/tests/unit/sqlite/test_sqlite.py index 581e780..0ad811f 100644 --- a/tests/unit/sqlite/test_sqlite.py +++ b/tests/unit/sqlite/test_sqlite.py @@ -50,8 +50,8 @@ def test_init_basic(self, mock_get_settings): sqlite = self.Sqlite() assert sqlite.filepath == "sqlite.db" - assert sqlite.echo == False - assert sqlite.is_connected == False + assert not sqlite.echo + assert not sqlite.is_connected @patch('ddcDatabases.sqlite.get_sqlite_settings') def test_init_with_parameters(self, mock_get_settings): @@ -75,9 +75,9 @@ def test_init_with_parameters(self, mock_get_settings): ) assert sqlite.filepath == "custom.db" - assert sqlite.echo == True - assert sqlite.autoflush == False - assert sqlite.expire_on_commit == False + assert sqlite.echo + assert not sqlite.autoflush + assert not sqlite.expire_on_commit def test_real_operations(self): """Test comprehensive SQLite operations""" @@ -162,12 +162,12 @@ def test_context_manager(self): # Test __enter__ session = sqlite.__enter__() assert session is not None - assert sqlite.is_connected == True + assert sqlite.is_connected assert sqlite.session is session # Test __exit__ sqlite.__exit__(None, None, None) - assert sqlite.is_connected == False + assert not sqlite.is_connected @patch('ddcDatabases.sqlite.create_engine') def test_engine_creation_error(self, mock_create_engine): @@ -191,9 +191,9 @@ def test_optional_parameters(self): ) assert sqlite.filepath == "custom.db" - assert sqlite.echo == True - assert sqlite.autoflush == False - assert sqlite.expire_on_commit == False + assert sqlite.echo + assert not sqlite.autoflush + assert not sqlite.expire_on_commit assert sqlite.extra_engine_args == extra_args @@ -229,7 +229,7 @@ def test_real_fetchall(self): # Access through the model object returned by ORM select assert results[0]['ModelTest'].id == 1 assert results[0]['ModelTest'].name == "test" - assert results[0]['ModelTest'].enabled == True + assert results[0]['ModelTest'].enabled def test_real_fetchvalue(self): """Test fetchvalue with real database""" @@ -287,15 +287,15 @@ def test_connection_state_management(self): sqlite = self.Sqlite(filepath=db_path) # Initially not connected - assert sqlite.is_connected == False + assert not sqlite.is_connected # Test context manager connection with sqlite as session: - assert sqlite.is_connected == True + assert sqlite.is_connected assert session is not None # After context manager, should be disconnected - assert sqlite.is_connected == False + assert not sqlite.is_connected def test_custom_settings_integration(self): """Test integration with custom settings""" @@ -308,9 +308,9 @@ def test_custom_settings_integration(self): ) assert sqlite.filepath == db_path - assert sqlite.echo == True - assert sqlite.autoflush == True - assert sqlite.expire_on_commit == True + assert sqlite.echo + assert sqlite.autoflush + assert sqlite.expire_on_commit # Test that it works with real database with sqlite as session: diff --git a/uv.lock b/uv.lock index 9346a79..cd8f3c4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.10" [[package]] name = "aiomysql" @@ -35,12 +35,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + [[package]] name = "asyncpg" version = "0.31.0" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11'" }, +] sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/d9/507c80bdac2e95e5a525644af94b03fa7f9a44596a84bd48a6e80f854f92/asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61", size = 644865, upload-time = "2025-11-24T23:25:23.527Z" }, + { url = "https://files.pythonhosted.org/packages/ea/03/f93b5e543f65c5f504e91405e8d21bb9e600548be95032951a754781a41d/asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be", size = 639297, upload-time = "2025-11-24T23:25:25.192Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/de2177e57e03a06e697f6c1ddf2a9a7fcfdc236ce69966f54ffc830fd481/asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8", size = 2816679, upload-time = "2025-11-24T23:25:26.718Z" }, + { url = "https://files.pythonhosted.org/packages/d0/98/1a853f6870ac7ad48383a948c8ff3c85dc278066a4d69fc9af7d3d4b1106/asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1", size = 2867087, upload-time = "2025-11-24T23:25:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/7e76f2a51f2360a7c90d2cf6d0d9b210c8bb0ae342edebd16173611a55c2/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3", size = 2747631, upload-time = "2025-11-24T23:25:30.154Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3f/716e10cb57c4f388248db46555e9226901688fbfabd0afb85b5e1d65d5a7/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8", size = 2855107, upload-time = "2025-11-24T23:25:31.888Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ec/3ebae9dfb23a1bd3f68acfd4f795983b65b413291c0e2b0d982d6ae6c920/asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095", size = 521990, upload-time = "2025-11-24T23:25:33.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/b4/9fbb4b0af4e36d96a61d026dd37acab3cf521a70290a09640b215da5ab7c/asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540", size = 581629, upload-time = "2025-11-24T23:25:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" }, + { url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" }, + { url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, @@ -75,6 +103,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + [[package]] name = "black" version = "26.1.0" @@ -86,9 +123,21 @@ dependencies = [ { name = "pathspec" }, { name = "platformdirs" }, { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, + { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, + { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, + { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, @@ -125,6 +174,31 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -179,6 +253,38 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, @@ -257,6 +363,31 @@ version = "7.13.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, + { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, + { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, + { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, + { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" }, + { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" }, { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, @@ -325,12 +456,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, ] +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cryptography" version = "46.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } wheels = [ @@ -376,6 +513,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, + { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, + { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, ] [[package]] @@ -408,9 +551,6 @@ pgsql = [ ] [package.dev-dependencies] -all = [ - { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, -] dev = [ { name = "black" }, { name = "coverage" }, @@ -422,14 +562,6 @@ dev = [ { name = "ruff" }, { name = "testcontainers", extra = ["mongodb", "mssql", "mysql", "oracle"] }, ] -test = [ - { name = "coverage" }, - { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "testcontainers", extra = ["mongodb", "mssql", "mysql", "oracle"] }, -] [package.metadata] requires-dist = [ @@ -447,13 +579,6 @@ requires-dist = [ provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] [package.metadata.requires-dev] -all = [ - { name = "ddcdatabases", extras = ["mongodb"] }, - { name = "ddcdatabases", extras = ["mssql"] }, - { name = "ddcdatabases", extras = ["mysql"] }, - { name = "ddcdatabases", extras = ["oracle"] }, - { name = "ddcdatabases", extras = ["pgsql"] }, -] dev = [ { name = "black", specifier = ">=26.1.0" }, { name = "coverage", specifier = ">=7.13.3" }, @@ -469,18 +594,6 @@ dev = [ { name = "ruff", specifier = ">=0.15.0" }, { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], specifier = ">=4.14.1" }, ] -test = [ - { name = "coverage", specifier = ">=7.13.3" }, - { name = "ddcdatabases", extras = ["mongodb"] }, - { name = "ddcdatabases", extras = ["mssql"] }, - { name = "ddcdatabases", extras = ["mysql"] }, - { name = "ddcdatabases", extras = ["oracle"] }, - { name = "ddcdatabases", extras = ["pgsql"] }, - { name = "pytest", specifier = ">=9.0.2" }, - { name = "pytest-asyncio", specifier = ">=1.3.0" }, - { name = "pytest-cov", specifier = ">=7.0.0" }, - { name = "testcontainers", extras = ["postgres", "mysql", "mssql", "mongodb", "oracle"], specifier = ">=4.14.1" }, -] [[package]] name = "dnspython" @@ -505,12 +618,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + [[package]] name = "greenlet" version = "3.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, + { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/ac9bf1ec008916d1a3373cae212884c1dcff4a4ba0d41127ce81a8deb4e9/greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8", size = 226100, upload-time = "2026-01-23T15:30:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, @@ -593,8 +735,11 @@ version = "2.2.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/24/cdaaef42aac7d53c0a01bb638da64961c293b1b6d204efd47400a68029d4/mysqlclient-2.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:2e3c11f7625029d7276ca506f8960a7fd3c5a0a0122c9e7404e6a8fe961b3d22", size = 207748, upload-time = "2025-01-10T11:56:24.357Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3e2de3f93cd60dd63bd229ec3e3b679f682982614bf513d046c2722aa4ce/mysqlclient-2.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:a22d99d26baf4af68ebef430e3131bb5a9b722b79a9fcfac6d9bbf8a88800687", size = 207745, upload-time = "2025-01-10T11:56:28.67Z" }, { url = "https://files.pythonhosted.org/packages/bb/b5/2a8a4bcba3440550f358b839638fe8ec9146fa3c9194890b4998a530c926/mysqlclient-2.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:4b4c0200890837fc64014cc938ef2273252ab544c1b12a6c1d674c23943f3f2e", size = 208032, upload-time = "2025-01-10T11:56:29.879Z" }, { url = "https://files.pythonhosted.org/packages/29/01/e80141f1cd0459e4c9a5dd309dee135bbae41d6c6c121252fdd853001a8a/mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5", size = 208000, upload-time = "2025-01-10T11:56:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/16/cc/5b1570be9f8597ee41e2a0bd7b62ba861ec2c81898d9449f3d6bfbe15d29/mysqlclient-2.2.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92af368ed9c9144737af569c86d3b6c74a012a6f6b792eb868384787b52bb585", size = 207800, upload-time = "2025-01-10T11:56:36.023Z" }, ] [[package]] @@ -607,6 +752,16 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f7/02/70a872d1a4a739b4f7371ab8d3d5ed8c6e57e142e2503531aafcb220893c/oracledb-3.4.2.tar.gz", hash = "sha256:46e0f2278ff1fe83fbc33a3b93c72d429323ec7eed47bc9484e217776cd437e5", size = 855467, upload-time = "2026-01-28T17:25:39.91Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/b8a0ca1c520fa43ae33260f6f8ca9bd468ade43da7986029bc214965df12/oracledb-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff3c89cecea62af8ca02aa33cab0f2edc0214c747eac7d3364ed6b2640cb55e4", size = 4243966, upload-time = "2026-01-28T17:25:45.05Z" }, + { url = "https://files.pythonhosted.org/packages/f6/43/26e2bbb2a6ee31392a339089e53cb2e386ca795ff4fbe2f673c167821bd6/oracledb-3.4.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e068ef844a327877bfefbef1bc6fb7284c727bb87af80095f08d95bcaf7b8bb2", size = 2426056, upload-time = "2026-01-28T17:25:47.176Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/11ee1d044295465a04ff45c6e3023d35400bb3f67bc5fed9408f0f2dc04c/oracledb-3.4.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f434a739405557bd57cb39b62238142bb27855a524a70dc6d397a2a8c576c9d", size = 2603062, upload-time = "2026-01-28T17:25:49.817Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bc/292f2f5f7b65a667787871e300889ab8f4a3b9cfd88c5d78f828a40f6d31/oracledb-3.4.2-cp310-cp310-win32.whl", hash = "sha256:00c79448017f367bb7ab6900efe0706658a53768abea2b4519a4c9b2d5743890", size = 1496639, upload-time = "2026-01-28T17:25:51.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/23/81931c16663e771937c0161bb90460668d2a5f7982b5030ab7bef3b3a4f9/oracledb-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:574c8280d49cbbe21dbe03fc28356d9b9a5b9e300ebcde6c6d106e51453a7e65", size = 1837314, upload-time = "2026-01-28T17:25:52.718Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/be263b668ba32b258d07c85f7bfb6967a9677e016c299207b28734f04c4b/oracledb-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b8e4b8a852251cef09038b75f30fce1227010835f4e19cfbd436027acba2697c", size = 4228552, upload-time = "2026-01-28T17:25:54.844Z" }, + { url = "https://files.pythonhosted.org/packages/91/bc/e832a649529da7c60409a81be41f3213b4c7ffda4fe424222b2145e8d43c/oracledb-3.4.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1617a1db020346883455af005efbefd51be2c4d797e43b1b38455a19f8526b48", size = 2421924, upload-time = "2026-01-28T17:25:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/86/21/d867c37e493a63b5521bd248110ad5b97b18253d64a30703e3e8f3d9631e/oracledb-3.4.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed78d7e7079a778062744ccf42141ce4806818c3f4dd6463e4a7edd561c9f86", size = 2599301, upload-time = "2026-01-28T17:25:58.529Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/9b1843ea27f7791449652d7f340f042c3053336d2c11caf29e59bab86189/oracledb-3.4.2-cp311-cp311-win32.whl", hash = "sha256:0e16fe3d057e0c41a23ad2ae95bfa002401690773376d476be608f79ac74bf05", size = 1492890, upload-time = "2026-01-28T17:26:00.662Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/cbc8afa2db0cec80530858d3e4574f9734fae8c0b7f1df261398aa026c5f/oracledb-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:f93cae08e8ed20f2d5b777a8602a71f9418389c661d2c937e84d94863e7e7011", size = 1843355, upload-time = "2026-01-28T17:26:02.637Z" }, { url = "https://files.pythonhosted.org/packages/8f/81/2e6154f34b71cd93b4946c73ea13b69d54b8d45a5f6bbffe271793240d21/oracledb-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a7396664e592881225ba66385ee83ce339d864f39003d6e4ca31a894a7e7c552", size = 4220806, upload-time = "2026-01-28T17:26:04.322Z" }, { url = "https://files.pythonhosted.org/packages/ab/a9/a1d59aaac77d8f727156ec6a3b03399917c90b7da4f02d057f92e5601f56/oracledb-3.4.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f04a2d62073407672f114d02529921de0677c6883ed7c64d8d1a3c04caa3238", size = 2233795, upload-time = "2026-01-28T17:26:05.877Z" }, { url = "https://files.pythonhosted.org/packages/94/ec/8c4a38020cd251572bd406ddcbde98ca052ec94b5684f9aa9ef1ddfcc68c/oracledb-3.4.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8d75e4f879b908be66cce05ba6c05791a5dbb4a15e39abc01aa25c8a2492bd9", size = 2424756, upload-time = "2026-01-28T17:26:07.35Z" }, @@ -676,6 +831,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/9d/054c8435b03324ed9abd5d5ab8c45065b1f42c23952cd23f13a5921d8465/poethepoet-0.40.0.tar.gz", hash = "sha256:91835f00d03d6c4f0e146f80fa510e298ad865e7edd27fe4cb9c94fdc090791b", size = 81114, upload-time = "2026-01-05T19:09:13.116Z" } wheels = [ @@ -705,6 +861,28 @@ name = "psycopg-binary" version = "3.3.2" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/25/d7/edfb0d9e56081246fd88490f99b1bafebd3588480cca601a4de0c41a3e08/psycopg_binary-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41", size = 4597785, upload-time = "2025-12-06T17:31:44.867Z" }, + { url = "https://files.pythonhosted.org/packages/71/45/8458201d9573dd851263a05cefddd4bfd31e8b3c6434b3e38d62aea9f15a/psycopg_binary-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4", size = 4664440, upload-time = "2025-12-06T17:31:49.1Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/484260d87456cfe88dc219c1919026f11949b9d1de8a6371ddbe027d4d60/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068", size = 5478355, upload-time = "2025-12-06T17:31:52.657Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/18c91630c30c83f534c2bfa75fb533293fc9c3ab31bb7f2bf1cd9579c53b/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e", size = 5152398, upload-time = "2025-12-06T17:31:56.092Z" }, + { url = "https://files.pythonhosted.org/packages/c0/14/7c705e1934107196d9dca2040cf34bce2ca26de62520e43073d2673052d4/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b", size = 6748982, upload-time = "2025-12-06T17:32:00.611Z" }, + { url = "https://files.pythonhosted.org/packages/56/18/80197c47798926f79e563af02a71d1abecab88cf45ddf8dc960700598da7/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391", size = 4991214, upload-time = "2025-12-06T17:32:03.897Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2e/e88e2f678f5d1a968d87e57b30915061c1157e916b8aaa9b0b78bca95e25/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c", size = 4517421, upload-time = "2025-12-06T17:32:07.287Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/d56813b24370723bcd62bf73871aee4d5fca0536f3476c4c4d5b037e3c7f/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8", size = 4206124, upload-time = "2025-12-06T17:32:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/91/81/5a11a898969edf0ee43d0613a6dfd689a0aa12d418c69e148a8ff153fbc7/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186", size = 3937067, upload-time = "2025-12-06T17:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/a1/33/a6180ff1e747a0395876d985e8e295c9d7cbe956a2d66f165e7c67cffe55/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19", size = 4243731, upload-time = "2025-12-06T17:32:16.803Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5b/9c1b6fbc900d5b525946ed9a477865c5016a5306080c0557248bb04f1a5b/psycopg_binary-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640", size = 3546403, upload-time = "2025-12-06T17:32:19.621Z" }, + { url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" }, + { url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" }, + { url = "https://files.pythonhosted.org/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" }, + { url = "https://files.pythonhosted.org/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" }, + { url = "https://files.pythonhosted.org/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" }, + { url = "https://files.pythonhosted.org/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" }, + { url = "https://files.pythonhosted.org/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" }, { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" }, { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" }, { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" }, @@ -773,6 +951,33 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, @@ -829,10 +1034,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] @@ -867,6 +1092,26 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/65/9c/a4895c4b785fc9865a84a56e14b5bd21ca75aadc3dab79c14187cdca189b/pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c", size = 2495323, upload-time = "2026-01-07T18:05:48.107Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/93/c36c0998dd91ad8b5031d2e77a903d5cd705b5ba05ca92bcc8731a2c3a8d/pymongo-4.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ed162b2227f98d5b270ecbe1d53be56c8c81db08a1a8f5f02d89c7bb4d19591d", size = 807993, upload-time = "2026-01-07T18:03:40.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/96/d2117d792fa9fedb2f6ccf0608db31f851e8382706d7c3c88c6ac92cc958/pymongo-4.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a9390dce61d705a88218f0d7b54d7e1fa1b421da8129fc7c009e029a9a6b81e", size = 808355, upload-time = "2026-01-07T18:03:42.13Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2e/e79b7b86c0dd6323d0985c201583c7921d67b842b502aae3f3327cbe3935/pymongo-4.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:92a232af9927710de08a6c16a9710cc1b175fb9179c0d946cd4e213b92b2a69a", size = 1182337, upload-time = "2026-01-07T18:03:44.126Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/07ec9966381c57d941fddc52637e9c9653e63773be410bd8605f74683084/pymongo-4.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d79aa147ce86aef03079096d83239580006ffb684eead593917186aee407767", size = 1200928, upload-time = "2026-01-07T18:03:45.52Z" }, + { url = "https://files.pythonhosted.org/packages/44/15/9d45e3cc6fa428b0a3600b0c1c86b310f28c91251c41493460695ab40b6b/pymongo-4.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19a1c96e7f39c7a59a9cfd4d17920cf9382f6f684faeff4649bf587dc59f8edc", size = 1239418, upload-time = "2026-01-07T18:03:47.03Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b3/f35ee51e2a3f05f673ad4f5e803ae1284c42f4413e8d121c4958f1af4eb9/pymongo-4.16.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efe020c46ce3c3a89af6baec6569635812129df6fb6cf76d4943af3ba6ee2069", size = 1229045, upload-time = "2026-01-07T18:03:48.377Z" }, + { url = "https://files.pythonhosted.org/packages/18/2d/1688b88d7c0a5c01da8c703dea831419435d9ce67c6ddbb0ac629c9c72d2/pymongo-4.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dc2c00bed568732b89e211b6adca389053d5e6d2d5a8979e80b813c3ec4d1f9", size = 1196517, upload-time = "2026-01-07T18:03:50.205Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c6/e89db0f23bd20757b627a5d8c73a609ffd6741887b9004ab229208a79764/pymongo-4.16.0-cp310-cp310-win32.whl", hash = "sha256:5b9c6d689bbe5beb156374508133218610e14f8c81e35bc17d7a14e30ab593e6", size = 794911, upload-time = "2026-01-07T18:03:52.701Z" }, + { url = "https://files.pythonhosted.org/packages/37/54/e00a5e517153f310a33132375159e42dceb12bee45b51b35aa0df14f1866/pymongo-4.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:2290909275c9b8f637b0a92eb9b89281e18a72922749ebb903403ab6cc7da914", size = 804801, upload-time = "2026-01-07T18:03:57.671Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0a/2572faf89195a944c99c6d756227019c8c5f4b5658ecc261c303645dfe69/pymongo-4.16.0-cp310-cp310-win_arm64.whl", hash = "sha256:6af1aaa26f0835175d2200e62205b78e7ec3ffa430682e322cc91aaa1a0dbf28", size = 797579, upload-time = "2026-01-07T18:03:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/907414a763c4270b581ad6d960d0c6221b74a70eda216a1fdd8fa82ba89f/pymongo-4.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f2077ec24e2f1248f9cac7b9a2dfb894e50cc7939fcebfb1759f99304caabef", size = 862561, upload-time = "2026-01-07T18:04:00.628Z" }, + { url = "https://files.pythonhosted.org/packages/8c/58/787d8225dd65cb2383c447346ea5e200ecfde89962d531111521e3b53018/pymongo-4.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d4f7ba040f72a9f43a44059872af5a8c8c660aa5d7f90d5344f2ed1c3c02721", size = 862923, upload-time = "2026-01-07T18:04:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a7/cc2865aae32bc77ade7b35f957a58df52680d7f8506f93c6edbf458e5738/pymongo-4.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8a0f73af1ea56c422b2dcfc0437459148a799ef4231c6aee189d2d4c59d6728f", size = 1426779, upload-time = "2026-01-07T18:04:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/81/25/3e96eb7998eec05382174da2fefc58d28613f46bbdf821045539d0ed60ab/pymongo-4.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa30cd16ddd2f216d07ba01d9635c873e97ddb041c61cf0847254edc37d1c60e", size = 1454207, upload-time = "2026-01-07T18:04:05.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/7b/8e817a7df8c5d565d39dd4ca417a5e0ef46cc5cc19aea9405f403fec6449/pymongo-4.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d638b0b1b294d95d0fdc73688a3b61e05cc4188872818cd240d51460ccabcb5", size = 1511654, upload-time = "2026-01-07T18:04:08.458Z" }, + { url = "https://files.pythonhosted.org/packages/39/7a/50c4d075ccefcd281cdcfccc5494caa5665b096b85e65a5d6afabb80e09e/pymongo-4.16.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:21d02cc10a158daa20cb040985e280e7e439832fc6b7857bff3d53ef6914ad50", size = 1496794, upload-time = "2026-01-07T18:04:10.355Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/ebdc1aaca5deeaf47310c369ef4083e8550e04e7bf7e3752cfb7d95fcdb8/pymongo-4.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fbb8d3552c2ad99d9e236003c0b5f96d5f05e29386ba7abae73949bfebc13dd", size = 1448371, upload-time = "2026-01-07T18:04:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/50fdd78c37f68ea49d590c027c96919fbccfd98f3a4cb39f84f79970bd37/pymongo-4.16.0-cp311-cp311-win32.whl", hash = "sha256:be1099a8295b1a722d03fb7b48be895d30f4301419a583dcf50e9045968a041c", size = 841024, upload-time = "2026-01-07T18:04:13.522Z" }, + { url = "https://files.pythonhosted.org/packages/4a/dd/a3aa1ade0cf9980744db703570afac70a62c85b432c391dea0577f6da7bb/pymongo-4.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:61567f712bda04c7545a037e3284b4367cad8d29b3dec84b4bf3b2147020a75b", size = 855838, upload-time = "2026-01-07T18:04:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/9ad82593ccb895e8722e4884bad4c5ce5e8ff6683b740d7823a6c2bcfacf/pymongo-4.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:c53338613043038005bf2e41a2fafa08d29cdbc0ce80891b5366c819456c1ae9", size = 845007, upload-time = "2026-01-07T18:04:17.099Z" }, { url = "https://files.pythonhosted.org/packages/6a/03/6dd7c53cbde98de469a3e6fb893af896dca644c476beb0f0c6342bcc368b/pymongo-4.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd4911c40a43a821dfd93038ac824b756b6e703e26e951718522d29f6eb166a8", size = 917619, upload-time = "2026-01-07T18:04:19.173Z" }, { url = "https://files.pythonhosted.org/packages/73/e1/328915f2734ea1f355dc9b0e98505ff670f5fab8be5e951d6ed70971c6aa/pymongo-4.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25a6b03a68f9907ea6ec8bc7cf4c58a1b51a18e23394f962a6402f8e46d41211", size = 917364, upload-time = "2026-01-07T18:04:20.861Z" }, { url = "https://files.pythonhosted.org/packages/41/fe/4769874dd9812a1bc2880a9785e61eba5340da966af888dd430392790ae0/pymongo-4.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:91ac0cb0fe2bf17616c2039dac88d7c9a5088f5cb5829b27c9d250e053664d31", size = 1686901, upload-time = "2026-01-07T18:04:22.219Z" }, @@ -915,6 +1160,22 @@ version = "2.3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/55/50/638ae329af72dd3dd262e4b0fb4a734c05074239c9131f0aaf0b32ed7264/pymssql-2.3.11.tar.gz", hash = "sha256:47ee71d9c37880dd82b830a5a7fc69374d04945c27043116b4c693858c60af66", size = 202219, upload-time = "2025-12-30T21:25:40.025Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/05/70/fb09a77beab0388c4a8f95a512c31e9c898fff145de5e5447a8dcca03bbd/pymssql-2.3.11-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:75d2a10501db24e82cfcbad5d749874980b4e4457822f19d911c2464868da6a4", size = 3171776, upload-time = "2025-12-30T21:24:23.706Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/2d88fb9cbedfcca21b31357396e6505b975b3bdbb18d12db53bc77f3d264/pymssql-2.3.11-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:ce49bf401d79f3a4062a5e9d62542f3d54a8a36de78a07b8397aa6d97ceab631", size = 2974524, upload-time = "2025-12-30T21:24:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/91/1a761f1e3de0d995af3696560a25437500a1256fd82ec61bb7154164c2c7/pymssql-2.3.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d82c1e73d15b4da01f6df7d9511d962579543c3fe92286edbea66976f0948894", size = 2445716, upload-time = "2025-12-30T21:24:26.769Z" }, + { url = "https://files.pythonhosted.org/packages/c0/25/6c261b74c9d6be8d6b37bab0a7e4a2b43d30da356f7e9e8ba79996bddc5e/pymssql-2.3.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6fcd194d33b512125d392d55049087a533c0523810b8b48d924ab38e1bb141cf", size = 2789577, upload-time = "2025-12-30T21:24:28.41Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fe/dc14bf7789697dce4936b15280194efaf89608936a3591f5ee0ed5e78a00/pymssql-2.3.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9152eea9473d8f634355f779c848bc963392936822d9b4b270502738499a9249", size = 3688035, upload-time = "2025-12-30T21:24:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/73/c5/635a73c4d5f8f3ba6ca879aa70e747668f0954bb8139c73ebad8cb71bb3b/pymssql-2.3.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e98e0b7f759ef94f2d988939b18a7b8816ad4dad253b2a81cf07bfb6ed47094", size = 3435145, upload-time = "2025-12-30T21:24:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7a/9f87357388c308b0aa21be11335cba84ce0d01568d2945166e447d20189e/pymssql-2.3.11-cp310-cp310-win32.whl", hash = "sha256:7503b8fff48c7d12f46541397245f820840ddf298144eab7a606b5cbfbb1756f", size = 1328519, upload-time = "2025-12-30T21:24:33.038Z" }, + { url = "https://files.pythonhosted.org/packages/64/f9/467e7ed83ffa4c06cacd3ecb34a1b00bb4d529e6a26b19082e5aff69b5fd/pymssql-2.3.11-cp310-cp310-win_amd64.whl", hash = "sha256:482e595d9f19ded0ae9b3a0f7b59957fd05fc212b752c5625d9bf95e1d26d46a", size = 2008743, upload-time = "2025-12-30T21:24:35.007Z" }, + { url = "https://files.pythonhosted.org/packages/ff/81/85cfc6a03a00968fa03a37648b7796c4eec0351c2b8daba752754e86eeb8/pymssql-2.3.11-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:aaa59c4a366bdd5670c7c2061947f6b53ab9d22e298ce08f76531acd7019682b", size = 3170430, upload-time = "2025-12-30T21:24:36.733Z" }, + { url = "https://files.pythonhosted.org/packages/37/f6/b8131a7017d81aa9b3633f819950ad7dbae90dc52340182d039b497a178f/pymssql-2.3.11-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:c69ea39f02e0bd851e64806c3a51837335b66534e3d28f60d9bd711aed7fad08", size = 2972371, upload-time = "2025-12-30T21:24:38.263Z" }, + { url = "https://files.pythonhosted.org/packages/06/4e/0a79dbea835fc37264a844abb790767f781f43c85f1d4d833b7bd3b42a6a/pymssql-2.3.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2a9028d180a722e2d564ce8d65b3415bc5e35c2f6d9bff1e851d37ba47bf4d24", size = 2438248, upload-time = "2025-12-30T21:24:40.136Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/4bc4035502ff789cae26dbeb4d090d090bc210f629c7c1f0f11b24ecf4bf/pymssql-2.3.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b9aebd415122eee53203f7f8801fc22e7399fcb41efda5b962fe5b3b84a7f093", size = 2776273, upload-time = "2025-12-30T21:24:41.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5c/edc024e913437ddb78a64c044f1db43807b024fed8d91dba02635af6ed43/pymssql-2.3.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:214a46696e0ac5fae8246f09ab90e0441f27fc713e929f3a67e15acd66cc96a9", size = 3679073, upload-time = "2025-12-30T21:24:42.811Z" }, + { url = "https://files.pythonhosted.org/packages/34/09/715b003100406e7caf6708e610d2f31e11a0ba4142b867813d6ce8d2ed94/pymssql-2.3.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2bc011bbce17b513ca96aac0eb865427bcdc25af35e845c0785f6ca6f47fb15", size = 3422246, upload-time = "2025-12-30T21:24:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/a6e512dfabd60c9947aa310dd04c08b7e021051292ae20fd5a821ec5e898/pymssql-2.3.11-cp311-cp311-win32.whl", hash = "sha256:9c28d611d333e553c94822949ca6762a66e88ff2c5daf8bb1ee4a5d8fca44325", size = 1327366, upload-time = "2025-12-30T21:24:47.075Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/dc7bc32365d44fdf303c6e7cbedfe95ab2cab823759f5a498ac632b6016d/pymssql-2.3.11-cp311-cp311-win_amd64.whl", hash = "sha256:63a93d09c851a22641e80e9eaa00c50a7ee7bc66df892b627305e0b2904800c8", size = 2009691, upload-time = "2025-12-30T21:24:48.382Z" }, { url = "https://files.pythonhosted.org/packages/3c/ed/d04f5dcd5fbb61c69db690e98e62999f36753e1237ce6207d66cbe900081/pymssql-2.3.11-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a95b44dc2a5088d971f4acea06cab567ef5ac4ab7dc71884c146fd6c77aaa5d7", size = 3157229, upload-time = "2025-12-30T21:24:49.669Z" }, { url = "https://files.pythonhosted.org/packages/34/0a/c1466a1ceb693641f266ebbc8bb00a05b03fb0943530d4ded72e08d3f6bf/pymssql-2.3.11-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:8c7b80a10983fbdfa1aba8aee92f3a22ca4ffba722d35fecbc38ac760da676a4", size = 2958404, upload-time = "2025-12-30T21:24:50.972Z" }, { url = "https://files.pythonhosted.org/packages/06/a4/8973234ebdb1c5254644f61a27c778caf8114fcc5bd7e1a0b14359d43862/pymssql-2.3.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:399c89b08a93e151a60a49d45cf937475eee69e39a096a385a0aeefdedb91b89", size = 2467764, upload-time = "2025-12-30T21:24:52.745Z" }, @@ -961,6 +1222,24 @@ version = "5.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8f/85/44b10070a769a56bd910009bb185c0c0a82daff8d567cd1a116d7d730c7d/pyodbc-5.3.0.tar.gz", hash = "sha256:2fe0e063d8fb66efd0ac6dc39236c4de1a45f17c33eaded0d553d21c199f4d05", size = 121770, upload-time = "2025-10-17T18:04:09.43Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/be/cd/d0ac9e8963cf43f3c0e8ebd284cd9c5d0e17457be76c35abe4998b7b6df2/pyodbc-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6682cdec78f1302d0c559422c8e00991668e039ed63dece8bf99ef62173376a5", size = 71888, upload-time = "2025-10-17T18:02:58.285Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/95ea2795ea8a0db60414e14f117869a5ba44bd52387886c1a210da637315/pyodbc-5.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9cd3f0a9796b3e1170a9fa168c7e7ca81879142f30e20f46663b882db139b7d2", size = 71813, upload-time = "2025-10-17T18:02:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/95/c9/6f4644b60af513ea1c9cab1ff4af633e8f300e8468f4ae3507f04524e641/pyodbc-5.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46185a1a7f409761716c71de7b95e7bbb004390c650d00b0b170193e3d6224bb", size = 318556, upload-time = "2025-10-17T18:03:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/19/3f/24876d9cb9c6ce1bd2b6f43f69ebc00b8eb47bf1ed99ee95e340bf90ed79/pyodbc-5.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:349a9abae62a968b98f6bbd23d2825151f8d9de50b3a8f5f3271b48958fdb672", size = 322048, upload-time = "2025-10-17T18:03:02.522Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/faf17353605ac60f80136bc3172ed2d69d7defcb9733166293fc14ac2c52/pyodbc-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac23feb7ddaa729f6b840639e92f83ff0ccaa7072801d944f1332cd5f5b05f47", size = 1286123, upload-time = "2025-10-17T18:03:04.157Z" }, + { url = "https://files.pythonhosted.org/packages/d4/61/c9d407d2aa3e89f9bb68acf6917b0045a788ae8c3f4045c34759cb77af63/pyodbc-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8aa396c6d6af52ccd51b8c8a5bffbb46fd44e52ce07ea4272c1d28e5e5b12722", size = 1343502, upload-time = "2025-10-17T18:03:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9f/f1b0f3238d873d4930aa2a2b8d5ba97132f6416764bf0c87368f8d6f2139/pyodbc-5.3.0-cp310-cp310-win32.whl", hash = "sha256:46869b9a6555ff003ed1d8ebad6708423adf2a5c88e1a578b9f029fb1435186e", size = 62968, upload-time = "2025-10-17T18:03:06.933Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/5f8ebdca4735aad0119aaaa6d5d73b379901b7a1dbb643aaa636040b27cf/pyodbc-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:705903acf6f43c44fc64e764578d9a88649eb21bf7418d78677a9d2e337f56f2", size = 69397, upload-time = "2025-10-17T18:03:08.49Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c8/480a942fd2e87dd7df6d3c1f429df075695ed8ae34d187fe95c64219fd49/pyodbc-5.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:c68d9c225a97aedafb7fff1c0e1bfe293093f77da19eaf200d0e988fa2718d16", size = 64446, upload-time = "2025-10-17T18:03:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c7/534986d97a26cb8f40ef456dfcf00d8483161eade6d53fa45fcf2d5c2b87/pyodbc-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebc3be93f61ea0553db88589e683ace12bf975baa954af4834ab89f5ee7bf8ae", size = 71958, upload-time = "2025-10-17T18:03:10.163Z" }, + { url = "https://files.pythonhosted.org/packages/69/3c/6fe3e9eae6db1c34d6616a452f9b954b0d5516c430f3dd959c9d8d725f2a/pyodbc-5.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9b987a25a384f31e373903005554230f5a6d59af78bce62954386736a902a4b3", size = 71843, upload-time = "2025-10-17T18:03:11.058Z" }, + { url = "https://files.pythonhosted.org/packages/44/0e/81a0315d0bf7e57be24338dbed616f806131ab706d87c70f363506dc13d5/pyodbc-5.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:676031723aac7dcbbd2813bddda0e8abf171b20ec218ab8dfb21d64a193430ea", size = 327191, upload-time = "2025-10-17T18:03:11.93Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/b95bb2068f911950322a97172c68675c85a3e87dc04a98448c339fcbef21/pyodbc-5.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5c30c5cd40b751f77bbc73edd32c4498630939bcd4e72ee7e6c9a4b982cc5ca", size = 332228, upload-time = "2025-10-17T18:03:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/dc/21/2433625f7d5922ee9a34e3805805fa0f1355d01d55206c337bb23ec869bf/pyodbc-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2035c7dfb71677cd5be64d3a3eb0779560279f0a8dc6e33673499498caa88937", size = 1296469, upload-time = "2025-10-17T18:03:14.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f4/c760caf7bb9b3ab988975d84bd3e7ebda739fe0075c82f476d04ee97324c/pyodbc-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5cbe4d753723c8a8f65020b7a259183ef5f14307587165ce37e8c7e251951852", size = 1353163, upload-time = "2025-10-17T18:03:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/14/ad/f9ca1e9e44fd91058f6e35b233b1bb6213d590185bfcc2a2c4f1033266e7/pyodbc-5.3.0-cp311-cp311-win32.whl", hash = "sha256:d255f6b117d05cfc046a5201fdf39535264045352ea536c35777cf66d321fbb8", size = 62925, upload-time = "2025-10-17T18:03:17.649Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/52b9b94efd8cfd11890ae04f31f50561710128d735e4e38a8fbb964cd2c2/pyodbc-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:f1ad0e93612a6201621853fc661209d82ff2a35892b7d590106fe8f97d9f1f2a", size = 69329, upload-time = "2025-10-17T18:03:18.474Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6f/bf5433bb345007f93003fa062e045890afb42e4e9fc6bd66acc2c3bd12ca/pyodbc-5.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:0df7ff47fab91ea05548095b00e5eb87ed88ddf4648c58c67b4db95ea4913e23", size = 64447, upload-time = "2025-10-17T18:03:19.691Z" }, { url = "https://files.pythonhosted.org/packages/f5/0c/7ecf8077f4b932a5d25896699ff5c394ffc2a880a9c2c284d6a3e6ea5949/pyodbc-5.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ebf6b5d989395efe722b02b010cb9815698a4d681921bf5db1c0e1195ac1bde", size = 72994, upload-time = "2025-10-17T18:03:20.551Z" }, { url = "https://files.pythonhosted.org/packages/03/78/9fbde156055d88c1ef3487534281a5b1479ee7a2f958a7e90714968749ac/pyodbc-5.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:197bb6ddafe356a916b8ee1b8752009057fce58e216e887e2174b24c7ab99269", size = 72535, upload-time = "2025-10-17T18:03:21.423Z" }, { url = "https://files.pythonhosted.org/packages/9f/f9/8c106dcd6946e95fee0da0f1ba58cd90eb872eebe8968996a2ea1f7ac3c1/pyodbc-5.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6ccb5315ec9e081f5cbd66f36acbc820ad172b8fa3736cf7f993cdf69bd8a96", size = 333565, upload-time = "2025-10-17T18:03:22.695Z" }, @@ -1005,10 +1284,12 @@ version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -1020,6 +1301,7 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -1033,7 +1315,7 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] @@ -1057,6 +1339,16 @@ 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/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, @@ -1085,6 +1377,12 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, @@ -1102,6 +1400,24 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -1192,6 +1508,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/40/26/66ba59328dc25e523bfcb0f8db48bdebe2035e0159d600e1f01c0fc93967/sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735", size = 2155051, upload-time = "2026-01-21T18:27:28.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/9336732941df972fbbfa394db9caa8bb0cf9fe03656ec728d12e9cbd6edc/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39", size = 3234666, upload-time = "2026-01-21T18:32:28.72Z" }, + { url = "https://files.pythonhosted.org/packages/38/62/865ae8b739930ec433cd4123760bee7f8dafdc10abefd725a025604fb0de/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f", size = 3232917, upload-time = "2026-01-21T18:44:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/38/805904b911857f2b5e00fdea44e9570df62110f834378706939825579296/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5", size = 3185790, upload-time = "2026-01-21T18:32:30.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/3260bb53aabd2d274856337456ea52f6a7eccf6cce208e558f870cec766b/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e", size = 3207206, upload-time = "2026-01-21T18:44:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/67c432d7f9d88bb1a61909b67e29f6354d59186c168fb5d381cf438d3b73/sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047", size = 2115296, upload-time = "2026-01-21T18:33:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8c/25fb284f570f9d48e6c240f0269a50cec9cf009a7e08be4c0aaaf0654972/sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061", size = 2138540, upload-time = "2026-01-21T18:33:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, @@ -1262,6 +1592,60 @@ oracle = [ { name = "sqlalchemy" }, ] +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1307,6 +1691,24 @@ version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/31/afb4cf08b9892430ec419a3f0f469fb978cb013f4432e0edb9c2cf06f081/wrapt-2.1.0.tar.gz", hash = "sha256:757ff1de7e1d8db1839846672aaecf4978af433cc57e808255b83980e9651914", size = 80924, upload-time = "2026-01-31T23:25:58.917Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/60/6f/9773ddbf70d2f787d049fb5a412c18fd8140b8a33e90e8b911f0d512a7b5/wrapt-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba00229045bc0ec808f12f7d2fd02166631657c56d5b7acbbb8f03ea70fc1cd6", size = 60561, upload-time = "2026-01-31T23:26:51.314Z" }, + { url = "https://files.pythonhosted.org/packages/a1/91/f6cc8762153ebcdccf7d7aa7ca3a75fe688b7cebc250f1eac72229943d09/wrapt-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:657c7d0dcca7df8cfdce9d4e9062d51d2a2b2c8f4bdd41dc908a717099cf552a", size = 61501, upload-time = "2026-01-31T23:26:30.857Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/ce91f1e9cd77bf6c11700ac0643ccb747d7c4cbd948f63fba90d345aa85a/wrapt-2.1.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cb21ff015afe80cc30daca53136427463c364fb7c1ca96e4b7013dc6f56b2829", size = 113531, upload-time = "2026-01-31T23:26:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/91/be/231563aaf305c930705b4455023155bf485974b431bb4bf9ddc53be5ae9b/wrapt-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8d25f31cf032bfa70ec1872cdf0f7e1f1154c5a5bc6c73444bb3375b904f97f", size = 115538, upload-time = "2026-01-31T23:25:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9e/576265c0173e85e77eb9713ccecedeab34a1785d493bfa511fd98b7154bc/wrapt-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:099c88ce146134786577b892d2748ac79c16c9f70304367eee17295732907045", size = 114719, upload-time = "2026-01-31T23:25:25.153Z" }, + { url = "https://files.pythonhosted.org/packages/77/07/b374bd08739bf2f5c1accbb4c77e34bd21c3e9a0c5c49f54269d014c263f/wrapt-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dfe3f09f5ce33a4e54a3340c3cde774fd19eca0da8a83343889a3673a33ee579", size = 113204, upload-time = "2026-01-31T23:26:02.779Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e6/09285cbca4467c9701bea4c8bece8bf7cebdf2721516e6b6ccc8737d086f/wrapt-2.1.0-cp310-cp310-win32.whl", hash = "sha256:73d77cc1698bf2f0580616a2eadb94aa15b47ae09ade7d9828a5c413dbbabab8", size = 57878, upload-time = "2026-01-31T23:25:18.276Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/b027af42f6c8aacf1ab83c0a4b278ded3488f452b8b0478c5204637338dc/wrapt-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8012f863320ece76c6b95527b8ee831b818e186cafa356620cba15ba19c904de", size = 60226, upload-time = "2026-01-31T23:26:38.631Z" }, + { url = "https://files.pythonhosted.org/packages/22/ab/1fc44e40f4a7277f67eac33c645c88d54192fa2a8c6cad2735f8eb86fe3b/wrapt-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccd99596ae95bc7b844196e6691b4987749ba7832c9ba437fdd99885ee5e7a84", size = 58648, upload-time = "2026-01-31T23:26:20.163Z" }, + { url = "https://files.pythonhosted.org/packages/97/0a/de541b2543e33144043cd58da09bda8d837ba42e13ae90baca32b0553023/wrapt-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d877003dbc601e1365bd03f6a980965a20d585f90c056f33e1fc241b63a6f0e7", size = 60558, upload-time = "2026-01-31T23:25:27.784Z" }, + { url = "https://files.pythonhosted.org/packages/84/2e/7e48207420e6ca7e7a05c0e4ebe9464ec9965c8face256f3ef8cc2acd862/wrapt-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:771ec962fe3ccb078177c9b8f3529e204ffcbb11d62d509e0a438e6a83f7ca68", size = 61501, upload-time = "2026-01-31T23:26:46.477Z" }, + { url = "https://files.pythonhosted.org/packages/67/2b/639a4970ecdc7143acb69a1162c76b0f1620218ad502c33e1a88d28f00b1/wrapt-2.1.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73e742368b52f9cf0921e1d2bcb8a6a44ede2e372e33df6e77caa136a942099f", size = 113954, upload-time = "2026-01-31T23:26:01.493Z" }, + { url = "https://files.pythonhosted.org/packages/81/5d/8d9177c8c0ecaf5313b462be63c5aa9672044b02bfd644dd65c6cb420d2a/wrapt-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e9129d1b582c55ad0dfb9e29e221daa0e02b18c67d8642bc8d08dd7038b3aed", size = 115994, upload-time = "2026-01-31T23:25:57.118Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e3/c5a514a0ed1dc463f5b6b4e31abbaa3b8df48b9fd391a6e8412608155a29/wrapt-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc9e37bfe67f6ea738851dd606640a87692ff81bcc76df313fb75d08e05e855f", size = 115245, upload-time = "2026-01-31T23:26:11.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/9c/2fc6a31f5758266de2cf9dc6111d3bda7b7dd6cbdcabfd755103bbcda08f/wrapt-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:46583aae3c807aa76f96355c4943031225785ed160c84052612bba0e9d456639", size = 113679, upload-time = "2026-01-31T23:25:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/6c/81/ce52694dc8184f4898c01c8af20e145b348fc7a0e4766a7345c45f0e9ce6/wrapt-2.1.0-cp311-cp311-win32.whl", hash = "sha256:e3958ba70aef2895d8c62c2d31f51ced188f60451212294677b92f4b32c12978", size = 57865, upload-time = "2026-01-31T23:25:50.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/31/0df5d38243c2a538e7bd481e676d286b41f98a729e0d37cfed9f4421ad4d/wrapt-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0ff9797e6e0b82b330ef80b0cdba7fcd0ca056d4c7af2ca44e3d05fd47929ede", size = 60227, upload-time = "2026-01-31T23:25:35.954Z" }, + { url = "https://files.pythonhosted.org/packages/a3/79/b587edbab21d6b8a7460234440c784e08344bcdf4fdfd9a6e9125ea14923/wrapt-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:4b0a29509ef7b501abe47b693a3c91d1f21c9a948711f6ce7afa81eb274c7eae", size = 58648, upload-time = "2026-01-31T23:25:32.887Z" }, { url = "https://files.pythonhosted.org/packages/f8/6f/c731b1fbbcdf9bd202809c6fa354c4237b663dd82a95035a7cbe899cfd25/wrapt-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a64c0fb29c89810973f312a04c067b63523e7303b9a2653820cbf16474c2e5cf", size = 61149, upload-time = "2026-01-31T23:25:29.092Z" }, { url = "https://files.pythonhosted.org/packages/b2/da/7022458a1d99f0c59720a0b0fd4b1966f8df6d41e741aadfe43bc5350547/wrapt-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5509d9150ed01c4149e40020fa68e917d5c4bb77d311e79535565c2a0418afcb", size = 61743, upload-time = "2026-01-31T23:26:14.338Z" }, { url = "https://files.pythonhosted.org/packages/b5/f4/57cc12c3fc6f4fe6ccfc15567cc1ac8aeb53a9946a675adc3df7a1ee4e6a/wrapt-2.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:52bb58b3207ace156b6134235fd43140994597704fd07d148cbcfb474ee084ea", size = 121331, upload-time = "2026-01-31T23:25:37.294Z" }, From b81644dae3904d825a9e66165905da51e31a95a4 Mon Sep 17 00:00:00 2001 From: ddc Date: Thu, 5 Feb 2026 17:04:05 -0300 Subject: [PATCH 10/18] v3.0.9 --- .github/workflows/workflow.yml | 10 +- ddcDatabases/core/exceptions.py | 4 +- ddcDatabases/core/operations.py | 6 +- ddcDatabases/core/persistent.py | 78 ++-- ddcDatabases/core/retry.py | 7 +- ddcDatabases/core/settings.py | 2 +- tests/integration/conftest.py | 32 +- tests/unit/core/test_async_functionality.py | 15 +- tests/unit/core/test_db_utils.py | 41 +- tests/unit/core/test_init_module.py | 23 +- tests/unit/core/test_persistent.py | 412 +++++++++++++++++- tests/unit/core/test_retry_logic.py | 6 +- tests/unit/core/test_settings.py | 134 +++--- tests/unit/mongodb/test_mongodb.py | 180 ++++---- tests/unit/mongodb/test_mongodb_persistent.py | 6 +- tests/unit/mssql/test_mssql.py | 48 +- tests/unit/mysql/test_mysql.py | 42 +- tests/unit/oracle/test_oracle.py | 52 +-- tests/unit/postgresql/test_postgresql.py | 106 ++--- .../postgresql/test_postgresql_persistent.py | 4 +- tests/unit/sqlite/test_sqlite.py | 63 ++- 21 files changed, 834 insertions(+), 437 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 9b470d4..5eb2770 100755 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -17,13 +17,11 @@ jobs: - uses: astral-sh/ruff-action@v3 with: args: "check" - - uses: astral-sh/ruff-action@v3 - with: - args: "format --check" test: name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} + needs: lint strategy: fail-fast: false matrix: @@ -51,8 +49,7 @@ jobs: uv venv if [[ '${{ matrix.os }}' == 'windows-latest' ]]; then # Skip MySQL on Windows - mysqlclient requires C compilation with MySQL headers - uv pip install -e .[mongodb,mssql,oracle,pgsql] - uv pip install pytest pytest-asyncio coverage pytest-cov + uv sync --group dev --no-install-package mysqlclient --no-install-package aiomysql elif [[ '${{ matrix.os }}' == 'macos-latest' ]]; then export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig" uv sync --group dev @@ -82,6 +79,7 @@ jobs: integration-test: name: Integration Tests runs-on: ubuntu-latest + needs: lint steps: - uses: actions/checkout@v6 @@ -119,7 +117,7 @@ jobs: build: name: Build Package runs-on: ubuntu-latest - needs: [lint, test, integration-test] + needs: [test, integration-test] if: startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/checkout@v6 diff --git a/ddcDatabases/core/exceptions.py b/ddcDatabases/core/exceptions.py index 8f84077..a33d7f1 100644 --- a/ddcDatabases/core/exceptions.py +++ b/ddcDatabases/core/exceptions.py @@ -9,12 +9,12 @@ class CustomBaseException(Exception): """Base exception with timestamp generation""" - __slots__ = ('original_exception',) + __slots__ = ("original_exception",) def __init__(self, msg: Any) -> None: self.original_exception = msg now = datetime.now(timezone.utc) - dt = now.isoformat(timespec='milliseconds') + dt = now.isoformat(timespec="milliseconds") _logger.error(f"[{dt}]:{repr(msg)}") raise msg diff --git a/ddcDatabases/core/operations.py b/ddcDatabases/core/operations.py index efcc9be..8572dea 100644 --- a/ddcDatabases/core/operations.py +++ b/ddcDatabases/core/operations.py @@ -22,11 +22,11 @@ _logger.addHandler(logging.NullHandler()) # Type variable for generic model types -T = TypeVar('T') +T = TypeVar("T") class DBUtils: - __slots__ = ('session', 'retry_config') + __slots__ = ("session", "retry_config") def __init__(self, session: Session, retry_config: BaseOperationRetryConfig | None = None) -> None: self.session = session @@ -201,7 +201,7 @@ def execute(self, stmt: Any) -> None: class DBUtilsAsync: - __slots__ = ('session', 'retry_config') + __slots__ = ("session", "retry_config") def __init__(self, session: AsyncSession, retry_config: BaseOperationRetryConfig | None = None) -> None: self.session = session diff --git a/ddcDatabases/core/persistent.py b/ddcDatabases/core/persistent.py index b80fd1e..4ccf2b2 100644 --- a/ddcDatabases/core/persistent.py +++ b/ddcDatabases/core/persistent.py @@ -36,8 +36,8 @@ _logger.addHandler(logging.NullHandler()) # Type variables -T = TypeVar('T') -SessionT = TypeVar('SessionT', Session, AsyncSession) +T = TypeVar("T") +SessionT = TypeVar("SessionT", Session, AsyncSession) @dataclass(slots=True, frozen=True) @@ -115,19 +115,19 @@ class BasePersistentConnection(IdleCheckerMixin, ABC, Generic[SessionT]): """ __slots__ = ( - '_connection_key', - '_engine', - '_session', - '_last_used', - '_lock', - '_config', - '_connection_retry_config', - '_operation_retry_config', - '_idle_checker_thread', - '_shutdown_event', - '_is_connected', - '_logger', - '__weakref__', # Required for WeakValueDictionary + "_connection_key", + "_engine", + "_session", + "_last_used", + "_lock", + "_config", + "_connection_retry_config", + "_operation_retry_config", + "_idle_checker_thread", + "_shutdown_event", + "_is_connected", + "_logger", + "__weakref__", # Required for WeakValueDictionary ) def __init__( @@ -202,10 +202,10 @@ class PersistentSQLAlchemyConnection(BasePersistentConnection[Session]): """ __slots__ = ( - '_connection_url', - '_engine_args', - '_autoflush', - '_expire_on_commit', + "_connection_url", + "_engine_args", + "_autoflush", + "_expire_on_commit", ) def __init__( @@ -367,11 +367,11 @@ class PersistentSQLAlchemyAsyncConnection(BasePersistentConnection[AsyncSession] """ __slots__ = ( - '_connection_url', - '_engine_args', - '_autoflush', - '_expire_on_commit', - '_async_lock', + "_connection_url", + "_engine_args", + "_autoflush", + "_expire_on_commit", + "_async_lock", ) def __init__( @@ -558,21 +558,21 @@ class PersistentMongoDBConnection(IdleCheckerMixin): """ __slots__ = ( - '_connection_key', - '_connection_url', - '_database', - '_client', - '_db', - '_last_used', - '_lock', - '_config', - '_connection_retry_config', - '_operation_retry_config', - '_idle_checker_thread', - '_shutdown_event', - '_is_connected', - '_logger', - '__weakref__', # Required for WeakValueDictionary + "_connection_key", + "_connection_url", + "_database", + "_client", + "_db", + "_last_used", + "_lock", + "_config", + "_connection_retry_config", + "_operation_retry_config", + "_idle_checker_thread", + "_shutdown_event", + "_is_connected", + "_logger", + "__weakref__", # Required for WeakValueDictionary ) def __init__( diff --git a/ddcDatabases/core/retry.py b/ddcDatabases/core/retry.py index f65db3b..9a8b0bc 100644 --- a/ddcDatabases/core/retry.py +++ b/ddcDatabases/core/retry.py @@ -10,7 +10,7 @@ from typing import Any, TypeVar # Type variable for generic return types -T = TypeVar('T') +T = TypeVar("T") _logger = logging.getLogger(__name__) _logger.addHandler(logging.NullHandler()) @@ -55,7 +55,7 @@ def _calculate_retry_delay(attempt: int, config: BaseRetryConfig) -> float: capped_delay = min(base_delay, config.max_retry_delay) # Add jitter (randomize +/- jitter%) - jitter = getattr(config, 'jitter', 0.0) or 0.0 + jitter = getattr(config, "jitter", 0.0) or 0.0 jitter_range = capped_delay * jitter jitter_offset = random.uniform(-jitter_range, jitter_range) @@ -98,8 +98,7 @@ def _handle_retry_exception( delay = _calculate_retry_delay(attempt, config) log.warning( - f"[{operation_name}] Attempt {attempt + 1}/{config.max_retries + 1} failed: {e!r}. " - f"Retrying in {delay:.2f}s..." + f"[{operation_name}] Attempt {attempt + 1}/{config.max_retries + 1} failed: {e!r}. Retrying in {delay:.2f}s..." ) return delay diff --git a/ddcDatabases/core/settings.py b/ddcDatabases/core/settings.py index 550f598..19e0c8e 100644 --- a/ddcDatabases/core/settings.py +++ b/ddcDatabases/core/settings.py @@ -7,7 +7,7 @@ from typing import TypeVar # Type variable for generic settings factory -T = TypeVar('T', bound=BaseSettings) +T = TypeVar("T", bound=BaseSettings) # Lazy loading flag for dotenv - thread-safe singleton pattern _dotenv_loaded = False diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8bd3eca..c060e7c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -2,6 +2,14 @@ from sqlalchemy import Boolean, Identity, String from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +# Testcontainer image versions +POSTGRES_IMAGE = "postgres:18-alpine" +MYSQL_IMAGE = "mysql:9" +MSSQL_IMAGE = "mcr.microsoft.com/mssql/server:2022-latest" +MONGODB_IMAGE = "mongo:8" +MARIADB_IMAGE = "mariadb:12" +ORACLE_IMAGE = "gvenzl/oracle-free:23-slim" + class Base(DeclarativeBase): pass @@ -14,49 +22,49 @@ class IntegrationModel(Base): enabled: Mapped[bool] = mapped_column(Boolean, default=True) -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def postgres_container(): from testcontainers.postgres import PostgresContainer - with PostgresContainer("postgres:18-alpine") as pg: + with PostgresContainer(POSTGRES_IMAGE) as pg: yield pg -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def mysql_container(): from testcontainers.mysql import MySqlContainer - with MySqlContainer("mysql:9") as mysql: + with MySqlContainer(MYSQL_IMAGE) as mysql: yield mysql -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def mssql_container(): from testcontainers.mssql import SqlServerContainer - with SqlServerContainer("mcr.microsoft.com/mssql/server:2022-latest", password="Strong@Pass123") as mssql: + with SqlServerContainer(MSSQL_IMAGE, password="Strong@Pass123") as mssql: yield mssql -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def mongodb_container(): from testcontainers.mongodb import MongoDbContainer - with MongoDbContainer("mongo:8") as mongo: + with MongoDbContainer(MONGODB_IMAGE) as mongo: yield mongo -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def mariadb_container(): from testcontainers.mysql import MySqlContainer - with MySqlContainer("mariadb:11") as mariadb: + with MySqlContainer(MARIADB_IMAGE) as mariadb: yield mariadb -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def oracle_container(): from testcontainers.oracle import OracleDbContainer - with OracleDbContainer("gvenzl/oracle-free:slim") as oracle: + with OracleDbContainer(ORACLE_IMAGE) as oracle: yield oracle diff --git a/tests/unit/core/test_async_functionality.py b/tests/unit/core/test_async_functionality.py index 73c0c3a..0870367 100644 --- a/tests/unit/core/test_async_functionality.py +++ b/tests/unit/core/test_async_functionality.py @@ -17,7 +17,7 @@ class AsyncTestModel(Base): - __tablename__ = 'async_test_model' + __tablename__ = "async_test_model" id = Column(Integer, primary_key=True) name = Column(String(50)) @@ -101,11 +101,10 @@ async def test_async_context_manager_entry(self): ) with ( - patch.object(conn, '_get_async_engine') as mock_get_engine, - patch('ddcDatabases.core.base.async_sessionmaker') as mock_sessionmaker, - patch.object(conn, '_test_connection_async') as mock_test_conn, + patch.object(conn, "_get_async_engine") as mock_get_engine, + patch("ddcDatabases.core.base.async_sessionmaker") as mock_sessionmaker, + patch.object(conn, "_test_connection_async") as mock_test_conn, ): - mock_engine = AsyncMock() mock_get_engine.return_value.__aenter__.return_value = mock_engine @@ -173,8 +172,8 @@ async def test_get_async_engine(self): async with conn._get_async_engine() as engine: # Our concrete implementation creates real engines assert engine is not None - assert hasattr(engine, 'dispose') # Engine should have disposed method - assert hasattr(engine, 'url') # Should have URL attribute + assert hasattr(engine, "dispose") # Engine should have disposed method + assert hasattr(engine, "url") # Should have URL attribute @pytest.mark.asyncio @@ -338,7 +337,7 @@ async def test_deleteall_success(self): mock_session.execute.assert_called_once() # Verify that delete statement was created correctly call_args = mock_session.execute.call_args[0][0] - assert hasattr(call_args, 'table') # Should be a delete statement + assert hasattr(call_args, "table") # Should be a delete statement mock_session.commit.assert_called_once() async def test_deleteall_exception_handling(self): diff --git a/tests/unit/core/test_db_utils.py b/tests/unit/core/test_db_utils.py index f424eb0..c6e192d 100644 --- a/tests/unit/core/test_db_utils.py +++ b/tests/unit/core/test_db_utils.py @@ -17,7 +17,7 @@ class DatabaseModel(Base): - __tablename__ = 'test_model' + __tablename__ = "test_model" id = Column(Integer, primary_key=True) name = Column(String(50)) @@ -124,7 +124,7 @@ def test_init(self): assert conn.session is None @pytest.mark.skipif(not POSTGRESQL_AVAILABLE, reason="PostgreSQL drivers not available") - @patch('sqlalchemy.engine.create_engine') + @patch("sqlalchemy.engine.create_engine") def test_get_engine(self, mock_create_engine): """Test _get_engine context manager""" connection_url = { @@ -586,7 +586,7 @@ async def test_deleteall_success(self): mock_session.execute.assert_called_once() # Check that delete statement was used call_args = mock_session.execute.call_args[0][0] - assert hasattr(call_args, 'table') + assert hasattr(call_args, "table") mock_session.commit.assert_called_once() @pytest.mark.asyncio @@ -663,9 +663,9 @@ def test_sync_context_manager(self): mock_engine = MagicMock() with ( - patch.object(conn, '_get_engine') as mock_get_engine, - patch('ddcDatabases.core.base.sessionmaker') as mock_sessionmaker, - patch.object(conn, '_test_connection_sync') as mock_test_conn, + patch.object(conn, "_get_engine") as mock_get_engine, + patch("ddcDatabases.core.base.sessionmaker") as mock_sessionmaker, + patch.object(conn, "_test_connection_sync") as mock_test_conn, ): mock_get_engine.return_value.__enter__.return_value = mock_engine mock_get_engine.return_value.__exit__.return_value = None @@ -704,11 +704,10 @@ async def test_async_context_manager(self): mock_engine = AsyncMock() with ( - patch.object(conn, '_get_async_engine') as mock_get_engine, - patch('ddcDatabases.core.base.async_sessionmaker') as mock_sessionmaker, - patch.object(conn, '_test_connection_async') as mock_test_conn, + patch.object(conn, "_get_async_engine") as mock_get_engine, + patch("ddcDatabases.core.base.async_sessionmaker") as mock_sessionmaker, + patch.object(conn, "_test_connection_async") as mock_test_conn, ): - mock_get_engine.return_value.__aenter__.return_value = mock_engine mock_get_engine.return_value.__aexit__.return_value = None @@ -729,7 +728,7 @@ async def test_async_context_manager(self): mock_engine.dispose.assert_called_once() @pytest.mark.skipif(not POSTGRESQL_AVAILABLE, reason="PostgreSQL drivers not available") - @patch('sqlalchemy.engine.create_engine') + @patch("sqlalchemy.engine.create_engine") def test_get_engine_context_manager(self, mock_create_engine): """Test _get_engine context manager - Lines 86-102""" connection_url = {"host": "localhost", "database": "test"} @@ -774,8 +773,8 @@ async def test_get_async_engine_context_manager(self): # Our concrete implementation creates real engines assert engine is not None # Check that the engine has the expected configuration - assert hasattr(engine, 'dispose') # Engine should have disposed method - assert hasattr(engine, 'url') # Should have URL attribute + assert hasattr(engine, "dispose") # Engine should have disposed method + assert hasattr(engine, "url") # Should have URL attribute def test_test_connection_sync_method(self): """Test _test_connection_sync method - Lines 122-132""" @@ -792,7 +791,7 @@ def test_test_connection_sync_method(self): mock_session = MagicMock() - with patch('ddcDatabases.core.base.ConnectionTester') as mock_tester_class: + with patch("ddcDatabases.core.base.ConnectionTester") as mock_tester_class: mock_tester = MagicMock() mock_tester_class.return_value = mock_tester @@ -802,10 +801,10 @@ def test_test_connection_sync_method(self): mock_tester_class.assert_called_once() call_kwargs = mock_tester_class.call_args[1] - assert call_kwargs['sync_session'] is mock_session - assert isinstance(call_kwargs['host_url'], URL) + assert call_kwargs["sync_session"] is mock_session + assert isinstance(call_kwargs["host_url"], URL) # Verify password was removed from connection URL - assert 'password' not in str(call_kwargs['host_url']) + assert "password" not in str(call_kwargs["host_url"]) mock_tester.test_connection_sync.assert_called_once() @@ -825,7 +824,7 @@ async def test_test_connection_async_method(self): mock_session = AsyncMock() - with patch('ddcDatabases.core.base.ConnectionTester') as mock_tester_class: + with patch("ddcDatabases.core.base.ConnectionTester") as mock_tester_class: mock_tester = MagicMock() mock_tester.test_connection_async = AsyncMock() mock_tester_class.return_value = mock_tester @@ -836,10 +835,10 @@ async def test_test_connection_async_method(self): mock_tester_class.assert_called_once() call_kwargs = mock_tester_class.call_args[1] - assert call_kwargs['async_session'] is mock_session - assert isinstance(call_kwargs['host_url'], URL) + assert call_kwargs["async_session"] is mock_session + assert isinstance(call_kwargs["host_url"], URL) # Verify password was removed from connection URL - assert 'password' not in str(call_kwargs['host_url']) + assert "password" not in str(call_kwargs["host_url"]) mock_tester.test_connection_async.assert_called_once() diff --git a/tests/unit/core/test_init_module.py b/tests/unit/core/test_init_module.py index db4bb22..d711e14 100644 --- a/tests/unit/core/test_init_module.py +++ b/tests/unit/core/test_init_module.py @@ -2,7 +2,6 @@ class TestInitModule: - def test_logging_configuration(self): """Test that logging is properly configured with NullHandler""" import logging @@ -24,11 +23,11 @@ def test_version_parsing_module_not_found(self): try: # Create a test scenario that will trigger ModuleNotFoundError - if 'ddcDatabases' in sys.modules: - del sys.modules['ddcDatabases'] + if "ddcDatabases" in sys.modules: + del sys.modules["ddcDatabases"] # Mock the version function to raise ModuleNotFoundError - with patch('importlib.metadata.version') as mock_version: + with patch("importlib.metadata.version") as mock_version: mock_version.side_effect = ModuleNotFoundError("No module named 'ddcDatabases'") # This should trigger the exception handling code @@ -52,11 +51,11 @@ def test_version_parsing_exception_path_direct(self): import sys # Force a re-import with mocked version to test the exception path - with patch('importlib.metadata.version') as mock_version: + with patch("importlib.metadata.version") as mock_version: mock_version.side_effect = ModuleNotFoundError("No module named 'ddcDatabases'") # Remove the module from cache if it exists - modules_to_remove = [name for name in sys.modules.keys() if name.startswith('ddcDatabases')] + modules_to_remove = [name for name in sys.modules.keys() if name.startswith("ddcDatabases")] for module_name in modules_to_remove: del sys.modules[module_name] @@ -71,12 +70,12 @@ def test_constants_accessibility(self): import ddcDatabases constants = [ - '__title__', - '__author__', - '__email__', - '__license__', - '__copyright__', - '__version__', + "__title__", + "__author__", + "__email__", + "__license__", + "__copyright__", + "__version__", ] for const in constants: diff --git a/tests/unit/core/test_persistent.py b/tests/unit/core/test_persistent.py index 1d8d60f..73b0ef6 100644 --- a/tests/unit/core/test_persistent.py +++ b/tests/unit/core/test_persistent.py @@ -201,7 +201,7 @@ def test_initialization(self): assert conn._database == "test_db" assert not conn.is_connected - @patch('pymongo.MongoClient') + @patch("pymongo.MongoClient") def test_connect_success(self, mock_client_class): """Test successful MongoDB connection.""" mock_client = MagicMock() @@ -242,7 +242,7 @@ def test_disconnect(self): assert conn._client is None assert conn._db is None - @patch('pymongo.MongoClient') + @patch("pymongo.MongoClient") def test_context_manager(self, mock_client_class): """Test MongoDB context manager usage.""" mock_client = MagicMock() @@ -538,8 +538,8 @@ async def test_async_connect_success(self): mock_session = AsyncMock() mock_session.execute = AsyncMock() - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_engine', return_value=mock_engine): - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_session', return_value=mock_session): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): conn = PersistentSQLAlchemyAsyncConnection( connection_key="test://localhost/db", connection_url="postgresql+asyncpg://user:pass@localhost/db", @@ -559,8 +559,8 @@ async def test_async_context_manager(self): mock_session = AsyncMock() mock_session.execute = AsyncMock() - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_engine', return_value=mock_engine): - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_session', return_value=mock_session): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): conn = PersistentSQLAlchemyAsyncConnection( connection_key="test://localhost/db", connection_url="postgresql+asyncpg://user:pass@localhost/db", @@ -623,8 +623,8 @@ async def test_async_connect_reconnects_on_lost_connection(self): mock_session2 = AsyncMock() mock_session2.execute = AsyncMock() - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_engine', return_value=mock_engine): - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_session', return_value=mock_session2): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session2): conn = PersistentSQLAlchemyAsyncConnection( connection_key="test://localhost/db", connection_url="postgresql+asyncpg://user:pass@localhost/db", @@ -646,7 +646,7 @@ async def test_async_connect_reconnects_on_lost_connection(self): class TestPersistentMongoDBConnectionAdvanced: """Test advanced PersistentMongoDBConnection scenarios.""" - @patch('pymongo.MongoClient') + @patch("pymongo.MongoClient") def test_connect_reconnects_on_ping_failure(self, mock_client_class): """Test that connect reconnects when ping fails.""" from pymongo.errors import PyMongoError @@ -761,7 +761,7 @@ def test_connect_reconnects_on_lost_connection(self): assert conn.is_connected # Simulate connection loss - with patch.object(session1, 'execute', side_effect=SQLAlchemyError("Connection lost")): + with patch.object(session1, "execute", side_effect=SQLAlchemyError("Connection lost")): # This should trigger reconnection session2 = conn.connect() assert conn.is_connected @@ -770,6 +770,378 @@ def test_connect_reconnects_on_lost_connection(self): conn.shutdown() +class TestSyncConnectSessionReuse: + """Test that connect() reuses existing valid session.""" + + def test_connect_reuses_valid_session(self): + """Test that a second connect() call reuses the existing session when healthy.""" + config = TEST_CONFIG + conn = PersistentSQLAlchemyConnection( + connection_key="test://localhost/db", + connection_url="sqlite:///:memory:", + config=config, + ) + + session1 = conn.connect() + session2 = conn.connect() + + assert session1 is session2 + assert conn.is_connected + + conn.shutdown() + + +class TestSyncConnectAutoReconnect: + """Test auto_reconnect=True branches in sync connect.""" + + def test_connect_with_auto_reconnect(self): + """Test connect() with auto_reconnect enabled uses retry_operation.""" + config = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=True, + ) + conn = PersistentSQLAlchemyConnection( + connection_key="test://localhost/db", + connection_url="sqlite:///:memory:", + config=config, + ) + + session = conn.connect() + assert conn.is_connected + assert session is not None + + conn.shutdown() + + +class TestSyncExecuteWithRetry: + """Test execute_with_retry for sync connections.""" + + def test_execute_with_retry_success(self): + """Test execute_with_retry commits on success.""" + config = TEST_CONFIG + conn = PersistentSQLAlchemyConnection( + connection_key="test://localhost/db", + connection_url="sqlite:///:memory:", + config=config, + ) + + def operation(session): + return "result" + + result = conn.execute_with_retry(operation) + assert result == "result" + assert conn.is_connected + + conn.shutdown() + + def test_execute_with_retry_failure_rollback(self): + """Test execute_with_retry rolls back on failure.""" + config = TEST_CONFIG + conn = PersistentSQLAlchemyConnection( + connection_key="test://localhost/db", + connection_url="sqlite:///:memory:", + config=config, + ) + + def failing_operation(session): + raise ValueError("operation failed") + + with pytest.raises(ValueError, match="operation failed"): + conn.execute_with_retry(failing_operation) + + conn.shutdown() + + def test_execute_with_retry_auto_reconnect(self): + """Test execute_with_retry with auto_reconnect enabled.""" + config = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=True, + ) + conn = PersistentSQLAlchemyConnection( + connection_key="test://localhost/db", + connection_url="sqlite:///:memory:", + config=config, + ) + + def operation(session): + return 42 + + result = conn.execute_with_retry(operation) + assert result == 42 + + conn.shutdown() + + +class TestAsyncExecuteWithRetry: + """Test execute_with_retry for async connections.""" + + @pytest.mark.asyncio + async def test_async_execute_with_retry_success(self): + """Test async execute_with_retry commits on success.""" + config = TEST_CONFIG + + mock_engine = MagicMock() + mock_session = AsyncMock() + mock_session.execute = AsyncMock() + mock_session.commit = AsyncMock() + mock_session.rollback = AsyncMock() + + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + config=config, + ) + + def operation(session): + return "async_result" + + result = await conn.execute_with_retry(operation) + assert result == "async_result" + mock_session.commit.assert_awaited_once() + + @pytest.mark.asyncio + async def test_async_execute_with_retry_failure_rollback(self): + """Test async execute_with_retry rolls back on failure.""" + config = TEST_CONFIG + + mock_engine = MagicMock() + mock_session = AsyncMock() + mock_session.execute = AsyncMock() + mock_session.commit = AsyncMock() + mock_session.rollback = AsyncMock() + + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + config=config, + ) + + def failing_operation(session): + raise ValueError("async op failed") + + with pytest.raises(ValueError, match="async op failed"): + await conn.execute_with_retry(failing_operation) + + mock_session.rollback.assert_awaited_once() + + @pytest.mark.asyncio + async def test_async_execute_with_retry_auto_reconnect(self): + """Test async execute_with_retry with auto_reconnect enabled.""" + config = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=True, + ) + + mock_engine = MagicMock() + mock_session = AsyncMock() + mock_session.execute = AsyncMock() + mock_session.commit = AsyncMock() + mock_session.rollback = AsyncMock() + + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + config=config, + ) + + def operation(session): + return "retried" + + result = await conn.execute_with_retry(operation) + assert result == "retried" + + @pytest.mark.asyncio + async def test_async_execute_with_retry_coroutine_result(self): + """Test async execute_with_retry handles coroutine return values.""" + config = TEST_CONFIG + + mock_engine = MagicMock() + mock_session = AsyncMock() + mock_session.execute = AsyncMock() + mock_session.commit = AsyncMock() + mock_session.rollback = AsyncMock() + + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + config=config, + ) + + async def async_operation(session): + return "coroutine_result" + + result = await conn.execute_with_retry(async_operation) + assert result == "coroutine_result" + + +class TestAsyncConnectSessionReuse: + """Test async connect() session reuse path.""" + + @pytest.mark.asyncio + async def test_async_connect_reuses_valid_session(self): + """Test that a second async_connect() reuses the existing session when healthy.""" + config = TEST_CONFIG + + mock_engine = MagicMock() + mock_session = AsyncMock() + mock_session.execute = AsyncMock() + + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + config=config, + ) + + session1 = await conn.async_connect() + session2 = await conn.async_connect() + + assert session1 is session2 + + +class TestAsyncConnectAutoReconnect: + """Test async auto_reconnect=True branch.""" + + @pytest.mark.asyncio + async def test_async_connect_with_auto_reconnect(self): + """Test async_connect() with auto_reconnect enabled uses retry_operation_async.""" + config = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=True, + ) + + mock_engine = MagicMock() + mock_session = AsyncMock() + mock_session.execute = AsyncMock() + + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + config=config, + ) + + session = await conn.async_connect() + assert conn.is_connected + assert session is mock_session + + +class TestMongoDBConnectSessionReuse: + """Test MongoDB connect() session reuse path.""" + + @patch("pymongo.MongoClient") + def test_mongodb_connect_reuses_valid_connection(self, mock_client_class): + """Test that a second connect() reuses the existing MongoDB connection.""" + mock_client = MagicMock() + mock_db = MagicMock() + mock_client.__getitem__ = MagicMock(return_value=mock_db) + mock_client_class.return_value = mock_client + + config = TEST_CONFIG + conn = PersistentMongoDBConnection( + connection_key="mongodb://localhost/db", + connection_url="mongodb://user:pass@localhost/admin", + database="test_db", + config=config, + ) + + db1 = conn.connect() + db2 = conn.connect() + + assert db1 is db2 + assert conn.is_connected + + conn.shutdown() + + +class TestMongoDBConnectAutoReconnect: + """Test MongoDB auto_reconnect=True branch.""" + + @patch("pymongo.MongoClient") + def test_mongodb_connect_with_auto_reconnect(self, mock_client_class): + """Test MongoDB connect() with auto_reconnect enabled.""" + mock_client = MagicMock() + mock_db = MagicMock() + mock_client.__getitem__ = MagicMock(return_value=mock_db) + mock_client_class.return_value = mock_client + + config = PersistentConnectionConfig( + idle_timeout=300, + health_check_interval=1, + auto_reconnect=True, + ) + conn = PersistentMongoDBConnection( + connection_key="mongodb://localhost/db", + connection_url="mongodb://user:pass@localhost/admin", + database="test_db", + config=config, + ) + + db = conn.connect() + assert conn.is_connected + assert db is mock_db + + conn.shutdown() + + +class TestCloseAllWithErrors: + """Test close_all_persistent_connections with shutdown errors.""" + + # noinspection PyMethodMayBeStatic + def setup_method(self): + close_all_persistent_connections() + + # noinspection PyMethodMayBeStatic + def teardown_method(self): + close_all_persistent_connections() + + def test_close_all_handles_shutdown_errors(self): + """Test that close_all handles errors during shutdown gracefully.""" + mock_conn = MagicMock() + mock_conn.shutdown.side_effect = Exception("Shutdown failed") + + with _registry_lock: + _persistent_connections["test://error/db"] = mock_conn + + # Should not raise + close_all_persistent_connections() + + with _registry_lock: + assert len(_persistent_connections) == 0 + + +class TestAsyncCreateEngineAndSession: + """Test async _create_engine and _create_session methods.""" + + def test_async_create_session(self): + """Test that _create_session returns an AsyncSession.""" + from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession + + mock_engine = MagicMock(spec=AsyncEngine) + + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://user:pass@localhost/db", + ) + session = conn._create_session(mock_engine) + + assert isinstance(session, AsyncSession) + + class TestRetrySettingsIntegration: """Test retry settings in settings classes.""" @@ -778,16 +1150,16 @@ def test_postgresql_retry_settings(self): from ddcDatabases.core.settings import PostgreSQLSettings settings = PostgreSQLSettings() - assert hasattr(settings, 'connection_enable_retry') - assert hasattr(settings, 'connection_max_retries') - assert hasattr(settings, 'connection_initial_retry_delay') - assert hasattr(settings, 'connection_max_retry_delay') - assert hasattr(settings, 'operation_enable_retry') - assert hasattr(settings, 'operation_max_retries') - assert hasattr(settings, 'operation_initial_retry_delay') - assert hasattr(settings, 'operation_max_retry_delay') - assert hasattr(settings, 'operation_jitter') - assert hasattr(settings, 'connection_disconnect_idle_timeout') + assert hasattr(settings, "connection_enable_retry") + assert hasattr(settings, "connection_max_retries") + assert hasattr(settings, "connection_initial_retry_delay") + assert hasattr(settings, "connection_max_retry_delay") + assert hasattr(settings, "operation_enable_retry") + assert hasattr(settings, "operation_max_retries") + assert hasattr(settings, "operation_initial_retry_delay") + assert hasattr(settings, "operation_max_retry_delay") + assert hasattr(settings, "operation_jitter") + assert hasattr(settings, "connection_disconnect_idle_timeout") assert settings.connection_enable_retry is True assert settings.connection_max_retries == 3 diff --git a/tests/unit/core/test_retry_logic.py b/tests/unit/core/test_retry_logic.py index 6916b26..aa60067 100644 --- a/tests/unit/core/test_retry_logic.py +++ b/tests/unit/core/test_retry_logic.py @@ -310,7 +310,7 @@ def test_retry_disabled(self): operation.assert_called_once() - @patch('ddcDatabases.core.retry.time.sleep') + @patch("ddcDatabases.core.retry.time.sleep") def test_delay_between_retries(self, mock_sleep): """Test that delays occur between retries.""" config = BaseOperationRetryConfig( @@ -348,7 +348,7 @@ def test_returns_falsy_value(self): """Test that falsy return values (0, False, None, '') are returned correctly.""" config = BaseOperationRetryConfig() - for falsy_value in [0, False, None, '', [], {}]: + for falsy_value in [0, False, None, "", [], {}]: operation = MagicMock(return_value=falsy_value) result = retry_operation(operation, config, "test_op") assert result == falsy_value @@ -433,7 +433,7 @@ async def test_returns_falsy_value(self): """Test that falsy return values are returned correctly in async.""" config = BaseOperationRetryConfig() - for falsy_value in [0, False, None, '', [], {}]: + for falsy_value in [0, False, None, "", [], {}]: async def operation(val=falsy_value): # noqa return val diff --git a/tests/unit/core/test_settings.py b/tests/unit/core/test_settings.py index c92e647..6a9fd3e 100644 --- a/tests/unit/core/test_settings.py +++ b/tests/unit/core/test_settings.py @@ -98,12 +98,12 @@ def test_env_override(self): with patch.dict( os.environ, { - 'SQLITE_FILE_PATH': 'tests/data/test.db', - 'SQLITE_ECHO': 'true', + "SQLITE_FILE_PATH": "tests/data/test.db", + "SQLITE_ECHO": "true", }, ): settings = SQLiteSettings() - assert settings.file_path == 'tests/data/test.db' + assert settings.file_path == "tests/data/test.db" assert settings.echo @@ -144,25 +144,25 @@ def test_env_override(self): with patch.dict( os.environ, { - 'POSTGRESQL_HOST': 'custom-host', - 'POSTGRESQL_PORT': '9999', - 'POSTGRESQL_USER': 'testuser', - 'POSTGRESQL_PASSWORD': 'testpass', - 'POSTGRESQL_DATABASE': 'testdb', - 'POSTGRESQL_ECHO': 'true', - 'POSTGRESQL_SCHEMA': 'custom_schema', - 'POSTGRESQL_SSL_MODE': 'require', + "POSTGRESQL_HOST": "custom-host", + "POSTGRESQL_PORT": "9999", + "POSTGRESQL_USER": "testuser", + "POSTGRESQL_PASSWORD": "testpass", + "POSTGRESQL_DATABASE": "testdb", + "POSTGRESQL_ECHO": "true", + "POSTGRESQL_SCHEMA": "custom_schema", + "POSTGRESQL_SSL_MODE": "require", }, ): settings = PostgreSQLSettings() - assert settings.host == 'custom-host' + assert settings.host == "custom-host" assert settings.port == 9999 - assert settings.user == 'testuser' - assert settings.password == 'testpass' - assert settings.database == 'testdb' + assert settings.user == "testuser" + assert settings.password == "testpass" + assert settings.database == "testdb" assert settings.echo - assert settings.schema == 'custom_schema' - assert settings.ssl_mode == 'require' + assert settings.schema == "custom_schema" + assert settings.ssl_mode == "require" class TestMSSQLSettings: @@ -193,23 +193,23 @@ def test_env_override(self): with patch.dict( os.environ, { - 'MSSQL_HOST': 'mssql-host', - 'MSSQL_PORT': '1434', - 'MSSQL_USER': 'testuser', - 'MSSQL_PASSWORD': 'testpass', - 'MSSQL_DATABASE': 'testdb', - 'MSSQL_SCHEMA': 'custom_schema', - 'MSSQL_ECHO': 'true', - 'MSSQL_SSL_ENCRYPT': 'true', + "MSSQL_HOST": "mssql-host", + "MSSQL_PORT": "1434", + "MSSQL_USER": "testuser", + "MSSQL_PASSWORD": "testpass", + "MSSQL_DATABASE": "testdb", + "MSSQL_SCHEMA": "custom_schema", + "MSSQL_ECHO": "true", + "MSSQL_SSL_ENCRYPT": "true", }, ): settings = MSSQLSettings() - assert settings.host == 'mssql-host' + assert settings.host == "mssql-host" assert settings.port == 1434 - assert settings.user == 'testuser' - assert settings.password == 'testpass' - assert settings.database == 'testdb' - assert settings.schema == 'custom_schema' + assert settings.user == "testuser" + assert settings.password == "testpass" + assert settings.database == "testdb" + assert settings.schema == "custom_schema" assert settings.echo is True assert settings.ssl_encrypt is True @@ -250,23 +250,23 @@ def test_env_override(self): with patch.dict( os.environ, { - 'MYSQL_HOST': 'mysql-host', - 'MYSQL_PORT': '3307', - 'MYSQL_USER': 'testuser', - 'MYSQL_PASSWORD': 'testpass', - 'MYSQL_DATABASE': 'testdb', - 'MYSQL_ECHO': 'true', - 'MYSQL_SSL_MODE': 'REQUIRED', + "MYSQL_HOST": "mysql-host", + "MYSQL_PORT": "3307", + "MYSQL_USER": "testuser", + "MYSQL_PASSWORD": "testpass", + "MYSQL_DATABASE": "testdb", + "MYSQL_ECHO": "true", + "MYSQL_SSL_MODE": "REQUIRED", }, ): settings = MySQLSettings() - assert settings.host == 'mysql-host' + assert settings.host == "mysql-host" assert settings.port == 3307 - assert settings.user == 'testuser' - assert settings.password == 'testpass' - assert settings.database == 'testdb' + assert settings.user == "testuser" + assert settings.password == "testpass" + assert settings.database == "testdb" assert settings.echo is True - assert settings.ssl_mode == 'REQUIRED' + assert settings.ssl_mode == "REQUIRED" class TestMongoDBSettings: @@ -294,21 +294,21 @@ def test_env_override(self): with patch.dict( os.environ, { - 'MONGODB_HOST': 'mongo-host', - 'MONGODB_PORT': '27018', - 'MONGODB_USER': 'testuser', - 'MONGODB_PASSWORD': 'testpass', - 'MONGODB_DATABASE': 'testdb', - 'MONGODB_BATCH_SIZE': '1000', - 'MONGODB_TLS_ENABLED': 'true', + "MONGODB_HOST": "mongo-host", + "MONGODB_PORT": "27018", + "MONGODB_USER": "testuser", + "MONGODB_PASSWORD": "testpass", + "MONGODB_DATABASE": "testdb", + "MONGODB_BATCH_SIZE": "1000", + "MONGODB_TLS_ENABLED": "true", }, ): settings = MongoDBSettings() - assert settings.host == 'mongo-host' + assert settings.host == "mongo-host" assert settings.port == 27018 - assert settings.user == 'testuser' - assert settings.password == 'testpass' - assert settings.database == 'testdb' + assert settings.user == "testuser" + assert settings.password == "testpass" + assert settings.database == "testdb" assert settings.batch_size == 1000 assert settings.tls_enabled is True @@ -335,21 +335,21 @@ def test_env_override(self): with patch.dict( os.environ, { - 'ORACLE_HOST': 'oracle-host', - 'ORACLE_PORT': '1522', - 'ORACLE_USER': 'testuser', - 'ORACLE_PASSWORD': 'testpass', - 'ORACLE_SERVICENAME': 'testservice', - 'ORACLE_ECHO': 'true', - 'ORACLE_SSL_ENABLED': 'true', + "ORACLE_HOST": "oracle-host", + "ORACLE_PORT": "1522", + "ORACLE_USER": "testuser", + "ORACLE_PASSWORD": "testpass", + "ORACLE_SERVICENAME": "testservice", + "ORACLE_ECHO": "true", + "ORACLE_SSL_ENABLED": "true", }, ): settings = OracleSettings() - assert settings.host == 'oracle-host' + assert settings.host == "oracle-host" assert settings.port == 1522 - assert settings.user == 'testuser' - assert settings.password == 'testpass' - assert settings.servicename == 'testservice' + assert settings.user == "testuser" + assert settings.password == "testpass" + assert settings.servicename == "testservice" assert settings.echo is True assert settings.ssl_enabled is True @@ -380,8 +380,8 @@ def setup_method(self): ddcDatabases.core.settings._dotenv_loaded = False - @patch('ddcDatabases.core.settings._dotenv_loaded', True) - @patch('ddcDatabases.core.settings.load_dotenv') + @patch("ddcDatabases.core.settings._dotenv_loaded", True) + @patch("ddcDatabases.core.settings.load_dotenv") def test_dotenv_not_loaded_if_already_loaded(self, mock_load_dotenv): """Test that dotenv is not loaded if already loaded""" get_postgresql_settings.cache_clear() @@ -401,7 +401,7 @@ def test_dotenv_loading_flag(self): import ddcDatabases.core.settings as settings_module # Patch load_dotenv before reloading to ensure it's mocked - with patch.object(settings_module, 'load_dotenv') as mock_load_dotenv: + with patch.object(settings_module, "load_dotenv") as mock_load_dotenv: # Reset the flag and cache settings_module._dotenv_loaded = False get_sqlite_settings.cache_clear() diff --git a/tests/unit/mongodb/test_mongodb.py b/tests/unit/mongodb/test_mongodb.py index dccc366..f7001e7 100644 --- a/tests/unit/mongodb/test_mongodb.py +++ b/tests/unit/mongodb/test_mongodb.py @@ -53,30 +53,30 @@ def setup_method(self): def _create_mock_settings(self, **overrides): """Create mock settings with default values and optional overrides""" mock_settings = MagicMock() - mock_settings.host = overrides.get('host', 'localhost') - mock_settings.port = overrides.get('port', 27017) - mock_settings.user = overrides.get('user', 'admin') - mock_settings.password = overrides.get('password', 'admin') - mock_settings.database = overrides.get('database', 'admin') - mock_settings.batch_size = overrides.get('batch_size', 2865) - mock_settings.limit = overrides.get('limit', 0) - mock_settings.driver = overrides.get('driver', 'mongodb') + mock_settings.host = overrides.get("host", "localhost") + mock_settings.port = overrides.get("port", 27017) + mock_settings.user = overrides.get("user", "admin") + mock_settings.password = overrides.get("password", "admin") + mock_settings.database = overrides.get("database", "admin") + mock_settings.batch_size = overrides.get("batch_size", 2865) + mock_settings.limit = overrides.get("limit", 0) + mock_settings.driver = overrides.get("driver", "mongodb") # TLS settings - mock_settings.tls_enabled = overrides.get('tls_enabled', False) - mock_settings.tls_ca_cert_path = overrides.get('tls_ca_cert_path', None) - mock_settings.tls_cert_key_path = overrides.get('tls_cert_key_path', None) - mock_settings.tls_allow_invalid_certificates = overrides.get('tls_allow_invalid_certificates', False) + mock_settings.tls_enabled = overrides.get("tls_enabled", False) + mock_settings.tls_ca_cert_path = overrides.get("tls_ca_cert_path", None) + mock_settings.tls_cert_key_path = overrides.get("tls_cert_key_path", None) + mock_settings.tls_allow_invalid_certificates = overrides.get("tls_allow_invalid_certificates", False) # Connection retry settings - mock_settings.connection_enable_retry = overrides.get('connection_enable_retry', False) - mock_settings.connection_max_retries = overrides.get('connection_max_retries', 0) - mock_settings.connection_initial_retry_delay = overrides.get('connection_initial_retry_delay', 0.0) - mock_settings.connection_max_retry_delay = overrides.get('connection_max_retry_delay', 0.0) + mock_settings.connection_enable_retry = overrides.get("connection_enable_retry", False) + mock_settings.connection_max_retries = overrides.get("connection_max_retries", 0) + mock_settings.connection_initial_retry_delay = overrides.get("connection_initial_retry_delay", 0.0) + mock_settings.connection_max_retry_delay = overrides.get("connection_max_retry_delay", 0.0) # Operation retry settings - mock_settings.operation_enable_retry = overrides.get('operation_enable_retry', False) - mock_settings.operation_max_retries = overrides.get('operation_max_retries', 0) - mock_settings.operation_initial_retry_delay = overrides.get('operation_initial_retry_delay', 0.0) - mock_settings.operation_max_retry_delay = overrides.get('operation_max_retry_delay', 0.0) - mock_settings.operation_jitter = overrides.get('operation_jitter', 0.1) + mock_settings.operation_enable_retry = overrides.get("operation_enable_retry", False) + mock_settings.operation_max_retries = overrides.get("operation_max_retries", 0) + mock_settings.operation_initial_retry_delay = overrides.get("operation_initial_retry_delay", 0.0) + mock_settings.operation_max_retry_delay = overrides.get("operation_max_retry_delay", 0.0) + mock_settings.operation_jitter = overrides.get("operation_jitter", 0.1) return mock_settings # noinspection PyMethodMayBeStatic @@ -111,7 +111,7 @@ def test_init_with_settings(self): assert mongodb._query_config.limit == 0 assert not mongodb.is_connected - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_init_with_parameters(self, mock_get_settings): """Test MongoDB initialization with override parameters""" mock_settings = self._create_mock_settings( @@ -143,8 +143,8 @@ def test_init_with_parameters(self, mock_get_settings): assert mongodb._query_config.batch_size == 500 assert mongodb._query_config.limit == 100 - @patch('ddcDatabases.mongodb.get_mongodb_settings') - @patch('ddcDatabases.mongodb.MongoClient') + @patch("ddcDatabases.mongodb.get_mongodb_settings") + @patch("ddcDatabases.mongodb.MongoClient") def test_init_with_query_parameter(self, mock_mongo_client, mock_get_settings): """Test MongoDB initialization with query parameter and cursor creation""" mock_settings = self._create_mock_settings() @@ -196,7 +196,7 @@ def patched_init(mongodb_self, *args, **kwargs): if not mongodb_self.user or not mongodb_self.password: raise RuntimeError("Missing username/password") - with patch.object(MongoDB, '__init__', patched_init): + with patch.object(MongoDB, "__init__", patched_init): with pytest.raises(RuntimeError, match="Missing username/password"): MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) @@ -221,7 +221,7 @@ def patched_init(mongodb_self, *args, **kwargs): if not mongodb_self.user or not mongodb_self.password: raise RuntimeError("Missing username/password") - with patch.object(MongoDB, '__init__', patched_init): + with patch.object(MongoDB, "__init__", patched_init): with pytest.raises(RuntimeError, match="Missing username/password"): MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) @@ -237,8 +237,8 @@ def patched_init(mongodb_self, *args, **kwargs): mongodb_self.user = mock_settings.user mongodb_self.password = mock_settings.password mongodb_self.database = mock_settings.database - mongodb_self.collection = kwargs.get('collection', None) - mongodb_self.query = kwargs.get('query', None) + mongodb_self.collection = kwargs.get("collection", None) + mongodb_self.query = kwargs.get("query", None) mongodb_self.is_connected = False mongodb_self.client = None mongodb_self.cursor_ref = None @@ -255,8 +255,7 @@ def patched_enter(mongodb_self): mongodb_self.is_connected = True return mongodb_self - with patch.object(MongoDB, '__init__', patched_init), patch.object(MongoDB, '__enter__', patched_enter): - + with patch.object(MongoDB, "__init__", patched_init), patch.object(MongoDB, "__enter__", patched_enter): mongodb = MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) with mongodb as mongo_instance: @@ -276,8 +275,8 @@ def patched_init(mongodb_self, *args, **kwargs): mongodb_self.user = mock_settings.user mongodb_self.password = mock_settings.password mongodb_self.database = mock_settings.database - mongodb_self.collection = kwargs.get('collection', None) - mongodb_self.query = kwargs.get('query', None) + mongodb_self.collection = kwargs.get("collection", None) + mongodb_self.query = kwargs.get("query", None) mongodb_self.is_connected = False mongodb_self.client = None mongodb_self.cursor_ref = None @@ -285,13 +284,12 @@ def patched_init(mongodb_self, *args, **kwargs): mongodb_self.batch_size = mock_settings.batch_size mongodb_self.limit = mock_settings.limit - with patch.object(MongoDB, '__init__', patched_init), patch('sys.exit') as mock_sys_exit: - + with patch.object(MongoDB, "__init__", patched_init), patch("sys.exit") as mock_sys_exit: # Create a patched __enter__ method that calls sys.exit def patched_enter(mongodb_self): mock_sys_exit(1) - with patch.object(MongoDB, '__enter__', patched_enter): + with patch.object(MongoDB, "__enter__", patched_enter): mongodb = MongoDB( collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"}) ) @@ -314,8 +312,8 @@ def patched_init(mongodb_self, *args, **kwargs): mongodb_self.user = mock_settings.user mongodb_self.password = mock_settings.password mongodb_self.database = mock_settings.database - mongodb_self.collection = kwargs.get('collection', None) - mongodb_self.query = kwargs.get('query', None) + mongodb_self.collection = kwargs.get("collection", None) + mongodb_self.query = kwargs.get("query", None) mongodb_self.is_connected = False mongodb_self.client = None mongodb_self.cursor_ref = None @@ -323,8 +321,7 @@ def patched_init(mongodb_self, *args, **kwargs): mongodb_self.batch_size = mock_settings.batch_size mongodb_self.limit = mock_settings.limit - with patch.object(MongoDB, '__init__', patched_init), patch('sys.exit') as mock_sys_exit: - + with patch.object(MongoDB, "__init__", patched_init), patch("sys.exit") as mock_sys_exit: # Create a patched __enter__ method that simulates client close and sys.exit def patched_enter(mongodb_self): mock_client = MagicMock() @@ -333,7 +330,7 @@ def patched_enter(mongodb_self): mock_client.close() mock_sys_exit(1) - with patch.object(MongoDB, '__enter__', patched_enter): + with patch.object(MongoDB, "__enter__", patched_enter): mongodb = MongoDB( collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"}), @@ -358,8 +355,8 @@ def patched_init(mongodb_self, *args, **kwargs): mongodb_self.user = mock_settings.user mongodb_self.password = mock_settings.password mongodb_self.database = mock_settings.database - mongodb_self.collection = kwargs.get('collection', None) - mongodb_self.query = kwargs.get('query', None) + mongodb_self.collection = kwargs.get("collection", None) + mongodb_self.query = kwargs.get("query", None) mongodb_self.is_connected = False mongodb_self.client = None mongodb_self.cursor_ref = None @@ -381,11 +378,10 @@ def patched_exit(mongodb_self, exc_type, exc_val, exc_tb): mongodb_self.is_connected = False with ( - patch.object(MongoDB, '__init__', patched_init), - patch.object(MongoDB, '__enter__', patched_enter), - patch.object(MongoDB, '__exit__', patched_exit), + patch.object(MongoDB, "__init__", patched_init), + patch.object(MongoDB, "__enter__", patched_enter), + patch.object(MongoDB, "__exit__", patched_exit), ): - mongodb = MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) with mongodb: @@ -394,8 +390,8 @@ def patched_exit(mongodb_self, exc_type, exc_val, exc_tb): assert not mongodb.is_connected mongodb.client.close.assert_called_once() - @patch('ddcDatabases.mongodb.get_mongodb_settings') - @patch('ddcDatabases.mongodb.MongoClient') + @patch("ddcDatabases.mongodb.get_mongodb_settings") + @patch("ddcDatabases.mongodb.MongoClient") def test_enter_method_client_close_condition(self, _mock_mongo_client, mock_get_settings): """Test __enter__ method client.close() condition - Line 47""" mock_settings = self._create_mock_settings() @@ -414,7 +410,7 @@ def test_enter_method_client_close_condition(self, _mock_mongo_client, mock_get_ # Verify client.close() was called mock_client.close.assert_called_once() - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_exit_method_with_none_client(self, mock_get_settings): """Test __exit__ method when client is None - Lines 51-53""" mock_settings = self._create_mock_settings() @@ -436,14 +432,14 @@ def test_missing_collection_runtime_error(self): with pytest.raises(ValueError, match="MongoDB collection name is required"): MongoDB(query_config=MongoDBQueryConfig(query={"test": "value"})) # collection is None - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_empty_query_defaults_to_empty_dict(self, mock_get_settings): """Test that missing query defaults to {} for fetching entire collection""" mock_get_settings.return_value = self._create_mock_settings() mongodb = MongoDB(collection="test_collection") # query is None assert mongodb._query_config.query == {} # Should default to empty dict - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_with_empty_query(self, mock_get_settings): """Test _create_cursor works with empty query (fetch entire collection)""" mock_settings = self._create_mock_settings() @@ -470,7 +466,7 @@ def test_create_cursor_with_empty_query(self, mock_get_settings): mock_cursor.batch_size.assert_called_once_with(2865) assert result == mock_cursor - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_exit_with_cursor_cleanup(self, mock_get_settings): """Test __exit__ method cleans up cursor_ref""" mock_settings = self._create_mock_settings() @@ -496,7 +492,7 @@ def test_exit_with_cursor_cleanup(self, mock_get_settings): mock_client.close.assert_called_once() assert mongodb.is_connected is False - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_exit_without_cursor_ref(self, mock_get_settings): """Test __exit__ method when cursor_ref is None""" mock_settings = self._create_mock_settings() @@ -515,7 +511,7 @@ def test_exit_without_cursor_ref(self, mock_get_settings): mock_client.close.assert_called_once() assert mongodb.is_connected is False - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_with_sorting(self, mock_get_settings): """Test _create_cursor with sorting parameters""" mock_settings = self._create_mock_settings() @@ -556,7 +552,7 @@ def test_create_cursor_with_sorting(self, mock_get_settings): # Return value should be the sorted cursor assert result is mock_sorted_cursor - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_with_ascending_sort(self, mock_get_settings): """Test _create_cursor with ascending sort""" mock_settings = self._create_mock_settings() @@ -583,7 +579,7 @@ def test_create_cursor_with_ascending_sort(self, mock_get_settings): mock_collection.create_index.assert_called_once_with([("name", ASCENDING)]) - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_with_none_query(self, mock_get_settings): """Test _create_cursor with None query (should default to empty dict)""" mock_settings = self._create_mock_settings() @@ -608,7 +604,7 @@ def test_create_cursor_with_none_query(self, mock_get_settings): # Verify empty dict was used for query mock_collection.find.assert_called_once_with({}, batch_size=2865, limit=0) - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_init_with_sort_parameters(self, mock_get_settings): """Test MongoDB initialization with sort parameters""" mock_settings = self._create_mock_settings() @@ -633,7 +629,7 @@ def test_init_with_sort_parameters(self, mock_get_settings): assert mongodb._query_config.batch_size == 1000 assert mongodb._query_config.limit == 50 - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_with_sort_column_only(self, mock_get_settings): """Test _create_cursor with only sort_column (should default to ascending)""" mock_settings = self._create_mock_settings() @@ -662,7 +658,7 @@ def test_create_cursor_with_sort_column_only(self, mock_get_settings): mock_collection.create_index.assert_called_once_with([("created_at", ASCENDING)]) mock_cursor.sort.assert_called_once_with("created_at", ASCENDING) - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_with_id_field_sorting(self, mock_get_settings): """Test _create_cursor with _id field sorting (should not create index but still sort)""" mock_settings = self._create_mock_settings() @@ -711,7 +707,7 @@ def test_enter_method_structure(self): """Test __enter__ method structural elements for coverage""" mock_settings = self._create_mock_settings() - with patch('ddcDatabases.mongodb.get_mongodb_settings', return_value=mock_settings): + with patch("ddcDatabases.mongodb.get_mongodb_settings", return_value=mock_settings): mongodb = MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) # Test connection URL creation (line 41) @@ -728,7 +724,7 @@ def test_enter_exception_handling_structure(self): """Test __enter__ exception handling structure for coverage""" mock_settings = self._create_mock_settings() - with patch('ddcDatabases.mongodb.get_mongodb_settings', return_value=mock_settings): + with patch("ddcDatabases.mongodb.get_mongodb_settings", return_value=mock_settings): mongodb = MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) # Test the exception handling logic structure @@ -747,7 +743,7 @@ def test_client_assignment_and_connection_flag(self): """Test client assignment and is_connected flag - Lines 42, 44, 45""" mock_settings = self._create_mock_settings() - with patch('ddcDatabases.mongodb.get_mongodb_settings', return_value=mock_settings): + with patch("ddcDatabases.mongodb.get_mongodb_settings", return_value=mock_settings): mongodb = MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) # Test the assignment logic that happens in __enter__ @@ -771,7 +767,7 @@ def test_test_connection_raises_connection_error_on_failure(self): mock_settings = self._create_mock_settings() - with patch('ddcDatabases.mongodb.get_mongodb_settings', return_value=mock_settings): + with patch("ddcDatabases.mongodb.get_mongodb_settings", return_value=mock_settings): mongodb = MongoDB(collection="test_collection", query_config=MongoDBQueryConfig(query={"test": "value"})) # Mock client with failing ping @@ -786,7 +782,7 @@ def test_create_cursor_ascending_sort_with_limit(self): """Test _create_cursor with ascending sort order and custom limit.""" mock_settings = self._create_mock_settings(batch_size=100, limit=10) - with patch('ddcDatabases.mongodb.get_mongodb_settings', return_value=mock_settings): + with patch("ddcDatabases.mongodb.get_mongodb_settings", return_value=mock_settings): mongodb = MongoDB(collection="test_collection") # Create proper nested mock structure @@ -818,7 +814,7 @@ def test_create_cursor_with_default_sort_order(self): """Test _create_cursor with no explicit sort order defaults to ascending.""" mock_settings = self._create_mock_settings(batch_size=100, limit=10) - with patch('ddcDatabases.mongodb.get_mongodb_settings', return_value=mock_settings): + with patch("ddcDatabases.mongodb.get_mongodb_settings", return_value=mock_settings): mongodb = MongoDB(collection="test_collection") # Create proper nested mock structure @@ -845,7 +841,7 @@ def test_create_cursor_with_default_sort_order(self): call_args = mock_cursor.sort.call_args assert call_args[0][1] == ASCENDING - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_enter_with_connection_error_calls_sys_exit(self, mock_get_settings): """Test that __enter__ calls sys.exit when connection fails.""" from pymongo.errors import PyMongoError @@ -855,7 +851,7 @@ def test_enter_with_connection_error_calls_sys_exit(self, mock_get_settings): mock_client = MagicMock() mock_client.admin.command.side_effect = PyMongoError("Connection failed") - with patch('ddcDatabases.mongodb.MongoClient', return_value=mock_client): + with patch("ddcDatabases.mongodb.MongoClient", return_value=mock_client): from ddcDatabases.mongodb import MongoDBConnectionRetryConfig mongodb = MongoDB( @@ -869,7 +865,7 @@ def test_enter_with_connection_error_calls_sys_exit(self, mock_get_settings): assert exc_info.value.code == 1 - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_tls_enabled(self, mock_get_settings): """Test MongoDB TLS configuration""" mock_settings = self._create_mock_settings() @@ -894,7 +890,7 @@ def test_tls_enabled(self, mock_get_settings): assert mongodb._tls_config.tls_cert_key_path == "/path/to/cert.pem" assert mongodb._tls_config.tls_allow_invalid_certificates - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_tls_disabled(self, mock_get_settings): """Test MongoDB with TLS disabled""" mock_settings = self._create_mock_settings() @@ -911,8 +907,8 @@ def test_tls_disabled(self, mock_get_settings): assert mongodb._tls_config.tls_cert_key_path is None assert not mongodb._tls_config.tls_allow_invalid_certificates - @patch('ddcDatabases.mongodb.get_mongodb_settings') - @patch('ddcDatabases.mongodb.MongoClient') + @patch("ddcDatabases.mongodb.get_mongodb_settings") + @patch("ddcDatabases.mongodb.MongoClient") def test_tls_connection_url(self, mock_mongo_client, mock_get_settings): """Test MongoDB TLS parameters are appended to connection URL""" mock_settings = self._create_mock_settings() @@ -952,7 +948,7 @@ def test_tls_connection_url(self, mock_mongo_client, mock_get_settings): assert _connection_url == expected_url - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_get_connection_info(self, mock_get_settings): """Test get_connection_info returns connection config""" mock_get_settings.return_value = self._create_mock_settings() @@ -965,7 +961,7 @@ def test_get_connection_info(self, mock_get_settings): assert conn_info.host == "localhost" assert conn_info.collection == "test_collection" - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_get_query_info(self, mock_get_settings): """Test get_query_info returns query config""" mock_get_settings.return_value = self._create_mock_settings() @@ -981,7 +977,7 @@ def test_get_query_info(self, mock_get_settings): assert query_info.query == {"status": "active"} assert query_info.batch_size == 500 - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_get_operation_retry_info(self, mock_get_settings): """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() @@ -991,13 +987,13 @@ def test_get_operation_retry_info(self, mock_get_settings): assert op_retry_info is mongodb._operation_retry_config # Check that it has the expected attributes from BaseOperationRetryConfig - assert hasattr(op_retry_info, 'enable_retry') - assert hasattr(op_retry_info, 'max_retries') - assert hasattr(op_retry_info, 'initial_retry_delay') - assert hasattr(op_retry_info, 'max_retry_delay') - assert hasattr(op_retry_info, 'jitter') + assert hasattr(op_retry_info, "enable_retry") + assert hasattr(op_retry_info, "max_retries") + assert hasattr(op_retry_info, "initial_retry_delay") + assert hasattr(op_retry_info, "max_retry_delay") + assert hasattr(op_retry_info, "jitter") - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_get_tls_info(self, mock_get_settings): """Test get_tls_info returns TLS config""" mock_get_settings.return_value = self._create_mock_settings() @@ -1013,7 +1009,7 @@ def test_get_tls_info(self, mock_get_settings): assert tls_info.tls_enabled assert tls_info.tls_ca_cert_path == "/path/to/ca.pem" - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_build_connection_url_with_tls(self, mock_get_settings): """Test _build_connection_url with TLS options""" mock_get_settings.return_value = self._create_mock_settings() @@ -1034,7 +1030,7 @@ def test_build_connection_url_with_tls(self, mock_get_settings): assert "&tlsCertificateKeyFile=/path/to/cert.pem" in url assert "&tlsAllowInvalidCertificates=true" in url - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_build_connection_url_without_tls(self, mock_get_settings): """Test _build_connection_url without TLS""" mock_get_settings.return_value = self._create_mock_settings() @@ -1045,7 +1041,7 @@ def test_build_connection_url_without_tls(self, mock_get_settings): assert "?tls=true" not in url assert "mongodb://admin:admin@localhost/admin" == url - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_test_connection_success(self, mock_get_settings): """Test _test_connection logs success message""" mock_get_settings.return_value = self._create_mock_settings() @@ -1063,7 +1059,7 @@ def test_test_connection_success(self, mock_get_settings): assert "Connected to" in str(mongodb.logger.info.call_args) @pytest.mark.asyncio - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") async def test_async_aenter_success(self, mock_get_settings): """Test async __aenter__ method structure and _create_cursor_async""" mock_get_settings.return_value = self._create_mock_settings() @@ -1091,7 +1087,7 @@ async def test_async_aenter_success(self, mock_get_settings): mock_collection.find.assert_called_once() @pytest.mark.asyncio - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") async def test_async_aexit_cleanup(self, mock_get_settings): """Test async __aexit__ cleans up resources""" mock_get_settings.return_value = self._create_mock_settings() @@ -1117,7 +1113,7 @@ async def mock_close(): mock_client.close.assert_called_once() @pytest.mark.asyncio - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") async def test_async_aexit_without_cursor(self, mock_get_settings): """Test async __aexit__ when cursor is None""" mock_get_settings.return_value = self._create_mock_settings() @@ -1135,7 +1131,7 @@ async def test_async_aexit_without_cursor(self, mock_get_settings): mock_client.close.assert_called_once() @pytest.mark.asyncio - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") async def test_async_test_connection_success(self, mock_get_settings): """Test _test_connection_async logs success message""" mock_get_settings.return_value = self._create_mock_settings() @@ -1156,7 +1152,7 @@ async def mock_command(cmd): assert "Async connected to" in str(mongodb.logger.info.call_args) @pytest.mark.asyncio - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") async def test_async_test_connection_failure(self, mock_get_settings): """Test _test_connection_async raises ConnectionError on failure""" from pymongo.errors import PyMongoError @@ -1175,7 +1171,7 @@ async def mock_command(_cmd): with pytest.raises(ConnectionError, match="Async connection to MongoDB failed"): await mongodb._test_connection_async() - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_async(self, mock_get_settings): """Test _create_cursor_async method""" mock_get_settings.return_value = self._create_mock_settings(batch_size=100, limit=10) @@ -1204,7 +1200,7 @@ def test_create_cursor_async(self, mock_get_settings): mock_cursor.batch_size.assert_called_once_with(100) assert cursor is mock_cursor - @patch('ddcDatabases.mongodb.get_mongodb_settings') + @patch("ddcDatabases.mongodb.get_mongodb_settings") def test_create_cursor_async_with_sorting(self, mock_get_settings): """Test _create_cursor_async with sorting""" from pymongo import DESCENDING @@ -1231,8 +1227,8 @@ def test_create_cursor_async_with_sorting(self, mock_get_settings): mock_cursor.sort.assert_called_once_with("created_at", DESCENDING) @pytest.mark.asyncio - @patch('ddcDatabases.mongodb.get_mongodb_settings') - @patch('ddcDatabases.mongodb.AsyncIOMotorClient') + @patch("ddcDatabases.mongodb.get_mongodb_settings") + @patch("ddcDatabases.mongodb.AsyncIOMotorClient") async def test_async_aenter_connection_error(self, mock_async_client, mock_get_settings): """Test async __aenter__ handles connection error""" from ddcDatabases.mongodb import MongoDBConnectionRetryConfig diff --git a/tests/unit/mongodb/test_mongodb_persistent.py b/tests/unit/mongodb/test_mongodb_persistent.py index d897c49..ef1509c 100644 --- a/tests/unit/mongodb/test_mongodb_persistent.py +++ b/tests/unit/mongodb/test_mongodb_persistent.py @@ -95,7 +95,7 @@ def test_different_databases_return_different_instances(self): ) assert conn1 is not conn2 - @patch('pymongo.MongoClient') + @patch("pymongo.MongoClient") def test_context_manager(self, mock_client_class): """Test MongoDB persistent connection context manager.""" mock_client = MagicMock() @@ -116,7 +116,7 @@ def test_context_manager(self, mock_client_class): conn.shutdown() - @patch('pymongo.MongoClient') + @patch("pymongo.MongoClient") def test_health_check_on_connect(self, mock_client_class): """Test that MongoDB health check (ping) runs on connect.""" mock_client = MagicMock() @@ -144,7 +144,7 @@ def test_connection_has_lock(self): connection_url="mongodb://user:pass@localhost/admin", database="test_db", ) - assert hasattr(conn, '_lock') + assert hasattr(conn, "_lock") def test_connection_key_format(self): """Test that connection key has correct format.""" diff --git a/tests/unit/mssql/test_mssql.py b/tests/unit/mssql/test_mssql.py index 99ff7aa..5ff7204 100644 --- a/tests/unit/mssql/test_mssql.py +++ b/tests/unit/mssql/test_mssql.py @@ -50,7 +50,7 @@ def _create_mock_settings(self, **overrides): # already covered by the credential validation tests that are working. # Core functionality (credential validation) is tested and working. - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_init_with_parameters(self, mock_get_settings): """Test MSSQL initialization with override parameters""" mock_settings = self._create_mock_settings( @@ -114,7 +114,7 @@ def patched_init(mssql_self, *args, **kwargs): if not mssql_self.connection_url["username"] or not mssql_self.connection_url["password"]: raise RuntimeError("Missing username/password") - with patch.object(MSSQL, '__init__', patched_init): + with patch.object(MSSQL, "__init__", patched_init): with pytest.raises(RuntimeError, match="Missing username/password"): MSSQL() @@ -153,7 +153,7 @@ def patched_init(mssql_self, *args, **kwargs): if not mssql_self.connection_url["username"] or not mssql_self.connection_url["password"]: raise RuntimeError("Missing username/password") - with patch.object(MSSQL, '__init__', patched_init): + with patch.object(MSSQL, "__init__", patched_init): with pytest.raises(RuntimeError, match="Missing username/password"): MSSQL() @@ -173,7 +173,7 @@ def patched_init(mssql_self, *args, **kwargs): # This test was testing URL schema parameters which is an edge case # Core functionality is covered by other tests - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_custom_odbcdriver_version(self, mock_get_settings): """Test custom ODBC driver version""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -185,7 +185,7 @@ def test_custom_odbcdriver_version(self, mock_get_settings): assert mssql._connection_config.odbcdriver_version == 18 assert mssql.connection_url["query"]["driver"] == "ODBC Driver 18 for SQL Server" - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_extra_engine_args(self, mock_get_settings): """Test extra engine arguments are properly included""" mock_settings = self._create_mock_settings() @@ -204,7 +204,7 @@ def test_extra_engine_args(self, mock_get_settings): assert mssql.engine_args["max_overflow"] == 50 assert not mssql.engine_args["echo"] - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_autoflush_and_expire_on_commit(self, mock_get_settings): """Test MSSQL autoflush and expire_on_commit parameters""" mock_settings = self._create_mock_settings() @@ -215,7 +215,7 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): assert not mssql._session_config.autoflush assert not mssql._session_config.expire_on_commit - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_autocommit_parameter(self, mock_get_settings): """Test MSSQL autocommit parameter""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -226,7 +226,7 @@ def test_autocommit_parameter(self, mock_get_settings): assert mssql._session_config.autocommit assert mssql.engine_args["connect_args"]["autocommit"] - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_connection_timeout_parameter(self, mock_get_settings): """Test MSSQL connection_timeout parameter""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -238,7 +238,7 @@ def test_connection_timeout_parameter(self, mock_get_settings): assert mssql.engine_args["connect_args"]["timeout"] == 60 assert mssql.engine_args["connect_args"]["login_timeout"] == 60 - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_pool_recycle_parameter(self, mock_get_settings): """Test MSSQL pool_recycle parameter""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -249,7 +249,7 @@ def test_pool_recycle_parameter(self, mock_get_settings): assert mssql._pool_config.pool_recycle == 7200 assert mssql.engine_args["pool_recycle"] == 7200 - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_all_parameters_defaults(self, mock_get_settings): """Test MSSQL parameters use settings defaults""" mock_settings = self._create_mock_settings(autoflush=False, expire_on_commit=False, odbcdriver_version=18) @@ -266,7 +266,7 @@ def test_all_parameters_defaults(self, mock_get_settings): assert mssql.engine_args["connect_args"]["timeout"] == 30 assert mssql.engine_args["pool_recycle"] == 3600 - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_test_connection_sync_url_creation(self, mock_get_settings): """Test _test_connection_sync URL creation logic - Lines 73-77""" mock_settings = self._create_mock_settings() @@ -299,7 +299,7 @@ def test_test_connection_sync_url_creation(self, mock_get_settings): assert _connection_url.port == 1433 assert _connection_url.database == "master" - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_test_connection_async_url_creation(self, mock_get_settings): """Test _test_connection_async URL creation logic - Lines 87-91""" mock_settings = self._create_mock_settings() @@ -332,7 +332,7 @@ def test_test_connection_async_url_creation(self, mock_get_settings): assert _connection_url.port == 1433 assert _connection_url.database == "master" - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_connection_url_modification_sync(self, mock_get_settings): """Test that connection_url is properly copied (not modified) in _test_connection_sync""" mock_settings = self._create_mock_settings() @@ -355,7 +355,7 @@ def test_connection_url_modification_sync(self, mock_get_settings): assert "password" in mssql.connection_url assert "query" in mssql.connection_url - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_connection_url_modification_async(self, mock_get_settings): """Test that connection_url is properly copied (not modified) in _test_connection_async""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -382,7 +382,7 @@ def test_connection_url_modification_async(self, mock_get_settings): assert "password" in mssql.connection_url assert "query" in mssql.connection_url - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_ssl_encrypt_enabled(self, mock_get_settings): """Test MSSQL SSL encrypt enabled""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -395,7 +395,7 @@ def test_ssl_encrypt_enabled(self, mock_get_settings): assert mssql.connection_url["query"]["Encrypt"] == "yes" assert mssql.connection_url["query"]["TrustServerCertificate"] == "no" - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_ssl_encrypt_disabled(self, mock_get_settings): """Test MSSQL SSL encrypt disabled""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -408,7 +408,7 @@ def test_ssl_encrypt_disabled(self, mock_get_settings): assert mssql.connection_url["query"]["Encrypt"] == "no" assert mssql.connection_url["query"]["TrustServerCertificate"] == "yes" - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_get_ssl_info(self, mock_get_settings): """Test get_ssl_info() returns the immutable SSL configuration""" mock_settings = self._create_mock_settings(odbcdriver_version=18) @@ -427,7 +427,7 @@ def test_get_ssl_info(self, mock_get_settings): assert not ssl_info.ssl_trust_server_certificate assert ssl_info.ssl_ca_cert_path == "/path/to/ca.pem" - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_get_connection_info(self, mock_get_settings): """Test get_connection_info returns connection config""" mock_get_settings.return_value = self._create_mock_settings() @@ -440,7 +440,7 @@ def test_get_connection_info(self, mock_get_settings): assert conn_info.host == "localhost" assert conn_info.port == 1433 - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_get_pool_info(self, mock_get_settings): """Test get_pool_info returns pool config""" mock_get_settings.return_value = self._create_mock_settings() @@ -453,7 +453,7 @@ def test_get_pool_info(self, mock_get_settings): assert pool_info.pool_size == 15 assert pool_info.max_overflow == 30 - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_get_session_info(self, mock_get_settings): """Test get_session_info returns session config""" mock_get_settings.return_value = self._create_mock_settings() @@ -466,7 +466,7 @@ def test_get_session_info(self, mock_get_settings): assert session_info.echo assert not session_info.autoflush - @patch('ddcDatabases.mssql.get_mssql_settings') + @patch("ddcDatabases.mssql.get_mssql_settings") def test_get_operation_retry_info(self, mock_get_settings): """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() @@ -475,6 +475,6 @@ def test_get_operation_retry_info(self, mock_get_settings): op_retry_info = mssql.get_operation_retry_info() assert op_retry_info is mssql._operation_retry_config - assert hasattr(op_retry_info, 'enable_retry') - assert hasattr(op_retry_info, 'max_retries') - assert hasattr(op_retry_info, 'jitter') + assert hasattr(op_retry_info, "enable_retry") + assert hasattr(op_retry_info, "max_retries") + assert hasattr(op_retry_info, "jitter") diff --git a/tests/unit/mysql/test_mysql.py b/tests/unit/mysql/test_mysql.py index e6cfcce..7e675b2 100644 --- a/tests/unit/mysql/test_mysql.py +++ b/tests/unit/mysql/test_mysql.py @@ -79,11 +79,11 @@ def patched_init(mysql_self, *args, **kwargs): if not mysql_self.connection_url["username"] or not mysql_self.connection_url["password"]: raise RuntimeError("Missing username/password") - with patch.object(MySQL, '__init__', patched_init): + with patch.object(MySQL, "__init__", patched_init): with pytest.raises(RuntimeError, match="Missing username/password"): MySQL() - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_init_with_parameters(self, mock_get_settings): """Test MySQL initialization with override parameters""" mock_settings = self._create_mock_settings( @@ -107,7 +107,7 @@ def test_init_with_parameters(self, mock_get_settings): assert mysql.connection_url["password"] == "custompass" assert mysql._session_config.echo - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_minimal_init(self, mock_get_settings): """Test MySQL minimal initialization""" mock_settings = self._create_mock_settings() @@ -119,7 +119,7 @@ def test_minimal_init(self, mock_get_settings): assert mysql.connection_url["port"] == 3306 assert mysql.sync_driver == "mysql+mysqldb" - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_extra_engine_args(self, mock_get_settings): """Test MySQL with extra engine arguments""" mock_settings = self._create_mock_settings(database="dev") @@ -136,7 +136,7 @@ def test_extra_engine_args(self, mock_get_settings): # Test that default args are still present assert not mysql.engine_args["echo"] - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_autoflush_and_expire_on_commit(self, mock_get_settings): """Test MySQL autoflush and expire_on_commit parameters""" mock_settings = self._create_mock_settings(database="dev") @@ -147,7 +147,7 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): assert not mysql._session_config.autoflush assert not mysql._session_config.expire_on_commit - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_pool_size_parameter(self, mock_get_settings): """Test MySQL pool_size parameter""" mock_settings = self._create_mock_settings(database="dev", pool_size=10) @@ -158,7 +158,7 @@ def test_pool_size_parameter(self, mock_get_settings): assert mysql._pool_config.pool_size == 15 assert mysql.engine_args["pool_size"] == 15 - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_max_overflow_parameter(self, mock_get_settings): """Test MySQL max_overflow parameter""" mock_settings = self._create_mock_settings(database="dev", max_overflow=20) @@ -169,7 +169,7 @@ def test_max_overflow_parameter(self, mock_get_settings): assert mysql._pool_config.max_overflow == 30 assert mysql.engine_args["max_overflow"] == 30 - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_pool_parameters_defaults(self, mock_get_settings): """Test MySQL pool parameters use settings defaults""" mock_settings = self._create_mock_settings(database="dev", pool_size=10, max_overflow=20) @@ -182,7 +182,7 @@ def test_pool_parameters_defaults(self, mock_get_settings): assert mysql.engine_args["pool_size"] == 10 assert mysql.engine_args["max_overflow"] == 20 - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_ssl_enabled(self, mock_get_settings): """Test MySQL SSL configuration""" mock_settings = self._create_mock_settings(database="dev", ssl_mode="DISABLED") @@ -205,7 +205,7 @@ def test_ssl_enabled(self, mock_get_settings): assert ssl_dict["cert"] == "/path/to/client.pem" assert ssl_dict["key"] == "/path/to/client-key.pem" - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_ssl_disabled(self, mock_get_settings): """Test MySQL without SSL does not add ssl to connect_args""" mock_settings = self._create_mock_settings(database="dev", ssl_mode="DISABLED") @@ -216,7 +216,7 @@ def test_ssl_disabled(self, mock_get_settings): assert mysql._ssl_config.ssl_mode == "DISABLED" assert "ssl" not in mysql.engine_args["connect_args"] - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_get_connection_info(self, mock_get_settings): """Test get_connection_info returns connection config""" mock_get_settings.return_value = self._create_mock_settings() @@ -229,7 +229,7 @@ def test_get_connection_info(self, mock_get_settings): assert conn_info.host == "127.0.0.1" # localhost normalized to 127.0.0.1 assert conn_info.port == 3306 - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_get_pool_info(self, mock_get_settings): """Test get_pool_info returns pool config""" mock_get_settings.return_value = self._create_mock_settings() @@ -242,7 +242,7 @@ def test_get_pool_info(self, mock_get_settings): assert pool_info.pool_size == 20 assert pool_info.max_overflow == 40 - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_get_session_info(self, mock_get_settings): """Test get_session_info returns session config""" mock_get_settings.return_value = self._create_mock_settings() @@ -255,7 +255,7 @@ def test_get_session_info(self, mock_get_settings): assert session_info.echo assert not session_info.autoflush - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_get_connection_retry_info(self, mock_get_settings): """Test get_connection_retry_info returns connection retry config""" mock_get_settings.return_value = self._create_mock_settings() @@ -264,10 +264,10 @@ def test_get_connection_retry_info(self, mock_get_settings): conn_retry_info = mysql.get_connection_retry_info() assert conn_retry_info is mysql._connection_retry_config - assert hasattr(conn_retry_info, 'enable_retry') - assert hasattr(conn_retry_info, 'max_retries') + assert hasattr(conn_retry_info, "enable_retry") + assert hasattr(conn_retry_info, "max_retries") - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_get_operation_retry_info(self, mock_get_settings): """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() @@ -276,11 +276,11 @@ def test_get_operation_retry_info(self, mock_get_settings): op_retry_info = mysql.get_operation_retry_info() assert op_retry_info is mysql._operation_retry_config - assert hasattr(op_retry_info, 'enable_retry') - assert hasattr(op_retry_info, 'max_retries') - assert hasattr(op_retry_info, 'jitter') + assert hasattr(op_retry_info, "enable_retry") + assert hasattr(op_retry_info, "max_retries") + assert hasattr(op_retry_info, "jitter") - @patch('ddcDatabases.mysql.get_mysql_settings') + @patch("ddcDatabases.mysql.get_mysql_settings") def test_get_ssl_info(self, mock_get_settings): """Test get_ssl_info returns SSL config""" mock_get_settings.return_value = self._create_mock_settings() diff --git a/tests/unit/oracle/test_oracle.py b/tests/unit/oracle/test_oracle.py index f3984b1..a6813d1 100644 --- a/tests/unit/oracle/test_oracle.py +++ b/tests/unit/oracle/test_oracle.py @@ -45,7 +45,7 @@ def _create_mock_settings(self, **overrides): setattr(mock_settings, key, value) return mock_settings - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_init_basic(self, mock_get_settings): """Test Oracle basic initialization""" mock_settings = self._create_mock_settings() @@ -100,11 +100,11 @@ def patched_init(oracle_self, *args, **kwargs): if not oracle_self.connection_url["username"] or not oracle_self.connection_url["password"]: raise RuntimeError("Missing username/password") - with patch.object(Oracle, '__init__', patched_init): + with patch.object(Oracle, "__init__", patched_init): with pytest.raises(RuntimeError, match="Missing username/password"): Oracle() - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_init_with_parameters(self, mock_get_settings): """Test Oracle initialization with override parameters""" mock_settings = self._create_mock_settings( @@ -128,7 +128,7 @@ def test_init_with_parameters(self, mock_get_settings): assert oracle.connection_url["query"]["service_name"] == "customxe" assert oracle._session_config.echo - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_minimal_init(self, mock_get_settings): """Test Oracle minimal initialization""" mock_settings = self._create_mock_settings() @@ -169,7 +169,7 @@ async def test_test_connection_async_oracle(self): assert result mock_session.execute.assert_called_once() - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_extra_engine_args(self, mock_get_settings): """Test Oracle with extra engine arguments""" mock_settings = self._create_mock_settings() @@ -186,7 +186,7 @@ def test_extra_engine_args(self, mock_get_settings): # Test that default args are still present assert not oracle.engine_args["echo"] - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_autoflush_and_expire_on_commit(self, mock_get_settings): """Test Oracle autoflush and expire_on_commit parameters""" mock_settings = self._create_mock_settings() @@ -197,7 +197,7 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): assert not oracle._session_config.autoflush assert not oracle._session_config.expire_on_commit - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_autocommit_parameter(self, mock_get_settings): """Test Oracle autocommit parameter""" mock_settings = self._create_mock_settings() @@ -207,7 +207,7 @@ def test_autocommit_parameter(self, mock_get_settings): assert not oracle._session_config.autocommit - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_connection_timeout_parameter(self, mock_get_settings): """Test Oracle connection_timeout parameter""" mock_settings = self._create_mock_settings() @@ -217,7 +217,7 @@ def test_connection_timeout_parameter(self, mock_get_settings): assert oracle._pool_config.connection_timeout == 60 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_all_parameters_defaults(self, mock_get_settings): """Test Oracle parameters use settings defaults""" mock_settings = self._create_mock_settings() @@ -230,7 +230,7 @@ def test_all_parameters_defaults(self, mock_get_settings): assert not oracle._session_config.autocommit assert oracle._pool_config.connection_timeout == 30 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_pool_recycle_parameter(self, mock_get_settings): """Test Oracle pool_recycle parameter""" mock_settings = self._create_mock_settings() @@ -241,7 +241,7 @@ def test_pool_recycle_parameter(self, mock_get_settings): assert oracle._pool_config.pool_recycle == 7200 assert oracle.engine_args["pool_recycle"] == 7200 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_pool_size_parameter(self, mock_get_settings): """Test Oracle pool_size parameter""" mock_settings = self._create_mock_settings() @@ -252,7 +252,7 @@ def test_pool_size_parameter(self, mock_get_settings): assert oracle._pool_config.pool_size == 15 assert oracle.engine_args["pool_size"] == 15 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_max_overflow_parameter(self, mock_get_settings): """Test Oracle max_overflow parameter""" mock_settings = self._create_mock_settings() @@ -263,7 +263,7 @@ def test_max_overflow_parameter(self, mock_get_settings): assert oracle._pool_config.max_overflow == 30 assert oracle.engine_args["max_overflow"] == 30 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_pool_parameters_defaults(self, mock_get_settings): """Test Oracle pool parameters use settings defaults""" mock_settings = self._create_mock_settings() @@ -278,7 +278,7 @@ def test_pool_parameters_defaults(self, mock_get_settings): assert oracle.engine_args["pool_size"] == 10 assert oracle.engine_args["max_overflow"] == 20 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_ssl_wallet_path(self, mock_get_settings): """Test Oracle SSL wallet path configuration""" mock_settings = self._create_mock_settings() @@ -290,7 +290,7 @@ def test_ssl_wallet_path(self, mock_get_settings): assert oracle._ssl_config.ssl_wallet_path == "/path/to/wallet" assert oracle.engine_args["connect_args"]["wallet_location"] == "/path/to/wallet" - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_ssl_disabled_no_wallet(self, mock_get_settings): """Test Oracle without SSL wallet does not add wallet_location""" mock_settings = self._create_mock_settings() @@ -302,7 +302,7 @@ def test_ssl_disabled_no_wallet(self, mock_get_settings): assert oracle._ssl_config.ssl_wallet_path is None assert "wallet_location" not in oracle.engine_args["connect_args"] - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_get_connection_info(self, mock_get_settings): """Test get_connection_info returns connection config""" mock_get_settings.return_value = self._create_mock_settings() @@ -315,7 +315,7 @@ def test_get_connection_info(self, mock_get_settings): assert conn_info.host == "localhost" assert conn_info.port == 1521 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_get_pool_info(self, mock_get_settings): """Test get_pool_info returns pool config""" mock_get_settings.return_value = self._create_mock_settings() @@ -328,7 +328,7 @@ def test_get_pool_info(self, mock_get_settings): assert pool_info.pool_size == 20 assert pool_info.max_overflow == 40 - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_get_session_info(self, mock_get_settings): """Test get_session_info returns session config""" mock_get_settings.return_value = self._create_mock_settings() @@ -341,7 +341,7 @@ def test_get_session_info(self, mock_get_settings): assert session_info.echo assert not session_info.autoflush - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_get_connection_retry_info(self, mock_get_settings): """Test get_connection_retry_info returns connection retry config""" mock_get_settings.return_value = self._create_mock_settings() @@ -350,10 +350,10 @@ def test_get_connection_retry_info(self, mock_get_settings): conn_retry_info = oracle.get_connection_retry_info() assert conn_retry_info is oracle._connection_retry_config - assert hasattr(conn_retry_info, 'enable_retry') - assert hasattr(conn_retry_info, 'max_retries') + assert hasattr(conn_retry_info, "enable_retry") + assert hasattr(conn_retry_info, "max_retries") - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_get_operation_retry_info(self, mock_get_settings): """Test get_operation_retry_info returns operation retry config""" mock_get_settings.return_value = self._create_mock_settings() @@ -362,11 +362,11 @@ def test_get_operation_retry_info(self, mock_get_settings): op_retry_info = oracle.get_operation_retry_info() assert op_retry_info is oracle._operation_retry_config - assert hasattr(op_retry_info, 'enable_retry') - assert hasattr(op_retry_info, 'max_retries') - assert hasattr(op_retry_info, 'jitter') + assert hasattr(op_retry_info, "enable_retry") + assert hasattr(op_retry_info, "max_retries") + assert hasattr(op_retry_info, "jitter") - @patch('ddcDatabases.oracle.get_oracle_settings') + @patch("ddcDatabases.oracle.get_oracle_settings") def test_get_ssl_info(self, mock_get_settings): """Test get_ssl_info returns SSL config""" mock_get_settings.return_value = self._create_mock_settings() diff --git a/tests/unit/postgresql/test_postgresql.py b/tests/unit/postgresql/test_postgresql.py index f65ddf8..496cb20 100644 --- a/tests/unit/postgresql/test_postgresql.py +++ b/tests/unit/postgresql/test_postgresql.py @@ -59,11 +59,11 @@ def patched_init(postgresql_self, *_args, **_kwargs): if not postgresql_self.connection_url["username"] or not postgresql_self.connection_url["password"]: raise RuntimeError("Missing username/password") - with patch.object(PostgreSQL, '__init__', patched_init): + with patch.object(PostgreSQL, "__init__", patched_init): with pytest.raises(RuntimeError, match="Missing username/password"): PostgreSQL() - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_init_with_parameters(self, mock_get_settings): """Test PostgreSQL initialization with override parameters""" mock_settings = MagicMock() @@ -93,7 +93,7 @@ def test_init_with_parameters(self, mock_get_settings): assert postgresql.connection_url["password"] == "custompass" assert postgresql._session_config.echo - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_extra_engine_args(self, mock_get_settings): """Test PostgreSQL with extra engine arguments""" mock_settings = MagicMock() @@ -118,7 +118,7 @@ def test_extra_engine_args(self, mock_get_settings): # Test that default args are still present assert not postgresql.engine_args["echo"] - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_autoflush_and_expire_on_commit(self, mock_get_settings): """Test PostgreSQL autoflush and expire_on_commit parameters""" mock_settings = MagicMock() @@ -137,7 +137,7 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): assert not postgresql._session_config.autoflush assert not postgresql._session_config.expire_on_commit - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_autocommit_parameter(self, mock_get_settings): """Test PostgreSQL autocommit parameter""" mock_settings = MagicMock() @@ -156,7 +156,7 @@ def test_autocommit_parameter(self, mock_get_settings): assert postgresql._session_config.autocommit - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_connect_args_psycopg_driver(self, mock_get_settings): """Test that psycopg driver sets correct connect_args""" mock_settings = MagicMock() @@ -196,7 +196,7 @@ def test_connect_args_different_driver(self): assert "psycopg" not in test_driver_pg8000 assert "pg8000" in test_driver_pg8000 - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_driver_detection_logic(self, mock_get_settings): """Test driver detection logic in init""" mock_settings = MagicMock() @@ -244,15 +244,15 @@ def test_engine_args_structure(self): postgresql = PostgreSQL(extra_engine_args=extra_args) # Test that extra args are properly included - assert postgresql.engine_args['pool_pre_ping'] - assert postgresql.engine_args['pool_recycle'] == 3600 - assert postgresql.engine_args['pool_timeout'] == 60 + assert postgresql.engine_args["pool_pre_ping"] + assert postgresql.engine_args["pool_recycle"] == 3600 + assert postgresql.engine_args["pool_timeout"] == 60 # Test that extra_engine_args dict is properly stored - assert hasattr(postgresql, 'extra_engine_args') + assert hasattr(postgresql, "extra_engine_args") assert postgresql.extra_engine_args == extra_args - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_context_manager_methods_exist(self, mock_get_settings): """Test that context manager methods exist and can be called safely""" mock_settings = MagicMock() @@ -269,12 +269,12 @@ def test_context_manager_methods_exist(self, mock_get_settings): postgresql = PostgreSQL() # Test that methods exist and are callable - assert hasattr(postgresql, '_get_engine') - assert hasattr(postgresql, '_get_async_engine') + assert hasattr(postgresql, "_get_engine") + assert hasattr(postgresql, "_get_async_engine") assert callable(postgresql._get_engine) assert callable(postgresql._get_async_engine) - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_base_engine_args_method(self, mock_get_settings): """Test _get_base_engine_args method - covers lines 70-87""" from sqlalchemy import URL @@ -333,7 +333,7 @@ def test_get_base_engine_args_method(self, mock_get_settings): assert result["connect_args"]["connect_timeout"] == 30 assert result["isolation_level"] == "READ_COMMITTED" - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_sync_driver_autocommit_logic(self, mock_get_settings): """Test autocommit logic for sync driver - covers autocommit branch""" mock_settings = MagicMock() @@ -378,7 +378,7 @@ def test_sync_driver_autocommit_logic(self, mock_get_settings): assert "isolation_level" in engine_args assert engine_args["isolation_level"] == "AUTOCOMMIT" - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_async_driver_autocommit_logic(self, mock_get_settings): """Test autocommit logic for async driver - covers async autocommit branch""" mock_settings = MagicMock() @@ -426,7 +426,7 @@ def test_async_driver_autocommit_logic(self, mock_get_settings): assert "command_timeout" in engine_args["connect_args"] assert engine_args["connect_args"]["command_timeout"] == 30 - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_pool_size_parameter(self, mock_get_settings): """Test PostgreSQL pool_size parameter""" mock_settings = MagicMock() @@ -445,7 +445,7 @@ def test_pool_size_parameter(self, mock_get_settings): assert postgresql._pool_config.pool_size == 15 - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_max_overflow_parameter(self, mock_get_settings): """Test PostgreSQL max_overflow parameter""" mock_settings = MagicMock() @@ -464,7 +464,7 @@ def test_max_overflow_parameter(self, mock_get_settings): assert postgresql._pool_config.max_overflow == 30 - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_pool_parameters_defaults(self, mock_get_settings): """Test PostgreSQL pool parameters use settings defaults""" mock_settings = MagicMock() @@ -485,7 +485,7 @@ def test_pool_parameters_defaults(self, mock_get_settings): assert postgresql._pool_config.pool_size == 25 assert postgresql._pool_config.max_overflow == 50 - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_enhanced_configuration_methods(self, mock_get_settings): """Test the new enhanced configuration getter methods""" mock_settings = MagicMock() @@ -554,7 +554,7 @@ def test_enhanced_configuration_methods(self, mock_get_settings): assert not session_config.expire_on_commit assert session_config.autocommit - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_engine_method_with_psycopg(self, mock_get_settings): """Test the _get_engine method with psycopg driver""" mock_settings = MagicMock() @@ -595,12 +595,12 @@ def test_get_engine_method_with_psycopg(self, mock_get_settings): with postgresql._get_engine() as engine: # Verify engine was created with expected properties assert engine is not None - assert hasattr(engine, 'dispose') - assert hasattr(engine, 'url') + assert hasattr(engine, "dispose") + assert hasattr(engine, "url") # Verify autocommit is configured via isolation_level in the URL or engine assert "psycopg" in str(engine.url) - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_engine_method_without_autocommit(self, mock_get_settings): """Test the _get_engine method without autocommit""" mock_settings = MagicMock() @@ -641,11 +641,11 @@ def test_get_engine_method_without_autocommit(self, mock_get_settings): with postgresql._get_engine() as engine: # Verify engine was created with expected properties assert engine is not None - assert hasattr(engine, 'dispose') - assert hasattr(engine, 'url') + assert hasattr(engine, "dispose") + assert hasattr(engine, "url") assert "psycopg" in str(engine.url) - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_engine_method_non_psycopg_driver(self, mock_get_settings): """Test the _get_engine method with non-psycopg driver""" mock_settings = MagicMock() @@ -686,11 +686,11 @@ def test_get_engine_method_non_psycopg_driver(self, mock_get_settings): with postgresql._get_engine() as engine: # Verify engine was created with expected properties assert engine is not None - assert hasattr(engine, 'dispose') - assert hasattr(engine, 'url') + assert hasattr(engine, "dispose") + assert hasattr(engine, "url") assert "psycopg" in str(engine.url) - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_async_engine_method_with_asyncpg(self, mock_get_settings): """Test the _get_async_engine method with asyncpg driver""" mock_settings = MagicMock() @@ -720,8 +720,8 @@ def test_get_async_engine_method_with_asyncpg(self, mock_get_settings): async def test_async(): async with postgresql._get_async_engine() as engine: # Verify we get a real SQLAlchemy AsyncEngine - assert hasattr(engine, 'dispose') - assert hasattr(engine, 'begin') + assert hasattr(engine, "dispose") + assert hasattr(engine, "begin") # Verify URL was constructed correctly assert "postgresql+asyncpg" in str(engine.url) assert "localhost" in str(engine.url) @@ -731,7 +731,7 @@ async def test_async(): asyncio.run(test_async()) - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_async_engine_method_without_autocommit(self, mock_get_settings): """Test the _get_async_engine method without autocommit""" mock_settings = MagicMock() @@ -758,8 +758,8 @@ def test_get_async_engine_method_without_autocommit(self, mock_get_settings): async def test_async(): async with postgresql._get_async_engine() as engine: # Verify we get a real SQLAlchemy AsyncEngine - assert hasattr(engine, 'dispose') - assert hasattr(engine, 'begin') + assert hasattr(engine, "dispose") + assert hasattr(engine, "begin") # Verify URL was constructed correctly assert "postgresql+asyncpg" in str(engine.url) @@ -767,7 +767,7 @@ async def test_async(): asyncio.run(test_async()) - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_async_engine_method_non_asyncpg_driver(self, mock_get_settings): """Test the _get_async_engine method with non-asyncpg driver""" mock_settings = MagicMock() @@ -794,8 +794,8 @@ def test_get_async_engine_method_non_asyncpg_driver(self, mock_get_settings): async def test_async(): async with postgresql._get_async_engine() as engine: # Verify we get a real SQLAlchemy AsyncEngine - assert hasattr(engine, 'dispose') - assert hasattr(engine, 'begin') + assert hasattr(engine, "dispose") + assert hasattr(engine, "begin") # Verify URL was constructed correctly assert "postgresql+asyncpg" in str(engine.url) @@ -803,7 +803,7 @@ async def test_async(): asyncio.run(test_async()) - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_repr_method(self, mock_get_settings): """Test the enhanced __repr__ method""" mock_settings = MagicMock() @@ -843,7 +843,7 @@ def test_repr_method(self, mock_get_settings): assert "echo=True" in repr_str assert ")" in repr_str - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_configuration_immutability(self, mock_get_settings): """Test that configuration objects are properly immutable""" mock_settings = MagicMock() @@ -881,7 +881,7 @@ def test_configuration_immutability(self, mock_get_settings): with pytest.raises(dataclasses.FrozenInstanceError): session_config.echo = True # noqa - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_schema_default(self, mock_get_settings): """Test PostgreSQL schema defaults to public""" mock_settings = MagicMock() @@ -913,7 +913,7 @@ def test_schema_default(self, mock_get_settings): conn_config = postgresql.get_connection_info() assert conn_config.schema == "public" - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_schema_custom(self, mock_get_settings): """Test PostgreSQL with custom schema sets search_path""" mock_settings = MagicMock() @@ -958,7 +958,7 @@ def test_schema_custom(self, mock_get_settings): assert sync_connect_args["options"] == "-c search_path=custom_schema" - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_schema_public_no_options(self, mock_get_settings): """Test PostgreSQL with public schema does not set search_path options""" mock_settings = MagicMock() @@ -997,7 +997,7 @@ def test_schema_public_no_options(self, mock_get_settings): assert "options" not in sync_connect_args - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_ssl_enabled_with_options(self, mock_get_settings): """Test PostgreSQL SSL configuration""" mock_settings = MagicMock() @@ -1054,7 +1054,7 @@ def test_ssl_enabled_with_options(self, mock_get_settings): assert sync_connect_args["sslcert"] == "/path/to/client.pem" assert sync_connect_args["sslkey"] == "/path/to/client-key.pem" - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_ssl_disabled_no_connect_args(self, mock_get_settings): """Test PostgreSQL with SSL disabled does not add SSL connect_args""" mock_settings = MagicMock() @@ -1091,7 +1091,7 @@ def test_ssl_disabled_no_connect_args(self, mock_get_settings): assert "sslmode" not in sync_connect_args - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_connection_info(self, mock_get_settings): """Test get_connection_info returns connection config""" from ddcDatabases.postgresql import PostgreSQL, PostgreSQLConnectionConfig @@ -1137,7 +1137,7 @@ def test_get_connection_info(self, mock_get_settings): assert conn_info.host == "localhost" assert conn_info.port == 5432 - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_pool_info(self, mock_get_settings): """Test get_pool_info returns pool config""" from ddcDatabases.postgresql import PostgreSQL, PostgreSQLPoolConfig @@ -1183,7 +1183,7 @@ def test_get_pool_info(self, mock_get_settings): assert pool_info.pool_size == 20 assert pool_info.max_overflow == 40 - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_session_info(self, mock_get_settings): """Test get_session_info returns session config""" from ddcDatabases.postgresql import PostgreSQL, PostgreSQLSessionConfig @@ -1229,7 +1229,7 @@ def test_get_session_info(self, mock_get_settings): assert session_info.echo assert not session_info.autoflush - @patch('ddcDatabases.postgresql.get_postgresql_settings') + @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_operation_retry_info(self, mock_get_settings): """Test get_operation_retry_info returns operation retry config""" from ddcDatabases.postgresql import PostgreSQL @@ -1271,6 +1271,6 @@ def test_get_operation_retry_info(self, mock_get_settings): op_retry_info = postgresql.get_operation_retry_info() assert op_retry_info is postgresql._operation_retry_config - assert hasattr(op_retry_info, 'enable_retry') - assert hasattr(op_retry_info, 'max_retries') - assert hasattr(op_retry_info, 'jitter') + assert hasattr(op_retry_info, "enable_retry") + assert hasattr(op_retry_info, "max_retries") + assert hasattr(op_retry_info, "jitter") diff --git a/tests/unit/postgresql/test_postgresql_persistent.py b/tests/unit/postgresql/test_postgresql_persistent.py index 925900d..5e109a6 100644 --- a/tests/unit/postgresql/test_postgresql_persistent.py +++ b/tests/unit/postgresql/test_postgresql_persistent.py @@ -159,8 +159,8 @@ async def test_context_manager_async(self): mock_session = AsyncMock() mock_session.execute = AsyncMock() - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_engine', return_value=mock_engine): - with patch.object(PersistentSQLAlchemyAsyncConnection, '_create_session', return_value=mock_session): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_engine", return_value=mock_engine): + with patch.object(PersistentSQLAlchemyAsyncConnection, "_create_session", return_value=mock_session): conn = PersistentSQLAlchemyAsyncConnection( connection_key="postgresql://test@localhost:5432/testdb", connection_url="postgresql+asyncpg://user:pass@localhost/db", diff --git a/tests/unit/sqlite/test_sqlite.py b/tests/unit/sqlite/test_sqlite.py index 0ad811f..dc02bb2 100644 --- a/tests/unit/sqlite/test_sqlite.py +++ b/tests/unit/sqlite/test_sqlite.py @@ -30,7 +30,7 @@ def setup_method(self): self.Sqlite = Sqlite self.DBUtils = DBUtils - @patch('ddcDatabases.sqlite.get_sqlite_settings') + @patch("ddcDatabases.sqlite.get_sqlite_settings") def test_init_basic(self, mock_get_settings): """Test SQLite basic initialization""" mock_settings = MagicMock() @@ -53,7 +53,7 @@ def test_init_basic(self, mock_get_settings): assert not sqlite.echo assert not sqlite.is_connected - @patch('ddcDatabases.sqlite.get_sqlite_settings') + @patch("ddcDatabases.sqlite.get_sqlite_settings") def test_init_with_parameters(self, mock_get_settings): """Test SQLite initialization with parameters""" mock_settings = MagicMock() @@ -81,7 +81,7 @@ def test_init_with_parameters(self, mock_get_settings): def test_real_operations(self): """Test comprehensive SQLite operations""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name with self.Sqlite(filepath=db_path) as session: @@ -100,7 +100,7 @@ def test_real_operations(self): results = db_utils.fetchall(stmt) assert len(results) == 1 # Access through the model object returned by ORM select - assert results[0]['ModelTest'].name == "test1" + assert results[0]["ModelTest"].name == "test1" # Test fetchvalue stmt = sa.select(ModelTest.name).where(ModelTest.id == 1) @@ -139,7 +139,7 @@ def test_real_operations(self): def test_fetchvalue_none_case(self): """Test fetchvalue returning None""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name with self.Sqlite(filepath=db_path) as session: @@ -154,7 +154,7 @@ def test_fetchvalue_none_case(self): def test_context_manager(self): """Test SQLite context manager entry/exit""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name sqlite = self.Sqlite(filepath=db_path) @@ -169,7 +169,7 @@ def test_context_manager(self): sqlite.__exit__(None, None, None) assert not sqlite.is_connected - @patch('ddcDatabases.sqlite.create_engine') + @patch("ddcDatabases.sqlite.create_engine") def test_engine_creation_error(self, mock_create_engine): """Test SQLite engine creation error handling""" mock_create_engine.side_effect = Exception("Engine creation failed") @@ -197,6 +197,33 @@ def test_optional_parameters(self): assert sqlite.extra_engine_args == extra_args +class TestSQLiteInfoMethods: + """Test SQLite info getter methods""" + + def setup_method(self): + from ddcDatabases import Sqlite + + self.Sqlite = Sqlite + + def test_get_session_info(self): + """Test get_session_info returns session config.""" + sqlite = self.Sqlite(filepath="test.db") + info = sqlite.get_session_info() + assert type(info).__name__ == "SqliteSessionConfig" + + def test_get_connection_retry_info(self): + """Test get_connection_retry_info returns connection retry config.""" + sqlite = self.Sqlite(filepath="test.db") + info = sqlite.get_connection_retry_info() + assert type(info).__name__ == "SqliteConnectionRetryConfig" + + def test_get_operation_retry_info(self): + """Test get_operation_retry_info returns operation retry config.""" + sqlite = self.Sqlite(filepath="test.db") + info = sqlite.get_operation_retry_info() + assert type(info).__name__ == "SqliteOperationRetryConfig" + + class TestSQLiteRealOperations: """Test with real SQLite database operations""" @@ -209,7 +236,7 @@ def setup_method(self): def test_real_fetchall(self): """Test fetchall with real database""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name with self.Sqlite(filepath=db_path) as session: @@ -227,13 +254,13 @@ def test_real_fetchall(self): assert len(results) == 1 # Access through the model object returned by ORM select - assert results[0]['ModelTest'].id == 1 - assert results[0]['ModelTest'].name == "test" - assert results[0]['ModelTest'].enabled + assert results[0]["ModelTest"].id == 1 + assert results[0]["ModelTest"].name == "test" + assert results[0]["ModelTest"].enabled def test_real_fetchvalue(self): """Test fetchvalue with real database""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name with self.Sqlite(filepath=db_path) as session: @@ -253,7 +280,7 @@ def test_real_fetchvalue(self): def test_real_insertbulk(self): """Test bulk insert with real database""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name with self.Sqlite(filepath=db_path) as session: @@ -275,13 +302,13 @@ def test_real_insertbulk(self): assert len(results) == 3 # Access through the model object returned by ORM select - assert results[0]['ModelTest'].name == "test1" - assert results[1]['ModelTest'].name == "test2" - assert results[2]['ModelTest'].name == "test3" + assert results[0]["ModelTest"].name == "test1" + assert results[1]["ModelTest"].name == "test2" + assert results[2]["ModelTest"].name == "test3" def test_connection_state_management(self): """Test connection state management""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name sqlite = self.Sqlite(filepath=db_path) @@ -299,7 +326,7 @@ def test_connection_state_management(self): def test_custom_settings_integration(self): """Test integration with custom settings""" - with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp: + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: db_path = tmp.name # Test with custom settings From fc1e557cb646f5c76fdbfde48addb5123c20385e Mon Sep 17 00:00:00 2001 From: ddc Date: Thu, 5 Feb 2026 17:21:06 -0300 Subject: [PATCH 11/18] v3.0.9 --- .github/workflows/workflow.yml | 10 +++++----- pyproject.toml | 5 ----- uv.lock | 6 ------ 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 5eb2770..9b944ad 100755 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -52,9 +52,9 @@ jobs: uv sync --group dev --no-install-package mysqlclient --no-install-package aiomysql elif [[ '${{ matrix.os }}' == 'macos-latest' ]]; then export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig" - uv sync --group dev + uv sync --all-extras --group dev else - uv sync --group dev + uv sync --all-extras --group dev fi shell: bash @@ -63,7 +63,7 @@ jobs: with: timeout_minutes: 2 max_attempts: 3 - command: uv run pytest tests/unit + command: uv run --no-sync pytest tests/unit shell: bash - name: Upload coverage to Codecov @@ -93,7 +93,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev pkg-config - name: Install dependencies - run: uv sync --group dev + run: uv sync --all-extras --group dev shell: bash - name: Install ODBC driver for MSSQL @@ -111,7 +111,7 @@ jobs: with: timeout_minutes: 3 max_attempts: 3 - command: uv run pytest tests/integration --no-cov + command: uv run --no-sync pytest tests/integration --no-cov shell: bash build: diff --git a/pyproject.toml b/pyproject.toml index 1b9979c..752cb53 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,11 +66,6 @@ pgsql = ["psycopg[binary]>=3.3.2", "asyncpg>=0.31.0"] [dependency-groups] dev = [ - "ddcDatabases[mongodb]", - "ddcDatabases[mssql]", - "ddcDatabases[mysql]", - "ddcDatabases[oracle]", - "ddcDatabases[pgsql]", "pytest>=9.0.2", "pytest-asyncio>=1.3.0", "coverage>=7.13.3", diff --git a/uv.lock b/uv.lock index cd8f3c4..8983318 100644 --- a/uv.lock +++ b/uv.lock @@ -554,7 +554,6 @@ pgsql = [ dev = [ { name = "black" }, { name = "coverage" }, - { name = "ddcdatabases", extra = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] }, { name = "poethepoet" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -582,11 +581,6 @@ provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] dev = [ { name = "black", specifier = ">=26.1.0" }, { name = "coverage", specifier = ">=7.13.3" }, - { name = "ddcdatabases", extras = ["mongodb"] }, - { name = "ddcdatabases", extras = ["mssql"] }, - { name = "ddcdatabases", extras = ["mysql"] }, - { name = "ddcdatabases", extras = ["oracle"] }, - { name = "ddcdatabases", extras = ["pgsql"] }, { name = "poethepoet", specifier = ">=0.40.0" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, From 4227a725c2a52afb8499ca6dbb2cab79683fdbdf Mon Sep 17 00:00:00 2001 From: ddc Date: Thu, 5 Feb 2026 17:25:14 -0300 Subject: [PATCH 12/18] v3.0.9 --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 9b944ad..3826181 100755 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -49,7 +49,7 @@ jobs: uv venv if [[ '${{ matrix.os }}' == 'windows-latest' ]]; then # Skip MySQL on Windows - mysqlclient requires C compilation with MySQL headers - uv sync --group dev --no-install-package mysqlclient --no-install-package aiomysql + uv sync --all-extras --group dev --no-install-package mysqlclient --no-install-package aiomysql elif [[ '${{ matrix.os }}' == 'macos-latest' ]]; then export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig" uv sync --all-extras --group dev From 37d63e6ac6774ffc8df7efe0af1078bc391abbe1 Mon Sep 17 00:00:00 2001 From: ddc Date: Fri, 6 Feb 2026 12:29:46 -0300 Subject: [PATCH 13/18] v3.0.9 --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4b84c55 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.10.0 + hooks: + - id: uv-lock From e1c30a507b4c12e5e0181b78fd2385923b59c40d Mon Sep 17 00:00:00 2001 From: ddc Date: Fri, 6 Feb 2026 15:33:41 -0300 Subject: [PATCH 14/18] v3.0.9 --- .github/workflows/workflow.yml | 22 ++++-- README.md | 27 +++++-- ddcDatabases/__init__.py | 5 +- pyproject.toml | 12 ++- tests/smoke_test.py | 9 +++ uv.lock | 136 ++++++++++++++++----------------- 6 files changed, 120 insertions(+), 91 deletions(-) create mode 100644 tests/smoke_test.py diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 3826181..ba1e76b 100755 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,8 +1,8 @@ name: CI/CD Pipeline 'on': + pull_request: push: - branches: ['**'] tags: ['v*'] env: @@ -32,6 +32,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} @@ -49,12 +51,12 @@ jobs: uv venv if [[ '${{ matrix.os }}' == 'windows-latest' ]]; then # Skip MySQL on Windows - mysqlclient requires C compilation with MySQL headers - uv sync --all-extras --group dev --no-install-package mysqlclient --no-install-package aiomysql + uv sync --locked --all-extras --dev --no-install-package mysqlclient --no-install-package aiomysql elif [[ '${{ matrix.os }}' == 'macos-latest' ]]; then export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig" - uv sync --all-extras --group dev + uv sync --locked --all-extras --dev else - uv sync --all-extras --group dev + uv sync --locked --all-extras --dev fi shell: bash @@ -85,6 +87,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} run: uv python install ${{ env.LATEST_PYTHON_VERSION }} @@ -93,7 +97,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev pkg-config - name: Install dependencies - run: uv sync --all-extras --group dev + run: uv sync --locked --all-extras --dev shell: bash - name: Install ODBC driver for MSSQL @@ -124,6 +128,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 + with: + enable-cache: true - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }} run: uv python install ${{ env.LATEST_PYTHON_VERSION }} @@ -131,6 +137,12 @@ jobs: - name: Build package run: uv build + - name: Smoke test (wheel) + run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py + + - name: Smoke test (sdist) + run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py + - name: Upload artifacts uses: actions/upload-artifact@v6 with: diff --git a/README.md b/README.md index ecf5d1d..1e77808 100755 --- a/README.md +++ b/README.md @@ -49,7 +49,9 @@ - [Available Methods](#available-methods) - [Logging](#logging) - [Development](#development) - - [Create DEV Environment, Running Tests and Building Wheel](#create-dev-environment-running-tests-and-building-wheel) + - [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests) + - [Update DEV Environment Packages](#update-dev-environment-packages) + - [Building Wheel](#building-wheel) - [Optionals](#optionals) - [License](#license) - [Support](#support) @@ -683,15 +685,30 @@ logging.getLogger("ddcDatabases").addHandler(logging.StreamHandler()) # Development -Must have [UV](https://docs.astral.sh/uv/getting-started/installation/) installed. +Must have [UV](https://uv.run/docs/getting-started/installation) installed. -## Create DEV Environment, Running Tests and Building Wheel +## Create DEV Environment and Running Tests + +> **Note:** All poe tasks automatically run ruff linter along with Black formatting ```shell -uv sync --all-extras -poe linter +uv sync --all-extras --all-groups poe test poe test-integration +``` + +## Update DEV Environment Packages +This will update all packages dependencies + +```shell +poe updatedev +``` + + +## Building Wheel +This will update all packages, run linter, both unit and integration tests and finally build the wheel + +```shell poe build ``` diff --git a/ddcDatabases/__init__.py b/ddcDatabases/__init__.py index 6d24341..a228172 100755 --- a/ddcDatabases/__init__.py +++ b/ddcDatabases/__init__.py @@ -1,9 +1,6 @@ import logging from .core.operations import DBUtils, DBUtilsAsync -from .core.persistent import ( - PersistentConnectionConfig, - close_all_persistent_connections, -) +from .core.persistent import PersistentConnectionConfig, close_all_persistent_connections from importlib.metadata import version __all__ = [ diff --git a/pyproject.toml b/pyproject.toml index 752cb53..b083b69 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,9 +66,7 @@ pgsql = ["psycopg[binary]>=3.3.2", "asyncpg>=0.31.0"] [dependency-groups] dev = [ - "pytest>=9.0.2", "pytest-asyncio>=1.3.0", - "coverage>=7.13.3", "pytest-cov>=7.0.0", "testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.1", "black>=26.1.0", @@ -78,11 +76,11 @@ dev = [ [tool.poe.tasks] linter.shell = "uv run ruff check --fix . && uv run black ." -profile = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov" -profile-integration = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov" -test = "uv run pytest tests/unit" -test-integration = "uv run pytest tests/integration --no-cov" -updatedev.sequence = ["linter", {shell = "uv lock && uv sync --all-extras --group dev"}] +profile.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov"}] +profile-integration.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov"}] +test.sequence = ["linter", {shell = "uv run pytest"}] +test-integration.sequence = ["linter", {shell = "uv run pytest tests/integration --no-cov"}] +updatedev.sequence = ["linter", {shell = "uv lock --upgrade && uv sync --all-extras --group dev"}] build.sequence = ["updatedev", "test", "test-integration", {shell = "uv build --wheel"}] [tool.pytest.ini_options] diff --git a/tests/smoke_test.py b/tests/smoke_test.py new file mode 100644 index 0000000..25b18d1 --- /dev/null +++ b/tests/smoke_test.py @@ -0,0 +1,9 @@ +"""Smoke test to verify the built package works correctly.""" + +from ddcDatabases import DBUtils, DBUtilsAsync, __version__ + +assert __version__, "Version should not be empty" +assert DBUtils, "DBUtils should be importable" +assert DBUtilsAsync, "DBUtilsAsync should be importable" + +print(f"ddcDatabases {__version__} OK") diff --git a/uv.lock b/uv.lock index 8983318..a37f2b9 100644 --- a/uv.lock +++ b/uv.lock @@ -553,9 +553,7 @@ pgsql = [ [package.dev-dependencies] dev = [ { name = "black" }, - { name = "coverage" }, { name = "poethepoet" }, - { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, @@ -580,9 +578,7 @@ provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=26.1.0" }, - { name = "coverage", specifier = ">=7.13.3" }, { name = "poethepoet", specifier = ">=0.40.0" }, - { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.15.0" }, @@ -1681,72 +1677,72 @@ wheels = [ [[package]] name = "wrapt" -version = "2.1.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/31/afb4cf08b9892430ec419a3f0f469fb978cb013f4432e0edb9c2cf06f081/wrapt-2.1.0.tar.gz", hash = "sha256:757ff1de7e1d8db1839846672aaecf4978af433cc57e808255b83980e9651914", size = 80924, upload-time = "2026-01-31T23:25:58.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/37/ae31f40bec90de2f88d9597d0b5281e23ffe85b893a47ca5d9c05c63a4f6/wrapt-2.1.1.tar.gz", hash = "sha256:5fdcb09bf6db023d88f312bd0767594b414655d58090fc1c46b3414415f67fac", size = 81329, upload-time = "2026-02-03T02:12:13.786Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/6f/9773ddbf70d2f787d049fb5a412c18fd8140b8a33e90e8b911f0d512a7b5/wrapt-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba00229045bc0ec808f12f7d2fd02166631657c56d5b7acbbb8f03ea70fc1cd6", size = 60561, upload-time = "2026-01-31T23:26:51.314Z" }, - { url = "https://files.pythonhosted.org/packages/a1/91/f6cc8762153ebcdccf7d7aa7ca3a75fe688b7cebc250f1eac72229943d09/wrapt-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:657c7d0dcca7df8cfdce9d4e9062d51d2a2b2c8f4bdd41dc908a717099cf552a", size = 61501, upload-time = "2026-01-31T23:26:30.857Z" }, - { url = "https://files.pythonhosted.org/packages/f1/74/ce91f1e9cd77bf6c11700ac0643ccb747d7c4cbd948f63fba90d345aa85a/wrapt-2.1.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cb21ff015afe80cc30daca53136427463c364fb7c1ca96e4b7013dc6f56b2829", size = 113531, upload-time = "2026-01-31T23:26:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/91/be/231563aaf305c930705b4455023155bf485974b431bb4bf9ddc53be5ae9b/wrapt-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8d25f31cf032bfa70ec1872cdf0f7e1f1154c5a5bc6c73444bb3375b904f97f", size = 115538, upload-time = "2026-01-31T23:25:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/9b/9e/576265c0173e85e77eb9713ccecedeab34a1785d493bfa511fd98b7154bc/wrapt-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:099c88ce146134786577b892d2748ac79c16c9f70304367eee17295732907045", size = 114719, upload-time = "2026-01-31T23:25:25.153Z" }, - { url = "https://files.pythonhosted.org/packages/77/07/b374bd08739bf2f5c1accbb4c77e34bd21c3e9a0c5c49f54269d014c263f/wrapt-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dfe3f09f5ce33a4e54a3340c3cde774fd19eca0da8a83343889a3673a33ee579", size = 113204, upload-time = "2026-01-31T23:26:02.779Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e6/09285cbca4467c9701bea4c8bece8bf7cebdf2721516e6b6ccc8737d086f/wrapt-2.1.0-cp310-cp310-win32.whl", hash = "sha256:73d77cc1698bf2f0580616a2eadb94aa15b47ae09ade7d9828a5c413dbbabab8", size = 57878, upload-time = "2026-01-31T23:25:18.276Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6c/b027af42f6c8aacf1ab83c0a4b278ded3488f452b8b0478c5204637338dc/wrapt-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8012f863320ece76c6b95527b8ee831b818e186cafa356620cba15ba19c904de", size = 60226, upload-time = "2026-01-31T23:26:38.631Z" }, - { url = "https://files.pythonhosted.org/packages/22/ab/1fc44e40f4a7277f67eac33c645c88d54192fa2a8c6cad2735f8eb86fe3b/wrapt-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccd99596ae95bc7b844196e6691b4987749ba7832c9ba437fdd99885ee5e7a84", size = 58648, upload-time = "2026-01-31T23:26:20.163Z" }, - { url = "https://files.pythonhosted.org/packages/97/0a/de541b2543e33144043cd58da09bda8d837ba42e13ae90baca32b0553023/wrapt-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d877003dbc601e1365bd03f6a980965a20d585f90c056f33e1fc241b63a6f0e7", size = 60558, upload-time = "2026-01-31T23:25:27.784Z" }, - { url = "https://files.pythonhosted.org/packages/84/2e/7e48207420e6ca7e7a05c0e4ebe9464ec9965c8face256f3ef8cc2acd862/wrapt-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:771ec962fe3ccb078177c9b8f3529e204ffcbb11d62d509e0a438e6a83f7ca68", size = 61501, upload-time = "2026-01-31T23:26:46.477Z" }, - { url = "https://files.pythonhosted.org/packages/67/2b/639a4970ecdc7143acb69a1162c76b0f1620218ad502c33e1a88d28f00b1/wrapt-2.1.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73e742368b52f9cf0921e1d2bcb8a6a44ede2e372e33df6e77caa136a942099f", size = 113954, upload-time = "2026-01-31T23:26:01.493Z" }, - { url = "https://files.pythonhosted.org/packages/81/5d/8d9177c8c0ecaf5313b462be63c5aa9672044b02bfd644dd65c6cb420d2a/wrapt-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e9129d1b582c55ad0dfb9e29e221daa0e02b18c67d8642bc8d08dd7038b3aed", size = 115994, upload-time = "2026-01-31T23:25:57.118Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e3/c5a514a0ed1dc463f5b6b4e31abbaa3b8df48b9fd391a6e8412608155a29/wrapt-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc9e37bfe67f6ea738851dd606640a87692ff81bcc76df313fb75d08e05e855f", size = 115245, upload-time = "2026-01-31T23:26:11.171Z" }, - { url = "https://files.pythonhosted.org/packages/35/9c/2fc6a31f5758266de2cf9dc6111d3bda7b7dd6cbdcabfd755103bbcda08f/wrapt-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:46583aae3c807aa76f96355c4943031225785ed160c84052612bba0e9d456639", size = 113679, upload-time = "2026-01-31T23:25:19.475Z" }, - { url = "https://files.pythonhosted.org/packages/6c/81/ce52694dc8184f4898c01c8af20e145b348fc7a0e4766a7345c45f0e9ce6/wrapt-2.1.0-cp311-cp311-win32.whl", hash = "sha256:e3958ba70aef2895d8c62c2d31f51ced188f60451212294677b92f4b32c12978", size = 57865, upload-time = "2026-01-31T23:25:50.947Z" }, - { url = "https://files.pythonhosted.org/packages/85/31/0df5d38243c2a538e7bd481e676d286b41f98a729e0d37cfed9f4421ad4d/wrapt-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0ff9797e6e0b82b330ef80b0cdba7fcd0ca056d4c7af2ca44e3d05fd47929ede", size = 60227, upload-time = "2026-01-31T23:25:35.954Z" }, - { url = "https://files.pythonhosted.org/packages/a3/79/b587edbab21d6b8a7460234440c784e08344bcdf4fdfd9a6e9125ea14923/wrapt-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:4b0a29509ef7b501abe47b693a3c91d1f21c9a948711f6ce7afa81eb274c7eae", size = 58648, upload-time = "2026-01-31T23:25:32.887Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6f/c731b1fbbcdf9bd202809c6fa354c4237b663dd82a95035a7cbe899cfd25/wrapt-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a64c0fb29c89810973f312a04c067b63523e7303b9a2653820cbf16474c2e5cf", size = 61149, upload-time = "2026-01-31T23:25:29.092Z" }, - { url = "https://files.pythonhosted.org/packages/b2/da/7022458a1d99f0c59720a0b0fd4b1966f8df6d41e741aadfe43bc5350547/wrapt-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5509d9150ed01c4149e40020fa68e917d5c4bb77d311e79535565c2a0418afcb", size = 61743, upload-time = "2026-01-31T23:26:14.338Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f4/57cc12c3fc6f4fe6ccfc15567cc1ac8aeb53a9946a675adc3df7a1ee4e6a/wrapt-2.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:52bb58b3207ace156b6134235fd43140994597704fd07d148cbcfb474ee084ea", size = 121331, upload-time = "2026-01-31T23:25:37.294Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a4/a96ea114298f81f02c07313da85fd46a2a57bbe12389d0619ac3371f691c/wrapt-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7112cbf72fc4035afe1e3314a311654c41dd92c2932021ef76f5ca87583917b3", size = 122907, upload-time = "2026-01-31T23:26:49.604Z" }, - { url = "https://files.pythonhosted.org/packages/ac/43/df73362b6e47f92aaff0fc3fc459314025c795f75d61724c83232dee199c/wrapt-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e90656b433808a0ab68e95aaf9f588aea5c8c7a514e180849dfc638ba00ec449", size = 121337, upload-time = "2026-01-31T23:26:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/51/4f/8147e3b9a7887cee4eeb3a3414265ad4649a156832a08063f55aa7842af0/wrapt-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e45f54903da38fc4f6f66397fd550fc0dac6164b4c5e721c1b4eb05664181821", size = 120461, upload-time = "2026-01-31T23:26:43.055Z" }, - { url = "https://files.pythonhosted.org/packages/35/b1/eea720fcca8a05dec848a6d11a47c20f59bdabdcc444ba3be0589350eb7a/wrapt-2.1.0-cp312-cp312-win32.whl", hash = "sha256:6653bf30dbbafd55cb4553195cc60b94920b6711a8835866c0e02aa9f22c5598", size = 58089, upload-time = "2026-01-31T23:26:47.773Z" }, - { url = "https://files.pythonhosted.org/packages/af/79/8a8f3f8c71ee3379191b69e47f32115fa25cdb6d5b581d74c64d5c897fa7/wrapt-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d61238a072501ed071a9f4b9567d10c2eb3d2f1a0258ae79b47160871d8f29c3", size = 60330, upload-time = "2026-01-31T23:26:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/08/4e/e992d05c3d2f7163883a65ead2620ff5fe7b3d44d7c2136ce981e40e453d/wrapt-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:9e971000347f61271725e801ef44fa5d01b52720e59737f0d96280bffb98c5d1", size = 58727, upload-time = "2026-01-31T23:26:53.222Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/b414826a5aaf2fdcfe73c2e649cbeb2e098fef4820d1217554ee64f45666/wrapt-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:875a10a6f3b667f90a39010af26acf684ba831d9b18a86b242899d57c74550fa", size = 61155, upload-time = "2026-01-31T23:26:24.462Z" }, - { url = "https://files.pythonhosted.org/packages/58/9e/8b21ea776bf2a3c858e3377ecde4b348893ec44dc1726baaf583ca22c56e/wrapt-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e00f8559ceac0fb45091daad5f15d37f2c22bdc28ed71521d47ff01aad8fff3d", size = 61747, upload-time = "2026-01-31T23:25:53.987Z" }, - { url = "https://files.pythonhosted.org/packages/da/ec/48cd2470ad09557dfe6fccfe9de98698cc0df3786a6d4d97e8edd574d67a/wrapt-2.1.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ce0cf4c79c19904aaf2e822af280d7b3c23ad902f57e31c5a19433bc86e5d36d", size = 121342, upload-time = "2026-01-31T23:26:32.156Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4e/e8447b31be27b6057cdfc904a38632a765c3407fb4d10d11e5c1d0c203d5/wrapt-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3dd4f8c2256fcde1a85037a1837afc52e8d32d086fd669ae469455fd9a988d6", size = 122951, upload-time = "2026-01-31T23:25:08.936Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b6/73a6c9277e844ffe11f3002ad27a84ff5418248def33af9435d24dfe6c5b/wrapt-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:737e1e491473047cb66944b8b8fd23f3f542019afd6cf0569d1356d18a7ea6d5", size = 121373, upload-time = "2026-01-31T23:26:18.322Z" }, - { url = "https://files.pythonhosted.org/packages/85/04/869384435fecf829dc05621ffa02dab0f2f830be5d42fa8d8ac7b0b4c9fa/wrapt-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38de19e30e266c15d542ceb0603e657db4e82c53e7f47fd70674ae5da2b41180", size = 120468, upload-time = "2026-01-31T23:25:13.689Z" }, - { url = "https://files.pythonhosted.org/packages/80/ac/42a5378d9b5b486122ae0572c46ae8d69ab6486b9f13961e6b9706297ff5/wrapt-2.1.0-cp313-cp313-win32.whl", hash = "sha256:bc7d496b6e16bd2f77e37e8969b21a7b58d6954e46c6689986fb67b9078100e5", size = 58095, upload-time = "2026-01-31T23:26:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/86/de/538fcef30f70a1aaadab4cab7d0396037518d7ec2b064557171147ce297f/wrapt-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:57df799e67b011847ef7ac64b05ed4633e56b64e7e7cab5eb83dc9689dbe0acf", size = 60344, upload-time = "2026-01-31T23:25:10.615Z" }, - { url = "https://files.pythonhosted.org/packages/08/13/27884668b21e9f0a625c13ebd6a8d70ad8371250ec8519881858404686bf/wrapt-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:01559d2961c29edc6263849fd9d32b29a20737da67648c7fd752a67bd96208c7", size = 58734, upload-time = "2026-01-31T23:26:00.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a3/e558c5b8f3a097aa1e942e2d75923adebfdfafb5a51ec425d1d062e49ab0/wrapt-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:66f588c8b3a44863156cfaccb516f946a64b3b03a6880822ab0b878135ca1f5c", size = 62972, upload-time = "2026-01-31T23:26:08.576Z" }, - { url = "https://files.pythonhosted.org/packages/93/b6/7157e98107099fad846f1e79308cc0954e26b25b01c03f1624ba7f57ec54/wrapt-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:355779ff720c11a2a5cffd03332dbce1005cb4747dca65b0fc8cdd5f8bf1037e", size = 63610, upload-time = "2026-01-31T23:26:39.9Z" }, - { url = "https://files.pythonhosted.org/packages/e4/8e/b8992671e4b4d3ce2a53af930588c204bf37b66eb212bd1722f2a5a8cf62/wrapt-2.1.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a0471df3fb4e85a9ff62f7142cdb169e31172467cdb79a713f9b1319c555903", size = 152538, upload-time = "2026-01-31T23:26:27.696Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f6/79f9fd4b3c0a8715e651fff1cc1182a983fd971376d5688a06fa94e31acd/wrapt-2.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bacf063143fa86f15b00a21259a81c95c527a18d504b8c820835366d361c879", size = 158702, upload-time = "2026-01-31T23:25:11.848Z" }, - { url = "https://files.pythonhosted.org/packages/9e/46/f88b52beb813eeb830d9134bc6eaf3e53cde4e3cfa1804e383754d4104fe/wrapt-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c87cd4f61a3b7cd65113e74006e1cd6352b74807fcc65d440e8342f001f8de5e", size = 155564, upload-time = "2026-01-31T23:25:15.033Z" }, - { url = "https://files.pythonhosted.org/packages/93/31/97145ea71e3e5a1b419af5c410b07b258155dc7cc1a6302791a93e991c83/wrapt-2.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2893498fe898719ac8fb6b4fe36ca86892bec1e2480d94e3bd1bc592c00527ad", size = 150165, upload-time = "2026-01-31T23:26:09.848Z" }, - { url = "https://files.pythonhosted.org/packages/10/bd/f33551d5bfbb0ddab81296cffc15570570039a973c0f99bba474be0fadf2/wrapt-2.1.0-cp313-cp313t-win32.whl", hash = "sha256:cbc07f101f5f1e7c23ec06a07e45715f459de992108eeb381b21b76d94dbaf4f", size = 59785, upload-time = "2026-01-31T23:25:52.23Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3a/9a76be7a36442f43841bb6336e262e09a915b2fb5dfc2822ffce1fb903d2/wrapt-2.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2ccc89cd504fc29c32f0b24046e8edf3ef0fcbc5d5efe8c91b303c099863d2c8", size = 63085, upload-time = "2026-01-31T23:26:05.363Z" }, - { url = "https://files.pythonhosted.org/packages/7a/35/65a13c2df008d189ebca5fec534011c5dd69ab4f47e6923b403321816fbf/wrapt-2.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:0b660be1c9cdfb4c711baab4ccbd0e9d1b65a0480d38729ec8cdbf3b29cb7f15", size = 60254, upload-time = "2026-01-31T23:25:06.052Z" }, - { url = "https://files.pythonhosted.org/packages/6f/eb/7c9eb1ea9b10ea98d9983a147c877a2ae927acb4a86e2dc4a0b548f05ad1/wrapt-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f7bf95bae7ac5f2bbcb307464b3b0ff70569dd3b036a87b1cf7efb2c76e66e5", size = 61316, upload-time = "2026-01-31T23:25:20.739Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c2/1c3d16d6b644f688913a00e2dc10f59adca817b5b3ee034ce4e9a692ab63/wrapt-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:be2f541a242818829526e5d08c716b6730970ed0dc1b76ba962a546947d0f005", size = 61813, upload-time = "2026-01-31T23:25:49.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/51/b6170084b6b771cc62374d924e328df2e81f687399a835f003497cad1110/wrapt-2.1.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad3aa174d06a14b4758d5a1678b9adde8b8e657c6695de9a3d4c223f4fcbbcce", size = 120309, upload-time = "2026-01-31T23:25:16.866Z" }, - { url = "https://files.pythonhosted.org/packages/f8/34/467829f0dd79f50878b2e67b67c67c816a6326a27d252d4192ef815b4a09/wrapt-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bffa584240d41bc3127510e07a752f94223d73bb1283ac2e99ac44235762efd2", size = 122690, upload-time = "2026-01-31T23:26:16.914Z" }, - { url = "https://files.pythonhosted.org/packages/df/5b/244c61a65e0bc9d4a18cfa2a2b3b05f8065290284fc60436a7ea5047ee10/wrapt-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9b2da9c8f1723994b335dbf9f496fbfabc76bcdd001f73772b8eb2118a714cea", size = 121115, upload-time = "2026-01-31T23:26:44.518Z" }, - { url = "https://files.pythonhosted.org/packages/86/7d/f9b5e103d3caf23a72c04a1baf2b61c4a14d1feb440d3c98c26725b4503a/wrapt-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:eabe95ea5fbe1524a53c0f3fc535c99f2aa376ec1451b0b79d943d2240d80e36", size = 119487, upload-time = "2026-01-31T23:25:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/f8/49/b61fdc4680dd5cd6828977341b9fd729e2c623338bfe65647f5c0ff8195e/wrapt-2.1.0-cp314-cp314-win32.whl", hash = "sha256:2cd647097df1df78f027ac7d5d663f05daa1a117b69cf7f476cb299f90557747", size = 58519, upload-time = "2026-01-31T23:25:04.426Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4f/42ab43e496d0d19caed9f69366d0f28f7f08c139297e78b17dab6ecbb6d5/wrapt-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0fc3e388a14ef8101c685dc80b4d2932924a639a03e5c44b5ffabbda2f1f2dc", size = 60767, upload-time = "2026-01-31T23:25:21.954Z" }, - { url = "https://files.pythonhosted.org/packages/ef/15/0337768ac97a8758bc0fc1afdf5f656075a7facf198f62bbe8a22b789277/wrapt-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7c06653908a23a85c4b2455b9d37c085f9756c09058df87b4a2fce2b2f8d58c2", size = 59056, upload-time = "2026-01-31T23:26:25.814Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f1/58f4674d1db44912003a51b34e8d9823a832fbbb39162e9dbe06e5f6424e/wrapt-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c70b4829c6f2f4af4cdaa16442032fcaf882063304160555e4a19b43fd2c6c9d", size = 63061, upload-time = "2026-01-31T23:26:06.601Z" }, - { url = "https://files.pythonhosted.org/packages/02/c1/07f6bf6619285f39cd616314217170c6160da99a46ad6ae4a60044f6ab5a/wrapt-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7fd4c4ee51ebdf245549d54a7c2181a4f39caac97c9dc8a050b5ba814067a29", size = 63620, upload-time = "2026-01-31T23:25:30.326Z" }, - { url = "https://files.pythonhosted.org/packages/46/82/f7df1648762260f60c4e22c066a17d95f20267c94bfe653fab4f08e2c297/wrapt-2.1.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b158558438874e5fd5cb505b5a635bd08c84857bc937973d9e12e1166cdf3b", size = 152546, upload-time = "2026-01-31T23:25:02.102Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/d953336e09bac13a9ffa9073e167c5dec8aaa4a717a8551bf64cb4683590/wrapt-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e2e156fe2d41700b837be9b1d8d80ebab44e9891589bc7c41578ef110184e29", size = 158704, upload-time = "2026-01-31T23:25:43.269Z" }, - { url = "https://files.pythonhosted.org/packages/39/a1/2ed57e46b30af2a5a750c85a9dd30d2244ef10e2f8db150560126d8cbd24/wrapt-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f1e9bac6a6c1ba65e0ac50e32c575266734a07b6c17e718c4babd91e2faa69b", size = 155563, upload-time = "2026-01-31T23:25:39.17Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8c/4f54f7ea5addf208be44459393185aaa193bd2d0b8ecf4683b159fcc5238/wrapt-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12687e6271df7ae5706bee44cc1f77fecb7805976ec9f14f58381b30ae2aceb5", size = 150189, upload-time = "2026-01-31T23:25:44.654Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cc/e8290a1cd94297fbc1e9fbad06481b5a7c918f2db6645c550f05ee47f359/wrapt-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:38bbe336ee32f67eb99f886bd4f040d91310b7e660061bb03b9083d26e8cf915", size = 60431, upload-time = "2026-01-31T23:25:48.34Z" }, - { url = "https://files.pythonhosted.org/packages/d0/df/af5d244938853e3adb1251ca1397e9fa78d3e92adc808a0af0a8547585d3/wrapt-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0fa64a9a07df7f85b352adc42b43e7f44085fb11191b8f5b9b77219f7aaf7e17", size = 63859, upload-time = "2026-01-31T23:26:23.2Z" }, - { url = "https://files.pythonhosted.org/packages/39/c4/28b6f2804e8bc05d17114dfed03a80bce5b83ca2113fd44eecbef12275d1/wrapt-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:da379cbdf3b7d97ace33a69a391b7a7e2130b1aca94dc447246217994233974c", size = 60446, upload-time = "2026-01-31T23:25:41.001Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/70983b75d4abd6f85cffc6df79c623220ec5a579ceaacabac35c904b7b52/wrapt-2.1.0-py3-none-any.whl", hash = "sha256:e035693a0d25ea5bf5826df3e203dff7d091b0d5442aaefec9ca8f2bab38417f", size = 43886, upload-time = "2026-01-31T23:25:07.22Z" }, + { url = "https://files.pythonhosted.org/packages/ca/21/293b657a27accfbbbb6007ebd78af0efa2083dac83e8f523272ea09b4638/wrapt-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e927375e43fd5a985b27a8992327c22541b6dede1362fc79df337d26e23604f", size = 60554, upload-time = "2026-02-03T02:11:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/25/e9/96dd77728b54a899d4ce2798d7b1296989ce687ed3c0cb917d6b3154bf5d/wrapt-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c99544b6a7d40ca22195563b6d8bc3986ee8bb82f272f31f0670fe9440c869", size = 61496, upload-time = "2026-02-03T02:12:54.732Z" }, + { url = "https://files.pythonhosted.org/packages/44/79/4c755b45df6ef30c0dd628ecfaa0c808854be147ca438429da70a162833c/wrapt-2.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2be3fa5f4efaf16ee7c77d0556abca35f5a18ad4ac06f0ef3904c3399010ce9", size = 113528, upload-time = "2026-02-03T02:12:26.405Z" }, + { url = "https://files.pythonhosted.org/packages/9f/63/23ce28f7b841217d9a6337a340fbb8d4a7fbd67a89d47f377c8550fa34aa/wrapt-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67c90c1ae6489a6cb1a82058902caa8006706f7b4e8ff766f943e9d2c8e608d0", size = 115536, upload-time = "2026-02-03T02:11:54.397Z" }, + { url = "https://files.pythonhosted.org/packages/23/7b/5ca8d3b12768670d16c8329e29960eedd56212770365a02a8de8bf73dc01/wrapt-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05c0db35ccffd7480143e62df1e829d101c7b86944ae3be7e4869a7efa621f53", size = 114716, upload-time = "2026-02-03T02:12:20.771Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3a/9789ccb14a096d30bb847bf3ee137bf682cc9750c2ce155f4c5ae1962abf/wrapt-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c2ec9f616755b2e1e0bf4d0961f59bb5c2e7a77407e7e2c38ef4f7d2fdde12c", size = 113200, upload-time = "2026-02-03T02:12:07.688Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e5/4ec3526ce6ce920b267c8d35d2c2f0874d3fad2744c8b7259353f1132baa/wrapt-2.1.1-cp310-cp310-win32.whl", hash = "sha256:203ba6b3f89e410e27dbd30ff7dccaf54dcf30fda0b22aa1b82d560c7f9fe9a1", size = 57876, upload-time = "2026-02-03T02:11:42.61Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4e/661c7c76ecd85375b2bc03488941a3a1078642af481db24949e2b9de01f4/wrapt-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:6f9426d9cfc2f8732922fc96198052e55c09bb9db3ddaa4323a18e055807410e", size = 60224, upload-time = "2026-02-03T02:11:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b7/53c7252d371efada4cb119e72e774fa2c6b3011fc33e3e552cdf48fb9488/wrapt-2.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:69c26f51b67076b40714cff81bdd5826c0b10c077fb6b0678393a6a2f952a5fc", size = 58645, upload-time = "2026-02-03T02:12:10.396Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a8/9254e4da74b30a105935197015b18b31b7a298bf046e67d8952ef74967bd/wrapt-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c366434a7fb914c7a5de508ed735ef9c133367114e1a7cb91dfb5cd806a1549", size = 60554, upload-time = "2026-02-03T02:11:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a1/378579880cc7af226354054a2c255f69615b379d8adad482bfe2f22a0dc2/wrapt-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d6a2068bd2e1e19e5a317c8c0b288267eec4e7347c36bc68a6e378a39f19ee7", size = 61491, upload-time = "2026-02-03T02:12:56.077Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/957b51c56acca35701665878ad31626182199fc4afecfe67dea072210f95/wrapt-2.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:891ab4713419217b2aed7dd106c9200f64e6a82226775a0d2ebd6bef2ebd1747", size = 113949, upload-time = "2026-02-03T02:11:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/cd/74/36bbebb4a3d2ae9c3e6929639721f8606cd0710a82a777c371aa69e36504/wrapt-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef36a0df38d2dc9d907f6617f89e113c5892e0a35f58f45f75901af0ce7d81", size = 115989, upload-time = "2026-02-03T02:12:19.398Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/f1177245a083c7be284bc90bddfe5aece32cdd5b858049cb69ce001a0e8d/wrapt-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76e9af3ebd86f19973143d4d592cbf3e970cf3f66ddee30b16278c26ae34b8ab", size = 115242, upload-time = "2026-02-03T02:11:08.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/3e/3b7cf5da27e59df61b1eae2d07dd03ff5d6f75b5408d694873cca7a8e33c/wrapt-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff562067485ebdeaef2fa3fe9b1876bc4e7b73762e0a01406ad81e2076edcebf", size = 113676, upload-time = "2026-02-03T02:12:41.026Z" }, + { url = "https://files.pythonhosted.org/packages/f7/65/8248d3912c705f2c66f81cb97c77436f37abcbedb16d633b5ab0d795d8cd/wrapt-2.1.1-cp311-cp311-win32.whl", hash = "sha256:9e60a30aa0909435ec4ea2a3c53e8e1b50ac9f640c0e9fe3f21fd248a22f06c5", size = 57863, upload-time = "2026-02-03T02:12:18.112Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/d29310ab335f71f00c50466153b3dc985aaf4a9fc03263e543e136859541/wrapt-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7d79954f51fcf84e5ec4878ab4aea32610d70145c5bbc84b3370eabfb1e096c2", size = 60224, upload-time = "2026-02-03T02:12:29.289Z" }, + { url = "https://files.pythonhosted.org/packages/0c/90/a6ec319affa6e2894962a0cb9d73c67f88af1a726d15314bfb5c88b8a08d/wrapt-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:d3ffc6b0efe79e08fd947605fd598515aebefe45e50432dc3b5cd437df8b1ada", size = 58643, upload-time = "2026-02-03T02:12:43.022Z" }, + { url = "https://files.pythonhosted.org/packages/df/cb/4d5255d19bbd12be7f8ee2c1fb4269dddec9cef777ef17174d357468efaa/wrapt-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab8e3793b239db021a18782a5823fcdea63b9fe75d0e340957f5828ef55fcc02", size = 61143, upload-time = "2026-02-03T02:11:46.313Z" }, + { url = "https://files.pythonhosted.org/packages/6f/07/7ed02daa35542023464e3c8b7cb937fa61f6c61c0361ecf8f5fecf8ad8da/wrapt-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c0300007836373d1c2df105b40777986accb738053a92fe09b615a7a4547e9f", size = 61740, upload-time = "2026-02-03T02:12:51.966Z" }, + { url = "https://files.pythonhosted.org/packages/c4/60/a237a4e4a36f6d966061ccc9b017627d448161b19e0a3ab80a7c7c97f859/wrapt-2.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2b27c070fd1132ab23957bcd4ee3ba707a91e653a9268dc1afbd39b77b2799f7", size = 121327, upload-time = "2026-02-03T02:11:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fe/9139058a3daa8818fc67e6460a2340e8bbcf3aef8b15d0301338bbe181ca/wrapt-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b0e36d845e8b6f50949b6b65fc6cd279f47a1944582ed4ec8258cd136d89a64", size = 122903, upload-time = "2026-02-03T02:12:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/91/10/b8479202b4164649675846a531763531f0a6608339558b5a0a718fc49a8d/wrapt-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aeea04a9889370fcfb1ef828c4cc583f36a875061505cd6cd9ba24d8b43cc36", size = 121333, upload-time = "2026-02-03T02:11:32.148Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/75fc793b791d79444aca2c03ccde64e8b99eda321b003f267d570b7b0985/wrapt-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d88b46bb0dce9f74b6817bc1758ff2125e1ca9e1377d62ea35b6896142ab6825", size = 120458, upload-time = "2026-02-03T02:11:16.039Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8f/c3f30d511082ca6d947c405f9d8f6c8eaf83cfde527c439ec2c9a30eb5ea/wrapt-2.1.1-cp312-cp312-win32.whl", hash = "sha256:63decff76ca685b5c557082dfbea865f3f5f6d45766a89bff8dc61d336348833", size = 58086, upload-time = "2026-02-03T02:12:35.041Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c8/37625b643eea2849f10c3b90f69c7462faa4134448d4443234adaf122ae5/wrapt-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b828235d26c1e35aca4107039802ae4b1411be0fe0367dd5b7e4d90e562fcbcd", size = 60328, upload-time = "2026-02-03T02:12:45.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/79/56242f07572d5682ba8065a9d4d9c2218313f576e3c3471873c2a5355ffd/wrapt-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:75128507413a9f1bcbe2db88fd18fbdbf80f264b82fa33a6996cdeaf01c52352", size = 58722, upload-time = "2026-02-03T02:12:27.949Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3cf290212855b19af9fcc41b725b5620b32f470d6aad970c2593500817eb/wrapt-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9646e17fa7c3e2e7a87e696c7de66512c2b4f789a8db95c613588985a2e139", size = 61150, upload-time = "2026-02-03T02:12:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/9d/33/5b8f89a82a9859ce82da4870c799ad11ce15648b6e1c820fec3e23f4a19f/wrapt-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:428cfc801925454395aa468ba7ddb3ed63dc0d881df7b81626cdd433b4e2b11b", size = 61743, upload-time = "2026-02-03T02:11:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2f/60c51304fbdf47ce992d9eefa61fbd2c0e64feee60aaa439baf42ea6f40b/wrapt-2.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5797f65e4d58065a49088c3b32af5410751cd485e83ba89e5a45e2aa8905af98", size = 121341, upload-time = "2026-02-03T02:11:20.461Z" }, + { url = "https://files.pythonhosted.org/packages/ad/03/ce5256e66dd94e521ad5e753c78185c01b6eddbed3147be541f4d38c0cb7/wrapt-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a2db44a71202c5ae4bb5f27c6d3afbc5b23053f2e7e78aa29704541b5dad789", size = 122947, upload-time = "2026-02-03T02:11:33.596Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/50ca8854b81b946a11a36fcd6ead32336e6db2c14b6e4a8b092b80741178/wrapt-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8d5350c3590af09c1703dd60ec78a7370c0186e11eaafb9dda025a30eee6492d", size = 121370, upload-time = "2026-02-03T02:11:09.886Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d9/d6a7c654e0043319b4cc137a4caaf7aa16b46b51ee8df98d1060254705b7/wrapt-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d9b076411bed964e752c01b49fd224cc385f3a96f520c797d38412d70d08359", size = 120465, upload-time = "2026-02-03T02:11:37.592Z" }, + { url = "https://files.pythonhosted.org/packages/55/90/65be41e40845d951f714b5a77e84f377a3787b1e8eee6555a680da6d0db5/wrapt-2.1.1-cp313-cp313-win32.whl", hash = "sha256:0bb7207130ce6486727baa85373503bf3334cc28016f6928a0fa7e19d7ecdc06", size = 58090, upload-time = "2026-02-03T02:12:53.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/66/6a09e0294c4fc8c26028a03a15191721c9271672467cc33e6617ee0d91d2/wrapt-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:cbfee35c711046b15147b0ae7db9b976f01c9520e6636d992cd9e69e5e2b03b1", size = 60341, upload-time = "2026-02-03T02:12:36.384Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/20ceb8b701e9a71555c87a5ddecbed76ec16742cf1e4b87bbaf26735f998/wrapt-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:7d2756061022aebbf57ba14af9c16e8044e055c22d38de7bf40d92b565ecd2b0", size = 58731, upload-time = "2026-02-03T02:12:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/80/b4/fe95beb8946700b3db371f6ce25115217e7075ca063663b8cca2888ba55c/wrapt-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4814a3e58bc6971e46baa910ecee69699110a2bf06c201e24277c65115a20c20", size = 62969, upload-time = "2026-02-03T02:11:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/477b0bdc784e3299edf69c279697372b8bd4c31d9c6966eae405442899df/wrapt-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:106c5123232ab9b9f4903692e1fa0bdc231510098f04c13c3081f8ad71c3d612", size = 63606, upload-time = "2026-02-03T02:12:02.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/55/9d0c1269ab76de87715b3b905df54dd25d55bbffd0b98696893eb613469f/wrapt-2.1.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1a40b83ff2535e6e56f190aff123821eea89a24c589f7af33413b9c19eb2c738", size = 152536, upload-time = "2026-02-03T02:11:24.492Z" }, + { url = "https://files.pythonhosted.org/packages/44/18/2004766030462f79ad86efaa62000b5e39b1ff001dcce86650e1625f40ae/wrapt-2.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:789cea26e740d71cf1882e3a42bb29052bc4ada15770c90072cb47bf73fb3dbf", size = 158697, upload-time = "2026-02-03T02:12:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/e1/bb/0a880fa0f35e94ee843df4ee4dd52a699c9263f36881311cfb412c09c3e5/wrapt-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ba49c14222d5e5c0ee394495a8655e991dc06cbca5398153aefa5ac08cd6ccd7", size = 155563, upload-time = "2026-02-03T02:11:49.737Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/cd1b7c4846c8678fac359a6eb975dc7ab5bd606030adb22acc8b4a9f53f1/wrapt-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ac8cda531fe55be838a17c62c806824472bb962b3afa47ecbd59b27b78496f4e", size = 150161, upload-time = "2026-02-03T02:12:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/38/ec/67c90a7082f452964b4621e4890e9a490f1add23cdeb7483cc1706743291/wrapt-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:b8af75fe20d381dd5bcc9db2e86a86d7fcfbf615383a7147b85da97c1182225b", size = 59783, upload-time = "2026-02-03T02:11:39.863Z" }, + { url = "https://files.pythonhosted.org/packages/ec/08/466afe4855847d8febdfa2c57c87e991fc5820afbdef01a273683dfd15a0/wrapt-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:45c5631c9b6c792b78be2d7352129f776dd72c605be2c3a4e9be346be8376d83", size = 63082, upload-time = "2026-02-03T02:12:09.075Z" }, + { url = "https://files.pythonhosted.org/packages/9a/62/60b629463c28b15b1eeadb3a0691e17568622b12aa5bfa7ebe9b514bfbeb/wrapt-2.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:da815b9263947ac98d088b6414ac83507809a1d385e4632d9489867228d6d81c", size = 60251, upload-time = "2026-02-03T02:11:21.794Z" }, + { url = "https://files.pythonhosted.org/packages/95/a0/1c2396e272f91efe6b16a6a8bce7ad53856c8f9ae4f34ceaa711d63ec9e1/wrapt-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aa1765054245bb01a37f615503290d4e207e3fd59226e78341afb587e9c1236", size = 61311, upload-time = "2026-02-03T02:12:44.41Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9a/d2faba7e61072a7507b5722db63562fdb22f5a24e237d460d18755627f15/wrapt-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:feff14b63a6d86c1eee33a57f77573649f2550935981625be7ff3cb7342efe05", size = 61805, upload-time = "2026-02-03T02:11:59.905Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/073989deb4b5d7d6e7ea424476a4ae4bda02140f2dbeaafb14ba4864dd60/wrapt-2.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81fc5f22d5fcfdbabde96bb3f5379b9f4476d05c6d524d7259dc5dfb501d3281", size = 120308, upload-time = "2026-02-03T02:12:04.46Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/84f37261295e38167a29eb82affaf1dc15948dc416925fe2091beee8e4ac/wrapt-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:951b228ecf66def855d22e006ab9a1fc12535111ae7db2ec576c728f8ddb39e8", size = 122688, upload-time = "2026-02-03T02:11:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ea/80/32db2eec6671f80c65b7ff175be61bc73d7f5223f6910b0c921bbc4bd11c/wrapt-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ddf582a95641b9a8c8bd643e83f34ecbbfe1b68bc3850093605e469ab680ae3", size = 121115, upload-time = "2026-02-03T02:12:39.068Z" }, + { url = "https://files.pythonhosted.org/packages/49/ef/dcd00383df0cd696614127902153bf067971a5aabcd3c9dcb2d8ef354b2a/wrapt-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fc5c500966bf48913f795f1984704e6d452ba2414207b15e1f8c339a059d5b16", size = 119484, upload-time = "2026-02-03T02:11:48.419Z" }, + { url = "https://files.pythonhosted.org/packages/76/29/0630280cdd2bd8f86f35cb6854abee1c9d6d1a28a0c6b6417cd15d378325/wrapt-2.1.1-cp314-cp314-win32.whl", hash = "sha256:4aa4baadb1f94b71151b8e44a0c044f6af37396c3b8bcd474b78b49e2130a23b", size = 58514, upload-time = "2026-02-03T02:11:58.616Z" }, + { url = "https://files.pythonhosted.org/packages/db/19/5bed84f9089ed2065f6aeda5dfc4f043743f642bc871454b261c3d7d322b/wrapt-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:860e9d3fd81816a9f4e40812f28be4439ab01f260603c749d14be3c0a1170d19", size = 60763, upload-time = "2026-02-03T02:12:24.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/b967f2f9669e4249b4fe82e630d2a01bc6b9e362b9b12ed91bbe23ae8df4/wrapt-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3c59e103017a2c1ea0ddf589cbefd63f91081d7ce9d491d69ff2512bb1157e23", size = 59051, upload-time = "2026-02-03T02:11:29.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/19/6fed62be29f97eb8a56aff236c3f960a4b4a86e8379dc7046a8005901a97/wrapt-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9fa7c7e1bee9278fc4f5dd8275bc8d25493281a8ec6c61959e37cc46acf02007", size = 63059, upload-time = "2026-02-03T02:12:06.368Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1c/b757fd0adb53d91547ed8fad76ba14a5932d83dde4c994846a2804596378/wrapt-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39c35e12e8215628984248bd9c8897ce0a474be2a773db207eb93414219d8469", size = 63618, upload-time = "2026-02-03T02:12:23.197Z" }, + { url = "https://files.pythonhosted.org/packages/10/fe/e5ae17b1480957c7988d991b93df9f2425fc51f128cf88144d6a18d0eb12/wrapt-2.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:94ded4540cac9125eaa8ddf5f651a7ec0da6f5b9f248fe0347b597098f8ec14c", size = 152544, upload-time = "2026-02-03T02:11:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cc/99aed210c6b547b8a6e4cb9d1425e4466727158a6aeb833aa7997e9e08dd/wrapt-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0af328373f97ed9bdfea24549ac1b944096a5a71b30e41c9b8b53ab3eec04a", size = 158700, upload-time = "2026-02-03T02:12:30.684Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/d442f745f4957944d5f8ad38bc3a96620bfff3562533b87e486e979f3d99/wrapt-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4ad839b55f0bf235f8e337ce060572d7a06592592f600f3a3029168e838469d3", size = 155561, upload-time = "2026-02-03T02:11:28.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/9891816280e0018c48f8dfd61b136af7b0dcb4a088895db2531acde5631b/wrapt-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d89c49356e5e2a50fa86b40e0510082abcd0530f926cbd71cf25bee6b9d82d7", size = 150188, upload-time = "2026-02-03T02:11:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/e2f273b6d70d41f98d0739aa9a269d0b633684a5fb17b9229709375748d4/wrapt-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:f4c7dd22cf7f36aafe772f3d88656559205c3af1b7900adfccb70edeb0d2abc4", size = 60425, upload-time = "2026-02-03T02:11:35.007Z" }, + { url = "https://files.pythonhosted.org/packages/1e/06/b500bfc38a4f82d89f34a13069e748c82c5430d365d9e6b75afb3ab74457/wrapt-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f76bc12c583ab01e73ba0ea585465a41e48d968f6d1311b4daec4f8654e356e3", size = 63855, upload-time = "2026-02-03T02:12:15.47Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/5f6193c32166faee1d2a613f278608e6f3b95b96589d020f0088459c46c9/wrapt-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7ea74fc0bec172f1ae5f3505b6655c541786a5cabe4bbc0d9723a56ac32eb9b9", size = 60443, upload-time = "2026-02-03T02:11:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/5a086bf4c22a41995312db104ec2ffeee2cf6accca9faaee5315c790377d/wrapt-2.1.1-py3-none-any.whl", hash = "sha256:3b0f4629eb954394a3d7c7a1c8cca25f0b07cefe6aa8545e862e9778152de5b7", size = 43886, upload-time = "2026-02-03T02:11:45.048Z" }, ] From c73143a5fd9d8ea480dd9a7c1f742efcbc3cdc2b Mon Sep 17 00:00:00 2001 From: ddc Date: Fri, 6 Feb 2026 17:42:45 -0300 Subject: [PATCH 15/18] v3.0.9 --- ddcDatabases/core/settings.py | 3 + ddcDatabases/mssql.py | 18 +- ddcDatabases/postgresql.py | 12 +- tests/unit/mssql/test_mssql_ssl.py | 181 ++++++++++ tests/unit/postgresql/test_postgresql_ssl.py | 362 +++++++++++++++++++ 5 files changed, 569 insertions(+), 7 deletions(-) diff --git a/ddcDatabases/core/settings.py b/ddcDatabases/core/settings.py index 19e0c8e..63f7078 100644 --- a/ddcDatabases/core/settings.py +++ b/ddcDatabases/core/settings.py @@ -1,3 +1,4 @@ +import warnings from .constants import SettingsMessages as Msg from collections.abc import Callable from dotenv import load_dotenv @@ -6,6 +7,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from typing import TypeVar +warnings.filterwarnings("ignore", message="Field name \"schema\".*shadows an attribute in parent") + # Type variable for generic settings factory T = TypeVar("T", bound=BaseSettings) diff --git a/ddcDatabases/mssql.py b/ddcDatabases/mssql.py index 88fbe38..9cb9924 100755 --- a/ddcDatabases/mssql.py +++ b/ddcDatabases/mssql.py @@ -113,23 +113,29 @@ def __init__( if _ssl.ssl_trust_server_certificate is not None else _settings.ssl_trust_server_certificate ), - ssl_ca_cert_path=_ssl.ssl_ca_cert_path, + ssl_ca_cert_path=( + _ssl.ssl_ca_cert_path if _ssl.ssl_ca_cert_path is not None else _settings.ssl_ca_cert_path + ), ) self.sync_driver = _settings.sync_driver self.async_driver = _settings.async_driver + _query = { + "driver": f"ODBC Driver {self._connection_config.odbcdriver_version} for SQL Server", + "Encrypt": "yes" if self._ssl_config.ssl_encrypt else "no", + "TrustServerCertificate": "yes" if self._ssl_config.ssl_trust_server_certificate else "no", + } + if self._ssl_config.ssl_ca_cert_path: + _query["ServerCertificate"] = self._ssl_config.ssl_ca_cert_path + self.connection_url = { "host": self._connection_config.host, "port": self._connection_config.port, "database": self._connection_config.database, "username": self._connection_config.user, "password": self._connection_config.password, - "query": { - "driver": f"ODBC Driver {self._connection_config.odbcdriver_version} for SQL Server", - "Encrypt": "yes" if self._ssl_config.ssl_encrypt else "no", - "TrustServerCertificate": "yes" if self._ssl_config.ssl_trust_server_certificate else "no", - }, + "query": _query, } self.extra_engine_args = extra_engine_args or {} diff --git a/ddcDatabases/postgresql.py b/ddcDatabases/postgresql.py index 55165f6..9d3556a 100755 --- a/ddcDatabases/postgresql.py +++ b/ddcDatabases/postgresql.py @@ -1,4 +1,5 @@ import logging +import ssl as _ssl_module from .core.base import BaseConnection from .core.configs import ( BaseConnectionConfig, @@ -281,7 +282,16 @@ async def _get_async_engine(self) -> AsyncGenerator[AsyncEngine, None]: if self._connection_config.schema and self._connection_config.schema != "public": async_connect_args["server_settings"] = {"search_path": self._connection_config.schema} if self._ssl_config.ssl_mode and self._ssl_config.ssl_mode != "disable": - async_connect_args["ssl"] = self._ssl_config.ssl_mode + if self._ssl_config.ssl_ca_cert_path: + ssl_context = _ssl_module.create_default_context(cafile=self._ssl_config.ssl_ca_cert_path) + if self._ssl_config.ssl_client_cert_path and self._ssl_config.ssl_client_key_path: + ssl_context.load_cert_chain( + certfile=self._ssl_config.ssl_client_cert_path, + keyfile=self._ssl_config.ssl_client_key_path, + ) + async_connect_args["ssl"] = ssl_context + else: + async_connect_args["ssl"] = self._ssl_config.ssl_mode _engine_args = self._get_base_engine_args(_connection_url, async_connect_args, async_engine_args) _engine = create_async_engine(**_engine_args) diff --git a/tests/unit/mssql/test_mssql_ssl.py b/tests/unit/mssql/test_mssql_ssl.py index abb73b0..cf39b30 100644 --- a/tests/unit/mssql/test_mssql_ssl.py +++ b/tests/unit/mssql/test_mssql_ssl.py @@ -1,6 +1,8 @@ """Tests for MSSQL SSL configuration.""" +import os import pytest +from unittest.mock import MagicMock, patch class TestMSSQLSSLConfig: @@ -65,3 +67,182 @@ def test_ssl_encrypt_with_trust_cert(self): config = MSSQLSSLConfig(ssl_encrypt=True, ssl_trust_server_certificate=True) assert config.ssl_encrypt is True assert config.ssl_trust_server_certificate is True + + +class TestMSSQLSSLConnectionURL: + """Test that MSSQL SSL cert paths are correctly added to connection URL.""" + + # noinspection PyMethodMayBeStatic + def setup_method(self): + """Clear cache before each test.""" + from ddcDatabases.core.settings import get_mssql_settings + + get_mssql_settings.cache_clear() + + import ddcDatabases.core.settings + + ddcDatabases.core.settings._dotenv_loaded = False + + @patch("ddcDatabases.mssql.get_mssql_settings") + def test_ssl_ca_cert_path_in_connection_url(self, mock_get_settings): + """Test that ssl_ca_cert_path is added to connection URL query as ServerCertificate.""" + from ddcDatabases.mssql import MSSQL, MSSQLSSLConfig + + mock_settings = MagicMock() + mock_settings.host = "localhost" + mock_settings.port = 1433 + mock_settings.user = "sa" + mock_settings.password = "password" + mock_settings.database = "master" + mock_settings.schema = "dbo" + mock_settings.echo = False + mock_settings.autoflush = True + mock_settings.expire_on_commit = True + mock_settings.autocommit = False + mock_settings.connection_timeout = 30 + mock_settings.pool_recycle = 3600 + mock_settings.pool_size = 25 + mock_settings.max_overflow = 50 + mock_settings.odbcdriver_version = 18 + mock_settings.sync_driver = "mssql+pyodbc" + mock_settings.async_driver = "mssql+aioodbc" + mock_settings.ssl_encrypt = False + mock_settings.ssl_trust_server_certificate = True + mock_settings.ssl_ca_cert_path = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None + mock_get_settings.return_value = mock_settings + + mssql = MSSQL( + ssl_config=MSSQLSSLConfig( + ssl_encrypt=True, + ssl_trust_server_certificate=False, + ssl_ca_cert_path="/path/to/ca.pem", + ) + ) + + assert mssql.connection_url["query"]["ServerCertificate"] == "/path/to/ca.pem" + assert mssql.connection_url["query"]["Encrypt"] == "yes" + assert mssql.connection_url["query"]["TrustServerCertificate"] == "no" + + @patch("ddcDatabases.mssql.get_mssql_settings") + def test_ssl_no_ca_cert_path_no_server_certificate_key(self, mock_get_settings): + """Test that ServerCertificate is not in query when ssl_ca_cert_path is None.""" + from ddcDatabases.mssql import MSSQL + + mock_settings = MagicMock() + mock_settings.host = "localhost" + mock_settings.port = 1433 + mock_settings.user = "sa" + mock_settings.password = "password" + mock_settings.database = "master" + mock_settings.schema = "dbo" + mock_settings.echo = False + mock_settings.autoflush = True + mock_settings.expire_on_commit = True + mock_settings.autocommit = False + mock_settings.connection_timeout = 30 + mock_settings.pool_recycle = 3600 + mock_settings.pool_size = 25 + mock_settings.max_overflow = 50 + mock_settings.odbcdriver_version = 18 + mock_settings.sync_driver = "mssql+pyodbc" + mock_settings.async_driver = "mssql+aioodbc" + mock_settings.ssl_encrypt = False + mock_settings.ssl_trust_server_certificate = True + mock_settings.ssl_ca_cert_path = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None + mock_get_settings.return_value = mock_settings + + mssql = MSSQL() + + assert "ServerCertificate" not in mssql.connection_url["query"] + + @patch("ddcDatabases.mssql.get_mssql_settings") + def test_ssl_ca_cert_path_falls_back_to_settings(self, mock_get_settings): + """Test that ssl_ca_cert_path falls back to settings when not provided in ssl_config.""" + from ddcDatabases.mssql import MSSQL + + mock_settings = MagicMock() + mock_settings.host = "localhost" + mock_settings.port = 1433 + mock_settings.user = "sa" + mock_settings.password = "password" + mock_settings.database = "master" + mock_settings.schema = "dbo" + mock_settings.echo = False + mock_settings.autoflush = True + mock_settings.expire_on_commit = True + mock_settings.autocommit = False + mock_settings.connection_timeout = 30 + mock_settings.pool_recycle = 3600 + mock_settings.pool_size = 25 + mock_settings.max_overflow = 50 + mock_settings.odbcdriver_version = 18 + mock_settings.sync_driver = "mssql+pyodbc" + mock_settings.async_driver = "mssql+aioodbc" + mock_settings.ssl_encrypt = False + mock_settings.ssl_trust_server_certificate = True + mock_settings.ssl_ca_cert_path = "/settings/path/to/ca.pem" + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None + mock_get_settings.return_value = mock_settings + + mssql = MSSQL() + + assert mssql._ssl_config.ssl_ca_cert_path == "/settings/path/to/ca.pem" + assert mssql.connection_url["query"]["ServerCertificate"] == "/settings/path/to/ca.pem" + + +class TestMSSQLSSLEnvVars: + """Test that MSSQL SSL settings are correctly read from environment variables.""" + + # noinspection PyMethodMayBeStatic + def setup_method(self): + """Clear cache before each test.""" + from ddcDatabases.core.settings import get_mssql_settings + + get_mssql_settings.cache_clear() + + import ddcDatabases.core.settings + + ddcDatabases.core.settings._dotenv_loaded = False + + def test_ssl_ca_cert_path_from_env_var(self): + """Test that MSSQL_SSL_CA_CERT_PATH env var is read by settings.""" + from ddcDatabases.core.settings import MSSQLSettings + + with patch.dict( + os.environ, + { + "MSSQL_SSL_CA_CERT_PATH": "/env/path/to/ca.pem", + "MSSQL_SSL_ENCRYPT": "true", + "MSSQL_SSL_TRUST_SERVER_CERTIFICATE": "false", + }, + ): + settings = MSSQLSettings() + assert settings.ssl_ca_cert_path == "/env/path/to/ca.pem" + assert settings.ssl_encrypt is True + assert settings.ssl_trust_server_certificate is False diff --git a/tests/unit/postgresql/test_postgresql_ssl.py b/tests/unit/postgresql/test_postgresql_ssl.py index 1bd0146..2d1c94b 100644 --- a/tests/unit/postgresql/test_postgresql_ssl.py +++ b/tests/unit/postgresql/test_postgresql_ssl.py @@ -1,6 +1,14 @@ """Tests for PostgreSQL SSL configuration.""" +import os import pytest +import ssl +from importlib.util import find_spec +from unittest.mock import AsyncMock, MagicMock, patch + +POSTGRESQL_AVAILABLE = find_spec("asyncpg") is not None and find_spec("psycopg") is not None + +pytestmark = pytest.mark.skipif(not POSTGRESQL_AVAILABLE, reason="PostgreSQL drivers not available") class TestPostgreSQLSSLConfig: @@ -100,3 +108,357 @@ def test_ssl_client_cert_without_key(self): ) assert config.ssl_client_cert_path == "/path/to/client.pem" assert config.ssl_client_key_path is None + + +class TestPostgreSQLSSLEngine: + """Test that SSL cert paths are correctly passed to sync/async engines.""" + + # noinspection PyMethodMayBeStatic + def setup_method(self): + """Clear cache before each test.""" + from ddcDatabases.core.settings import get_postgresql_settings + + get_postgresql_settings.cache_clear() + + import ddcDatabases.core.settings + + ddcDatabases.core.settings._dotenv_loaded = False + + @patch("ddcDatabases.postgresql.get_postgresql_settings") + def test_async_engine_ssl_with_cert_paths_uses_ssl_context(self, mock_get_settings): + """Test that _get_async_engine passes an ssl.SSLContext when cert paths are provided.""" + from ddcDatabases.postgresql import PostgreSQL, PostgreSQLSSLConfig + + mock_settings = MagicMock() + mock_settings.host = "localhost" + mock_settings.port = 5432 + mock_settings.user = "postgres" + mock_settings.password = "password" + mock_settings.database = "postgres" + mock_settings.schema = "public" + mock_settings.echo = False + mock_settings.autoflush = False + mock_settings.expire_on_commit = False + mock_settings.autocommit = False + mock_settings.connection_timeout = 30 + mock_settings.pool_recycle = 3600 + mock_settings.pool_size = 25 + mock_settings.max_overflow = 50 + mock_settings.sync_driver = "postgresql+psycopg" + mock_settings.async_driver = "postgresql+asyncpg" + mock_settings.ssl_mode = "verify-full" + mock_settings.ssl_ca_cert_path = "/path/to/ca.pem" + mock_settings.ssl_client_cert_path = "/path/to/client.pem" + mock_settings.ssl_client_key_path = "/path/to/client-key.pem" + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None + mock_get_settings.return_value = mock_settings + + postgresql = PostgreSQL( + ssl_config=PostgreSQLSSLConfig( + ssl_mode="verify-full", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.pem", + ssl_client_key_path="/path/to/client-key.pem", + ) + ) + + captured_args = {} + mock_ssl_context = MagicMock(spec=ssl.SSLContext) + + with ( + patch("ddcDatabases.postgresql.create_async_engine") as mock_create, + patch("ddcDatabases.postgresql._ssl_module.create_default_context", return_value=mock_ssl_context), + ): + mock_engine = MagicMock() + mock_engine.dispose = AsyncMock() + mock_create.return_value = mock_engine + + import asyncio + + async def _test(): + async with postgresql._get_async_engine() as engine: + captured_args.update(mock_create.call_args.kwargs) + + asyncio.run(_test()) + + connect_args = captured_args.get("connect_args", {}) + assert "ssl" in connect_args, "ssl must be in async connect_args when cert paths are set" + assert connect_args["ssl"] is mock_ssl_context + mock_ssl_context.load_cert_chain.assert_called_once_with( + certfile="/path/to/client.pem", + keyfile="/path/to/client-key.pem", + ) + + @patch("ddcDatabases.postgresql.get_postgresql_settings") + def test_async_engine_ssl_without_cert_paths_uses_mode_string(self, mock_get_settings): + """Test that _get_async_engine passes the mode string when no cert paths are provided.""" + from ddcDatabases.postgresql import PostgreSQL, PostgreSQLSSLConfig + + mock_settings = MagicMock() + mock_settings.host = "localhost" + mock_settings.port = 5432 + mock_settings.user = "postgres" + mock_settings.password = "password" + mock_settings.database = "postgres" + mock_settings.schema = "public" + mock_settings.echo = False + mock_settings.autoflush = False + mock_settings.expire_on_commit = False + mock_settings.autocommit = False + mock_settings.connection_timeout = 30 + mock_settings.pool_recycle = 3600 + mock_settings.pool_size = 25 + mock_settings.max_overflow = 50 + mock_settings.sync_driver = "postgresql+psycopg" + mock_settings.async_driver = "postgresql+asyncpg" + mock_settings.ssl_mode = "require" + mock_settings.ssl_ca_cert_path = None + mock_settings.ssl_client_cert_path = None + mock_settings.ssl_client_key_path = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None + mock_get_settings.return_value = mock_settings + + postgresql = PostgreSQL(ssl_config=PostgreSQLSSLConfig(ssl_mode="require")) + + captured_args = {} + + with patch("ddcDatabases.postgresql.create_async_engine") as mock_create: + mock_engine = MagicMock() + mock_engine.dispose = AsyncMock() + mock_create.return_value = mock_engine + + import asyncio + + async def _test(): + async with postgresql._get_async_engine() as engine: + captured_args.update(mock_create.call_args.kwargs) + + asyncio.run(_test()) + + connect_args = captured_args.get("connect_args", {}) + assert connect_args.get("ssl") == "require", f"Expected mode string 'require', got {connect_args.get('ssl')!r}" + + @patch("ddcDatabases.postgresql.get_postgresql_settings") + def test_async_engine_ssl_ca_only_no_client_certs(self, mock_get_settings): + """Test that _get_async_engine creates SSLContext with CA cert only (no client cert/key).""" + from ddcDatabases.postgresql import PostgreSQL, PostgreSQLSSLConfig + + mock_settings = MagicMock() + mock_settings.host = "localhost" + mock_settings.port = 5432 + mock_settings.user = "postgres" + mock_settings.password = "password" + mock_settings.database = "postgres" + mock_settings.schema = "public" + mock_settings.echo = False + mock_settings.autoflush = False + mock_settings.expire_on_commit = False + mock_settings.autocommit = False + mock_settings.connection_timeout = 30 + mock_settings.pool_recycle = 3600 + mock_settings.pool_size = 25 + mock_settings.max_overflow = 50 + mock_settings.sync_driver = "postgresql+psycopg" + mock_settings.async_driver = "postgresql+asyncpg" + mock_settings.ssl_mode = "verify-ca" + mock_settings.ssl_ca_cert_path = "/path/to/ca.pem" + mock_settings.ssl_client_cert_path = None + mock_settings.ssl_client_key_path = None + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None + mock_get_settings.return_value = mock_settings + + postgresql = PostgreSQL( + ssl_config=PostgreSQLSSLConfig( + ssl_mode="verify-ca", + ssl_ca_cert_path="/path/to/ca.pem", + ) + ) + + captured_args = {} + mock_ssl_context = MagicMock(spec=ssl.SSLContext) + + with ( + patch("ddcDatabases.postgresql.create_async_engine") as mock_create, + patch("ddcDatabases.postgresql._ssl_module.create_default_context", return_value=mock_ssl_context), + ): + mock_engine = MagicMock() + mock_engine.dispose = AsyncMock() + mock_create.return_value = mock_engine + + import asyncio + + async def _test(): + async with postgresql._get_async_engine() as engine: + captured_args.update(mock_create.call_args.kwargs) + + asyncio.run(_test()) + + connect_args = captured_args.get("connect_args", {}) + assert connect_args["ssl"] is mock_ssl_context + mock_ssl_context.load_cert_chain.assert_not_called() + + @patch("ddcDatabases.postgresql.get_postgresql_settings") + def test_sync_engine_ssl_with_cert_paths(self, mock_get_settings): + """Test that _get_engine passes SSL cert paths as connect_args for psycopg.""" + from ddcDatabases.postgresql import PostgreSQL, PostgreSQLSSLConfig + + mock_settings = MagicMock() + mock_settings.host = "localhost" + mock_settings.port = 5432 + mock_settings.user = "postgres" + mock_settings.password = "password" + mock_settings.database = "postgres" + mock_settings.schema = "public" + mock_settings.echo = False + mock_settings.autoflush = False + mock_settings.expire_on_commit = False + mock_settings.autocommit = False + mock_settings.connection_timeout = 30 + mock_settings.pool_recycle = 3600 + mock_settings.pool_size = 25 + mock_settings.max_overflow = 50 + mock_settings.sync_driver = "postgresql+psycopg" + mock_settings.async_driver = "postgresql+asyncpg" + mock_settings.ssl_mode = "verify-full" + mock_settings.ssl_ca_cert_path = "/path/to/ca.pem" + mock_settings.ssl_client_cert_path = "/path/to/client.pem" + mock_settings.ssl_client_key_path = "/path/to/client-key.pem" + mock_settings.connection_enable_retry = None + mock_settings.connection_max_retries = None + mock_settings.connection_initial_retry_delay = None + mock_settings.connection_max_retry_delay = None + mock_settings.operation_enable_retry = None + mock_settings.operation_max_retries = None + mock_settings.operation_initial_retry_delay = None + mock_settings.operation_max_retry_delay = None + mock_settings.operation_jitter = None + mock_get_settings.return_value = mock_settings + + postgresql = PostgreSQL( + ssl_config=PostgreSQLSSLConfig( + ssl_mode="verify-full", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.pem", + ssl_client_key_path="/path/to/client-key.pem", + ) + ) + + with patch("ddcDatabases.postgresql.create_engine") as mock_create: + mock_engine = MagicMock() + mock_create.return_value = mock_engine + + with postgresql._get_engine() as engine: + captured_args = mock_create.call_args.kwargs + + connect_args = captured_args.get("connect_args", {}) + assert connect_args["sslmode"] == "verify-full" + assert connect_args["sslrootcert"] == "/path/to/ca.pem" + assert connect_args["sslcert"] == "/path/to/client.pem" + assert connect_args["sslkey"] == "/path/to/client-key.pem" + + +class TestPostgreSQLSSLEnvVars: + """Test that SSL settings are correctly read from environment variables.""" + + # noinspection PyMethodMayBeStatic + def setup_method(self): + """Clear cache before each test.""" + from ddcDatabases.core.settings import get_postgresql_settings + + get_postgresql_settings.cache_clear() + + import ddcDatabases.core.settings + + ddcDatabases.core.settings._dotenv_loaded = False + + def test_ssl_cert_paths_from_env_vars(self): + """Test that SSL cert paths are read from POSTGRESQL_SSL_* env vars.""" + from ddcDatabases.core.settings import PostgreSQLSettings + + with patch.dict( + os.environ, + { + "POSTGRESQL_SSL_MODE": "verify-full", + "POSTGRESQL_SSL_CA_CERT_PATH": "/env/path/to/ca.pem", + "POSTGRESQL_SSL_CLIENT_CERT_PATH": "/env/path/to/client.pem", + "POSTGRESQL_SSL_CLIENT_KEY_PATH": "/env/path/to/client-key.pem", + }, + ): + settings = PostgreSQLSettings() + assert settings.ssl_mode == "verify-full" + assert settings.ssl_ca_cert_path == "/env/path/to/ca.pem" + assert settings.ssl_client_cert_path == "/env/path/to/client.pem" + assert settings.ssl_client_key_path == "/env/path/to/client-key.pem" + + def test_ssl_env_vars_propagate_to_async_engine(self): + """Test that SSL cert paths from env vars result in SSLContext for async engine.""" + from ddcDatabases.core.settings import PostgreSQLSettings + from ddcDatabases.postgresql import PostgreSQL + + with patch.dict( + os.environ, + { + "POSTGRESQL_SSL_MODE": "verify-full", + "POSTGRESQL_SSL_CA_CERT_PATH": "/env/path/to/ca.pem", + "POSTGRESQL_SSL_CLIENT_CERT_PATH": "/env/path/to/client.pem", + "POSTGRESQL_SSL_CLIENT_KEY_PATH": "/env/path/to/client-key.pem", + }, + ): + mock_settings = PostgreSQLSettings() + + with patch("ddcDatabases.postgresql.get_postgresql_settings", return_value=mock_settings): + postgresql = PostgreSQL() + + assert postgresql._ssl_config.ssl_ca_cert_path == "/env/path/to/ca.pem" + assert postgresql._ssl_config.ssl_client_cert_path == "/env/path/to/client.pem" + assert postgresql._ssl_config.ssl_client_key_path == "/env/path/to/client-key.pem" + + captured_args = {} + mock_ssl_context = MagicMock(spec=ssl.SSLContext) + + with ( + patch("ddcDatabases.postgresql.create_async_engine") as mock_create, + patch("ddcDatabases.postgresql._ssl_module.create_default_context", return_value=mock_ssl_context), + ): + mock_engine = MagicMock() + mock_engine.dispose = AsyncMock() + mock_create.return_value = mock_engine + + import asyncio + + async def _test(): + async with postgresql._get_async_engine() as engine: + captured_args.update(mock_create.call_args.kwargs) + + asyncio.run(_test()) + + connect_args = captured_args.get("connect_args", {}) + assert connect_args["ssl"] is mock_ssl_context + mock_ssl_context.load_cert_chain.assert_called_once_with( + certfile="/env/path/to/client.pem", + keyfile="/env/path/to/client-key.pem", + ) From 8cf89105db7a85d98f25076462bcf0ea7e9bee72 Mon Sep 17 00:00:00 2001 From: ddc Date: Fri, 6 Feb 2026 17:54:49 -0300 Subject: [PATCH 16/18] v3.0.9 --- ddcDatabases/postgresql.py | 1 + tests/unit/postgresql/test_postgresql_ssl.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/ddcDatabases/postgresql.py b/ddcDatabases/postgresql.py index 9d3556a..dff5220 100755 --- a/ddcDatabases/postgresql.py +++ b/ddcDatabases/postgresql.py @@ -284,6 +284,7 @@ async def _get_async_engine(self) -> AsyncGenerator[AsyncEngine, None]: if self._ssl_config.ssl_mode and self._ssl_config.ssl_mode != "disable": if self._ssl_config.ssl_ca_cert_path: ssl_context = _ssl_module.create_default_context(cafile=self._ssl_config.ssl_ca_cert_path) + ssl_context.minimum_version = _ssl_module.TLSVersion.TLSv1_3 if self._ssl_config.ssl_client_cert_path and self._ssl_config.ssl_client_key_path: ssl_context.load_cert_chain( certfile=self._ssl_config.ssl_client_cert_path, diff --git a/tests/unit/postgresql/test_postgresql_ssl.py b/tests/unit/postgresql/test_postgresql_ssl.py index 2d1c94b..6099f79 100644 --- a/tests/unit/postgresql/test_postgresql_ssl.py +++ b/tests/unit/postgresql/test_postgresql_ssl.py @@ -192,6 +192,7 @@ async def _test(): connect_args = captured_args.get("connect_args", {}) assert "ssl" in connect_args, "ssl must be in async connect_args when cert paths are set" assert connect_args["ssl"] is mock_ssl_context + assert mock_ssl_context.minimum_version == ssl.TLSVersion.TLSv1_3 mock_ssl_context.load_cert_chain.assert_called_once_with( certfile="/path/to/client.pem", keyfile="/path/to/client-key.pem", @@ -319,6 +320,7 @@ async def _test(): connect_args = captured_args.get("connect_args", {}) assert connect_args["ssl"] is mock_ssl_context + assert mock_ssl_context.minimum_version == ssl.TLSVersion.TLSv1_3 mock_ssl_context.load_cert_chain.assert_not_called() @patch("ddcDatabases.postgresql.get_postgresql_settings") @@ -458,6 +460,7 @@ async def _test(): connect_args = captured_args.get("connect_args", {}) assert connect_args["ssl"] is mock_ssl_context + assert mock_ssl_context.minimum_version == ssl.TLSVersion.TLSv1_3 mock_ssl_context.load_cert_chain.assert_called_once_with( certfile="/env/path/to/client.pem", keyfile="/env/path/to/client-key.pem", From 0c960c15604251719d0cf193206597d0eca8cc35 Mon Sep 17 00:00:00 2001 From: ddc Date: Fri, 6 Feb 2026 18:06:56 -0300 Subject: [PATCH 17/18] v3.0.9 --- ddcDatabases/postgresql.py | 3 ++- tests/unit/postgresql/test_postgresql_ssl.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ddcDatabases/postgresql.py b/ddcDatabases/postgresql.py index dff5220..8c9ea28 100755 --- a/ddcDatabases/postgresql.py +++ b/ddcDatabases/postgresql.py @@ -283,8 +283,9 @@ async def _get_async_engine(self) -> AsyncGenerator[AsyncEngine, None]: async_connect_args["server_settings"] = {"search_path": self._connection_config.schema} if self._ssl_config.ssl_mode and self._ssl_config.ssl_mode != "disable": if self._ssl_config.ssl_ca_cert_path: - ssl_context = _ssl_module.create_default_context(cafile=self._ssl_config.ssl_ca_cert_path) + ssl_context = _ssl_module.SSLContext(_ssl_module.PROTOCOL_TLS_CLIENT) ssl_context.minimum_version = _ssl_module.TLSVersion.TLSv1_3 + ssl_context.load_verify_locations(cafile=self._ssl_config.ssl_ca_cert_path) if self._ssl_config.ssl_client_cert_path and self._ssl_config.ssl_client_key_path: ssl_context.load_cert_chain( certfile=self._ssl_config.ssl_client_cert_path, diff --git a/tests/unit/postgresql/test_postgresql_ssl.py b/tests/unit/postgresql/test_postgresql_ssl.py index 6099f79..46834db 100644 --- a/tests/unit/postgresql/test_postgresql_ssl.py +++ b/tests/unit/postgresql/test_postgresql_ssl.py @@ -175,7 +175,7 @@ def test_async_engine_ssl_with_cert_paths_uses_ssl_context(self, mock_get_settin with ( patch("ddcDatabases.postgresql.create_async_engine") as mock_create, - patch("ddcDatabases.postgresql._ssl_module.create_default_context", return_value=mock_ssl_context), + patch("ddcDatabases.postgresql._ssl_module.SSLContext", return_value=mock_ssl_context), ): mock_engine = MagicMock() mock_engine.dispose = AsyncMock() @@ -193,6 +193,7 @@ async def _test(): assert "ssl" in connect_args, "ssl must be in async connect_args when cert paths are set" assert connect_args["ssl"] is mock_ssl_context assert mock_ssl_context.minimum_version == ssl.TLSVersion.TLSv1_3 + mock_ssl_context.load_verify_locations.assert_called_once_with(cafile="/path/to/ca.pem") mock_ssl_context.load_cert_chain.assert_called_once_with( certfile="/path/to/client.pem", keyfile="/path/to/client-key.pem", @@ -304,7 +305,7 @@ def test_async_engine_ssl_ca_only_no_client_certs(self, mock_get_settings): with ( patch("ddcDatabases.postgresql.create_async_engine") as mock_create, - patch("ddcDatabases.postgresql._ssl_module.create_default_context", return_value=mock_ssl_context), + patch("ddcDatabases.postgresql._ssl_module.SSLContext", return_value=mock_ssl_context), ): mock_engine = MagicMock() mock_engine.dispose = AsyncMock() @@ -321,6 +322,7 @@ async def _test(): connect_args = captured_args.get("connect_args", {}) assert connect_args["ssl"] is mock_ssl_context assert mock_ssl_context.minimum_version == ssl.TLSVersion.TLSv1_3 + mock_ssl_context.load_verify_locations.assert_called_once_with(cafile="/path/to/ca.pem") mock_ssl_context.load_cert_chain.assert_not_called() @patch("ddcDatabases.postgresql.get_postgresql_settings") @@ -444,7 +446,7 @@ def test_ssl_env_vars_propagate_to_async_engine(self): with ( patch("ddcDatabases.postgresql.create_async_engine") as mock_create, - patch("ddcDatabases.postgresql._ssl_module.create_default_context", return_value=mock_ssl_context), + patch("ddcDatabases.postgresql._ssl_module.SSLContext", return_value=mock_ssl_context), ): mock_engine = MagicMock() mock_engine.dispose = AsyncMock() @@ -461,6 +463,7 @@ async def _test(): connect_args = captured_args.get("connect_args", {}) assert connect_args["ssl"] is mock_ssl_context assert mock_ssl_context.minimum_version == ssl.TLSVersion.TLSv1_3 + mock_ssl_context.load_verify_locations.assert_called_once_with(cafile="/env/path/to/ca.pem") mock_ssl_context.load_cert_chain.assert_called_once_with( certfile="/env/path/to/client.pem", keyfile="/env/path/to/client-key.pem", From 9fe1cf0aba98ae48d3495ab52ab87ca4f62ba478 Mon Sep 17 00:00:00 2001 From: ddc Date: Mon, 9 Feb 2026 14:27:02 -0300 Subject: [PATCH 18/18] v3.0.9 --- README.md | 2 +- ddcDatabases/__init__.py | 2 +- ddcDatabases/core/persistent.py | 89 ++- pyproject.toml | 6 +- tests/unit/core/test_init_module.py | 29 +- tests/unit/core/test_persistent.py | 301 ++++++++++ tests/unit/postgresql/test_postgresql.py | 720 +++++------------------ uv.lock | 208 ++++--- 8 files changed, 670 insertions(+), 687 deletions(-) diff --git a/README.md b/README.md index 1e77808..7acf37c 100755 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Donate Sponsor
+ Code style: black uv Ruff - Code style: black
Python License: MIT diff --git a/ddcDatabases/__init__.py b/ddcDatabases/__init__.py index a228172..15af24f 100755 --- a/ddcDatabases/__init__.py +++ b/ddcDatabases/__init__.py @@ -195,7 +195,7 @@ __all__ = tuple(__all__) __title__ = "ddcDatabases" __author__ = "Daniel Costa" -__email__ = "danieldcsta@gmail.com" +__email__ = "ddcsoftwares@proton.me" __license__ = "MIT" __copyright__ = "Copyright 2024-present DDC Softwares" __version__ = version(__title__) diff --git a/ddcDatabases/core/persistent.py b/ddcDatabases/core/persistent.py index 4ccf2b2..2a74327 100644 --- a/ddcDatabases/core/persistent.py +++ b/ddcDatabases/core/persistent.py @@ -10,6 +10,7 @@ import asyncio import logging +import ssl as _ssl_module import threading import time import weakref @@ -780,6 +781,12 @@ def __new__( ), ) + # Build SSL connect_args from settings + ssl_mode = _settings.ssl_mode + ssl_ca_cert_path = _settings.ssl_ca_cert_path + ssl_client_cert_path = _settings.ssl_client_cert_path + ssl_client_key_path = _settings.ssl_client_key_path + with _registry_lock: if connection_key in _persistent_connections: return cast( @@ -796,10 +803,32 @@ def __new__( port=port, database=database, ) + + # Build asyncpg SSL connect_args + async_connect_args = {} + if ssl_mode and ssl_mode != "disable": + if ssl_ca_cert_path: + ssl_context = _ssl_module.SSLContext(_ssl_module.PROTOCOL_TLS_CLIENT) + ssl_context.minimum_version = _ssl_module.TLSVersion.TLSv1_3 + ssl_context.load_verify_locations(cafile=ssl_ca_cert_path) + if ssl_client_cert_path and ssl_client_key_path: + ssl_context.load_cert_chain( + certfile=ssl_client_cert_path, + keyfile=ssl_client_key_path, + ) + async_connect_args["ssl"] = ssl_context + else: + async_connect_args["ssl"] = ssl_mode + + merged_kwargs = {**engine_kwargs} + if async_connect_args: + existing_connect_args = merged_kwargs.get("connect_args", {}) + merged_kwargs["connect_args"] = {**existing_connect_args, **async_connect_args} + conn = PersistentSQLAlchemyAsyncConnection( connection_key=connection_key, connection_url=connection_url, - engine_args=engine_kwargs, + engine_args=merged_kwargs, config=config, connection_retry_config=connection_retry_config, operation_retry_config=operation_retry_config, @@ -814,10 +843,27 @@ def __new__( port=port, database=database, ) + + # Build psycopg SSL connect_args + sync_connect_args = {} + if ssl_mode and ssl_mode != "disable": + sync_connect_args["sslmode"] = ssl_mode + if ssl_ca_cert_path: + sync_connect_args["sslrootcert"] = ssl_ca_cert_path + if ssl_client_cert_path: + sync_connect_args["sslcert"] = ssl_client_cert_path + if ssl_client_key_path: + sync_connect_args["sslkey"] = ssl_client_key_path + + merged_kwargs = {**engine_kwargs} + if sync_connect_args: + existing_connect_args = merged_kwargs.get("connect_args", {}) + merged_kwargs["connect_args"] = {**existing_connect_args, **sync_connect_args} + conn = PersistentSQLAlchemyConnection( connection_key=connection_key, connection_url=connection_url, - engine_args=engine_kwargs, + engine_args=merged_kwargs, config=config, connection_retry_config=connection_retry_config, operation_retry_config=operation_retry_config, @@ -917,6 +963,12 @@ def __new__( ), ) + # Build SSL connect_args from settings + ssl_mode = _settings.ssl_mode + ssl_ca_cert_path = _settings.ssl_ca_cert_path + ssl_client_cert_path = _settings.ssl_client_cert_path + ssl_client_key_path = _settings.ssl_client_key_path + with _registry_lock: if connection_key in _persistent_connections: return cast( @@ -924,6 +976,24 @@ def __new__( _persistent_connections[connection_key], ) + # Build MySQL SSL connect_args (same format for both pymysql and aiomysql) + ssl_connect_args = {} + if ssl_mode and ssl_mode != "DISABLED": + ssl_dict: dict[str, str] = {} + if ssl_ca_cert_path: + ssl_dict["ca"] = ssl_ca_cert_path + if ssl_client_cert_path: + ssl_dict["cert"] = ssl_client_cert_path + if ssl_client_key_path: + ssl_dict["key"] = ssl_client_key_path + if ssl_dict: + ssl_connect_args["ssl"] = ssl_dict + + merged_kwargs = {**engine_kwargs} + if ssl_connect_args: + existing_connect_args = merged_kwargs.get("connect_args", {}) + merged_kwargs["connect_args"] = {**existing_connect_args, **ssl_connect_args} + if async_mode: connection_url = URL.create( drivername="mysql+aiomysql", @@ -936,7 +1006,7 @@ def __new__( conn = PersistentSQLAlchemyAsyncConnection( connection_key=connection_key, connection_url=connection_url, - engine_args=engine_kwargs, + engine_args=merged_kwargs, config=config, connection_retry_config=connection_retry_config, operation_retry_config=operation_retry_config, @@ -954,7 +1024,7 @@ def __new__( conn = PersistentSQLAlchemyConnection( connection_key=connection_key, connection_url=connection_url, - engine_args=engine_kwargs, + engine_args=merged_kwargs, config=config, connection_retry_config=connection_retry_config, operation_retry_config=operation_retry_config, @@ -1054,6 +1124,13 @@ def __new__( ), ) + # Build SSL query params from settings + _query: dict[str, str] = {"driver": "ODBC Driver 18 for SQL Server"} + _query["Encrypt"] = "yes" if _settings.ssl_encrypt else "no" + _query["TrustServerCertificate"] = "yes" if _settings.ssl_trust_server_certificate else "no" + if _settings.ssl_ca_cert_path: + _query["ServerCertificate"] = _settings.ssl_ca_cert_path + with _registry_lock: if connection_key in _persistent_connections: return cast( @@ -1069,7 +1146,7 @@ def __new__( host=host, port=port, database=database, - query={"driver": "ODBC Driver 18 for SQL Server", "TrustServerCertificate": "yes"}, + query=_query, ) conn = PersistentSQLAlchemyAsyncConnection( connection_key=connection_key, @@ -1088,7 +1165,7 @@ def __new__( host=host, port=port, database=database, - query={"driver": "ODBC Driver 18 for SQL Server", "TrustServerCertificate": "yes"}, + query=_query, ) conn = PersistentSQLAlchemyConnection( connection_key=connection_key, diff --git a/pyproject.toml b/pyproject.toml index b083b69..9ff23a4 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ urls.Homepage = "https://pypi.org/project/ddcDatabases" license = {text = "MIT"} readme = "README.md" authors = [ - {name = "Daniel Costa", email = "danieldcsta@gmail.com"}, + {name = "Daniel Costa", email = "ddcsoftwares@proton.me"}, ] maintainers = [ {name = "Daniel Costa"}, @@ -69,9 +69,9 @@ dev = [ "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", "testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.1", - "black>=26.1.0", - "poethepoet>=0.40.0", + "poethepoet>=0.41.0", "ruff>=0.15.0", + "black>=26.1.0", ] [tool.poe.tasks] diff --git a/tests/unit/core/test_init_module.py b/tests/unit/core/test_init_module.py index d711e14..98c5393 100644 --- a/tests/unit/core/test_init_module.py +++ b/tests/unit/core/test_init_module.py @@ -50,20 +50,27 @@ def test_version_parsing_exception_path_direct(self): """Test the actual exception path in the module initialization""" import sys - # Force a re-import with mocked version to test the exception path - with patch("importlib.metadata.version") as mock_version: - mock_version.side_effect = ModuleNotFoundError("No module named 'ddcDatabases'") + original_modules = sys.modules.copy() + + try: + # Force a re-import with mocked version to test the exception path + with patch("importlib.metadata.version") as mock_version: + mock_version.side_effect = ModuleNotFoundError("No module named 'ddcDatabases'") - # Remove the module from cache if it exists - modules_to_remove = [name for name in sys.modules.keys() if name.startswith("ddcDatabases")] - for module_name in modules_to_remove: - del sys.modules[module_name] + # Remove the module from cache if it exists + modules_to_remove = [name for name in sys.modules.keys() if name.startswith("ddcDatabases")] + for module_name in modules_to_remove: + del sys.modules[module_name] - # Import the module, which should trigger the exception path + # Import the module, which should trigger the exception path - # The module should have the fallback version - # Note: This might not work as expected due to module caching, - # but it demonstrates the intended test case + # The module should have the fallback version + # Note: This might not work as expected due to module caching, + # but it demonstrates the intended test case + finally: + # Restore original modules to prevent contamination of subsequent tests + sys.modules.clear() + sys.modules.update(original_modules) def test_constants_accessibility(self): """Test that all module constants are accessible""" diff --git a/tests/unit/core/test_persistent.py b/tests/unit/core/test_persistent.py index 73b0ef6..3da5a8d 100644 --- a/tests/unit/core/test_persistent.py +++ b/tests/unit/core/test_persistent.py @@ -1,6 +1,7 @@ """Tests for persistent connection functionality.""" import pytest +import ssl as _ssl_module import time from ddcDatabases.core.configs import BaseOperationRetryConfig as OperationRetryConfig from ddcDatabases.core.configs import BaseRetryConfig as RetryConfig @@ -1225,3 +1226,303 @@ def test_sqlite_retry_settings(self): assert settings.connection_max_retries == 1 # Minimal retries for file-based DB assert settings.operation_enable_retry is False # SQLite disabled by default assert settings.operation_max_retries == 1 # Minimal retries for file-based DB + + +def _mock_pg_settings(**overrides): + """Create a mock PostgreSQL settings object with SSL defaults.""" + settings = MagicMock() + settings.host = overrides.get("host", "localhost") + settings.port = overrides.get("port", 5432) + settings.user = overrides.get("user", "postgres") + settings.password = overrides.get("password", "password") + settings.database = overrides.get("database", "testdb") + settings.ssl_mode = overrides.get("ssl_mode", "disable") + settings.ssl_ca_cert_path = overrides.get("ssl_ca_cert_path", None) + settings.ssl_client_cert_path = overrides.get("ssl_client_cert_path", None) + settings.ssl_client_key_path = overrides.get("ssl_client_key_path", None) + settings.persistent_idle_timeout = overrides.get("persistent_idle_timeout", 300) + settings.persistent_health_check_interval = overrides.get("persistent_health_check_interval", 30) + settings.persistent_auto_reconnect = overrides.get("persistent_auto_reconnect", False) + return settings + + +def _mock_mysql_settings(**overrides): + """Create a mock MySQL settings object with SSL defaults.""" + settings = MagicMock() + settings.host = overrides.get("host", "localhost") + settings.port = overrides.get("port", 3306) + settings.user = overrides.get("user", "root") + settings.password = overrides.get("password", "password") + settings.database = overrides.get("database", "testdb") + settings.ssl_mode = overrides.get("ssl_mode", "DISABLED") + settings.ssl_ca_cert_path = overrides.get("ssl_ca_cert_path", None) + settings.ssl_client_cert_path = overrides.get("ssl_client_cert_path", None) + settings.ssl_client_key_path = overrides.get("ssl_client_key_path", None) + settings.persistent_idle_timeout = overrides.get("persistent_idle_timeout", 300) + settings.persistent_health_check_interval = overrides.get("persistent_health_check_interval", 30) + settings.persistent_auto_reconnect = overrides.get("persistent_auto_reconnect", False) + return settings + + +def _mock_mssql_settings(**overrides): + """Create a mock MSSQL settings object with SSL defaults.""" + settings = MagicMock() + settings.host = overrides.get("host", "localhost") + settings.port = overrides.get("port", 1433) + settings.user = overrides.get("user", "sa") + settings.password = overrides.get("password", "password") + settings.database = overrides.get("database", "master") + settings.ssl_encrypt = overrides.get("ssl_encrypt", False) + settings.ssl_trust_server_certificate = overrides.get("ssl_trust_server_certificate", True) + settings.ssl_ca_cert_path = overrides.get("ssl_ca_cert_path", None) + settings.persistent_idle_timeout = overrides.get("persistent_idle_timeout", 300) + settings.persistent_health_check_interval = overrides.get("persistent_health_check_interval", 30) + settings.persistent_auto_reconnect = overrides.get("persistent_auto_reconnect", False) + return settings + + +class TestAsyncCreateEngine: + """Test PersistentSQLAlchemyAsyncConnection._create_engine.""" + + @patch("ddcDatabases.core.persistent.create_async_engine") + def test_create_async_engine_called(self, mock_create_async_engine): + """Test that _create_engine calls create_async_engine with correct args.""" + mock_engine = MagicMock() + mock_create_async_engine.return_value = mock_engine + + conn = PersistentSQLAlchemyAsyncConnection( + connection_key="test://localhost/db", + connection_url="postgresql+asyncpg://localhost/test", + engine_args={"echo": True}, + ) + + engine = conn._create_engine() + + assert engine is mock_engine + mock_create_async_engine.assert_called_once_with( + "postgresql+asyncpg://localhost/test", + echo=True, + ) + + +class TestPostgreSQLSSLAsync: + """Tests for PostgreSQL SSL configuration in async mode.""" + + def setup_method(self): + close_all_persistent_connections() + + def teardown_method(self): + close_all_persistent_connections() + + @patch("ddcDatabases.core.persistent.get_postgresql_settings") + def test_ssl_verify_full_with_ca_cert_creates_ssl_context(self, mock_get_settings): + """Test async ssl_mode=verify-full with CA cert creates SSLContext.""" + mock_get_settings.return_value = _mock_pg_settings( + ssl_mode="verify-full", + ssl_ca_cert_path="/path/to/ca.pem", + ) + + with patch.object(_ssl_module.SSLContext, "load_verify_locations"): + conn = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=True, + ) + + assert isinstance(conn, PersistentSQLAlchemyAsyncConnection) + connect_args = conn._engine_args.get("connect_args", {}) + assert "ssl" in connect_args + assert isinstance(connect_args["ssl"], _ssl_module.SSLContext) + + @patch("ddcDatabases.core.persistent.get_postgresql_settings") + def test_ssl_verify_full_with_client_certs_loads_cert_chain(self, mock_get_settings): + """Test async ssl_mode=verify-full with client certs calls load_cert_chain.""" + mock_get_settings.return_value = _mock_pg_settings( + ssl_mode="verify-full", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.crt", + ssl_client_key_path="/path/to/client.key", + ) + + with ( + patch.object(_ssl_module.SSLContext, "load_verify_locations"), + patch.object(_ssl_module.SSLContext, "load_cert_chain") as mock_load_cert, + ): + conn = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=True, + ) + + assert isinstance(conn, PersistentSQLAlchemyAsyncConnection) + mock_load_cert.assert_called_once_with( + certfile="/path/to/client.crt", + keyfile="/path/to/client.key", + ) + + @patch("ddcDatabases.core.persistent.get_postgresql_settings") + def test_ssl_verify_full_no_ca_cert_passes_ssl_mode_directly(self, mock_get_settings): + """Test async ssl_mode=verify-full with no CA cert passes ssl_mode string.""" + mock_get_settings.return_value = _mock_pg_settings( + ssl_mode="verify-full", + ) + + conn = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=True, + ) + + assert isinstance(conn, PersistentSQLAlchemyAsyncConnection) + connect_args = conn._engine_args.get("connect_args", {}) + assert connect_args.get("ssl") == "verify-full" + + +class TestPostgreSQLSSLSync: + """Tests for PostgreSQL SSL configuration in sync mode.""" + + def setup_method(self): + close_all_persistent_connections() + + def teardown_method(self): + close_all_persistent_connections() + + @patch("ddcDatabases.core.persistent.get_postgresql_settings") + def test_ssl_verify_full_with_all_certs_populates_connect_args(self, mock_get_settings): + """Test sync ssl_mode=verify-full with CA/client certs populates connect_args.""" + mock_get_settings.return_value = _mock_pg_settings( + ssl_mode="verify-full", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.crt", + ssl_client_key_path="/path/to/client.key", + ) + + conn = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + + assert isinstance(conn, PersistentSQLAlchemyConnection) + connect_args = conn._engine_args.get("connect_args", {}) + assert connect_args["sslmode"] == "verify-full" + assert connect_args["sslrootcert"] == "/path/to/ca.pem" + assert connect_args["sslcert"] == "/path/to/client.crt" + assert connect_args["sslkey"] == "/path/to/client.key" + + @patch("ddcDatabases.core.persistent.get_postgresql_settings") + def test_ssl_verify_full_no_certs_only_sslmode(self, mock_get_settings): + """Test sync ssl_mode=verify-full with no certs only sets sslmode.""" + mock_get_settings.return_value = _mock_pg_settings( + ssl_mode="verify-full", + ) + + conn = PostgreSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + + assert isinstance(conn, PersistentSQLAlchemyConnection) + connect_args = conn._engine_args.get("connect_args", {}) + assert connect_args["sslmode"] == "verify-full" + assert "sslrootcert" not in connect_args + assert "sslcert" not in connect_args + assert "sslkey" not in connect_args + + +class TestMySQLSSL: + """Tests for MySQL SSL configuration.""" + + def setup_method(self): + close_all_persistent_connections() + + def teardown_method(self): + close_all_persistent_connections() + + @patch("ddcDatabases.core.persistent.get_mysql_settings") + def test_ssl_with_all_certs_populates_connect_args(self, mock_get_settings): + """Test MySQL SSL with CA/client certs populates connect_args.""" + mock_get_settings.return_value = _mock_mysql_settings( + ssl_mode="REQUIRED", + ssl_ca_cert_path="/path/to/ca.pem", + ssl_client_cert_path="/path/to/client.crt", + ssl_client_key_path="/path/to/client.key", + ) + + conn = MySQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + + assert isinstance(conn, PersistentSQLAlchemyConnection) + connect_args = conn._engine_args.get("connect_args", {}) + ssl_dict = connect_args.get("ssl", {}) + assert ssl_dict["ca"] == "/path/to/ca.pem" + assert ssl_dict["cert"] == "/path/to/client.crt" + assert ssl_dict["key"] == "/path/to/client.key" + + @patch("ddcDatabases.core.persistent.get_mysql_settings") + def test_ssl_with_ca_only(self, mock_get_settings): + """Test MySQL SSL with only CA cert.""" + mock_get_settings.return_value = _mock_mysql_settings( + ssl_mode="REQUIRED", + ssl_ca_cert_path="/path/to/ca.pem", + ) + + conn = MySQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + + assert isinstance(conn, PersistentSQLAlchemyConnection) + connect_args = conn._engine_args.get("connect_args", {}) + ssl_dict = connect_args.get("ssl", {}) + assert ssl_dict["ca"] == "/path/to/ca.pem" + assert "cert" not in ssl_dict + assert "key" not in ssl_dict + + +class TestMSSQLSSLCACert: + """Tests for MSSQL SSL CA cert path.""" + + def setup_method(self): + close_all_persistent_connections() + + def teardown_method(self): + close_all_persistent_connections() + + @patch("ddcDatabases.core.persistent.get_mssql_settings") + def test_ssl_ca_cert_path_sets_server_certificate(self, mock_get_settings): + """Test with ssl_ca_cert_path set puts ServerCertificate in connection URL query.""" + mock_get_settings.return_value = _mock_mssql_settings( + ssl_ca_cert_path="/path/to/ca.pem", + ) + + conn = MSSQLPersistent( + host="localhost", + user="test", + password="test", + database="testdb", + async_mode=False, + ) + + assert isinstance(conn, PersistentSQLAlchemyConnection) + url = conn._connection_url + assert "ServerCertificate" in str(url) diff --git a/tests/unit/postgresql/test_postgresql.py b/tests/unit/postgresql/test_postgresql.py index 496cb20..bdbed1e 100644 --- a/tests/unit/postgresql/test_postgresql.py +++ b/tests/unit/postgresql/test_postgresql.py @@ -15,6 +15,46 @@ ) +def _make_pg_mock_settings(**overrides): + """Create a complete mock PostgreSQL settings with sensible defaults.""" + settings = MagicMock() + defaults = { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "password", + "database": "postgres", + "schema": "public", + "echo": False, + "autoflush": False, + "expire_on_commit": False, + "autocommit": False, + "connection_timeout": 30, + "pool_recycle": 3600, + "pool_size": 25, + "max_overflow": 50, + "sync_driver": "postgresql+psycopg", + "async_driver": "postgresql+asyncpg", + "ssl_mode": "disable", + "ssl_ca_cert_path": None, + "ssl_client_cert_path": None, + "ssl_client_key_path": None, + "connection_enable_retry": True, + "connection_max_retries": 3, + "connection_initial_retry_delay": 1.0, + "connection_max_retry_delay": 30.0, + "operation_enable_retry": True, + "operation_max_retries": 3, + "operation_initial_retry_delay": 0.5, + "operation_max_retry_delay": 10.0, + "operation_jitter": 0.1, + } + defaults.update(overrides) + for key, value in defaults.items(): + setattr(settings, key, value) + return settings + + class TestPostgreSQL: """Test PostgreSQL database connection class""" @@ -66,15 +106,12 @@ def patched_init(postgresql_self, *_args, **_kwargs): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_init_with_parameters(self, mock_get_settings): """Test PostgreSQL initialization with override parameters""" - mock_settings = MagicMock() - mock_settings.user = "defaultuser" - mock_settings.password = "defaultpass" - mock_settings.host = "defaulthost" - mock_settings.port = 5432 - mock_settings.database = "defaultdb" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings( + user="defaultuser", + password="defaultpass", + host="defaulthost", + database="defaultdb", + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL( @@ -96,15 +133,7 @@ def test_init_with_parameters(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_extra_engine_args(self, mock_get_settings): """Test PostgreSQL with extra engine arguments""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings extra_args = {"pool_timeout": 60, "connect_timeout": 30} @@ -121,15 +150,7 @@ def test_extra_engine_args(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_autoflush_and_expire_on_commit(self, mock_get_settings): """Test PostgreSQL autoflush and expire_on_commit parameters""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autoflush=False, expire_on_commit=False)) @@ -140,16 +161,7 @@ def test_autoflush_and_expire_on_commit(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_autocommit_parameter(self, mock_get_settings): """Test PostgreSQL autocommit parameter""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autocommit = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=True)) @@ -159,15 +171,7 @@ def test_autocommit_parameter(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_connect_args_psycopg_driver(self, mock_get_settings): """Test that psycopg driver sets correct connect_args""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -199,15 +203,7 @@ def test_connect_args_different_driver(self): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_driver_detection_logic(self, mock_get_settings): """Test driver detection logic in init""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -255,15 +251,7 @@ def test_engine_args_structure(self): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_context_manager_methods_exist(self, mock_get_settings): """Test that context manager methods exist and can be called safely""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -279,22 +267,7 @@ def test_get_base_engine_args_method(self, mock_get_settings): """Test _get_base_engine_args method - covers lines 70-87""" from sqlalchemy import URL - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -336,22 +309,7 @@ def test_get_base_engine_args_method(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_sync_driver_autocommit_logic(self, mock_get_settings): """Test autocommit logic for sync driver - covers autocommit branch""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=True)) @@ -381,22 +339,7 @@ def test_sync_driver_autocommit_logic(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_async_driver_autocommit_logic(self, mock_get_settings): """Test autocommit logic for async driver - covers async autocommit branch""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=True)) @@ -429,16 +372,7 @@ def test_async_driver_autocommit_logic(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_pool_size_parameter(self, mock_get_settings): """Test PostgreSQL pool_size parameter""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.pool_size = 10 + mock_settings = _make_pg_mock_settings(pool_size=10) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(pool_config=PostgreSQLPoolConfig(pool_size=15)) @@ -448,16 +382,7 @@ def test_pool_size_parameter(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_max_overflow_parameter(self, mock_get_settings): """Test PostgreSQL max_overflow parameter""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.max_overflow = 20 + mock_settings = _make_pg_mock_settings(max_overflow=20) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(pool_config=PostgreSQLPoolConfig(max_overflow=30)) @@ -467,17 +392,7 @@ def test_max_overflow_parameter(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_pool_parameters_defaults(self, mock_get_settings): """Test PostgreSQL pool parameters use settings defaults""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -488,36 +403,30 @@ def test_pool_parameters_defaults(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_enhanced_configuration_methods(self, mock_get_settings): """Test the new enhanced configuration getter methods""" - mock_settings = MagicMock() - mock_settings.host = "testhost" - mock_settings.port = 5432 - mock_settings.user = "testuser" - mock_settings.password = "testpass" - mock_settings.database = "testdb" - mock_settings.schema = None - mock_settings.echo = True - mock_settings.autoflush = True - mock_settings.expire_on_commit = True - mock_settings.autocommit = False - mock_settings.connection_timeout = 45 - mock_settings.pool_recycle = 7200 - mock_settings.pool_size = 30 - mock_settings.max_overflow = 60 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = None - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = None - mock_settings.connection_max_retries = None - mock_settings.connection_initial_retry_delay = None - mock_settings.connection_max_retry_delay = None - mock_settings.operation_enable_retry = None - mock_settings.operation_max_retries = None - mock_settings.operation_initial_retry_delay = None - mock_settings.operation_max_retry_delay = None - mock_settings.operation_jitter = None + mock_settings = _make_pg_mock_settings( + host="testhost", + user="testuser", + password="testpass", + database="testdb", + schema=None, + echo=True, + autoflush=True, + expire_on_commit=True, + connection_timeout=45, + pool_recycle=7200, + pool_size=30, + max_overflow=60, + ssl_mode=None, + connection_enable_retry=None, + connection_max_retries=None, + connection_initial_retry_delay=None, + connection_max_retry_delay=None, + operation_enable_retry=None, + operation_max_retries=None, + operation_initial_retry_delay=None, + operation_max_retry_delay=None, + operation_jitter=None, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL( @@ -557,36 +466,19 @@ def test_enhanced_configuration_methods(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_engine_method_with_psycopg(self, mock_get_settings): """Test the _get_engine method with psycopg driver""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = None - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = None - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = None - mock_settings.connection_max_retries = None - mock_settings.connection_initial_retry_delay = None - mock_settings.connection_max_retry_delay = None - mock_settings.operation_enable_retry = None - mock_settings.operation_max_retries = None - mock_settings.operation_initial_retry_delay = None - mock_settings.operation_max_retry_delay = None - mock_settings.operation_jitter = None + mock_settings = _make_pg_mock_settings( + schema=None, + ssl_mode=None, + connection_enable_retry=None, + connection_max_retries=None, + connection_initial_retry_delay=None, + connection_max_retry_delay=None, + operation_enable_retry=None, + operation_max_retries=None, + operation_initial_retry_delay=None, + operation_max_retry_delay=None, + operation_jitter=None, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=True)) @@ -603,36 +495,19 @@ def test_get_engine_method_with_psycopg(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_engine_method_without_autocommit(self, mock_get_settings): """Test the _get_engine method without autocommit""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = None - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = None - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = None - mock_settings.connection_max_retries = None - mock_settings.connection_initial_retry_delay = None - mock_settings.connection_max_retry_delay = None - mock_settings.operation_enable_retry = None - mock_settings.operation_max_retries = None - mock_settings.operation_initial_retry_delay = None - mock_settings.operation_max_retry_delay = None - mock_settings.operation_jitter = None + mock_settings = _make_pg_mock_settings( + schema=None, + ssl_mode=None, + connection_enable_retry=None, + connection_max_retries=None, + connection_initial_retry_delay=None, + connection_max_retry_delay=None, + operation_enable_retry=None, + operation_max_retries=None, + operation_initial_retry_delay=None, + operation_max_retry_delay=None, + operation_jitter=None, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=False)) @@ -648,36 +523,19 @@ def test_get_engine_method_without_autocommit(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_engine_method_non_psycopg_driver(self, mock_get_settings): """Test the _get_engine method with non-psycopg driver""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = None - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = None - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = None - mock_settings.connection_max_retries = None - mock_settings.connection_initial_retry_delay = None - mock_settings.connection_max_retry_delay = None - mock_settings.operation_enable_retry = None - mock_settings.operation_max_retries = None - mock_settings.operation_initial_retry_delay = None - mock_settings.operation_max_retry_delay = None - mock_settings.operation_jitter = None + mock_settings = _make_pg_mock_settings( + schema=None, + ssl_mode=None, + connection_enable_retry=None, + connection_max_retries=None, + connection_initial_retry_delay=None, + connection_max_retry_delay=None, + operation_enable_retry=None, + operation_max_retries=None, + operation_initial_retry_delay=None, + operation_max_retry_delay=None, + operation_jitter=None, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -693,22 +551,7 @@ def test_get_engine_method_non_psycopg_driver(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_async_engine_method_with_asyncpg(self, mock_get_settings): """Test the _get_async_engine method with asyncpg driver""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 45 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings(connection_timeout=45) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL( @@ -734,22 +577,7 @@ async def test_async(): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_async_engine_method_without_autocommit(self, mock_get_settings): """Test the _get_async_engine method without autocommit""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(autocommit=False)) @@ -770,22 +598,7 @@ async def test_async(): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_get_async_engine_method_non_asyncpg_driver(self, mock_get_settings): """Test the _get_async_engine method with non-asyncpg driver""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" # Use asyncpg (available driver) + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -806,22 +619,12 @@ async def test_async(): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_repr_method(self, mock_get_settings): """Test the enhanced __repr__ method""" - mock_settings = MagicMock() - mock_settings.user = "testuser" - mock_settings.password = "testpass" - mock_settings.host = "testhost" - mock_settings.port = 5432 - mock_settings.database = "testdb" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings( + user="testuser", + password="testpass", + host="testhost", + database="testdb", + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL( @@ -846,22 +649,7 @@ def test_repr_method(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_configuration_immutability(self, mock_get_settings): """Test that configuration objects are properly immutable""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -884,27 +672,7 @@ def test_configuration_immutability(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_schema_default(self, mock_get_settings): """Test PostgreSQL schema defaults to public""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -916,27 +684,7 @@ def test_schema_default(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_schema_custom(self, mock_get_settings): """Test PostgreSQL with custom schema sets search_path""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(schema="custom_schema") @@ -961,27 +709,7 @@ def test_schema_custom(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_schema_public_no_options(self, mock_get_settings): """Test PostgreSQL with public schema does not set search_path options""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(schema="public") @@ -1000,27 +728,7 @@ def test_schema_public_no_options(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_ssl_enabled_with_options(self, mock_get_settings): """Test PostgreSQL SSL configuration""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL( @@ -1057,27 +765,7 @@ def test_ssl_enabled_with_options(self, mock_get_settings): @patch("ddcDatabases.postgresql.get_postgresql_settings") def test_ssl_disabled_no_connect_args(self, mock_get_settings): """Test PostgreSQL with SSL disabled does not add SSL connect_args""" - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None + mock_settings = _make_pg_mock_settings() mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -1096,36 +784,10 @@ def test_get_connection_info(self, mock_get_settings): """Test get_connection_info returns connection config""" from ddcDatabases.postgresql import PostgreSQL, PostgreSQLConnectionConfig - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = True - mock_settings.connection_max_retries = 3 - mock_settings.connection_initial_retry_delay = 1.0 - mock_settings.connection_max_retry_delay = 30.0 - mock_settings.operation_enable_retry = True - mock_settings.operation_max_retries = 3 - mock_settings.operation_initial_retry_delay = 1.0 - mock_settings.operation_max_retry_delay = 30.0 - mock_settings.operation_jitter = 0.1 + mock_settings = _make_pg_mock_settings( + operation_initial_retry_delay=1.0, + operation_max_retry_delay=30.0, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() @@ -1142,36 +804,10 @@ def test_get_pool_info(self, mock_get_settings): """Test get_pool_info returns pool config""" from ddcDatabases.postgresql import PostgreSQL, PostgreSQLPoolConfig - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = True - mock_settings.connection_max_retries = 3 - mock_settings.connection_initial_retry_delay = 1.0 - mock_settings.connection_max_retry_delay = 30.0 - mock_settings.operation_enable_retry = True - mock_settings.operation_max_retries = 3 - mock_settings.operation_initial_retry_delay = 1.0 - mock_settings.operation_max_retry_delay = 30.0 - mock_settings.operation_jitter = 0.1 + mock_settings = _make_pg_mock_settings( + operation_initial_retry_delay=1.0, + operation_max_retry_delay=30.0, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(pool_config=PostgreSQLPoolConfig(pool_size=20, max_overflow=40)) @@ -1188,36 +824,10 @@ def test_get_session_info(self, mock_get_settings): """Test get_session_info returns session config""" from ddcDatabases.postgresql import PostgreSQL, PostgreSQLSessionConfig - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = True - mock_settings.connection_max_retries = 3 - mock_settings.connection_initial_retry_delay = 1.0 - mock_settings.connection_max_retry_delay = 30.0 - mock_settings.operation_enable_retry = True - mock_settings.operation_max_retries = 3 - mock_settings.operation_initial_retry_delay = 1.0 - mock_settings.operation_max_retry_delay = 30.0 - mock_settings.operation_jitter = 0.1 + mock_settings = _make_pg_mock_settings( + operation_initial_retry_delay=1.0, + operation_max_retry_delay=30.0, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL(session_config=PostgreSQLSessionConfig(echo=True, autoflush=False)) @@ -1234,36 +844,10 @@ def test_get_operation_retry_info(self, mock_get_settings): """Test get_operation_retry_info returns operation retry config""" from ddcDatabases.postgresql import PostgreSQL - mock_settings = MagicMock() - mock_settings.user = "postgres" - mock_settings.password = "password" - mock_settings.host = "localhost" - mock_settings.port = 5432 - mock_settings.database = "postgres" - mock_settings.schema = "public" - mock_settings.echo = False - mock_settings.autoflush = False - mock_settings.expire_on_commit = False - mock_settings.autocommit = False - mock_settings.connection_timeout = 30 - mock_settings.pool_recycle = 3600 - mock_settings.pool_size = 25 - mock_settings.max_overflow = 50 - mock_settings.sync_driver = "postgresql+psycopg" - mock_settings.async_driver = "postgresql+asyncpg" - mock_settings.ssl_mode = "disable" - mock_settings.ssl_ca_cert_path = None - mock_settings.ssl_client_cert_path = None - mock_settings.ssl_client_key_path = None - mock_settings.connection_enable_retry = True - mock_settings.connection_max_retries = 3 - mock_settings.connection_initial_retry_delay = 1.0 - mock_settings.connection_max_retry_delay = 30.0 - mock_settings.operation_enable_retry = True - mock_settings.operation_max_retries = 3 - mock_settings.operation_initial_retry_delay = 1.0 - mock_settings.operation_max_retry_delay = 30.0 - mock_settings.operation_jitter = 0.1 + mock_settings = _make_pg_mock_settings( + operation_initial_retry_delay=1.0, + operation_max_retry_delay=30.0, + ) mock_get_settings.return_value = mock_settings postgresql = PostgreSQL() diff --git a/uv.lock b/uv.lock index a37f2b9..e8ba680 100644 --- a/uv.lock +++ b/uv.lock @@ -359,101 +359,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.3" +version = "7.13.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, - { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, - { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, - { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, - { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, - { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, - { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, - { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" }, - { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" }, - { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" }, - { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" }, - { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" }, - { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" }, - { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, - { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, - { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" }, - { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" }, - { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" }, - { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" }, - { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" }, - { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" }, - { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" }, - { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" }, - { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" }, - { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" }, - { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" }, - { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" }, - { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" }, - { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" }, - { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" }, - { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" }, - { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" }, - { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" }, - { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" }, - { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" }, - { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" }, - { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" }, - { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" }, - { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" }, - { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" }, - { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" }, - { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" }, - { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" }, - { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] @@ -578,7 +592,7 @@ provides-extras = ["mongodb", "mssql", "mysql", "oracle", "pgsql"] [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=26.1.0" }, - { name = "poethepoet", specifier = ">=0.40.0" }, + { name = "poethepoet", specifier = ">=0.41.0" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.15.0" }, @@ -816,16 +830,16 @@ wheels = [ [[package]] name = "poethepoet" -version = "0.40.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/9d/054c8435b03324ed9abd5d5ab8c45065b1f42c23952cd23f13a5921d8465/poethepoet-0.40.0.tar.gz", hash = "sha256:91835f00d03d6c4f0e146f80fa510e298ad865e7edd27fe4cb9c94fdc090791b", size = 81114, upload-time = "2026-01-05T19:09:13.116Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/b9/fa92286560f70eaa40d473ea48376d20c6c21f63627d33c6bb1c5e385175/poethepoet-0.41.0.tar.gz", hash = "sha256:dcaad621dc061f6a90b17d091bebb9ca043d67bfe9bd6aa4185aea3ebf7ff3e6", size = 87780, upload-time = "2026-02-08T20:45:36.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/bc/73327d12b176abea7a3c6c7d760e1a953992f7b59d72c0354e39d7a353b5/poethepoet-0.40.0-py3-none-any.whl", hash = "sha256:afd276ae31d5c53573c0c14898118d4848ccee3709b6b0be6a1c6cbe522bbc8a", size = 106672, upload-time = "2026-01-05T19:09:11.536Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/0b83e0222ce5921b3f9081eeca8c6fb3e1cfd5ca0d06338adf93b28ce061/poethepoet-0.41.0-py3-none-any.whl", hash = "sha256:4bab9fd8271664c5d21407e8f12827daeb6aa484dc6cc7620f0c3b4e62b42ee4", size = 113590, upload-time = "2026-02-08T20:45:34.697Z" }, ] [[package]]