From f65489b5d82e549c6b201c95cc05049d14d64498 Mon Sep 17 00:00:00 2001 From: Ilyas Gasanov Date: Wed, 15 Jan 2025 09:48:16 +0300 Subject: [PATCH] [DOP-22348] Add transformations for Transfers with dataframe row filtering --- docs/changelog/next_release/184.feature.rst | 1 + poetry.lock | 240 +++++++++++++++++- pyproject.toml | 1 + .../2023-11-23_0007_create_transfer_table.py | 1 + syncmaster/db/models/transfer.py | 1 + syncmaster/db/repositories/transfer.py | 22 +- syncmaster/dto/transfers.py | 2 + syncmaster/schemas/v1/connections/oracle.py | 4 +- syncmaster/schemas/v1/transfers/__init__.py | 18 +- .../v1/transfers/transformations/__init__.py | 2 + .../transformations/dataframe_rows_filter.py | 96 +++++++ syncmaster/schemas/v1/transformation_types.py | 5 + syncmaster/server/api/v1/transfers.py | 4 + syncmaster/worker/controller.py | 5 +- syncmaster/worker/handlers/db/base.py | 31 ++- syncmaster/worker/handlers/db/clickhouse.py | 19 +- syncmaster/worker/handlers/db/hive.py | 26 +- syncmaster/worker/handlers/db/mssql.py | 28 +- syncmaster/worker/handlers/db/mysql.py | 26 +- syncmaster/worker/handlers/db/oracle.py | 28 +- syncmaster/worker/handlers/db/postgres.py | 17 +- syncmaster/worker/handlers/file/base.py | 42 ++- syncmaster/worker/handlers/file/s3.py | 7 +- .../test_run_transfer/conftest.py | 88 ++++++- .../test_run_transfer/test_clickhouse.py | 37 ++- .../test_run_transfer/test_hive.py | 27 +- .../test_run_transfer/test_mssql.py | 67 ++++- .../test_run_transfer/test_mysql.py | 27 +- .../test_run_transfer/test_oracle.py | 27 +- .../test_run_transfer/test_s3.py | 35 ++- .../scheduler_fixtures/transfer_fixture.py | 5 +- .../test_transfers/test_create_transfer.py | 128 +++++++++- .../test_create_transfer.py | 2 + .../test_file_transfers/test_read_transfer.py | 106 +++++--- .../test_update_transfer.py | 122 ++++++--- .../test_transfers/test_read_transfer.py | 2 + .../test_transfers/test_read_transfers.py | 5 + .../test_transfers/test_update_transfer.py | 3 + .../transfer_fixtures/transfer_fixture.py | 5 +- .../transfer_with_user_role_fixtures.py | 5 +- tests/test_unit/utils.py | 2 + 41 files changed, 1187 insertions(+), 132 deletions(-) create mode 100644 docs/changelog/next_release/184.feature.rst create mode 100644 syncmaster/schemas/v1/transfers/transformations/__init__.py create mode 100644 syncmaster/schemas/v1/transfers/transformations/dataframe_rows_filter.py create mode 100644 syncmaster/schemas/v1/transformation_types.py diff --git a/docs/changelog/next_release/184.feature.rst b/docs/changelog/next_release/184.feature.rst new file mode 100644 index 00000000..3ffebc37 --- /dev/null +++ b/docs/changelog/next_release/184.feature.rst @@ -0,0 +1 @@ +Add transformations for **Transfers** with dataframe row filtering \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index aca83835..2f0999fe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -6,6 +6,7 @@ version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, @@ -17,6 +18,8 @@ version = "1.14.0" description = "A database migration tool for SQLAlchemy." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"}, {file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"}, @@ -36,6 +39,8 @@ version = "5.3.1" description = "Low-level AMQP client for Python (fork of amqplib)." optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, @@ -50,6 +55,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main", "docs", "test"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -61,6 +67,7 @@ version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["main", "test"] files = [ {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, @@ -82,6 +89,8 @@ version = "3.11.0" description = "In-process task scheduler with Cron-like capabilities" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"scheduler\"" files = [ {file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"}, {file = "apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133"}, @@ -109,6 +118,7 @@ version = "23.1.0" description = "Argon2 for Python" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, @@ -129,6 +139,7 @@ version = "21.2.0" description = "Low-level CFFI bindings for Argon2" optional = false python-versions = ">=3.6" +groups = ["test"] files = [ {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, @@ -166,6 +177,8 @@ version = "4.3.4" description = "Middleware correlating project logs to individual requests" optional = true python-versions = "<4.0,>=3.8" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\"" files = [ {file = "asgi_correlation_id-4.3.4-py3-none-any.whl", hash = "sha256:36ce69b06c7d96b4acb89c7556a4c4f01a972463d3d49c675026cbbd08e9a0a2"}, {file = "asgi_correlation_id-4.3.4.tar.gz", hash = "sha256:ea6bc310380373cb9f731dc2e8b2b6fb978a76afe33f7a2384f697b8d6cd811d"}, @@ -184,6 +197,8 @@ version = "0.2.2" description = "Python decorator for async properties." optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7"}, {file = "async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380"}, @@ -195,6 +210,8 @@ version = "0.30.0" description = "An asyncio PostgreSQL driver" optional = true python-versions = ">=3.8.0" +groups = ["main"] +markers = "extra == \"server\" or extra == \"scheduler\"" files = [ {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, @@ -258,6 +275,7 @@ version = "2.2.0" description = "Seamlessly integrate pydantic models in your Sphinx documentation." optional = false python-versions = "<4.0.0,>=3.8.1" +groups = ["docs"] files = [ {file = "autodoc_pydantic-2.2.0-py3-none-any.whl", hash = "sha256:8c6a36fbf6ed2700ea9c6d21ea76ad541b621fbdf16b5a80ee04673548af4d95"}, ] @@ -281,6 +299,7 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, @@ -295,6 +314,7 @@ version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" +groups = ["docs"] files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, @@ -316,10 +336,12 @@ version = "0.23.1" description = "The bidirectional mapping library for Python." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, ] +markers = {main = "extra == \"worker\""} [[package]] name = "billiard" @@ -327,6 +349,8 @@ version = "4.2.1" description = "Python multiprocessing fork with improvements and bugfixes" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, @@ -338,6 +362,7 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -382,6 +407,8 @@ version = "5.4.0" description = "Distributed Task Queue." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, @@ -438,6 +465,7 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "docs", "test"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -449,6 +477,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -518,6 +547,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "extra == \"server\" and platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" @@ -528,6 +558,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -539,6 +570,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main", "docs", "test"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -633,6 +665,7 @@ files = [ {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] +markers = {main = "extra == \"server\""} [[package]] name = "click" @@ -640,10 +673,12 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "docs"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -654,6 +689,8 @@ version = "0.3.1" description = "Enables git-like *did-you-mean* feature in click" optional = true python-versions = ">=3.6.2" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, @@ -668,6 +705,8 @@ version = "1.1.1" description = "An extension module for click to enable registering CLI commands via setuptools entry-points." optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, @@ -685,6 +724,8 @@ version = "0.3.0" description = "REPL plugin for Click" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, @@ -703,10 +744,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "docs", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "extra == \"server\" and platform_system == \"Windows\" or extra == \"worker\" and platform_system == \"Windows\" or extra == \"scheduler\" and platform_system == \"Windows\"", dev = "platform_system == \"Windows\"", docs = "platform_system == \"Windows\" or sys_platform == \"win32\"", test = "sys_platform == \"win32\""} [[package]] name = "coloredlogs" @@ -714,6 +757,8 @@ version = "15.0.1" description = "Colored terminal output for Python's logging module" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, @@ -731,6 +776,7 @@ version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, @@ -805,6 +851,8 @@ version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = true python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, @@ -812,6 +860,7 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -822,6 +871,7 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -854,6 +904,8 @@ version = "2.1.0" description = "A library to handle automated deprecations" optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, @@ -868,6 +920,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -879,6 +932,7 @@ version = "0.6.2" description = "Pythonic argument parser, that will make you smile" optional = false python-versions = "*" +groups = ["test"] files = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -889,6 +943,7 @@ version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -900,10 +955,12 @@ version = "2.4.0" description = "ETL Entities lib for onETL" optional = false python-versions = ">=3.7" +groups = ["main", "test"] files = [ {file = "etl_entities-2.4.0-py3-none-any.whl", hash = "sha256:44fcbeb790003124cc1fa7ddd226fadbd979f737995519d5fc6d5a5d8e634b29"}, {file = "etl_entities-2.4.0.tar.gz", hash = "sha256:7bbf28a0d2ad2bff4fac954486f2afeda88e3171e37e1e0e7de18e40c797db93"}, ] +markers = {main = "extra == \"worker\""} [package.dependencies] bidict = "*" @@ -919,10 +976,12 @@ version = "1.0.4" description = "Catch an exception and exit with an exit code" optional = false python-versions = ">=3.7" +groups = ["main", "test"] files = [ {file = "evacuator-1.0.4-py3-none-any.whl", hash = "sha256:0c3a5fafa2c41fba1272e18bead116954cd3ac7bf97c3050fa77f516def88c1b"}, {file = "evacuator-1.0.4.tar.gz", hash = "sha256:4fac38ee4241e826fced8115ab7cdc8ca2fd18e2e200083405c4a802e836e926"}, ] +markers = {main = "extra == \"worker\""} [[package]] name = "faker" @@ -930,6 +989,7 @@ version = "33.3.1" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "Faker-33.3.1-py3-none-any.whl", hash = "sha256:ac4cf2f967ce02c898efa50651c43180bd658a7707cfd676fcc5410ad1482c03"}, {file = "faker-33.3.1.tar.gz", hash = "sha256:49dde3b06a5602177bc2ad013149b6f60a290b7154539180d37b6f876ae79b20"}, @@ -945,6 +1005,8 @@ version = "0.115.6" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, @@ -965,6 +1027,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -981,6 +1044,7 @@ version = "7.1.1" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, @@ -997,6 +1061,7 @@ version = "1.2.3" description = "Flake8 plug-in loading the configuration from pyproject.toml" optional = false python-versions = ">= 3.6" +groups = ["dev"] files = [ {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, ] @@ -1013,6 +1078,7 @@ version = "2.4.6" description = "A simple immutable dictionary" optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ {file = "frozendict-2.4.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3a05c0a50cab96b4bb0ea25aa752efbfceed5ccb24c007612bc63e51299336f"}, {file = "frozendict-2.4.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5b94d5b07c00986f9e37a38dd83c13f5fe3bf3f1ccc8e88edea8fe15d6cd88c"}, @@ -1054,6 +1120,7 @@ files = [ {file = "frozendict-2.4.6-py313-none-any.whl", hash = "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757"}, {file = "frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e"}, ] +markers = {main = "extra == \"worker\""} [[package]] name = "furo" @@ -1061,6 +1128,7 @@ version = "2024.8.6" description = "A clean customisable Sphinx documentation theme." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, @@ -1078,6 +1146,7 @@ version = "24.11.1" description = "Coroutine-based network library" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "gevent-24.11.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:92fe5dfee4e671c74ffaa431fd7ffd0ebb4b339363d24d0d944de532409b935e"}, {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7bfcfe08d038e1fa6de458891bca65c1ada6d145474274285822896a858c870"}, @@ -1138,6 +1207,7 @@ version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "test"] files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -1213,6 +1283,7 @@ files = [ {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, ] +markers = {main = "extra == \"server\" and python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"worker\" and python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"scheduler\" and python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")", dev = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")", test = "platform_python_implementation == \"CPython\""} [package.extras] docs = ["Sphinx", "furo"] @@ -1224,6 +1295,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main", "test"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -1235,6 +1307,7 @@ version = "2.7.3" description = "HdfsCLI: API and command line interface for HDFS." optional = false python-versions = "*" +groups = ["test"] files = [ {file = "hdfs-2.7.3.tar.gz", hash = "sha256:752a21e43f82197dce43697c73f454ba490838108c73a57a9247efb66d1c0479"}, ] @@ -1255,6 +1328,7 @@ version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -1276,6 +1350,7 @@ version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, @@ -1301,6 +1376,8 @@ version = "10.0" description = "Human friendly output for text interfaces using Python" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, @@ -1315,10 +1392,12 @@ version = "4.11.0" description = "Python humanize utilities" optional = false python-versions = ">=3.9" +groups = ["main", "test"] files = [ {file = "humanize-4.11.0-py3-none-any.whl", hash = "sha256:b53caaec8532bcb2fff70c8826f904c35943f8cecaca29d272d9df38092736c0"}, {file = "humanize-4.11.0.tar.gz", hash = "sha256:e66f36020a2d5a974c504bd2555cf770621dbdbb6d82f94a6857c0b1ea2608be"}, ] +markers = {main = "extra == \"worker\""} [package.extras] tests = ["freezegun", "pytest", "pytest-cov"] @@ -1329,6 +1408,7 @@ version = "2.6.5" description = "File identification library for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566"}, {file = "identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc"}, @@ -1343,6 +1423,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "docs", "test"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1357,6 +1438,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["docs"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -1368,10 +1450,12 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] +markers = {main = "extra == \"worker\""} [package.dependencies] zipp = ">=3.20" @@ -1391,6 +1475,7 @@ version = "24.7.2" description = "A small library that versions your Python projects." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, @@ -1408,6 +1493,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -1419,6 +1505,8 @@ version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -1430,10 +1518,12 @@ version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main", "docs"] files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\""} [package.dependencies] MarkupSafe = ">=2.0" @@ -1447,6 +1537,8 @@ version = "1.5.6" description = "Implementation of JOSE Web standards" optional = true python-versions = ">= 3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"}, {file = "jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039"}, @@ -1462,6 +1554,8 @@ version = "5.4.2" description = "Messaging library for Python." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, @@ -1495,6 +1589,8 @@ version = "1.3.8" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, @@ -1514,6 +1610,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main", "docs"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1577,6 +1674,7 @@ files = [ {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\""} [[package]] name = "mccabe" @@ -1584,6 +1682,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1595,6 +1694,7 @@ version = "7.2.14" description = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "minio-7.2.14-py3-none-any.whl", hash = "sha256:868dfe907e1702ce4bec86df1f3ced577a73ca85f344ef898d94fe2b5237f8c1"}, {file = "minio-7.2.14.tar.gz", hash = "sha256:f5c24bf236fefd2edc567cd4455dc49a11ad8ff7ac984bb031b849d82f01222a"}, @@ -1613,6 +1713,7 @@ version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -1671,6 +1772,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -1682,6 +1784,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1693,6 +1796,7 @@ version = "2.2.1" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" +groups = ["test"] files = [ {file = "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440"}, {file = "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab"}, @@ -1757,6 +1861,7 @@ version = "1.8.0" description = "Sphinx extension to support docstrings in Numpy format" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "numpydoc-1.8.0-py3-none-any.whl", hash = "sha256:72024c7fd5e17375dec3608a27c03303e8ad00c81292667955c6fea7a3ccf541"}, {file = "numpydoc-1.8.0.tar.gz", hash = "sha256:022390ab7464a44f8737f79f8b31ce1d3cfa4b4af79ccaa1aac5e8368db587fb"}, @@ -1777,10 +1882,12 @@ version = "0.12.5" description = "One ETL tool to rule them all" optional = false python-versions = ">=3.7" +groups = ["main", "test"] files = [ {file = "onetl-0.12.5-py3-none-any.whl", hash = "sha256:395868cc3fbb751056b951b4fec26ef754ad53e55e2a3ca1f7894102869efbc5"}, {file = "onetl-0.12.5.tar.gz", hash = "sha256:02447817d24c5bff7b9872ebd4deec1a6fe13f01821de9569661b1d70a06fc79"}, ] +markers = {main = "extra == \"worker\""} [package.dependencies] etl-entities = ">=2.2,<2.5" @@ -1816,10 +1923,12 @@ version = "4.1.0" description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" optional = false python-versions = ">=3.7" +groups = ["main", "test"] files = [ {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, ] +markers = {main = "extra == \"worker\""} [package.extras] dev = ["black", "mypy", "pytest"] @@ -1830,10 +1939,12 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs", "test"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\""} [[package]] name = "pandas-stubs" @@ -1841,6 +1952,7 @@ version = "2.2.3.241126" description = "Type annotations for pandas" optional = false python-versions = ">=3.10" +groups = ["test"] files = [ {file = "pandas_stubs-2.2.3.241126-py3-none-any.whl", hash = "sha256:74aa79c167af374fe97068acc90776c0ebec5266a6e5c69fe11e9c2cf51f2267"}, {file = "pandas_stubs-2.2.3.241126.tar.gz", hash = "sha256:cf819383c6d9ae7d4dabf34cd47e1e45525bb2f312e6ad2939c2c204cb708acd"}, @@ -1856,6 +1968,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1867,10 +1980,12 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "test"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] +markers = {main = "extra == \"worker\""} [package.extras] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] @@ -1883,6 +1998,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1898,6 +2014,7 @@ version = "4.0.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, @@ -1916,6 +2033,8 @@ version = "0.21.1" description = "Python client for the Prometheus monitoring system." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, @@ -1930,6 +2049,8 @@ version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = true python-versions = ">=3.7.0" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, @@ -1944,6 +2065,7 @@ version = "6.1.1" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main", "test"] files = [ {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, @@ -1963,6 +2085,7 @@ files = [ {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"}, {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"}, ] +markers = {main = "extra == \"worker\""} [package.extras] dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] @@ -1974,6 +2097,8 @@ version = "3.2.3" description = "PostgreSQL database adapter for Python" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\"" files = [ {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, @@ -1998,6 +2123,8 @@ version = "3.2.3" description = "PostgreSQL database adapter for Python -- C optimisation distribution" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" and implementation_name != \"pypy\" or extra == \"worker\" and implementation_name != \"pypy\"" files = [ {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"}, {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"}, @@ -2071,10 +2198,12 @@ version = "0.10.9.7" description = "Enables Python programs to dynamically access arbitrary Java objects" optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "py4j-0.10.9.7-py2.py3-none-any.whl", hash = "sha256:85defdfd2b2376eb3abf5ca6474b51ab7e0de341c75a02f46dc9b5976f5a5c1b"}, {file = "py4j-0.10.9.7.tar.gz", hash = "sha256:0b6e5315bb3ada5cf62ac651d107bb2ebc02def3dee9d9548e3baac644ea8dbb"}, ] +markers = {main = "extra == \"worker\""} [[package]] name = "pycodestyle" @@ -2082,6 +2211,7 @@ version = "2.12.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, @@ -2093,10 +2223,12 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "extra == \"server\" and platform_python_implementation != \"PyPy\""} [[package]] name = "pycryptodome" @@ -2104,6 +2236,7 @@ version = "3.21.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["test"] files = [ {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, @@ -2145,6 +2278,7 @@ version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main", "docs", "test"] files = [ {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, @@ -2165,6 +2299,7 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main", "docs", "test"] files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -2277,10 +2412,12 @@ version = "2.7.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"}, {file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.dependencies] pydantic = ">=2.7.0" @@ -2297,6 +2434,7 @@ version = "3.2.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, @@ -2308,6 +2446,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -2322,6 +2461,8 @@ version = "2.10.1" description = "JSON Web Token implementation in Python" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, @@ -2339,6 +2480,8 @@ version = "3.5.4" description = "A python implementation of GNU readline." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" and sys_platform == \"win32\" or extra == \"worker\" and sys_platform == \"win32\" or extra == \"scheduler\" and sys_platform == \"win32\"" files = [ {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, @@ -2353,9 +2496,11 @@ version = "3.5.4" description = "Apache Spark Python API" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "pyspark-3.5.4.tar.gz", hash = "sha256:1c2926d63020902163f58222466adf6f8016f6c43c1f319b8e7a71dbaa05fc51"}, ] +markers = {main = "extra == \"worker\""} [package.dependencies] py4j = "0.10.9.7" @@ -2373,6 +2518,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -2393,6 +2539,7 @@ version = "0.25.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, @@ -2411,6 +2558,7 @@ version = "2.2.1" description = "A simple plugin to list unused fixtures in pytest" optional = false python-versions = "*" +groups = ["test"] files = [ {file = "pytest-deadfixtures-2.2.1.tar.gz", hash = "sha256:ca15938a4e8330993ccec9c6c847383d88b3cd574729530647dc6b492daa9c1e"}, {file = "pytest_deadfixtures-2.2.1-py2.py3-none-any.whl", hash = "sha256:db71533f2d9456227084e00a1231e732973e299ccb7c37ab92e95032ab6c083e"}, @@ -2419,12 +2567,28 @@ files = [ [package.dependencies] pytest = ">=3.0.0" +[[package]] +name = "pytest-lazy-fixtures" +version = "1.1.1" +description = "Allows you to use fixtures in @pytest.mark.parametrize." +optional = false +python-versions = "<4.0,>=3.8" +groups = ["test"] +files = [ + {file = "pytest_lazy_fixtures-1.1.1-py3-none-any.whl", hash = "sha256:a4b396a361faf56c6305535fd0175ce82902ca7cf668c4d812a25ed2bcde8183"}, + {file = "pytest_lazy_fixtures-1.1.1.tar.gz", hash = "sha256:0c561f0d29eea5b55cf29b9264a3241999ffdb74c6b6e8c4ccc0bd2c934d01ed"}, +] + +[package.dependencies] +pytest = ">=7" + [[package]] name = "pytest-mock" version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, @@ -2442,6 +2606,7 @@ version = "3.16.0" description = "Pytest plugin to randomly order tests and control random.seed." optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"}, {file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"}, @@ -2456,10 +2621,12 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.dependencies] six = ">=1.5" @@ -2470,10 +2637,12 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.extras] cli = ["click (>=5.0)"] @@ -2484,6 +2653,8 @@ version = "3.2.1" description = "JSON Log Formatter for the Python Logging Package" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090"}, {file = "python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008"}, @@ -2498,6 +2669,8 @@ version = "5.1.1" description = "python-keycloak is a Python package providing access to the Keycloak API." optional = true python-versions = "<4.0,>=3.9" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "python_keycloak-5.1.1-py3-none-any.whl", hash = "sha256:0df2ae75c80cc0646dc2a57f353f284edcbde3ca77e15526f403e3b38dfcffca"}, {file = "python_keycloak-5.1.1.tar.gz", hash = "sha256:406c5ad621c0fc911aacb8665b4eec0aebca7e88546ca6d313ebbe966080604b"}, @@ -2517,6 +2690,8 @@ version = "0.0.20" description = "A streaming multipart parser for Python" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, @@ -2528,6 +2703,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "test"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -2583,6 +2759,7 @@ files = [ {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [[package]] name = "requests" @@ -2590,10 +2767,12 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "docs", "test"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] +markers = {main = "extra == \"server\""} [package.dependencies] certifi = ">=2017.4.17" @@ -2611,6 +2790,8 @@ version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -2625,6 +2806,7 @@ version = "0.25.5" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "responses-0.25.5-py3-none-any.whl", hash = "sha256:b3e1ae252f69301b84146ff615a869a4182fbe17e8b606f1ac54142515dad5eb"}, {file = "responses-0.25.5.tar.gz", hash = "sha256:e53991613f76d17ba293c1e3cce8af107c5c7a6a513edf25195aafd89a870dd3"}, @@ -2644,6 +2826,7 @@ version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" +groups = ["docs", "test"] files = [ {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, @@ -2664,10 +2847,12 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [[package]] name = "sniffio" @@ -2675,6 +2860,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main", "test"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2686,6 +2872,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -2697,6 +2884,7 @@ version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, @@ -2708,6 +2896,7 @@ version = "7.4.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -2742,6 +2931,7 @@ version = "0.5.2" description = "A sphinx extension that automatically documents argparse commands and options" optional = false python-versions = ">=3.10" +groups = ["docs"] files = [ {file = "sphinx_argparse-0.5.2-py3-none-any.whl", hash = "sha256:d771b906c36d26dee669dbdbb5605c558d9440247a5608b810f7fa6e26ab1fd3"}, {file = "sphinx_argparse-0.5.2.tar.gz", hash = "sha256:e5352f8fa894b6fb6fda0498ba28a9f8d435971ef4bbc1a6c9c6414e7644f032"}, @@ -2763,6 +2953,7 @@ version = "1.0.0b2" description = "A modern skeleton for Sphinx themes." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, @@ -2780,6 +2971,7 @@ version = "0.5.2" description = "Add a copy button to each of your code cells." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, @@ -2798,6 +2990,7 @@ version = "0.6.1" description = "A sphinx extension for designing beautiful, view size responsive web components." optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c"}, {file = "sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632"}, @@ -2823,6 +3016,7 @@ version = "1.0.1" description = "Sphinx Extension adding support for custom favicons" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx-favicon-1.0.1.tar.gz", hash = "sha256:df796de32125609c1b4a8964db74270ebf4502089c27cd53f542354dc0b57e8e"}, {file = "sphinx_favicon-1.0.1-py3-none-any.whl", hash = "sha256:7c93d6b634cb4c9687ceab67a8526f05d3b02679df94e273e51a43282e6b034c"}, @@ -2842,6 +3036,7 @@ version = "5.0.0" description = "A Sphinx extension for linking to your project's issue tracker" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinx_issues-5.0.0-py3-none-any.whl", hash = "sha256:d80704a01c8af3d76586771a67a9e48f2d1a6091a0377458c49908460a6a31ea"}, {file = "sphinx_issues-5.0.0.tar.gz", hash = "sha256:192e43cf071ed7aead401cd14fd15076ecb0866238c095d672180618740c6bae"}, @@ -2860,6 +3055,7 @@ version = "0.3.8" description = "Get the \"last updated\" time for each Sphinx page from Git" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx_last_updated_by_git-0.3.8-py3-none-any.whl", hash = "sha256:6382c8285ac1f222483a58569b78c0371af5e55f7fbf9c01e5e8a72d6fdfa499"}, {file = "sphinx_last_updated_by_git-0.3.8.tar.gz", hash = "sha256:c145011f4609d841805b69a9300099fc02fed8f5bb9e5bcef77d97aea97b7761"}, @@ -2874,6 +3070,7 @@ version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -2890,6 +3087,7 @@ version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -2906,6 +3104,7 @@ version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -2922,6 +3121,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["docs"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -2936,6 +3136,7 @@ version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -2952,6 +3153,7 @@ version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -2968,6 +3170,7 @@ version = "0.4.0a0" description = "An RST directive for injecting a Towncrier-generated changelog draft containing fragments for the unreleased (next) project version" optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "sphinxcontrib-towncrier-0.4.0a0.tar.gz", hash = "sha256:d9b1513fc07781432dd3a0b2ca797cfe0e99e9b5bc5e5c8bf112d5d142afb6dc"}, {file = "sphinxcontrib_towncrier-0.4.0a0-py3-none-any.whl", hash = "sha256:ec734e3d0920e2ce26e99681119f398a9e1fc0aa6c2d7ed1f052f1219dcd4653"}, @@ -2983,6 +3186,7 @@ version = "2.0.37" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, @@ -3042,6 +3246,7 @@ files = [ {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"}, {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"}, ] +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.dependencies] greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} @@ -3079,6 +3284,8 @@ version = "0.41.2" description = "Various utility functions for SQLAlchemy." optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"}, @@ -3107,6 +3314,8 @@ version = "0.41.3" description = "The little ASGI library that shines." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\"" files = [ {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, @@ -3124,6 +3333,8 @@ version = "0.23.0" description = "Prometheus metrics exporter for Starlette applications." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "starlette_exporter-0.23.0-py3-none-any.whl", hash = "sha256:ea1a27f2aae48122931e2384a361a03e00261efbb4a665ce1ae2e46f29123d5e"}, {file = "starlette_exporter-0.23.0.tar.gz", hash = "sha256:f80998db2d4a3462808a9bce56950046b113d3fab6ec6c20cb6de4431d974969"}, @@ -3139,6 +3350,7 @@ version = "0.9.0" description = "Pretty-print tabular data" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -3153,6 +3365,7 @@ version = "23.11.0" description = "Building newsfiles for your project." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "towncrier-23.11.0-py3-none-any.whl", hash = "sha256:2e519ca619426d189e3c98c99558fe8be50c9ced13ea1fc20a4a353a95d2ded7"}, {file = "towncrier-23.11.0.tar.gz", hash = "sha256:13937c247e3f8ae20ac44d895cf5f96a60ad46cfdcc1671759530d7837d9ee5d"}, @@ -3172,6 +3385,7 @@ version = "2024.2.0.20241221" description = "Typing stubs for pytz" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "types_pytz-2024.2.0.20241221-py3-none-any.whl", hash = "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5"}, {file = "types_pytz-2024.2.0.20241221.tar.gz", hash = "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9"}, @@ -3183,6 +3397,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs", "test"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -3194,6 +3409,8 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = true python-versions = ">=2" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -3205,6 +3422,8 @@ version = "5.2" description = "tzinfo object for the local timezone" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"scheduler\"" files = [ {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, @@ -3222,10 +3441,12 @@ version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main", "docs", "test"] files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] +markers = {main = "extra == \"server\""} [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] @@ -3239,6 +3460,8 @@ version = "2024.7.10" description = "New time-based UUID formats which are suited for use as a database key" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\"" files = [ {file = "uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7"}, {file = "uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0"}, @@ -3250,6 +3473,8 @@ version = "0.34.0" description = "The lightning-fast ASGI server." optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"server\"" files = [ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, @@ -3268,6 +3493,8 @@ version = "5.1.0" description = "Python promises." optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, @@ -3279,6 +3506,7 @@ version = "20.28.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, @@ -3299,6 +3527,8 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -3310,10 +3540,12 @@ version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" +groups = ["main", "test"] files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] +markers = {main = "extra == \"worker\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] @@ -3329,6 +3561,7 @@ version = "5.0" description = "Very basic event publishing system" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, @@ -3347,6 +3580,7 @@ version = "7.2" description = "Interfaces for Python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"}, {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"}, @@ -3401,6 +3635,6 @@ server = ["alembic", "asgi-correlation-id", "asyncpg", "celery", "coloredlogs", worker = ["asgi-correlation-id", "celery", "coloredlogs", "jinja2", "onetl", "psycopg", "pydantic-settings", "python-json-logger", "pyyaml", "sqlalchemy", "sqlalchemy-utils", "uuid6"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" -content-hash = "3039534b9e1914e77953ada2f359fd6204a30f547fd792c9caf8d749cdbe1152" +content-hash = "7c25839c6a797fa575c00c755405399e8448a5f3ac368bb228e6bb41940ef1a2" diff --git a/pyproject.toml b/pyproject.toml index 83b012b1..0e0039c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,7 @@ pytest-asyncio = "^0.25.1" pytest-randomly = "^3.15.0" pytest-deadfixtures = "^2.2.1" pytest-mock = "^3.14.0" +pytest-lazy-fixtures = "^1.1.1" onetl = {extras = ["spark", "s3", "hdfs"], version = "^0.12.0"} faker = "^33.3.0" coverage = "^7.6.1" diff --git a/syncmaster/db/migrations/versions/2023-11-23_0007_create_transfer_table.py b/syncmaster/db/migrations/versions/2023-11-23_0007_create_transfer_table.py index bc98cc66..f0777703 100644 --- a/syncmaster/db/migrations/versions/2023-11-23_0007_create_transfer_table.py +++ b/syncmaster/db/migrations/versions/2023-11-23_0007_create_transfer_table.py @@ -44,6 +44,7 @@ def upgrade(): sa.Column("strategy_params", sa.JSON(), nullable=False), sa.Column("source_params", sa.JSON(), nullable=False), sa.Column("target_params", sa.JSON(), nullable=False), + sa.Column("transformations", sa.JSON(), nullable=False), sa.Column("is_scheduled", sa.Boolean(), nullable=False), sa.Column("schedule", sa.String(length=32), nullable=False), sa.Column("queue_id", sa.BigInteger(), nullable=False), diff --git a/syncmaster/db/models/transfer.py b/syncmaster/db/models/transfer.py index c2029871..11e29928 100644 --- a/syncmaster/db/models/transfer.py +++ b/syncmaster/db/models/transfer.py @@ -46,6 +46,7 @@ class Transfer( strategy_params: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default={}) source_params: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default={}) target_params: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default={}) + transformations: Mapped[list[dict[str, Any]]] = mapped_column(JSON, nullable=False, default=list) is_scheduled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) schedule: Mapped[str] = mapped_column(String(32), nullable=False, default="") queue_id: Mapped[int] = mapped_column( diff --git a/syncmaster/db/repositories/transfer.py b/syncmaster/db/repositories/transfer.py index 48b5808b..8cd8e453 100644 --- a/syncmaster/db/repositories/transfer.py +++ b/syncmaster/db/repositories/transfer.py @@ -115,6 +115,7 @@ async def create( source_params: dict[str, Any], target_params: dict[str, Any], strategy_params: dict[str, Any], + transformations: list[dict[str, Any]], queue_id: int, is_scheduled: bool, schedule: str | None, @@ -130,6 +131,7 @@ async def create( source_params=source_params, target_params=target_params, strategy_params=strategy_params, + transformations=transformations, queue_id=queue_id, is_scheduled=is_scheduled, schedule=schedule or "", @@ -154,20 +156,21 @@ async def update( source_params: dict[str, Any], target_params: dict[str, Any], strategy_params: dict[str, Any], + transformations: list[dict[str, Any]], is_scheduled: bool | None, schedule: str | None, new_queue_id: int | None, ) -> Transfer: try: - for key in transfer.source_params: - if key not in source_params or source_params[key] is None: - source_params[key] = transfer.source_params[key] - for key in transfer.target_params: - if key not in target_params or target_params[key] is None: - target_params[key] = transfer.target_params[key] - for key in transfer.strategy_params: - if key not in strategy_params or strategy_params[key] is None: - strategy_params[key] = transfer.strategy_params[key] + for old, new in [ + (transfer.source_params, source_params), + (transfer.target_params, target_params), + (transfer.strategy_params, strategy_params), + ]: + for key in old: + if key not in new or new[key] is None: + new[key] = old[key] + return await self._update( Transfer.id == transfer.id, name=name or transfer.name, @@ -179,6 +182,7 @@ async def update( target_connection_id=target_connection_id or transfer.target_connection_id, source_params=source_params, target_params=target_params, + transformations=transformations or transfer.transformations, queue_id=new_queue_id or transfer.queue_id, ) except IntegrityError as e: diff --git a/syncmaster/dto/transfers.py b/syncmaster/dto/transfers.py index d09914d3..d9fff7bc 100644 --- a/syncmaster/dto/transfers.py +++ b/syncmaster/dto/transfers.py @@ -15,6 +15,7 @@ class TransferDTO: @dataclass class DBTransferDTO(TransferDTO): table_name: str + transformations: list[dict] | None = None @dataclass @@ -23,6 +24,7 @@ class FileTransferDTO(TransferDTO): file_format: CSV | JSONLine | JSON | Excel | XML | ORC | Parquet options: dict df_schema: dict | None = None + transformations: list[dict] | None = None _format_parsers = { "csv": CSV, diff --git a/syncmaster/schemas/v1/connections/oracle.py b/syncmaster/schemas/v1/connections/oracle.py index 1e1364c0..c51ea253 100644 --- a/syncmaster/schemas/v1/connections/oracle.py +++ b/syncmaster/schemas/v1/connections/oracle.py @@ -24,7 +24,7 @@ class CreateOracleConnectionDataSchema(BaseModel): additional_params: dict = Field(default_factory=dict) @model_validator(mode="before") - def check_owner_id(cls, values): + def validate_connection_identifiers(cls, values): sid, service_name = values.get("sid"), values.get("service_name") if sid and service_name: raise ValueError("You must specify either sid or service_name but not both") @@ -47,7 +47,7 @@ class UpdateOracleConnectionDataSchema(BaseModel): additional_params: dict | None = Field(default_factory=dict) @model_validator(mode="before") - def check_owner_id(cls, values): + def validate_connection_identifiers(cls, values): sid, service_name = values.get("sid"), values.get("service_name") if sid and service_name: raise ValueError("You must specify either sid or service_name but not both") diff --git a/syncmaster/schemas/v1/transfers/__init__.py b/syncmaster/schemas/v1/transfers/__init__.py index d90732d3..6ee713e1 100644 --- a/syncmaster/schemas/v1/transfers/__init__.py +++ b/syncmaster/schemas/v1/transfers/__init__.py @@ -2,7 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations -from pydantic import BaseModel, Field, model_validator +from typing import Annotated + +from pydantic import BaseModel, Field, field_validator, model_validator from syncmaster.schemas.v1.connections.connection import ReadConnectionSchema from syncmaster.schemas.v1.page import PageSchema @@ -27,6 +29,9 @@ S3ReadTransferTarget, ) from syncmaster.schemas.v1.transfers.strategy import FullStrategy, IncrementalStrategy +from syncmaster.schemas.v1.transfers.transformations.dataframe_rows_filter import ( + DataframeRowsFilter, +) from syncmaster.schemas.v1.types import NameConstr ReadTransferSchemaSource = ( @@ -97,6 +102,8 @@ | None ) +TransformationSchema = DataframeRowsFilter + class CopyTransferSchema(BaseModel): new_group_id: int @@ -129,6 +136,9 @@ class ReadTransferSchema(BaseModel): ..., discriminator="type", ) + transformations: list[Annotated[TransformationSchema, Field(..., discriminator="type")]] = Field( + default_factory=list, + ) class Config: from_attributes = True @@ -158,9 +168,12 @@ class CreateTransferSchema(BaseModel): discriminator="type", description="Incremental or archive download options", ) + transformations: list[ + Annotated[TransformationSchema, Field(None, discriminator="type", description="List of transformations")] + ] = Field(default_factory=list) @model_validator(mode="before") - def check_owner_id(cls, values): + def validate_scheduling(cls, values): is_scheduled, schedule = values.get("is_scheduled"), values.get("schedule") if is_scheduled and schedule is None: # TODO make checking cron string @@ -179,6 +192,7 @@ class UpdateTransferSchema(BaseModel): source_params: UpdateTransferSchemaSource = Field(discriminator="type", default=None) target_params: UpdateTransferSchemaTarget = Field(discriminator="type", default=None) strategy_params: FullStrategy | IncrementalStrategy | None = Field(discriminator="type", default=None) + transformations: list[Annotated[TransformationSchema, Field(discriminator="type", default=None)]] = None class ReadFullTransferSchema(ReadTransferSchema): diff --git a/syncmaster/schemas/v1/transfers/transformations/__init__.py b/syncmaster/schemas/v1/transfers/transformations/__init__.py new file mode 100644 index 00000000..eb9bf462 --- /dev/null +++ b/syncmaster/schemas/v1/transfers/transformations/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2023-2024 MTS PJSC +# SPDX-License-Identifier: Apache-2.0 diff --git a/syncmaster/schemas/v1/transfers/transformations/dataframe_rows_filter.py b/syncmaster/schemas/v1/transfers/transformations/dataframe_rows_filter.py new file mode 100644 index 00000000..b0c4914c --- /dev/null +++ b/syncmaster/schemas/v1/transfers/transformations/dataframe_rows_filter.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2023-2024 MTS PJSC +# SPDX-License-Identifier: Apache-2.0 +from typing import Annotated, Literal + +from pydantic import BaseModel, Field + +from syncmaster.schemas.v1.transformation_types import DATAFRAME_ROWS_FILTER + + +class BaseRowFilter(BaseModel): + field: str + + +class IsNullFilter(BaseRowFilter): + type: Literal["is_null"] + + +class IsNotNullFilter(BaseRowFilter): + type: Literal["is_not_null"] + + +class EqualFilter(BaseRowFilter): + type: Literal["equal"] + value: str + + +class NotEqualFilter(BaseRowFilter): + type: Literal["not_equal"] + value: str + + +class GreaterThanFilter(BaseRowFilter): + type: Literal["greater_than"] + value: str + + +class GreaterOrEqualFilter(BaseRowFilter): + type: Literal["greater_or_equal"] + value: str + + +class LessThanFilter(BaseRowFilter): + type: Literal["less_than"] + value: str + + +class LessOrEqualFilter(BaseRowFilter): + type: Literal["less_or_equal"] + value: str + + +class LikeFilter(BaseRowFilter): + type: Literal["like"] + value: str + + +class ILikeFilter(BaseRowFilter): + type: Literal["ilike"] + value: str + + +class NotLikeFilter(BaseRowFilter): + type: Literal["not_like"] + value: str + + +class NotILikeFilter(BaseRowFilter): + type: Literal["not_ilike"] + value: str + + +class RegexpFilter(BaseRowFilter): + type: Literal["regexp"] + value: str + + +RowFilter = ( + IsNullFilter + | IsNotNullFilter + | EqualFilter + | NotEqualFilter + | GreaterThanFilter + | GreaterOrEqualFilter + | LessThanFilter + | LessOrEqualFilter + | LikeFilter + | ILikeFilter + | NotLikeFilter + | NotILikeFilter + | RegexpFilter +) + + +class DataframeRowsFilter(BaseModel): + type: DATAFRAME_ROWS_FILTER + filters: list[Annotated[RowFilter, Field(..., discriminator="type")]] = Field(default_factory=list) diff --git a/syncmaster/schemas/v1/transformation_types.py b/syncmaster/schemas/v1/transformation_types.py new file mode 100644 index 00000000..9393306e --- /dev/null +++ b/syncmaster/schemas/v1/transformation_types.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023-2024 MTS PJSC +# SPDX-License-Identifier: Apache-2.0 +from typing import Literal + +DATAFRAME_ROWS_FILTER = Literal["dataframe_rows_filter"] diff --git a/syncmaster/server/api/v1/transfers.py b/syncmaster/server/api/v1/transfers.py index 919a588c..f8f2ccff 100644 --- a/syncmaster/server/api/v1/transfers.py +++ b/syncmaster/server/api/v1/transfers.py @@ -130,6 +130,7 @@ async def create_transfer( source_params=transfer_data.source_params.dict(), target_params=transfer_data.target_params.dict(), strategy_params=transfer_data.strategy_params.dict(), + transformations=[tr.dict() for tr in transfer_data.transformations], queue_id=transfer_data.queue_id, is_scheduled=transfer_data.is_scheduled, schedule=transfer_data.schedule, @@ -326,6 +327,9 @@ async def update_transfer( source_params=transfer_data.source_params.dict() if transfer_data.source_params else {}, target_params=transfer_data.target_params.dict() if transfer_data.target_params else {}, strategy_params=transfer_data.strategy_params.dict() if transfer_data.strategy_params else {}, + transformations=( + [tr.dict() for tr in transfer_data.transformations] if transfer_data.transformations else [] + ), is_scheduled=transfer_data.is_scheduled, schedule=transfer_data.schedule, new_queue_id=transfer_data.new_queue_id, diff --git a/syncmaster/worker/controller.py b/syncmaster/worker/controller.py index 37d6fcd0..d89be147 100644 --- a/syncmaster/worker/controller.py +++ b/syncmaster/worker/controller.py @@ -99,11 +99,13 @@ def __init__( self.source_handler = self.get_handler( connection_data=source_connection.data, transfer_params=run.transfer.source_params, + transformations=run.transfer.transformations, connection_auth_data=source_auth_data, ) self.target_handler = self.get_handler( connection_data=target_connection.data, transfer_params=run.transfer.target_params, + transformations=run.transfer.transformations, connection_auth_data=target_auth_data, ) @@ -126,6 +128,7 @@ def get_handler( connection_data: dict[str, Any], connection_auth_data: dict, transfer_params: dict[str, Any], + transformations: list[dict], ) -> Handler: connection_data.update(connection_auth_data) connection_data.pop("type") @@ -138,5 +141,5 @@ def get_handler( return handler( connection_dto=connection_dto(**connection_data), - transfer_dto=transfer_dto(**transfer_params), + transfer_dto=transfer_dto(**transfer_params, transformations=transformations), ) diff --git a/syncmaster/worker/handlers/db/base.py b/syncmaster/worker/handlers/db/base.py index 5f1c2c39..9ef69a89 100644 --- a/syncmaster/worker/handlers/db/base.py +++ b/syncmaster/worker/handlers/db/base.py @@ -19,11 +19,26 @@ class DBHandler(Handler): connection: BaseDBConnection transfer_dto: DBTransferDTO + _operators = { + "is_null": "IS NULL", + "is_not_null": "IS NOT NULL", + "equal": "=", + "not_equal": "!=", + "greater_than": ">", + "greater_or_equal": ">=", + "less_than": "<", + "less_or_equal": "<=", + "like": "LIKE", + "ilike": "ILIKE", + "not_like": "NOT LIKE", + "not_ilike": "NOT ILIKE", + } def read(self) -> DataFrame: reader = DBReader( connection=self.connection, table=self.transfer_dto.table_name, + where=self._get_filter_expression(), ) return reader.run() @@ -32,7 +47,19 @@ def write(self, df: DataFrame) -> None: connection=self.connection, table=self.transfer_dto.table_name, ) - return writer.run(df=self.normalize_column_names(df)) + return writer.run(df=self._normalize_column_names(df)) @abstractmethod - def normalize_column_names(self, df: DataFrame) -> DataFrame: ... + def _normalize_column_names(self, df: DataFrame) -> DataFrame: ... + + @abstractmethod + def _make_filter_expression(self, filters: list[dict]) -> str | None: ... + + def _get_filter_expression(self) -> str | None: + filters = [] + for transformation in self.transfer_dto.transformations: + if transformation["type"] == "dataframe_rows_filter": + filters.extend(transformation["filters"]) + if filters: + return self._make_filter_expression(filters) + return None diff --git a/syncmaster/worker/handlers/db/clickhouse.py b/syncmaster/worker/handlers/db/clickhouse.py index ad7cc25d..29488e16 100644 --- a/syncmaster/worker/handlers/db/clickhouse.py +++ b/syncmaster/worker/handlers/db/clickhouse.py @@ -21,6 +21,10 @@ class ClickhouseHandler(DBHandler): connection: Clickhouse connection_dto: ClickhouseConnectionDTO transfer_dto: ClickhouseTransferDTO + _operators = { + "regexp": "REGEXP", + **DBHandler._operators, + } def connect(self, spark: SparkSession): ClickhouseDialectRegistry = ( @@ -37,7 +41,7 @@ def connect(self, spark: SparkSession): ).check() def write(self, df: DataFrame) -> None: - normalized_df = self.normalize_column_names(df) + normalized_df = self._normalize_column_names(df) sort_column = next( (col for col in normalized_df.columns if col.lower().endswith("id")), normalized_df.columns[0], # if there is no column with "id", take the first column @@ -55,7 +59,18 @@ def write(self, df: DataFrame) -> None: ) return writer.run(df=normalized_df) - def normalize_column_names(self, df: DataFrame) -> DataFrame: + def _normalize_column_names(self, df: DataFrame) -> DataFrame: for column_name in df.columns: df = df.withColumnRenamed(column_name, column_name.lower()) return df + + def _make_filter_expression(self, filters: list[dict]) -> str | None: + expressions = [] + for filter in filters: + field = f'"{filter["field"]}"' + op = self._operators[filter["type"]] + value = filter.get("value") + + expressions.append(f"{field} {op} '{value}'" if value is not None else f"{field} {op}") + + return " AND ".join(expressions) or None diff --git a/syncmaster/worker/handlers/db/hive.py b/syncmaster/worker/handlers/db/hive.py index 8f224946..440d3394 100644 --- a/syncmaster/worker/handlers/db/hive.py +++ b/syncmaster/worker/handlers/db/hive.py @@ -20,6 +20,10 @@ class HiveHandler(DBHandler): connection: Hive connection_dto: HiveConnectionDTO transfer_dto: HiveTransferDTO + _operators = { + "regexp": "RLIKE", + **DBHandler._operators, + } def connect(self, spark: SparkSession): self.connection = Hive( @@ -31,7 +35,27 @@ def read(self) -> DataFrame: self.connection.spark.catalog.refreshTable(self.transfer_dto.table_name) return super().read() - def normalize_column_names(self, df: DataFrame) -> DataFrame: + def _normalize_column_names(self, df: DataFrame) -> DataFrame: for column_name in df.columns: df = df.withColumnRenamed(column_name, column_name.lower()) return df + + def _make_filter_expression(self, filters: list[dict]) -> str | None: + expressions = [] + for filter in filters: + op = self._operators[filter["type"]] + field = f"`{filter["field"]}`" + value = filter.get("value") + + if value is None: + expressions.append(f"{field} {op}") + continue + + if op == "ILIKE": + expressions.append(f"LOWER({field}) LIKE LOWER('{value}')") + elif op == "NOT ILIKE": + expressions.append(f"NOT LOWER({field}) LIKE LOWER('{value}')") + else: + expressions.append(f"{field} {op} '{value}'") + + return " AND ".join(expressions) or None diff --git a/syncmaster/worker/handlers/db/mssql.py b/syncmaster/worker/handlers/db/mssql.py index 6b352d8e..bbe3a5c2 100644 --- a/syncmaster/worker/handlers/db/mssql.py +++ b/syncmaster/worker/handlers/db/mssql.py @@ -20,6 +20,12 @@ class MSSQLHandler(DBHandler): connection: MSSQL connection_dto: MSSQLConnectionDTO transfer_dto: MSSQLTransferDTO + _operators = { + "regexp": "LIKE", + # MSSQL doesn't support traditional regexp currently + # https://learn.microsoft.com/ru-ru/sql/t-sql/language-elements/wildcard-character-s-to-match-transact-sql?view=sql-server-ver16 + **DBHandler._operators, + } def connect(self, spark: SparkSession): self.connection = MSSQL( @@ -32,7 +38,27 @@ def connect(self, spark: SparkSession): spark=spark, ).check() - def normalize_column_names(self, df: DataFrame) -> DataFrame: + def _normalize_column_names(self, df: DataFrame) -> DataFrame: for column_name in df.columns: df = df.withColumnRenamed(column_name, column_name.lower()) return df + + def _make_filter_expression(self, filters: list[dict]) -> str | None: + expressions = [] + for filter in filters: + op = self._operators[filter["type"]] + field = f'"{filter["field"]}"' + value = filter.get("value") + + if value is None: + expressions.append(f"{field} {op}") + continue + + if op == "ILIKE": + expressions.append(f"LOWER({field}) LIKE LOWER('{value}')") + elif op == "NOT ILIKE": + expressions.append(f"NOT LOWER({field}) LIKE LOWER('{value}')") + else: + expressions.append(f"{field} {op} '{value}'") + + return " AND ".join(expressions) or None diff --git a/syncmaster/worker/handlers/db/mysql.py b/syncmaster/worker/handlers/db/mysql.py index 56f2570f..b62a5ca6 100644 --- a/syncmaster/worker/handlers/db/mysql.py +++ b/syncmaster/worker/handlers/db/mysql.py @@ -20,6 +20,10 @@ class MySQLHandler(DBHandler): connection: MySQL connection_dto: MySQLConnectionDTO transfer_dto: MySQLTransferDTO + _operators = { + "regexp": "RLIKE", + **DBHandler._operators, + } def connect(self, spark: SparkSession): self.connection = MySQL( @@ -31,7 +35,27 @@ def connect(self, spark: SparkSession): spark=spark, ).check() - def normalize_column_names(self, df: DataFrame) -> DataFrame: + def _normalize_column_names(self, df: DataFrame) -> DataFrame: for column_name in df.columns: df = df.withColumnRenamed(column_name, column_name.lower()) return df + + def _make_filter_expression(self, filters: list[dict]) -> str | None: + expressions = [] + for filter in filters: + op = self._operators[filter["type"]] + field = f"`{filter["field"]}`" + value = filter.get("value") + + if value is None: + expressions.append(f"{field} {op}") + continue + + if op == "ILIKE": + expressions.append(f"LOWER({field}) LIKE LOWER('{value}')") + elif op == "NOT ILIKE": + expressions.append(f"NOT LOWER({field}) LIKE LOWER('{value}')") + else: + expressions.append(f"{field} {op} '{value}'") + + return " AND ".join(expressions) or None diff --git a/syncmaster/worker/handlers/db/oracle.py b/syncmaster/worker/handlers/db/oracle.py index d4498433..78aa4ffc 100644 --- a/syncmaster/worker/handlers/db/oracle.py +++ b/syncmaster/worker/handlers/db/oracle.py @@ -20,6 +20,10 @@ class OracleHandler(DBHandler): connection: Oracle connection_dto: OracleConnectionDTO transfer_dto: OracleTransferDTO + _operators = { + "regexp": "REGEXP_LIKE", + **DBHandler._operators, + } def connect(self, spark: SparkSession): self.connection = Oracle( @@ -33,7 +37,29 @@ def connect(self, spark: SparkSession): spark=spark, ).check() - def normalize_column_names(self, df: DataFrame) -> DataFrame: + def _normalize_column_names(self, df: DataFrame) -> DataFrame: for column_name in df.columns: df = df.withColumnRenamed(column_name, column_name.upper()) return df + + def _make_filter_expression(self, filters: list[dict]) -> str | None: + expressions = [] + for filter in filters: + field = f'"{filter["field"]}"' + op = self._operators[filter["type"]] + value = filter.get("value") + + if value is None: + expressions.append(f"{field} {op}") + continue + + if op == "REGEXP_LIKE": + expressions.append(f"{op}({field}, '{value}')") + elif op == "ILIKE": + expressions.append(f"LOWER({field}) LIKE LOWER('{value}')") + elif op == "NOT ILIKE": + expressions.append(f"NOT LOWER({field}) LIKE LOWER('{value}')") + else: + expressions.append(f"{field} {op} '{value}'") + + return " AND ".join(expressions) or None diff --git a/syncmaster/worker/handlers/db/postgres.py b/syncmaster/worker/handlers/db/postgres.py index 44587544..e5fcfc60 100644 --- a/syncmaster/worker/handlers/db/postgres.py +++ b/syncmaster/worker/handlers/db/postgres.py @@ -20,6 +20,10 @@ class PostgresHandler(DBHandler): connection: Postgres connection_dto: PostgresConnectionDTO transfer_dto: PostgresTransferDTO + _operators = { + "regexp": "~", + **DBHandler._operators, + } def connect(self, spark: SparkSession): self.connection = Postgres( @@ -32,7 +36,18 @@ def connect(self, spark: SparkSession): spark=spark, ).check() - def normalize_column_names(self, df: DataFrame) -> DataFrame: + def _normalize_column_names(self, df: DataFrame) -> DataFrame: for column_name in df.columns: df = df.withColumnRenamed(column_name, column_name.lower()) return df + + def _make_filter_expression(self, filters: list[dict]) -> str | None: + expressions = [] + for filter in filters: + field = f'"{filter["field"]}"' + op = self._operators[filter["type"]] + value = filter.get("value") + + expressions.append(f"{field} {op} '{value}'" if value is not None else f"{field} {op}") + + return " AND ".join(expressions) or None diff --git a/syncmaster/worker/handlers/file/base.py b/syncmaster/worker/handlers/file/base.py index fb409391..08a85cf2 100644 --- a/syncmaster/worker/handlers/file/base.py +++ b/syncmaster/worker/handlers/file/base.py @@ -20,6 +20,21 @@ class FileHandler(Handler): connection: BaseFileDFConnection connection_dto: ConnectionDTO transfer_dto: FileTransferDTO + _operators = { + "is_null": "IS NULL", + "is_not_null": "IS NOT NULL", + "equal": "=", + "not_equal": "!=", + "greater_than": ">", + "greater_or_equal": ">=", + "less_than": "<", + "less_or_equal": "<=", + "like": "LIKE", + "ilike": "ILIKE", + "not_like": "NOT LIKE", + "not_ilike": "NOT ILIKE", + "regexp": "RLIKE", + } def read(self) -> DataFrame: from pyspark.sql.types import StructType @@ -31,8 +46,13 @@ def read(self) -> DataFrame: df_schema=StructType.fromJson(self.transfer_dto.df_schema) if self.transfer_dto.df_schema else None, options=self.transfer_dto.options, ) + df = reader.run() - return reader.run() + filter_expression = self._get_filter_expression() + if filter_expression: + df = df.where(filter_expression) + + return df def write(self, df: DataFrame): writer = FileDFWriter( @@ -43,3 +63,23 @@ def write(self, df: DataFrame): ) return writer.run(df=df) + + def _get_filter_expression(self) -> str | None: + filters = [] + for transformation in self.transfer_dto.transformations: + if transformation["type"] == "dataframe_rows_filter": + filters.extend(transformation["filters"]) + if filters: + return self._make_filter_expression(filters) + return None + + def _make_filter_expression(self, filters: list[dict]) -> str: + expressions = [] + for filter in filters: + field = filter["field"] + op = self._operators[filter["type"]] + value = filter.get("value") + + expressions.append(f"{field} {op} '{value}'" if value is not None else f"{field} {op}") + + return " AND ".join(expressions) diff --git a/syncmaster/worker/handlers/file/s3.py b/syncmaster/worker/handlers/file/s3.py index a805ad38..6cf4c4e9 100644 --- a/syncmaster/worker/handlers/file/s3.py +++ b/syncmaster/worker/handlers/file/s3.py @@ -45,5 +45,10 @@ def read(self) -> DataFrame: df_schema=StructType.fromJson(self.transfer_dto.df_schema) if self.transfer_dto.df_schema else None, options={**options, **self.transfer_dto.options}, ) + df = reader.run() - return reader.run() + filter_expression = self._get_filter_expression() + if filter_expression: + df = df.where(filter_expression) + + return df diff --git a/tests/test_integration/test_run_transfer/conftest.py b/tests/test_integration/test_run_transfer/conftest.py index 820ca5de..5ab914e4 100644 --- a/tests/test_integration/test_run_transfer/conftest.py +++ b/tests/test_integration/test_run_transfer/conftest.py @@ -237,8 +237,8 @@ def mssql_for_conftest(test_settings: TestSettings) -> MSSQLConnectionDTO: ) def mssql_for_worker(test_settings: TestSettings) -> MSSQLConnectionDTO: return MSSQLConnectionDTO( - host=test_settings.TEST_MSSQL_HOST_FOR_CONFTEST, - port=test_settings.TEST_MSSQL_PORT_FOR_CONFTEST, + host=test_settings.TEST_MSSQL_HOST_FOR_WORKER, + port=test_settings.TEST_MSSQL_PORT_FOR_WORKER, user=test_settings.TEST_MSSQL_USER, password=test_settings.TEST_MSSQL_PASSWORD, database_name=test_settings.TEST_MSSQL_DB, @@ -1257,6 +1257,7 @@ def init_df_with_mixed_column_naming(spark: SparkSession) -> DataFrame: StructField("Id", IntegerType()), StructField("Phone Number", StringType()), StructField("region", StringType()), + StructField("NUMBER", IntegerType()), StructField("birth_DATE", DateType()), StructField("Registered At", TimestampType()), StructField("account_balance", DoubleType()), @@ -1269,10 +1270,93 @@ def init_df_with_mixed_column_naming(spark: SparkSession) -> DataFrame: 1, "+79123456789", "Mordor", + 1, datetime.date(year=2023, month=3, day=11), datetime.datetime.now(), 1234.2343, ), + ( + 2, + "+79234567890", + "Gondor", + 2, + datetime.date(2022, 6, 19), + datetime.datetime.now(), + 2345.5678, + ), + ( + 3, + "+79345678901", + "Rohan", + 3, + datetime.date(2021, 11, 5), + datetime.datetime.now(), + 3456.7890, + ), + ( + 4, + "+79456789012", + "Shire", + 4, + datetime.date(2020, 1, 30), + datetime.datetime.now(), + 4567.8901, + ), + ( + 5, + "+79567890123", + "Isengard", + 5, + datetime.date(2023, 8, 15), + datetime.datetime.now(), + 5678.9012, + ), ], schema=df_schema, ) + + +@pytest.fixture +def dataframe_rows_filter_transformations(): + return [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "is_not_null", + "field": "BIRTH_DATE", + }, + { + "type": "less_or_equal", + "field": "NUMBER", + "value": "25", + }, + { + "type": "not_like", + "field": "REGION", + "value": "%port", + }, + { + "type": "not_ilike", + "field": "REGION", + "value": "new%", + }, + { + "type": "regexp", + "field": "PHONE_NUMBER", + "value": "^[^+].*", + }, + ], + }, + ] + + +@pytest.fixture +def expected_dataframe_rows_filter(): + return lambda df: ( + df["BIRTH_DATE"].isNotNull() + & (df["NUMBER"] <= "25") + & (~df["REGION"].like("%port")) + & (~df["REGION"].ilike("new%")) + & (df["PHONE_NUMBER"].rlike("^[^+].*")) + ) diff --git a/tests/test_integration/test_run_transfer/test_clickhouse.py b/tests/test_integration/test_run_transfer/test_clickhouse.py index eb651db5..bd18f779 100644 --- a/tests/test_integration/test_run_transfer/test_clickhouse.py +++ b/tests/test_integration/test_run_transfer/test_clickhouse.py @@ -6,6 +6,7 @@ from onetl.connection import Clickhouse from onetl.db import DBReader from pyspark.sql import DataFrame +from pytest_lazy_fixtures import lf from sqlalchemy.ext.asyncio import AsyncSession from syncmaster.db.models import Connection, Group, Queue, Status, Transfer @@ -24,6 +25,7 @@ async def postgres_to_clickhouse( clickhouse_for_conftest: Clickhouse, clickhouse_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -39,6 +41,7 @@ async def postgres_to_clickhouse( "type": "clickhouse", "table_name": f"{clickhouse_for_conftest.user}.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -54,6 +57,7 @@ async def clickhouse_to_postgres( clickhouse_for_conftest: Clickhouse, clickhouse_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -69,6 +73,7 @@ async def clickhouse_to_postgres( "type": "postgres", "table_name": "public.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -76,6 +81,15 @@ async def clickhouse_to_postgres( await session.commit() +@pytest.mark.parametrize( + "transformations, expected_filter", + [ + ( + lf("dataframe_rows_filter_transformations"), + lf("expected_dataframe_rows_filter"), + ), + ], +) async def test_run_transfer_postgres_to_clickhouse( client: AsyncClient, group_owner: MockUser, @@ -83,11 +97,14 @@ async def test_run_transfer_postgres_to_clickhouse( prepare_clickhouse, init_df: DataFrame, postgres_to_clickhouse: Transfer, + transformations, + expected_filter, ): # Arrange _, fill_with_data = prepare_postgres fill_with_data(init_df) clickhouse, _ = prepare_clickhouse + init_df = init_df.where(expected_filter(init_df)) # Act result = await client.post( @@ -122,6 +139,7 @@ async def test_run_transfer_postgres_to_clickhouse( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_clickhouse_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -129,6 +147,7 @@ async def test_run_transfer_postgres_to_clickhouse_mixed_naming( prepare_clickhouse, init_df_with_mixed_column_naming: DataFrame, postgres_to_clickhouse: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -169,21 +188,33 @@ async def test_run_transfer_postgres_to_clickhouse_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() +@pytest.mark.parametrize( + "transformations, expected_filter", + [ + ( + lf("dataframe_rows_filter_transformations"), + lf("expected_dataframe_rows_filter"), + ), + ], +) async def test_run_transfer_clickhouse_to_postgres( client: AsyncClient, group_owner: MockUser, prepare_clickhouse, prepare_postgres, init_df: DataFrame, + transformations, + expected_filter, clickhouse_to_postgres: Transfer, ): # Arrange _, fill_with_data = prepare_clickhouse fill_with_data(init_df) postgres, _ = prepare_postgres + init_df = init_df.where(expected_filter(init_df)) # Act result = await client.post( @@ -220,6 +251,7 @@ async def test_run_transfer_clickhouse_to_postgres( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_clickhouse_to_postgres_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -227,6 +259,7 @@ async def test_run_transfer_clickhouse_to_postgres_mixed_naming( prepare_postgres, init_df_with_mixed_column_naming: DataFrame, clickhouse_to_postgres: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_clickhouse @@ -268,4 +301,4 @@ async def test_run_transfer_clickhouse_to_postgres_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() diff --git a/tests/test_integration/test_run_transfer/test_hive.py b/tests/test_integration/test_run_transfer/test_hive.py index 67757876..2a25ea1c 100644 --- a/tests/test_integration/test_run_transfer/test_hive.py +++ b/tests/test_integration/test_run_transfer/test_hive.py @@ -5,6 +5,7 @@ from httpx import AsyncClient from onetl.db import DBReader from pyspark.sql import DataFrame +from pytest_lazy_fixtures import lf from sqlalchemy.ext.asyncio import AsyncSession from syncmaster.db.models import Connection, Group, Queue, Status, Transfer @@ -22,6 +23,7 @@ async def postgres_to_hive( queue: Queue, hive_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -37,6 +39,7 @@ async def postgres_to_hive( "type": "hive", "table_name": "default.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -51,6 +54,7 @@ async def hive_to_postgres( queue: Queue, hive_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -66,6 +70,7 @@ async def hive_to_postgres( "type": "postgres", "table_name": "public.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -73,6 +78,7 @@ async def hive_to_postgres( await session.commit() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_hive( client: AsyncClient, group_owner: MockUser, @@ -80,6 +86,7 @@ async def test_run_transfer_postgres_to_hive( prepare_hive, init_df: DataFrame, postgres_to_hive: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -120,6 +127,7 @@ async def test_run_transfer_postgres_to_hive( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_hive_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -127,6 +135,7 @@ async def test_run_transfer_postgres_to_hive_mixed_naming( prepare_hive, init_df_with_mixed_column_naming: DataFrame, postgres_to_hive: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -167,9 +176,18 @@ async def test_run_transfer_postgres_to_hive_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() +@pytest.mark.parametrize( + "transformations, expected_filter", + [ + ( + lf("dataframe_rows_filter_transformations"), + lf("expected_dataframe_rows_filter"), + ), + ], +) async def test_run_transfer_hive_to_postgres( client: AsyncClient, group_owner: MockUser, @@ -177,11 +195,14 @@ async def test_run_transfer_hive_to_postgres( prepare_postgres, init_df: DataFrame, hive_to_postgres: Transfer, + transformations, + expected_filter, ): # Arrange _, fill_with_data = prepare_hive fill_with_data(init_df) postgres, _ = prepare_postgres + init_df = init_df.where(expected_filter(init_df)) # Act result = await client.post( @@ -216,6 +237,7 @@ async def test_run_transfer_hive_to_postgres( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_hive_to_postgres_mixes_naming( client: AsyncClient, group_owner: MockUser, @@ -223,6 +245,7 @@ async def test_run_transfer_hive_to_postgres_mixes_naming( prepare_postgres, init_df_with_mixed_column_naming: DataFrame, hive_to_postgres: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_hive @@ -264,4 +287,4 @@ async def test_run_transfer_hive_to_postgres_mixes_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() diff --git a/tests/test_integration/test_run_transfer/test_mssql.py b/tests/test_integration/test_run_transfer/test_mssql.py index 93ef3895..8d845987 100644 --- a/tests/test_integration/test_run_transfer/test_mssql.py +++ b/tests/test_integration/test_run_transfer/test_mssql.py @@ -25,6 +25,7 @@ async def postgres_to_mssql( mssql_for_conftest: MSSQL, mssql_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -40,6 +41,7 @@ async def postgres_to_mssql( "type": "mssql", "table_name": "dbo.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -55,6 +57,7 @@ async def mssql_to_postgres( mssql_for_conftest: MSSQL, mssql_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -70,6 +73,7 @@ async def mssql_to_postgres( "type": "postgres", "table_name": "public.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -77,6 +81,7 @@ async def mssql_to_postgres( await session.commit() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_mssql( client: AsyncClient, group_owner: MockUser, @@ -84,6 +89,7 @@ async def test_run_transfer_postgres_to_mssql( prepare_mssql, init_df: DataFrame, postgres_to_mssql: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -128,6 +134,7 @@ async def test_run_transfer_postgres_to_mssql( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_mssql_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -135,6 +142,7 @@ async def test_run_transfer_postgres_to_mssql_mixed_naming( prepare_mssql, init_df_with_mixed_column_naming: DataFrame, postgres_to_mssql: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -183,9 +191,55 @@ async def test_run_transfer_postgres_to_mssql_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() - - + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() + + +@pytest.mark.parametrize( + "transformations, expected_filter", + [ + ( + [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "is_not_null", + "field": "BIRTH_DATE", + }, + { + "type": "less_or_equal", + "field": "NUMBER", + "value": "25", + }, + { + "type": "not_like", + "field": "REGION", + "value": "%port", + }, + { + "type": "not_ilike", + "field": "REGION", + "value": "new%", + }, + { + "type": "regexp", + "field": "PHONE_NUMBER", + "value": "^[0-9!@#$.,;_]%", + # available expressions are limited + }, + ], + }, + ], + lambda df: ( + df["BIRTH_DATE"].isNotNull() + & (df["NUMBER"] <= "25") + & (~df["REGION"].like("%port")) + & (~df["REGION"].ilike("new%")) + & (df["PHONE_NUMBER"].rlike("[0-9!@#$.,;_]%")) + ), + ), + ], +) async def test_run_transfer_mssql_to_postgres( client: AsyncClient, group_owner: MockUser, @@ -193,11 +247,14 @@ async def test_run_transfer_mssql_to_postgres( prepare_postgres, init_df: DataFrame, mssql_to_postgres: Transfer, + transformations, + expected_filter, ): # Arrange _, fill_with_data = prepare_mssql fill_with_data(init_df) postgres, _ = prepare_postgres + init_df = init_df.where(expected_filter(init_df)) # Act result = await client.post( @@ -238,6 +295,7 @@ async def test_run_transfer_mssql_to_postgres( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_mssql_to_postgres_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -245,6 +303,7 @@ async def test_run_transfer_mssql_to_postgres_mixed_naming( prepare_postgres, init_df_with_mixed_column_naming: DataFrame, mssql_to_postgres: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_mssql @@ -293,4 +352,4 @@ async def test_run_transfer_mssql_to_postgres_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() diff --git a/tests/test_integration/test_run_transfer/test_mysql.py b/tests/test_integration/test_run_transfer/test_mysql.py index 513c2098..fca399ef 100644 --- a/tests/test_integration/test_run_transfer/test_mysql.py +++ b/tests/test_integration/test_run_transfer/test_mysql.py @@ -7,6 +7,7 @@ from onetl.db import DBReader from pyspark.sql import DataFrame from pyspark.sql.functions import col, from_unixtime +from pytest_lazy_fixtures import lf from sqlalchemy.ext.asyncio import AsyncSession from syncmaster.db.models import Connection, Group, Queue, Status, Transfer @@ -25,6 +26,7 @@ async def postgres_to_mysql( mysql_for_conftest: MySQL, mysql_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -40,6 +42,7 @@ async def postgres_to_mysql( "type": "mysql", "table_name": f"{mysql_for_conftest.database_name}.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -55,6 +58,7 @@ async def mysql_to_postgres( mysql_for_conftest: MySQL, mysql_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -70,6 +74,7 @@ async def mysql_to_postgres( "type": "postgres", "table_name": "public.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -77,6 +82,7 @@ async def mysql_to_postgres( await session.commit() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_mysql( client: AsyncClient, group_owner: MockUser, @@ -84,6 +90,7 @@ async def test_run_transfer_postgres_to_mysql( prepare_mysql, init_df: DataFrame, postgres_to_mysql: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -133,6 +140,7 @@ async def test_run_transfer_postgres_to_mysql( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_mysql_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -140,6 +148,7 @@ async def test_run_transfer_postgres_to_mysql_mixed_naming( prepare_mysql, init_df_with_mixed_column_naming: DataFrame, postgres_to_mysql: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -190,9 +199,18 @@ async def test_run_transfer_postgres_to_mysql_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() +@pytest.mark.parametrize( + "transformations, expected_filter", + [ + ( + lf("dataframe_rows_filter_transformations"), + lf("expected_dataframe_rows_filter"), + ), + ], +) async def test_run_transfer_mysql_to_postgres( client: AsyncClient, group_owner: MockUser, @@ -200,11 +218,14 @@ async def test_run_transfer_mysql_to_postgres( prepare_postgres, init_df: DataFrame, mysql_to_postgres: Transfer, + transformations, + expected_filter, ): # Arrange _, fill_with_data = prepare_mysql fill_with_data(init_df) postgres, _ = prepare_postgres + init_df = init_df.where(expected_filter(init_df)) # Act result = await client.post( @@ -250,6 +271,7 @@ async def test_run_transfer_mysql_to_postgres( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_mysql_to_postgres_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -257,6 +279,7 @@ async def test_run_transfer_mysql_to_postgres_mixed_naming( prepare_postgres, init_df_with_mixed_column_naming: DataFrame, mysql_to_postgres: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_mysql @@ -307,4 +330,4 @@ async def test_run_transfer_mysql_to_postgres_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() diff --git a/tests/test_integration/test_run_transfer/test_oracle.py b/tests/test_integration/test_run_transfer/test_oracle.py index 9737ab0f..aae80d6f 100644 --- a/tests/test_integration/test_run_transfer/test_oracle.py +++ b/tests/test_integration/test_run_transfer/test_oracle.py @@ -6,6 +6,7 @@ from onetl.connection import Oracle from onetl.db import DBReader from pyspark.sql import DataFrame +from pytest_lazy_fixtures import lf from sqlalchemy.ext.asyncio import AsyncSession from syncmaster.db.models import Connection, Group, Queue, Status, Transfer @@ -24,6 +25,7 @@ async def postgres_to_oracle( oracle_for_conftest: Oracle, oracle_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -39,6 +41,7 @@ async def postgres_to_oracle( "type": "oracle", "table_name": f"{oracle_for_conftest.user}.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -54,6 +57,7 @@ async def oracle_to_postgres( oracle_for_conftest: Oracle, oracle_connection: Connection, postgres_connection: Connection, + transformations: list[dict], ): result = await create_transfer( session=session, @@ -69,6 +73,7 @@ async def oracle_to_postgres( "type": "postgres", "table_name": "public.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -76,6 +81,7 @@ async def oracle_to_postgres( await session.commit() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_oracle( client: AsyncClient, group_owner: MockUser, @@ -83,6 +89,7 @@ async def test_run_transfer_postgres_to_oracle( prepare_oracle, init_df: DataFrame, postgres_to_oracle: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -122,6 +129,7 @@ async def test_run_transfer_postgres_to_oracle( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_postgres_to_oracle_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -129,6 +137,7 @@ async def test_run_transfer_postgres_to_oracle_mixed_naming( prepare_oracle, init_df_with_mixed_column_naming: DataFrame, postgres_to_oracle: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_postgres @@ -169,9 +178,18 @@ async def test_run_transfer_postgres_to_oracle_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() +@pytest.mark.parametrize( + "transformations, expected_filter", + [ + ( + lf("dataframe_rows_filter_transformations"), + lf("expected_dataframe_rows_filter"), + ), + ], +) async def test_run_transfer_oracle_to_postgres( client: AsyncClient, group_owner: MockUser, @@ -179,11 +197,14 @@ async def test_run_transfer_oracle_to_postgres( prepare_postgres, init_df: DataFrame, oracle_to_postgres: Transfer, + transformations, + expected_filter, ): # Arrange _, fill_with_data = prepare_oracle fill_with_data(init_df) postgres, _ = prepare_postgres + init_df = init_df.where(expected_filter(init_df)) # Act result = await client.post( @@ -220,6 +241,7 @@ async def test_run_transfer_oracle_to_postgres( assert df.sort("ID").collect() == init_df.sort("ID").collect() +@pytest.mark.parametrize("transformations", [[]]) async def test_run_transfer_oracle_to_postgres_mixed_naming( client: AsyncClient, group_owner: MockUser, @@ -227,6 +249,7 @@ async def test_run_transfer_oracle_to_postgres_mixed_naming( prepare_postgres, init_df_with_mixed_column_naming: DataFrame, oracle_to_postgres: Transfer, + transformations, ): # Arrange _, fill_with_data = prepare_oracle @@ -268,4 +291,4 @@ async def test_run_transfer_oracle_to_postgres_mixed_naming( for field in init_df_with_mixed_column_naming.schema: df = df.withColumn(field.name, df[field.name].cast(field.dataType)) - assert df.collect() == init_df_with_mixed_column_naming.collect() + assert df.sort("ID").collect() == init_df_with_mixed_column_naming.sort("ID").collect() diff --git a/tests/test_integration/test_run_transfer/test_s3.py b/tests/test_integration/test_run_transfer/test_s3.py index 1ad7ac7a..8c6fde45 100644 --- a/tests/test_integration/test_run_transfer/test_s3.py +++ b/tests/test_integration/test_run_transfer/test_s3.py @@ -10,6 +10,7 @@ from pyspark.sql import DataFrame from pyspark.sql.functions import col, date_format, date_trunc, to_timestamp from pytest import FixtureRequest +from pytest_lazy_fixtures import lf from sqlalchemy.ext.asyncio import AsyncSession from syncmaster.db.models import Connection, Group, Queue, Status @@ -37,6 +38,7 @@ async def s3_to_postgres( prepare_s3, source_file_format, file_format_flavor: str, + transformations: list[dict], ): format_name, file_format = source_file_format format_name_in_path = "xlsx" if format_name == "excel" else format_name @@ -62,6 +64,7 @@ async def s3_to_postgres( "type": "postgres", "table_name": "public.target_table", }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -78,6 +81,7 @@ async def postgres_to_s3( postgres_connection: Connection, target_file_format, file_format_flavor: str, + transformations: list[dict], ): format_name, file_format = target_file_format result = await create_transfer( @@ -99,6 +103,7 @@ async def postgres_to_s3( }, "options": {}, }, + transformations=transformations, queue_id=queue.id, ) yield result @@ -107,41 +112,54 @@ async def postgres_to_s3( @pytest.mark.parametrize( - "source_file_format, file_format_flavor", + "source_file_format, file_format_flavor, transformations, expected_filter", [ pytest.param( ("csv", {}), "with_header", - id="csv", + lf("dataframe_rows_filter_transformations"), + lf("expected_dataframe_rows_filter"), ), pytest.param( ("json", {}), "without_compression", + [], + None, id="json", ), pytest.param( ("jsonline", {}), "without_compression", + [], + None, id="jsonline", ), pytest.param( ("excel", {}), "with_header", + [], + None, id="excel", ), pytest.param( ("orc", {}), "without_compression", + [], + None, id="orc", ), pytest.param( ("parquet", {}), "without_compression", + [], + None, id="parquet", ), pytest.param( ("xml", {}), "without_compression", + [], + None, id="xml", ), ], @@ -155,10 +173,14 @@ async def test_run_transfer_s3_to_postgres( s3_to_postgres: Transfer, source_file_format, file_format_flavor, + transformations, + expected_filter, ): # Arrange postgres, _ = prepare_postgres file_format, _ = source_file_format + if expected_filter: + init_df = init_df.where(expected_filter(init_df)) # Act result = await client.post( @@ -202,36 +224,42 @@ async def test_run_transfer_s3_to_postgres( @pytest.mark.parametrize( - "target_file_format, file_format_flavor", + "target_file_format, file_format_flavor, transformations", [ pytest.param( ("csv", {"compression": "lz4"}), "with_compression", + [], id="csv", ), pytest.param( ("jsonline", {}), "without_compression", + [], id="jsonline", ), pytest.param( ("excel", {}), "with_header", + [], id="excel", ), pytest.param( ("orc", {"compression": "none"}), "with_compression", + [], id="orc", ), pytest.param( ("parquet", {"compression": "gzip"}), "with_compression", + [], id="parquet", ), pytest.param( ("xml", {"compression": "none"}), "without_compression", + [], id="xml", ), ], @@ -247,6 +275,7 @@ async def test_run_transfer_postgres_to_s3( postgres_to_s3: Transfer, target_file_format, file_format_flavor: str, + transformations, ): format_name, format = target_file_format diff --git a/tests/test_integration/test_scheduler/scheduler_fixtures/transfer_fixture.py b/tests/test_integration/test_scheduler/scheduler_fixtures/transfer_fixture.py index 173b5c87..e99bd89f 100644 --- a/tests/test_integration/test_scheduler/scheduler_fixtures/transfer_fixture.py +++ b/tests/test_integration/test_scheduler/scheduler_fixtures/transfer_fixture.py @@ -105,8 +105,9 @@ async def group_transfer_integration_mock( source_connection_id=source_connection.id, target_connection_id=target_connection.id, queue_id=queue.id, - source_params=create_transfer_data, - target_params=create_transfer_data, + source_params=create_transfer_data.get("source_and_target_params") if create_transfer_data else None, + target_params=create_transfer_data.get("source_and_target_params") if create_transfer_data else None, + transformations=create_transfer_data.get("transformations") if create_transfer_data else None, ) yield MockTransfer( diff --git a/tests/test_unit/test_transfers/test_create_transfer.py b/tests/test_unit/test_transfers/test_create_transfer.py index e3e7b6e0..3c005246 100644 --- a/tests/test_unit/test_transfers/test_create_transfer.py +++ b/tests/test_unit/test_transfers/test_create_transfer.py @@ -36,6 +36,23 @@ async def test_developer_plus_can_create_transfer( "source_params": {"type": "postgres", "table_name": "source_table"}, "target_params": {"type": "postgres", "table_name": "target_table"}, "strategy_params": {"type": "full"}, + "transformations": [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "equal", + "field": "col1", + "value": "something", + }, + { + "type": "greater_than", + "field": "col2", + "value": "20", + }, + ], + }, + ], "queue_id": group_queue.id, }, ) @@ -64,6 +81,7 @@ async def test_developer_plus_can_create_transfer( "source_params": transfer.source_params, "target_params": transfer.target_params, "strategy_params": transfer.strategy_params, + "transformations": transfer.transformations, "queue_id": transfer.queue_id, } @@ -211,6 +229,23 @@ async def test_superuser_can_create_transfer( "source_params": {"type": "postgres", "table_name": "source_table"}, "target_params": {"type": "postgres", "table_name": "target_table"}, "strategy_params": {"type": "full"}, + "transformations": [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "equal", + "field": "col1", + "value": "something", + }, + { + "type": "greater_than", + "field": "col2", + "value": "20", + }, + ], + }, + ], "queue_id": group_queue.id, }, ) @@ -236,6 +271,7 @@ async def test_superuser_can_create_transfer( "source_params": transfer.source_params, "target_params": transfer.target_params, "strategy_params": transfer.strategy_params, + "transformations": transfer.transformations, "queue_id": transfer.queue_id, } @@ -387,6 +423,95 @@ async def test_superuser_can_create_transfer( }, }, ), + ( + { + "transformations": [ + { + "type": "some unknown transformation type", + "filters": [ + { + "type": "equal", + "field": "col1", + "value": "something", + }, + ], + }, + ], + }, + { + "error": { + "code": "invalid_request", + "message": "Invalid request", + "details": [ + { + "location": ["body", "transformations", 0], + "message": ( + "Input tag 'some unknown transformation type' found using 'type' " + "does not match any of the expected tags: 'dataframe_rows_filter'" + ), + "code": "union_tag_invalid", + "context": { + "discriminator": "'type'", + "expected_tags": "'dataframe_rows_filter'", + "tag": "some unknown transformation type", + }, + "input": { + "type": "some unknown transformation type", + "filters": [ + { + "type": "equal", + "field": "col1", + "value": "something", + }, + ], + }, + }, + ], + }, + }, + ), + ( + { + "transformations": [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "equals_today", + "field": "col1", + "value": "something", + }, + ], + }, + ], + }, + { + "error": { + "code": "invalid_request", + "message": "Invalid request", + "details": [ + { + "location": ["body", "transformations", 0, "dataframe_rows_filter", "filters", 0], + "message": ( + "Input tag 'equals_today' found using 'type' does not match any of the expected tags: 'is_null', 'is_not_null', 'equal', 'not_equal', " + "'greater_than', 'greater_or_equal', 'less_than', 'less_or_equal', 'like', 'ilike', 'not_like', 'not_ilike', 'regexp'" + ), + "code": "union_tag_invalid", + "context": { + "discriminator": "'type'", + "tag": "equals_today", + "expected_tags": "'is_null', 'is_not_null', 'equal', 'not_equal', 'greater_than', 'greater_or_equal', 'less_than', 'less_or_equal', 'like', 'ilike', 'not_like', 'not_ilike', 'regexp'", + }, + "input": { + "type": "equals_today", + "field": "col1", + "value": "something", + }, + }, + ], + }, + }, + ), ), ) async def test_check_fields_validation_on_create_transfer( @@ -414,6 +539,7 @@ async def test_check_fields_validation_on_create_transfer( "source_params": {"type": "postgres", "table_name": "source_table"}, "target_params": {"type": "postgres", "table_name": "target_table"}, "strategy_params": {"type": "full"}, + "transformations": [], "queue_id": group_queue.id, } transfer_data.update(new_data) @@ -584,7 +710,7 @@ async def test_developer_plus_cannot_create_transfer_with_other_group_queue( } -async def test_developer_plus_can_not_create_transfer_with_target_format_json( +async def test_developer_plus_cannot_create_transfer_with_target_format_json( client: AsyncClient, two_group_connections: tuple[MockConnection, MockConnection], session: AsyncSession, diff --git a/tests/test_unit/test_transfers/test_file_transfers/test_create_transfer.py b/tests/test_unit/test_transfers/test_file_transfers/test_create_transfer.py index 38ac7590..2f16cf2b 100644 --- a/tests/test_unit/test_transfers/test_file_transfers/test_create_transfer.py +++ b/tests/test_unit/test_transfers/test_file_transfers/test_create_transfer.py @@ -146,6 +146,7 @@ async def test_developer_plus_can_create_s3_transfer( "source_params": transfer.source_params, "target_params": transfer.target_params, "strategy_params": transfer.strategy_params, + "transformations": transfer.transformations, "queue_id": transfer.queue_id, } @@ -304,6 +305,7 @@ async def test_developer_plus_can_create_hdfs_transfer( "source_params": transfer.source_params, "target_params": transfer.target_params, "strategy_params": transfer.strategy_params, + "transformations": transfer.transformations, "queue_id": transfer.queue_id, } diff --git a/tests/test_unit/test_transfers/test_file_transfers/test_read_transfer.py b/tests/test_unit/test_transfers/test_file_transfers/test_read_transfer.py index 412f90b6..53d2f544 100644 --- a/tests/test_unit/test_transfers/test_file_transfers/test_read_transfer.py +++ b/tests/test_unit/test_transfers/test_file_transfers/test_read_transfer.py @@ -10,58 +10,85 @@ "create_transfer_data", [ { - "type": "s3", - "directory_path": "/some/pure/path", - "file_format": { - "delimiter": ",", - "encoding": "utf-8", - "escape": "\\", - "include_header": False, - "line_sep": "\n", - "quote": '"', - "type": "csv", - "compression": "gzip", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/pure/path", + "file_format": { + "delimiter": ",", + "encoding": "utf-8", + "escape": "\\", + "include_header": False, + "line_sep": "\n", + "quote": '"', + "type": "csv", + "compression": "gzip", + }, + "options": {}, }, - "options": {}, + "transformations": [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "not_equal", + "field": "col1", + "value": "something", + }, + { + "type": "less_than", + "field": "col2", + "value": "20", + }, + ], + }, + ], }, { - "type": "s3", - "directory_path": "/some/excel/path", - "file_format": { - "type": "excel", - "include_header": True, - "start_cell": "A1", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/excel/path", + "file_format": { + "type": "excel", + "include_header": True, + "start_cell": "A1", + }, + "options": {}, }, - "options": {}, }, { - "type": "s3", - "directory_path": "/some/xml/path", - "file_format": { - "type": "xml", - "root_tag": "data", - "row_tag": "record", - "compression": "bzip2", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/xml/path", + "file_format": { + "type": "xml", + "root_tag": "data", + "row_tag": "record", + "compression": "bzip2", + }, + "options": {}, }, - "options": {}, }, { - "type": "s3", - "directory_path": "/some/orc/path", - "file_format": { - "type": "orc", - "compression": "zlib", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/orc/path", + "file_format": { + "type": "orc", + "compression": "zlib", + }, + "options": {}, }, - "options": {}, }, { - "type": "s3", - "directory_path": "/some/parquet/path", - "file_format": { - "type": "parquet", - "compression": "lz4", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/parquet/path", + "file_format": { + "type": "parquet", + "compression": "lz4", + }, + "options": {}, }, - "options": {}, }, ], ) @@ -104,6 +131,7 @@ async def test_guest_plus_can_read_s3_transfer( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, } assert result.status_code == 200 diff --git a/tests/test_unit/test_transfers/test_file_transfers/test_update_transfer.py b/tests/test_unit/test_transfers/test_file_transfers/test_update_transfer.py index f65a7f12..be09ae69 100644 --- a/tests/test_unit/test_transfers/test_file_transfers/test_update_transfer.py +++ b/tests/test_unit/test_transfers/test_file_transfers/test_update_transfer.py @@ -10,58 +10,85 @@ "create_transfer_data", [ { - "type": "s3", - "directory_path": "/some/pure/path", - "file_format": { - "delimiter": ",", - "encoding": "utf-8", - "escape": "\\", - "include_header": False, - "line_sep": "\n", - "quote": '"', - "type": "csv", - "compression": "gzip", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/pure/path", + "file_format": { + "delimiter": ",", + "encoding": "utf-8", + "escape": "\\", + "include_header": False, + "line_sep": "\n", + "quote": '"', + "type": "csv", + "compression": "gzip", + }, + "options": {}, }, - "options": {}, }, { - "type": "s3", - "directory_path": "/some/excel/path", - "file_format": { - "type": "excel", - "include_header": True, - "start_cell": "A1", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/excel/path", + "file_format": { + "type": "excel", + "include_header": True, + "start_cell": "A1", + }, + "options": {}, }, - "options": {}, }, { - "type": "s3", - "directory_path": "/some/xml/path", - "file_format": { - "type": "xml", - "root_tag": "data", - "row_tag": "record", - "compression": "bzip2", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/xml/path", + "file_format": { + "type": "xml", + "root_tag": "data", + "row_tag": "record", + "compression": "bzip2", + }, + "options": {}, }, - "options": {}, }, { - "type": "s3", - "directory_path": "/some/orc/path", - "file_format": { - "type": "orc", - "compression": "snappy", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/orc/path", + "file_format": { + "type": "orc", + "compression": "snappy", + }, + "options": {}, }, - "options": {}, }, { - "type": "s3", - "directory_path": "/some/parquet/path", - "file_format": { - "type": "parquet", - "compression": "snappy", + "source_and_target_params": { + "type": "s3", + "directory_path": "/some/parquet/path", + "file_format": { + "type": "parquet", + "compression": "snappy", + }, + "options": {}, }, - "options": {}, + "transformations": [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "greater_than", + "field": "col2", + "value": "30", + }, + { + "type": "like", + "field": "col1", + "value": "some%", + }, + ], + }, + ], }, ], ) @@ -87,6 +114,17 @@ async def test_developer_plus_can_update_s3_transfer( ): # Arrange user = group_transfer.owner_group.get_member_of_role(role_developer_plus) + transformations = [ + { + "type": "dataframe_rows_filter", + "filters": [ + { + "type": "is_not_null", + "field": "col2", + }, + ], + }, + ] # Act result = await client.patch( @@ -96,9 +134,10 @@ async def test_developer_plus_can_update_s3_transfer( "source_params": { "type": "s3", "directory_path": "/some/new/test/directory", - "file_format": create_transfer_data["file_format"], + "file_format": create_transfer_data["source_and_target_params"]["file_format"], "options": {"some": "option"}, }, + "transformations": transformations, }, ) @@ -107,7 +146,7 @@ async def test_developer_plus_can_update_s3_transfer( source_params.update( { "directory_path": "/some/new/test/directory", - "file_format": create_transfer_data["file_format"], + "file_format": create_transfer_data["source_and_target_params"]["file_format"], "options": {"some": "option"}, }, ) @@ -126,5 +165,6 @@ async def test_developer_plus_can_update_s3_transfer( "source_params": source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": transformations, "queue_id": group_transfer.transfer.queue_id, } diff --git a/tests/test_unit/test_transfers/test_read_transfer.py b/tests/test_unit/test_transfers/test_read_transfer.py index b49bb001..f1886fe8 100644 --- a/tests/test_unit/test_transfers/test_read_transfer.py +++ b/tests/test_unit/test_transfers/test_read_transfer.py @@ -33,6 +33,7 @@ async def test_guest_plus_can_read_transfer( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, } assert result.status_code == 200 @@ -110,6 +111,7 @@ async def test_superuser_can_read_transfer( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, } assert result.status_code == 200 diff --git a/tests/test_unit/test_transfers/test_read_transfers.py b/tests/test_unit/test_transfers/test_read_transfers.py index 2108d0a1..9390c657 100644 --- a/tests/test_unit/test_transfers/test_read_transfers.py +++ b/tests/test_unit/test_transfers/test_read_transfers.py @@ -49,6 +49,7 @@ async def test_guest_plus_can_read_transfers( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, }, ], @@ -116,6 +117,7 @@ async def test_superuser_can_read_transfers( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, }, ], @@ -173,6 +175,7 @@ async def test_search_transfers_with_query( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, }, ], @@ -253,6 +256,7 @@ async def test_filter_transfers( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, }, ], @@ -352,6 +356,7 @@ async def test_filter_transfers_with_multiple_transfers( "source_params": t.source_params, "target_params": t.target_params, "strategy_params": t.strategy_params, + "transformations": t.transformations, "queue_id": t.queue_id, } for t in expected_transfers diff --git a/tests/test_unit/test_transfers/test_update_transfer.py b/tests/test_unit/test_transfers/test_update_transfer.py index 8ca196b6..1ced65c7 100644 --- a/tests/test_unit/test_transfers/test_update_transfer.py +++ b/tests/test_unit/test_transfers/test_update_transfer.py @@ -36,6 +36,7 @@ async def test_developer_plus_can_update_transfer( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, } @@ -89,6 +90,7 @@ async def test_superuser_can_update_transfer( "source_params": group_transfer.source_params, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, } @@ -206,6 +208,7 @@ async def test_check_connection_types_and_its_params_transfer( }, "target_params": group_transfer.target_params, "strategy_params": group_transfer.strategy_params, + "transformations": group_transfer.transformations, "queue_id": group_transfer.transfer.queue_id, } assert result.status_code == 200 diff --git a/tests/test_unit/test_transfers/transfer_fixtures/transfer_fixture.py b/tests/test_unit/test_transfers/transfer_fixtures/transfer_fixture.py index 084b5faa..4ac28bb0 100644 --- a/tests/test_unit/test_transfers/transfer_fixtures/transfer_fixture.py +++ b/tests/test_unit/test_transfers/transfer_fixtures/transfer_fixture.py @@ -109,8 +109,9 @@ async def group_transfer( source_connection_id=source_connection.id, target_connection_id=target_connection.id, queue_id=queue.id, - source_params=create_transfer_data, - target_params=create_transfer_data, + source_params=create_transfer_data.get("source_and_target_params") if create_transfer_data else None, + target_params=create_transfer_data.get("source_and_target_params") if create_transfer_data else None, + transformations=create_transfer_data.get("transformations") if create_transfer_data else None, ) yield MockTransfer( diff --git a/tests/test_unit/test_transfers/transfer_fixtures/transfer_with_user_role_fixtures.py b/tests/test_unit/test_transfers/transfer_fixtures/transfer_with_user_role_fixtures.py index 91ec5403..e11d53fd 100644 --- a/tests/test_unit/test_transfers/transfer_fixtures/transfer_with_user_role_fixtures.py +++ b/tests/test_unit/test_transfers/transfer_fixtures/transfer_with_user_role_fixtures.py @@ -1,4 +1,5 @@ import secrets +from collections.abc import AsyncGenerator import pytest_asyncio from sqlalchemy.ext.asyncio import AsyncSession @@ -38,7 +39,7 @@ async def group_transfer_with_same_name_maintainer_plus( group_transfer: MockTransfer, role_maintainer_plus: UserTestRoles, role_maintainer_or_below_without_guest: UserTestRoles, -) -> str: +) -> AsyncGenerator[str, None]: user = group_transfer.owner_group.get_member_of_role(role_maintainer_plus) await add_user_to_group( @@ -113,7 +114,7 @@ async def group_transfer_and_group_connection_developer_plus( role_developer_plus: UserTestRoles, role_maintainer_or_below_without_guest: UserTestRoles, settings: Settings, -) -> tuple[str, Connection]: +) -> AsyncGenerator[tuple[str, Connection], None]: user = group_transfer.owner_group.get_member_of_role(role_developer_plus) await add_user_to_group( diff --git a/tests/test_unit/utils.py b/tests/test_unit/utils.py index b8330f01..558bf0f4 100644 --- a/tests/test_unit/utils.py +++ b/tests/test_unit/utils.py @@ -180,6 +180,7 @@ async def create_transfer( group_id: int | None = None, source_params: dict | None = None, target_params: dict | None = None, + transformations: list | None = None, is_scheduled: bool = True, schedule: str = "* * * * *", strategy_params: dict | None = None, @@ -193,6 +194,7 @@ async def create_transfer( source_params=source_params or {"type": "postgres", "table_name": "table1"}, target_connection_id=target_connection_id, target_params=target_params or {"type": "postgres", "table_name": "table1"}, + transformations=transformations or [], is_scheduled=is_scheduled, schedule=schedule, strategy_params=strategy_params or {"type": "full"},