From 2b08be7ef77b17410ac35b5109f7abeda4f03868 Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Wed, 18 Jun 2025 15:11:58 +0530 Subject: [PATCH 01/13] Add optional dependencies for testing This commit introduces optional dependencies specifically for testing purposes. The `pytest` and `pytest-mock` packages are added to the `pyproject.toml` file under the `optional-dependencies` section, allowing developers to easily install testing tools when needed. Additionally, a new `pytest.ini` configuration section is created to standardize test settings, including options for verbosity and test discovery patterns. Signed-off-by: Krishnaswamy Subramanian --- python/pyproject.toml | 17 ++ python/uv.lock | 438 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 python/uv.lock diff --git a/python/pyproject.toml b/python/pyproject.toml index c0b5f5772..90d4a05e7 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -35,6 +35,12 @@ dependencies = [ "kubeflow_trainer_api@git+https://github.com/kubeflow/trainer.git@master#subdirectory=api/python_api" ] +[project.optional-dependencies] +test = [ + "pytest>=7.0.0", + "pytest-mock>=3.10.0", +] + [project.urls] Homepage = "https://github.com/kubeflow/trainer" Documentation = "https://www.kubeflow.org/docs/components/trainer/" @@ -48,3 +54,14 @@ path = "kubeflow/trainer/__init__.py" [tool.hatch.metadata] allow-direct-references = true + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", + "-v", + "--tb=short", +] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] diff --git a/python/uv.lock b/python/uv.lock new file mode 100644 index 000000000..313b3d385 --- /dev/null +++ b/python/uv.lock @@ -0,0 +1,438 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "kubeflow" +source = { editable = "." } +dependencies = [ + { name = "kubeflow-trainer-api" }, + { name = "kubernetes" }, + { name = "pydantic" }, +] + +[package.optional-dependencies] +test = [ + { name = "pytest" }, + { name = "pytest-mock" }, +] + +[package.metadata] +requires-dist = [ + { name = "kubeflow-trainer-api", git = "https://github.com/kubeflow/trainer.git?subdirectory=api%2Fpython_api&rev=master" }, + { name = "kubernetes", specifier = ">=27.2.0" }, + { name = "pydantic", specifier = ">=2.10.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, + { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.10.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "kubeflow-trainer-api" +version = "2.0.0" +source = { git = "https://github.com/kubeflow/trainer.git?subdirectory=api%2Fpython_api&rev=master#b71a69064fa033c2ca51f6a24dbe199b552cada9" } +dependencies = [ + { name = "pydantic" }, +] + +[[package]] +name = "kubernetes" +version = "33.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/52/19ebe8004c243fdfa78268a96727c71e08f00ff6fe69a301d0b7fcbce3c2/kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993", size = 1036779, upload-time = "2025-06-09T21:57:58.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/43/d9bebfc3db7dea6ec80df5cb2aad8d274dd18ec2edd6c4f21f32c237cbbb/kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5", size = 1941335, upload-time = "2025-06-09T21:57:56.327Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/8a/6ea75ff7acf89f43afb157604429af4661a9840b1f2cece602b6a13c1893/oauthlib-3.3.0.tar.gz", hash = "sha256:4e707cf88d7dfc22a8cce22ca736a2eef9967c1dd3845efc0703fc922353eeb2", size = 190292, upload-time = "2025-06-17T23:19:18.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/3d/760b1456010ed11ce87c0109007f0166078dfdada7597f0091ae76eb7305/oauthlib-3.3.0-py3-none-any.whl", hash = "sha256:a2b3a0a2a4ec2feb4b9110f56674a39b2cc2f23e14713f4ed20441dfba14e934", size = 165155, upload-time = "2025-06-17T23:19:16.771Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] From c5cf2bc1e1d6e8098600200e2c23deb8b69c120a Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Wed, 18 Jun 2025 17:40:52 +0530 Subject: [PATCH 02/13] Add centralized trainer configurations and detection logic This commit introduces a new enumeration `TrainerFramework` to centralize the definitions of various trainer frameworks used in the Kubeflow SDK. The trainer configurations have been refactored into a dictionary `TRAINER_CONFIGS`, which maps each framework to its respective configuration, reducing duplication and improving maintainability. Additionally, the trainer detection logic has been enhanced to utilize image name patterns for identifying the appropriate trainer framework based on the container image name. This improves the robustness of trainer type detection and ensures backward compatibility with the existing `ALL_TRAINERS` mapping. - Added `TrainerFramework` enum for trainer framework constants. - Refactored trainer configurations into `TRAINER_CONFIGS`. - Enhanced trainer detection logic to support image name patterns. - Added unit tests for the new detection logic and configurations. Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/types/types.py | 48 +-- python/kubeflow/trainer/utils/utils.py | 80 ++++- python/tests/__init__.py | 1 + python/tests/test_utils.py | 403 +++++++++++++++++++++++++ 4 files changed, 509 insertions(+), 23 deletions(-) create mode 100644 python/tests/__init__.py create mode 100644 python/tests/test_utils.py diff --git a/python/kubeflow/trainer/types/types.py b/python/kubeflow/trainer/types/types.py index 97b375c1d..48af540ab 100644 --- a/python/kubeflow/trainer/types/types.py +++ b/python/kubeflow/trainer/types/types.py @@ -21,6 +21,15 @@ from kubeflow.trainer.constants import constants +# Trainer framework constants for easy reference +class TrainerFramework(Enum): + """Trainer framework constants.""" + TORCH = "torch" + DEEPSPEED = "deepspeed" + MLX = "mlx" + TORCHTUNE = "torchtune" + + # Configuration for the Custom Trainer. @dataclass class CustomTrainer: @@ -230,19 +239,16 @@ class Initializer: model: Optional[HuggingFaceModelInitializer] = None -# The dict where key is the container image and value its representation. -# Each Trainer representation defines trainer parameters (e.g. type, framework, entrypoint). -# TODO (andreyvelich): We should allow user to overrides the default image names. -ALL_TRAINERS: Dict[str, Trainer] = { - # Custom Trainers. - "pytorch/pytorch": Trainer( +# Centralized trainer configurations to eliminate duplication +TRAINER_CONFIGS: Dict[TrainerFramework, Trainer] = { + TrainerFramework.TORCH: Trainer( trainer_type=TrainerType.CUSTOM_TRAINER, framework=Framework.TORCH, entrypoint=[constants.TORCH_ENTRYPOINT], ), - "ghcr.io/kubeflow/trainer/mlx-runtime": Trainer( + TrainerFramework.DEEPSPEED: Trainer( trainer_type=TrainerType.CUSTOM_TRAINER, - framework=Framework.MLX, + framework=Framework.DEEPSPEED, entrypoint=[ constants.MPI_ENTRYPOINT, "--hostfile", @@ -251,9 +257,9 @@ class Initializer: "-c", ], ), - "ghcr.io/kubeflow/trainer/deepspeed-runtime": Trainer( + TrainerFramework.MLX: Trainer( trainer_type=TrainerType.CUSTOM_TRAINER, - framework=Framework.DEEPSPEED, + framework=Framework.MLX, entrypoint=[ constants.MPI_ENTRYPOINT, "--hostfile", @@ -262,20 +268,28 @@ class Initializer: "-c", ], ), - # Builtin Trainers. - "ghcr.io/kubeflow/trainer/torchtune-trainer": Trainer( + TrainerFramework.TORCHTUNE: Trainer( trainer_type=TrainerType.BUILTIN_TRAINER, framework=Framework.TORCHTUNE, entrypoint=constants.DEFAULT_TORCHTUNE_COMMAND, ), } + +# The dict where key is the container image and value its representation. +# Each Trainer representation defines trainer parameters (e.g. type, framework, entrypoint). +# TODO (andreyvelich): We should allow user to overrides the default image names. +ALL_TRAINERS: Dict[str, Trainer] = { + # Custom Trainers. + "pytorch/pytorch": TRAINER_CONFIGS[TrainerFramework.TORCH], + "ghcr.io/kubeflow/trainer/mlx-runtime": TRAINER_CONFIGS[TrainerFramework.MLX], + "ghcr.io/kubeflow/trainer/deepspeed-runtime": TRAINER_CONFIGS[TrainerFramework.DEEPSPEED], + # Builtin Trainers. + "ghcr.io/kubeflow/trainer/torchtune-trainer": TRAINER_CONFIGS[TrainerFramework.TORCHTUNE], +} + # The default trainer configuration when runtime detection fails -DEFAULT_TRAINER = Trainer( - trainer_type=TrainerType.CUSTOM_TRAINER, - framework=Framework.TORCH, - entrypoint=[constants.TORCH_ENTRYPOINT], -) +DEFAULT_TRAINER = TRAINER_CONFIGS[TrainerFramework.TORCH] # The default runtime configuration for the train() API DEFAULT_RUNTIME = Runtime( diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index efd8f8e1d..001fbd334 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import inspect import os import queue @@ -107,6 +108,74 @@ def get_runtime_trainer_container( return None +def _detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Trainer]: + """ + Detect trainer type based on image name patterns using regex. + + This method uses pattern matching on the image name to determine + the likely trainer type. + + Args: + image_name: The container image name + + Returns: + Trainer object if detected, None otherwise + """ + # DeepSpeed patterns + if re.search(r"deepspeed", image_name, re.IGNORECASE): + return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.DEEPSPEED]) + + # MLX patterns + if re.search(r"mlx", image_name, re.IGNORECASE): + return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.MLX]) + + # TorchTune patterns (check before PyTorch to avoid conflicts) + if re.search(r"torchtune", image_name, re.IGNORECASE): + return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.TORCHTUNE]) + + # PyTorch patterns (more specific to avoid matching torchtune) + if re.search(r"pytorch", image_name, re.IGNORECASE): + return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.TORCH]) + + # Generic torch patterns (but not torchtune) + if re.search(r"^torch(?!tune)", image_name, re.IGNORECASE): + return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.TORCH]) + + return None + + +def _detect_trainer( + trainer_container: models.IoK8sApiCoreV1Container, +) -> types.Trainer: + """ + Detect trainer type with precedence logic. + + This method implements the precedence order: + 1. Check existing ALL_TRAINERS mapping (backward compatibility) + 2. Use image pattern matching + 3. Fall back to DEFAULT_TRAINER + + Args: + trainer_container: The trainer container object + + Returns: + Trainer object + """ + image_name = trainer_container.image.split(":")[0] + + # 1. Check existing ALL_TRAINERS mapping (backward compatibility) + if image_name in types.ALL_TRAINERS: + return copy.deepcopy(types.ALL_TRAINERS[image_name]) + + # 2. Use image pattern matching + trainer = _detect_trainer_from_image_patterns(image_name) + if trainer: + return trainer + + # 3. Fall back to DEFAULT_TRAINER + return copy.deepcopy(types.DEFAULT_TRAINER) + + def get_runtime_trainer( replicated_jobs: List[models.JobsetV1alpha2ReplicatedJob], ml_policy: models.TrainerV1alpha1MLPolicy, @@ -121,20 +190,19 @@ def get_runtime_trainer( if not (trainer_container and trainer_container.image): raise Exception(f"Runtime doesn't have trainer container {replicated_jobs}") - # Extract image name from the container image to get appropriate Trainer. - image_name = trainer_container.image.split(":")[0] - trainer = types.ALL_TRAINERS.get(image_name, types.DEFAULT_TRAINER) + # Use the new detection logic with fallback + trainer = _detect_trainer(trainer_container) # Get the container devices. if devices := get_container_devices(trainer_container.resources): _, trainer.accelerator_count = devices # Torch and MPI plugins override accelerator count. - if ml_policy.torch and ml_policy.torch.num_proc_per_node: + if ml_policy.torch and ml_policy.torch.num_proc_per_node is not None: num_proc = ml_policy.torch.num_proc_per_node.actual_instance if isinstance(num_proc, int): trainer.accelerator_count = num_proc - elif ml_policy.mpi and ml_policy.mpi.num_proc_per_node: + elif ml_policy.mpi and ml_policy.mpi.num_proc_per_node is not None: trainer.accelerator_count = ml_policy.mpi.num_proc_per_node # Multiply accelerator_count by the number of nodes. @@ -212,7 +280,7 @@ def get_trainjob_node_step( # TODO (andreyvelich): We should also override the device_count # based on OMPI_MCA_orte_set_default_slots value. Right now, it is hard to do # since we inject this env only to the Launcher Pod. - step.name = f"{constants.NODE}-{job_index+1}" + step.name = f"{constants.NODE}-{job_index + 1}" if container.env: for env in container.env: diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 000000000..7d88c97b0 --- /dev/null +++ b/python/tests/__init__.py @@ -0,0 +1 @@ +# Tests for Kubeflow SDK \ No newline at end of file diff --git a/python/tests/test_utils.py b/python/tests/test_utils.py new file mode 100644 index 000000000..2efc3cd85 --- /dev/null +++ b/python/tests/test_utils.py @@ -0,0 +1,403 @@ +import pytest +from unittest.mock import Mock, patch +from kubeflow.trainer.utils import utils +from kubeflow.trainer.types import types +from kubeflow.trainer.constants import constants + + +class TestTrainerDetection: + """Test cases for trainer detection logic.""" + + @pytest.mark.parametrize( + "image_name,expected_framework", + [ + # Known images from ALL_TRAINERS + ("pytorch/pytorch", types.TrainerFramework.TORCH), + ("ghcr.io/kubeflow/trainer/mlx-runtime", types.TrainerFramework.MLX), + ( + "ghcr.io/kubeflow/trainer/deepspeed-runtime", + types.TrainerFramework.DEEPSPEED, + ), + ( + "ghcr.io/kubeflow/trainer/torchtune-trainer", + types.TrainerFramework.TORCHTUNE, + ), + # Custom images with pattern matching - lowercase + ("my-org/deepspeed-custom:latest", types.TrainerFramework.DEEPSPEED), + ("custom-mlx-runtime:v1.0", types.TrainerFramework.MLX), + ("pytorch-training:latest", types.TrainerFramework.TORCH), + ("torchtune-finetuning:latest", types.TrainerFramework.TORCHTUNE), + # Custom images with pattern matching - uppercase + ("my-org/DeepSpeed-Custom:latest", types.TrainerFramework.DEEPSPEED), + ("custom-MLX-runtime:v1.0", types.TrainerFramework.MLX), + ("PyTorch-training:latest", types.TrainerFramework.TORCH), + ("TorchTune-finetuning:latest", types.TrainerFramework.TORCHTUNE), + # Custom images with pattern matching - mixed case + ("my-org/DeepSpeed-custom:latest", types.TrainerFramework.DEEPSPEED), + ("custom-Mlx-runtime:v1.0", types.TrainerFramework.MLX), + ("PyTorch-Training:latest", types.TrainerFramework.TORCH), + ("TorchTune-Finetuning:latest", types.TrainerFramework.TORCHTUNE), + # Custom images with pattern matching - all caps + ("my-org/DEEPSPEED-CUSTOM:latest", types.TrainerFramework.DEEPSPEED), + ("custom-MLX-RUNTIME:v1.0", types.TrainerFramework.MLX), + ("PYTORCH-TRAINING:latest", types.TrainerFramework.TORCH), + ("TORCHTUNE-FINETUNING:latest", types.TrainerFramework.TORCHTUNE), + # Edge cases - partial matches + ("my-deepspeed-runtime:latest", types.TrainerFramework.DEEPSPEED), + ("mlx-custom:latest", types.TrainerFramework.MLX), + ("torch-custom:latest", types.TrainerFramework.TORCH), + ("pytorch-custom:latest", types.TrainerFramework.TORCH), + ("torchtune-custom:latest", types.TrainerFramework.TORCHTUNE), + # Edge cases - with numbers and special characters + ("deepspeed-v2.1:latest", types.TrainerFramework.DEEPSPEED), + ("mlx_runtime_1.0:latest", types.TrainerFramework.MLX), + ("pytorch_2.0_cuda:latest", types.TrainerFramework.TORCH), + ("torchtune-llama-3b:latest", types.TrainerFramework.TORCHTUNE), + # Edge cases - with registry prefixes + ("docker.io/myorg/deepspeed:latest", types.TrainerFramework.DEEPSPEED), + ("ghcr.io/myorg/mlx-runtime:latest", types.TrainerFramework.MLX), + ("quay.io/myorg/pytorch-training:latest", types.TrainerFramework.TORCH), + ( + "registry.example.com/myorg/torchtune:latest", + types.TrainerFramework.TORCHTUNE, + ), + # Edge cases - with ports and complex paths + ( + "registry.example.com:5000/myorg/deepspeed:latest", + types.TrainerFramework.DEEPSPEED, + ), + ("ghcr.io/myorg/mlx/runtime:v1.0", types.TrainerFramework.MLX), + ("docker.io/myorg/pytorch/training:latest", types.TrainerFramework.TORCH), + ( + "quay.io/myorg/torchtune/finetuning:latest", + types.TrainerFramework.TORCHTUNE, + ), + # Edge cases - no match (should fall back to default) + ("unknown-image:latest", types.TrainerFramework.TORCH), + ("", types.TrainerFramework.TORCH), + ("nginx:latest", types.TrainerFramework.TORCH), + ("ubuntu:20.04", types.TrainerFramework.TORCH), + ], + ) + def test_trainer_detection_from_image_patterns( + self, image_name, expected_framework + ): + """Test trainer detection using image pattern matching with various case scenarios.""" + trainer = utils._detect_trainer_from_image_patterns(image_name) + if expected_framework == types.TrainerFramework.TORCH and trainer is None: + # For unknown images, the _detect_trainer function should return default + # but _detect_trainer_from_image_patterns returns None + assert trainer is None + else: + assert trainer is not None + assert trainer.framework.value == expected_framework.value + + @pytest.mark.parametrize( + "image_name,expected_framework", + [ + # Known images (should use ALL_TRAINERS mapping) + ("pytorch/pytorch", types.TrainerFramework.TORCH), + ("ghcr.io/kubeflow/trainer/mlx-runtime", types.TrainerFramework.MLX), + ( + "ghcr.io/kubeflow/trainer/deepspeed-runtime", + types.TrainerFramework.DEEPSPEED, + ), + ( + "ghcr.io/kubeflow/trainer/torchtune-trainer", + types.TrainerFramework.TORCHTUNE, + ), + # Custom images with pattern matching - various cases + ("my-deepspeed-runtime:latest", types.TrainerFramework.DEEPSPEED), + ("custom-pytorch:latest", types.TrainerFramework.TORCH), + ("mlx-custom:latest", types.TrainerFramework.MLX), + ("torchtune-custom:latest", types.TrainerFramework.TORCHTUNE), + ("DeepSpeed-Custom:latest", types.TrainerFramework.DEEPSPEED), + ("PyTorch-Custom:latest", types.TrainerFramework.TORCH), + ("MLX-Custom:latest", types.TrainerFramework.MLX), + ("TorchTune-Custom:latest", types.TrainerFramework.TORCHTUNE), + # Fallback to default + ("completely-unknown:latest", types.TrainerFramework.TORCH), + ("nginx:latest", types.TrainerFramework.TORCH), + ], + ) + def test_trainer_detection_precedence(self, image_name, expected_framework): + """Test the precedence order of trainer detection methods.""" + # Create mock trainer container + trainer_container = Mock() + trainer_container.image = image_name + + trainer = utils._detect_trainer(trainer_container) + assert trainer is not None + assert trainer.framework.value == expected_framework.value + + def test_centralized_trainer_configs(self): + """Test that centralized trainer configurations are properly defined.""" + # Verify all trainer frameworks have configurations + for framework in types.TrainerFramework: + assert framework in types.TRAINER_CONFIGS + trainer = types.TRAINER_CONFIGS[framework] + assert trainer.framework.value == framework.value + + def test_all_trainers_uses_centralized_configs(self): + """Test that ALL_TRAINERS uses centralized configurations.""" + for image_name, trainer in types.ALL_TRAINERS.items(): + # Find the corresponding centralized config + found_config = None + for framework in types.TrainerFramework: + if types.TRAINER_CONFIGS[framework] == trainer: + found_config = framework + break + + assert found_config is not None, ( + f"Trainer for {image_name} not found in centralized configs" + ) + assert trainer.framework.value == found_config.value + + def test_default_trainer_uses_centralized_config(self): + """Test that DEFAULT_TRAINER uses centralized configuration.""" + assert ( + types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.TrainerFramework.TORCH] + ) + assert ( + types.DEFAULT_TRAINER.framework.value == types.TrainerFramework.TORCH.value + ) + + +class TestAcceleratorCountLogic: + """Test cases for accelerator count logic in get_runtime_trainer.""" + + @pytest.mark.parametrize( + "ml_policy_config,expected_accelerator_count", + [ + # Torch policies with different num_proc_per_node values + ({"torch": {"num_proc_per_node": 4}}, 4), + ({"torch": {"num_proc_per_node": 8}}, 8), + ( + {"torch": {"num_proc_per_node": "auto"}}, + None, + ), # String values should not set accelerator count + ( + {"torch": {"num_proc_per_node": "gpu"}}, + None, + ), # String values should not set accelerator count + ( + {"torch": {"num_proc_per_node": "cpu"}}, + None, + ), # String values should not set accelerator count + # MPI policies with different num_proc_per_node values + ({"mpi": {"num_proc_per_node": 2}}, 2), + ({"mpi": {"num_proc_per_node": 16}}, 16), + ({"mpi": {"num_proc_per_node": 1}}, 1), + # No policies + ({}, None), + ({"torch": {}}, None), + ({"mpi": {}}, None), + ], + ) + def test_accelerator_count_from_ml_policy( + self, ml_policy_config, expected_accelerator_count + ): + """Test that accelerator count is correctly set from ML policy.""" + with patch.object( + utils, "get_container_devices", return_value=None + ) as mock_get_devices: + # Create mock replicated jobs with proper structure + mock_container = Mock() + mock_container.image = "pytorch/pytorch:latest" + mock_resources = Mock() + mock_resources.limits = None + mock_container.resources = mock_resources + mock_container.name = constants.NODE + + mock_replicated_job = Mock() + mock_replicated_job.template = Mock() + mock_replicated_job.template.spec = Mock() + mock_replicated_job.template.spec.template = Mock() + mock_replicated_job.template.spec.template.spec = Mock() + mock_replicated_job.template.spec.template.spec.containers = [ + mock_container + ] + mock_replicated_job.template.metadata = Mock() + mock_replicated_job.template.metadata.labels = { + constants.TRAINJOB_ANCESTOR_LABEL: "trainer" + } + replicated_jobs = [mock_replicated_job] + + # Create mock ML policy + ml_policy = Mock() + ml_policy.num_nodes = None + + if "torch" in ml_policy_config: + ml_policy.torch = Mock() + if "num_proc_per_node" in ml_policy_config["torch"]: + mock_nppp_obj = Mock() + mock_nppp_obj.actual_instance = ml_policy_config["torch"][ + "num_proc_per_node" + ] + ml_policy.torch.num_proc_per_node = mock_nppp_obj + else: + ml_policy.torch.num_proc_per_node = None # Explicitly None + else: + ml_policy.torch = None + + if "mpi" in ml_policy_config: + ml_policy.mpi = Mock() + if "num_proc_per_node" in ml_policy_config["mpi"]: + ml_policy.mpi.num_proc_per_node = ml_policy_config["mpi"][ + "num_proc_per_node" + ] + else: + ml_policy.mpi.num_proc_per_node = None # Explicitly None + else: + ml_policy.mpi = None + + # Create mock runtime metadata + runtime_metadata = Mock() + runtime_metadata.labels = {} + + # Call the function + trainer = utils.get_runtime_trainer( + replicated_jobs, ml_policy, runtime_metadata + ) + + # Check accelerator count + if expected_accelerator_count is not None: + assert trainer.accelerator_count == expected_accelerator_count + else: + assert trainer.accelerator_count == constants.UNKNOWN + + @pytest.mark.parametrize( + "ml_policy_config,num_nodes,expected_accelerator_count", + [ + # Torch with num_nodes + ({"torch": {"num_proc_per_node": 4}}, 2, 8), # 4 * 2 = 8 + ({"torch": {"num_proc_per_node": 8}}, 3, 24), # 8 * 3 = 24 + # MPI with num_nodes + ({"mpi": {"num_proc_per_node": 2}}, 4, 8), # 2 * 4 = 8 + ({"mpi": {"num_proc_per_node": 16}}, 2, 32), # 16 * 2 = 32 + # String values should not be multiplied + ({"torch": {"num_proc_per_node": "auto"}}, 2, None), + ({"torch": {"num_proc_per_node": "gpu"}}, 3, None), + ], + ) + def test_accelerator_count_with_num_nodes( + self, ml_policy_config, num_nodes, expected_accelerator_count + ): + """Test that accelerator count is correctly multiplied by number of nodes.""" + with patch.object( + utils, "get_container_devices", return_value=None + ) as mock_get_devices: + # Create mock replicated jobs with proper structure + mock_container = Mock() + mock_container.image = "pytorch/pytorch:latest" + mock_resources = Mock() + mock_resources.limits = None + mock_container.resources = mock_resources + mock_container.name = constants.NODE + mock_replicated_job = Mock() + mock_replicated_job.template = Mock() + mock_replicated_job.template.spec = Mock() + mock_replicated_job.template.spec.template = Mock() + mock_replicated_job.template.spec.template.spec = Mock() + mock_replicated_job.template.spec.template.spec.containers = [ + mock_container + ] + mock_replicated_job.template.metadata = Mock() + mock_replicated_job.template.metadata.labels = { + constants.TRAINJOB_ANCESTOR_LABEL: "trainer" + } + replicated_jobs = [mock_replicated_job] + + # Create mock ML policy + ml_policy = Mock() + ml_policy.num_nodes = num_nodes # Use the num_nodes parameter + + if "torch" in ml_policy_config: + ml_policy.torch = Mock() + if "num_proc_per_node" in ml_policy_config["torch"]: + mock_nppp_obj = Mock() + mock_nppp_obj.actual_instance = ml_policy_config["torch"][ + "num_proc_per_node" + ] + ml_policy.torch.num_proc_per_node = mock_nppp_obj + else: + ml_policy.torch.num_proc_per_node = None # Explicitly None + else: + ml_policy.torch = None + + if "mpi" in ml_policy_config: + ml_policy.mpi = Mock() + if "num_proc_per_node" in ml_policy_config["mpi"]: + ml_policy.mpi.num_proc_per_node = ml_policy_config["mpi"][ + "num_proc_per_node" + ] + else: + ml_policy.mpi.num_proc_per_node = None # Explicitly None + else: + ml_policy.mpi = None + + # Create mock runtime metadata + runtime_metadata = Mock() + runtime_metadata.labels = {} + + # Call the function + trainer = utils.get_runtime_trainer( + replicated_jobs, ml_policy, runtime_metadata + ) + + # Check accelerator count + if expected_accelerator_count is not None: + assert trainer.accelerator_count == expected_accelerator_count + else: + assert trainer.accelerator_count == constants.UNKNOWN + + def test_accelerator_count_precedence(self): + """Test that torch policy takes precedence over mpi policy for accelerator count.""" + with patch.object( + utils, "get_container_devices", return_value=None + ) as mock_get_devices: + # Create mock replicated jobs with proper structure + mock_container = Mock() + mock_container.image = "pytorch/pytorch:latest" + mock_resources = Mock() + mock_resources.limits = None + mock_container.resources = mock_resources + mock_container.name = constants.NODE + + mock_replicated_job = Mock() + mock_replicated_job.template = Mock() + mock_replicated_job.template.spec = Mock() + mock_replicated_job.template.spec.template = Mock() + mock_replicated_job.template.spec.template.spec = Mock() + mock_replicated_job.template.spec.template.spec.containers = [ + mock_container + ] + mock_replicated_job.template.metadata = Mock() + mock_replicated_job.template.metadata.labels = { + constants.TRAINJOB_ANCESTOR_LABEL: "trainer" + } + replicated_jobs = [mock_replicated_job] + + # Create mock ML policy with both torch and mpi + ml_policy = Mock() + ml_policy.torch = Mock() + mock_nppp_torch = Mock() + mock_nppp_torch.actual_instance = 4 # Should take precedence + ml_policy.torch.num_proc_per_node = mock_nppp_torch + + ml_policy.mpi = Mock() + ml_policy.mpi.num_proc_per_node = 8 # Should be ignored + + ml_policy.num_nodes = None + + # Create mock runtime metadata + runtime_metadata = Mock() + runtime_metadata.labels = {} + + # Call the function + trainer = utils.get_runtime_trainer( + replicated_jobs, ml_policy, runtime_metadata + ) + + # Torch policy should take precedence + assert trainer.accelerator_count == 4 From e57b0031ecb700c4df87e6f19c67e1e82c37b83e Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 23 Jun 2025 14:33:21 +0530 Subject: [PATCH 03/13] Refactor TrainerFramework to Framework Enum Updated the TrainerFramework Enum to a more generic Framework Enum to improve code maintainability and clarity. This change simplifies the trainer configurations and associated functions by using the new Framework Enum, ensuring consistent references throughout the codebase. - Replaced TrainerFramework with Framework in types.py - Updated references in utils.py to reflect the new Enum - Adjusted test cases in test_utils.py to accommodate changes Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/types/types.py | 29 +++--- python/kubeflow/trainer/utils/utils.py | 10 +-- python/tests/test_utils.py | 120 ++++++++++++------------- 3 files changed, 75 insertions(+), 84 deletions(-) diff --git a/python/kubeflow/trainer/types/types.py b/python/kubeflow/trainer/types/types.py index 48af540ab..bd5c5bbe4 100644 --- a/python/kubeflow/trainer/types/types.py +++ b/python/kubeflow/trainer/types/types.py @@ -21,15 +21,6 @@ from kubeflow.trainer.constants import constants -# Trainer framework constants for easy reference -class TrainerFramework(Enum): - """Trainer framework constants.""" - TORCH = "torch" - DEEPSPEED = "deepspeed" - MLX = "mlx" - TORCHTUNE = "torchtune" - - # Configuration for the Custom Trainer. @dataclass class CustomTrainer: @@ -240,13 +231,13 @@ class Initializer: # Centralized trainer configurations to eliminate duplication -TRAINER_CONFIGS: Dict[TrainerFramework, Trainer] = { - TrainerFramework.TORCH: Trainer( +TRAINER_CONFIGS: Dict[Framework, Trainer] = { + Framework.TORCH: Trainer( trainer_type=TrainerType.CUSTOM_TRAINER, framework=Framework.TORCH, entrypoint=[constants.TORCH_ENTRYPOINT], ), - TrainerFramework.DEEPSPEED: Trainer( + Framework.DEEPSPEED: Trainer( trainer_type=TrainerType.CUSTOM_TRAINER, framework=Framework.DEEPSPEED, entrypoint=[ @@ -257,7 +248,7 @@ class Initializer: "-c", ], ), - TrainerFramework.MLX: Trainer( + Framework.MLX: Trainer( trainer_type=TrainerType.CUSTOM_TRAINER, framework=Framework.MLX, entrypoint=[ @@ -268,7 +259,7 @@ class Initializer: "-c", ], ), - TrainerFramework.TORCHTUNE: Trainer( + Framework.TORCHTUNE: Trainer( trainer_type=TrainerType.BUILTIN_TRAINER, framework=Framework.TORCHTUNE, entrypoint=constants.DEFAULT_TORCHTUNE_COMMAND, @@ -281,15 +272,15 @@ class Initializer: # TODO (andreyvelich): We should allow user to overrides the default image names. ALL_TRAINERS: Dict[str, Trainer] = { # Custom Trainers. - "pytorch/pytorch": TRAINER_CONFIGS[TrainerFramework.TORCH], - "ghcr.io/kubeflow/trainer/mlx-runtime": TRAINER_CONFIGS[TrainerFramework.MLX], - "ghcr.io/kubeflow/trainer/deepspeed-runtime": TRAINER_CONFIGS[TrainerFramework.DEEPSPEED], + "pytorch/pytorch": TRAINER_CONFIGS[Framework.TORCH], + "ghcr.io/kubeflow/trainer/mlx-runtime": TRAINER_CONFIGS[Framework.MLX], + "ghcr.io/kubeflow/trainer/deepspeed-runtime": TRAINER_CONFIGS[Framework.DEEPSPEED], # Builtin Trainers. - "ghcr.io/kubeflow/trainer/torchtune-trainer": TRAINER_CONFIGS[TrainerFramework.TORCHTUNE], + "ghcr.io/kubeflow/trainer/torchtune-trainer": TRAINER_CONFIGS[Framework.TORCHTUNE], } # The default trainer configuration when runtime detection fails -DEFAULT_TRAINER = TRAINER_CONFIGS[TrainerFramework.TORCH] +DEFAULT_TRAINER = TRAINER_CONFIGS[Framework.TORCH] # The default runtime configuration for the train() API DEFAULT_RUNTIME = Runtime( diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index 001fbd334..67144b2d9 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -123,23 +123,23 @@ def _detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Train """ # DeepSpeed patterns if re.search(r"deepspeed", image_name, re.IGNORECASE): - return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.DEEPSPEED]) + return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.DEEPSPEED]) # MLX patterns if re.search(r"mlx", image_name, re.IGNORECASE): - return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.MLX]) + return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.MLX]) # TorchTune patterns (check before PyTorch to avoid conflicts) if re.search(r"torchtune", image_name, re.IGNORECASE): - return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.TORCHTUNE]) + return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCHTUNE]) # PyTorch patterns (more specific to avoid matching torchtune) if re.search(r"pytorch", image_name, re.IGNORECASE): - return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.TORCH]) + return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCH]) # Generic torch patterns (but not torchtune) if re.search(r"^torch(?!tune)", image_name, re.IGNORECASE): - return copy.deepcopy(types.TRAINER_CONFIGS[types.TrainerFramework.TORCH]) + return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCH]) return None diff --git a/python/tests/test_utils.py b/python/tests/test_utils.py index 2efc3cd85..3ccb3adfe 100644 --- a/python/tests/test_utils.py +++ b/python/tests/test_utils.py @@ -12,71 +12,71 @@ class TestTrainerDetection: "image_name,expected_framework", [ # Known images from ALL_TRAINERS - ("pytorch/pytorch", types.TrainerFramework.TORCH), - ("ghcr.io/kubeflow/trainer/mlx-runtime", types.TrainerFramework.MLX), + ("pytorch/pytorch", types.Framework.TORCH), + ("ghcr.io/kubeflow/trainer/mlx-runtime", types.Framework.MLX), ( "ghcr.io/kubeflow/trainer/deepspeed-runtime", - types.TrainerFramework.DEEPSPEED, + types.Framework.DEEPSPEED, ), ( "ghcr.io/kubeflow/trainer/torchtune-trainer", - types.TrainerFramework.TORCHTUNE, + types.Framework.TORCHTUNE, ), # Custom images with pattern matching - lowercase - ("my-org/deepspeed-custom:latest", types.TrainerFramework.DEEPSPEED), - ("custom-mlx-runtime:v1.0", types.TrainerFramework.MLX), - ("pytorch-training:latest", types.TrainerFramework.TORCH), - ("torchtune-finetuning:latest", types.TrainerFramework.TORCHTUNE), + ("my-org/deepspeed-custom:latest", types.Framework.DEEPSPEED), + ("custom-mlx-runtime:v1.0", types.Framework.MLX), + ("pytorch-training:latest", types.Framework.TORCH), + ("torchtune-finetuning:latest", types.Framework.TORCHTUNE), # Custom images with pattern matching - uppercase - ("my-org/DeepSpeed-Custom:latest", types.TrainerFramework.DEEPSPEED), - ("custom-MLX-runtime:v1.0", types.TrainerFramework.MLX), - ("PyTorch-training:latest", types.TrainerFramework.TORCH), - ("TorchTune-finetuning:latest", types.TrainerFramework.TORCHTUNE), + ("my-org/DeepSpeed-Custom:latest", types.Framework.DEEPSPEED), + ("custom-MLX-runtime:v1.0", types.Framework.MLX), + ("PyTorch-training:latest", types.Framework.TORCH), + ("TorchTune-finetuning:latest", types.Framework.TORCHTUNE), # Custom images with pattern matching - mixed case - ("my-org/DeepSpeed-custom:latest", types.TrainerFramework.DEEPSPEED), - ("custom-Mlx-runtime:v1.0", types.TrainerFramework.MLX), - ("PyTorch-Training:latest", types.TrainerFramework.TORCH), - ("TorchTune-Finetuning:latest", types.TrainerFramework.TORCHTUNE), + ("my-org/DeepSpeed-custom:latest", types.Framework.DEEPSPEED), + ("custom-Mlx-runtime:v1.0", types.Framework.MLX), + ("PyTorch-Training:latest", types.Framework.TORCH), + ("TorchTune-Finetuning:latest", types.Framework.TORCHTUNE), # Custom images with pattern matching - all caps - ("my-org/DEEPSPEED-CUSTOM:latest", types.TrainerFramework.DEEPSPEED), - ("custom-MLX-RUNTIME:v1.0", types.TrainerFramework.MLX), - ("PYTORCH-TRAINING:latest", types.TrainerFramework.TORCH), - ("TORCHTUNE-FINETUNING:latest", types.TrainerFramework.TORCHTUNE), + ("my-org/DEEPSPEED-CUSTOM:latest", types.Framework.DEEPSPEED), + ("custom-MLX-RUNTIME:v1.0", types.Framework.MLX), + ("PYTORCH-TRAINING:latest", types.Framework.TORCH), + ("TORCHTUNE-FINETUNING:latest", types.Framework.TORCHTUNE), # Edge cases - partial matches - ("my-deepspeed-runtime:latest", types.TrainerFramework.DEEPSPEED), - ("mlx-custom:latest", types.TrainerFramework.MLX), - ("torch-custom:latest", types.TrainerFramework.TORCH), - ("pytorch-custom:latest", types.TrainerFramework.TORCH), - ("torchtune-custom:latest", types.TrainerFramework.TORCHTUNE), + ("my-deepspeed-runtime:latest", types.Framework.DEEPSPEED), + ("mlx-custom:latest", types.Framework.MLX), + ("torch-custom:latest", types.Framework.TORCH), + ("pytorch-custom:latest", types.Framework.TORCH), + ("torchtune-custom:latest", types.Framework.TORCHTUNE), # Edge cases - with numbers and special characters - ("deepspeed-v2.1:latest", types.TrainerFramework.DEEPSPEED), - ("mlx_runtime_1.0:latest", types.TrainerFramework.MLX), - ("pytorch_2.0_cuda:latest", types.TrainerFramework.TORCH), - ("torchtune-llama-3b:latest", types.TrainerFramework.TORCHTUNE), + ("deepspeed-v2.1:latest", types.Framework.DEEPSPEED), + ("mlx_runtime_1.0:latest", types.Framework.MLX), + ("pytorch_2.0_cuda:latest", types.Framework.TORCH), + ("torchtune-llama-3b:latest", types.Framework.TORCHTUNE), # Edge cases - with registry prefixes - ("docker.io/myorg/deepspeed:latest", types.TrainerFramework.DEEPSPEED), - ("ghcr.io/myorg/mlx-runtime:latest", types.TrainerFramework.MLX), - ("quay.io/myorg/pytorch-training:latest", types.TrainerFramework.TORCH), + ("docker.io/myorg/deepspeed:latest", types.Framework.DEEPSPEED), + ("ghcr.io/myorg/mlx-runtime:latest", types.Framework.MLX), + ("quay.io/myorg/pytorch-training:latest", types.Framework.TORCH), ( "registry.example.com/myorg/torchtune:latest", - types.TrainerFramework.TORCHTUNE, + types.Framework.TORCHTUNE, ), # Edge cases - with ports and complex paths ( "registry.example.com:5000/myorg/deepspeed:latest", - types.TrainerFramework.DEEPSPEED, + types.Framework.DEEPSPEED, ), - ("ghcr.io/myorg/mlx/runtime:v1.0", types.TrainerFramework.MLX), - ("docker.io/myorg/pytorch/training:latest", types.TrainerFramework.TORCH), + ("ghcr.io/myorg/mlx/runtime:v1.0", types.Framework.MLX), + ("docker.io/myorg/pytorch/training:latest", types.Framework.TORCH), ( "quay.io/myorg/torchtune/finetuning:latest", - types.TrainerFramework.TORCHTUNE, + types.Framework.TORCHTUNE, ), # Edge cases - no match (should fall back to default) - ("unknown-image:latest", types.TrainerFramework.TORCH), - ("", types.TrainerFramework.TORCH), - ("nginx:latest", types.TrainerFramework.TORCH), - ("ubuntu:20.04", types.TrainerFramework.TORCH), + ("unknown-image:latest", types.Framework.TORCH), + ("", types.Framework.TORCH), + ("nginx:latest", types.Framework.TORCH), + ("ubuntu:20.04", types.Framework.TORCH), ], ) def test_trainer_detection_from_image_patterns( @@ -84,7 +84,7 @@ def test_trainer_detection_from_image_patterns( ): """Test trainer detection using image pattern matching with various case scenarios.""" trainer = utils._detect_trainer_from_image_patterns(image_name) - if expected_framework == types.TrainerFramework.TORCH and trainer is None: + if expected_framework == types.Framework.TORCH and trainer is None: # For unknown images, the _detect_trainer function should return default # but _detect_trainer_from_image_patterns returns None assert trainer is None @@ -96,28 +96,28 @@ def test_trainer_detection_from_image_patterns( "image_name,expected_framework", [ # Known images (should use ALL_TRAINERS mapping) - ("pytorch/pytorch", types.TrainerFramework.TORCH), - ("ghcr.io/kubeflow/trainer/mlx-runtime", types.TrainerFramework.MLX), + ("pytorch/pytorch", types.Framework.TORCH), + ("ghcr.io/kubeflow/trainer/mlx-runtime", types.Framework.MLX), ( "ghcr.io/kubeflow/trainer/deepspeed-runtime", - types.TrainerFramework.DEEPSPEED, + types.Framework.DEEPSPEED, ), ( "ghcr.io/kubeflow/trainer/torchtune-trainer", - types.TrainerFramework.TORCHTUNE, + types.Framework.TORCHTUNE, ), # Custom images with pattern matching - various cases - ("my-deepspeed-runtime:latest", types.TrainerFramework.DEEPSPEED), - ("custom-pytorch:latest", types.TrainerFramework.TORCH), - ("mlx-custom:latest", types.TrainerFramework.MLX), - ("torchtune-custom:latest", types.TrainerFramework.TORCHTUNE), - ("DeepSpeed-Custom:latest", types.TrainerFramework.DEEPSPEED), - ("PyTorch-Custom:latest", types.TrainerFramework.TORCH), - ("MLX-Custom:latest", types.TrainerFramework.MLX), - ("TorchTune-Custom:latest", types.TrainerFramework.TORCHTUNE), + ("my-deepspeed-runtime:latest", types.Framework.DEEPSPEED), + ("custom-pytorch:latest", types.Framework.TORCH), + ("mlx-custom:latest", types.Framework.MLX), + ("torchtune-custom:latest", types.Framework.TORCHTUNE), + ("DeepSpeed-Custom:latest", types.Framework.DEEPSPEED), + ("PyTorch-Custom:latest", types.Framework.TORCH), + ("MLX-Custom:latest", types.Framework.MLX), + ("TorchTune-Custom:latest", types.Framework.TORCHTUNE), # Fallback to default - ("completely-unknown:latest", types.TrainerFramework.TORCH), - ("nginx:latest", types.TrainerFramework.TORCH), + ("completely-unknown:latest", types.Framework.TORCH), + ("nginx:latest", types.Framework.TORCH), ], ) def test_trainer_detection_precedence(self, image_name, expected_framework): @@ -133,7 +133,7 @@ def test_trainer_detection_precedence(self, image_name, expected_framework): def test_centralized_trainer_configs(self): """Test that centralized trainer configurations are properly defined.""" # Verify all trainer frameworks have configurations - for framework in types.TrainerFramework: + for framework in types.Framework: assert framework in types.TRAINER_CONFIGS trainer = types.TRAINER_CONFIGS[framework] assert trainer.framework.value == framework.value @@ -143,7 +143,7 @@ def test_all_trainers_uses_centralized_configs(self): for image_name, trainer in types.ALL_TRAINERS.items(): # Find the corresponding centralized config found_config = None - for framework in types.TrainerFramework: + for framework in types.Framework: if types.TRAINER_CONFIGS[framework] == trainer: found_config = framework break @@ -156,10 +156,10 @@ def test_all_trainers_uses_centralized_configs(self): def test_default_trainer_uses_centralized_config(self): """Test that DEFAULT_TRAINER uses centralized configuration.""" assert ( - types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.TrainerFramework.TORCH] + types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.Framework.TORCH] ) assert ( - types.DEFAULT_TRAINER.framework.value == types.TrainerFramework.TORCH.value + types.DEFAULT_TRAINER.framework.value == types.Framework.TORCH.value ) From f3f3bcf3de06a0093052e90802f1f2b02e3278de Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 23 Jun 2025 14:44:11 +0530 Subject: [PATCH 04/13] Update trainer detection tests to handle edge cases Refactor the test cases in `test_utils.py` to adjust the expected output for edge cases where no matching framework is found. This change ensures that the tests handle cases where the image does not correspond to any known framework by returning `None` instead of a default framework. Signed-off-by: Krishnaswamy Subramanian --- python/tests/test_utils.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/python/tests/test_utils.py b/python/tests/test_utils.py index 3ccb3adfe..0ad5ddaf0 100644 --- a/python/tests/test_utils.py +++ b/python/tests/test_utils.py @@ -72,11 +72,11 @@ class TestTrainerDetection: "quay.io/myorg/torchtune/finetuning:latest", types.Framework.TORCHTUNE, ), - # Edge cases - no match (should fall back to default) - ("unknown-image:latest", types.Framework.TORCH), - ("", types.Framework.TORCH), - ("nginx:latest", types.Framework.TORCH), - ("ubuntu:20.04", types.Framework.TORCH), + # Edge cases - no match + ("unknown-image:latest", None), + ("", None), + ("nginx:latest", None), + ("ubuntu:20.04", None), ], ) def test_trainer_detection_from_image_patterns( @@ -84,13 +84,11 @@ def test_trainer_detection_from_image_patterns( ): """Test trainer detection using image pattern matching with various case scenarios.""" trainer = utils._detect_trainer_from_image_patterns(image_name) - if expected_framework == types.Framework.TORCH and trainer is None: - # For unknown images, the _detect_trainer function should return default - # but _detect_trainer_from_image_patterns returns None + + if expected_framework is None: assert trainer is None else: - assert trainer is not None - assert trainer.framework.value == expected_framework.value + assert trainer.framework == expected_framework @pytest.mark.parametrize( "image_name,expected_framework", @@ -128,7 +126,7 @@ def test_trainer_detection_precedence(self, image_name, expected_framework): trainer = utils._detect_trainer(trainer_container) assert trainer is not None - assert trainer.framework.value == expected_framework.value + assert trainer.framework == expected_framework def test_centralized_trainer_configs(self): """Test that centralized trainer configurations are properly defined.""" @@ -136,7 +134,7 @@ def test_centralized_trainer_configs(self): for framework in types.Framework: assert framework in types.TRAINER_CONFIGS trainer = types.TRAINER_CONFIGS[framework] - assert trainer.framework.value == framework.value + assert trainer.framework == framework def test_all_trainers_uses_centralized_configs(self): """Test that ALL_TRAINERS uses centralized configurations.""" @@ -151,16 +149,12 @@ def test_all_trainers_uses_centralized_configs(self): assert found_config is not None, ( f"Trainer for {image_name} not found in centralized configs" ) - assert trainer.framework.value == found_config.value + assert trainer.framework == found_config def test_default_trainer_uses_centralized_config(self): """Test that DEFAULT_TRAINER uses centralized configuration.""" - assert ( - types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.Framework.TORCH] - ) - assert ( - types.DEFAULT_TRAINER.framework.value == types.Framework.TORCH.value - ) + assert types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.Framework.TORCH] + assert types.DEFAULT_TRAINER.framework == types.Framework.TORCH class TestAcceleratorCountLogic: From 9ef621e6e2727fa820aaf0f01ad25236948c1370 Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 30 Jun 2025 16:28:12 +0530 Subject: [PATCH 05/13] Reorganize tests to co-location with source files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move test files from tests/ directory to be co-located with source files and split types-related tests into a separate file: - tests/test_utils.py → kubeflow/trainer/utils/utils_test.py - Extract types tests → kubeflow/trainer/types/types_test.py - Update pyproject.toml testpaths: ["tests"] → ["kubeflow"] - Remove tests/ directory This improves code organization by keeping tests next to the code they validate, making it easier to maintain test coverage when modifying source files. Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/types/types_test.py | 18 +++++++++++++ .../trainer/utils/utils_test.py} | 26 ------------------- python/pyproject.toml | 2 +- python/tests/__init__.py | 1 - 4 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 python/kubeflow/trainer/types/types_test.py rename python/{tests/test_utils.py => kubeflow/trainer/utils/utils_test.py} (92%) delete mode 100644 python/tests/__init__.py diff --git a/python/kubeflow/trainer/types/types_test.py b/python/kubeflow/trainer/types/types_test.py new file mode 100644 index 000000000..e43a42ebb --- /dev/null +++ b/python/kubeflow/trainer/types/types_test.py @@ -0,0 +1,18 @@ +from kubeflow.trainer.types import types + + +class TestTrainerConfigurations: + """Test cases for trainer configurations and types.""" + + def test_centralized_trainer_configs(self): + """Test that centralized trainer configurations are properly defined.""" + # Verify all trainer frameworks have configurations + for framework in types.Framework: + assert framework in types.TRAINER_CONFIGS + trainer = types.TRAINER_CONFIGS[framework] + assert trainer.framework == framework + + def test_default_trainer_uses_centralized_config(self): + """Test that DEFAULT_TRAINER uses centralized configuration.""" + assert types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.Framework.TORCH] + assert types.DEFAULT_TRAINER.framework == types.Framework.TORCH \ No newline at end of file diff --git a/python/tests/test_utils.py b/python/kubeflow/trainer/utils/utils_test.py similarity index 92% rename from python/tests/test_utils.py rename to python/kubeflow/trainer/utils/utils_test.py index 0ad5ddaf0..2e5b494bf 100644 --- a/python/tests/test_utils.py +++ b/python/kubeflow/trainer/utils/utils_test.py @@ -128,33 +128,7 @@ def test_trainer_detection_precedence(self, image_name, expected_framework): assert trainer is not None assert trainer.framework == expected_framework - def test_centralized_trainer_configs(self): - """Test that centralized trainer configurations are properly defined.""" - # Verify all trainer frameworks have configurations - for framework in types.Framework: - assert framework in types.TRAINER_CONFIGS - trainer = types.TRAINER_CONFIGS[framework] - assert trainer.framework == framework - - def test_all_trainers_uses_centralized_configs(self): - """Test that ALL_TRAINERS uses centralized configurations.""" - for image_name, trainer in types.ALL_TRAINERS.items(): - # Find the corresponding centralized config - found_config = None - for framework in types.Framework: - if types.TRAINER_CONFIGS[framework] == trainer: - found_config = framework - break - - assert found_config is not None, ( - f"Trainer for {image_name} not found in centralized configs" - ) - assert trainer.framework == found_config - def test_default_trainer_uses_centralized_config(self): - """Test that DEFAULT_TRAINER uses centralized configuration.""" - assert types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.Framework.TORCH] - assert types.DEFAULT_TRAINER.framework == types.Framework.TORCH class TestAcceleratorCountLogic: diff --git a/python/pyproject.toml b/python/pyproject.toml index 90d4a05e7..7191d0b04 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -61,7 +61,7 @@ addopts = [ "-v", "--tb=short", ] -testpaths = ["tests"] +testpaths = ["kubeflow"] python_files = ["test_*.py", "*_test.py"] python_classes = ["Test*"] python_functions = ["test_*"] diff --git a/python/tests/__init__.py b/python/tests/__init__.py deleted file mode 100644 index 7d88c97b0..000000000 --- a/python/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Tests for Kubeflow SDK \ No newline at end of file From 82ecdf3eb304fad61bf47b5652b9765a057037e3 Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 30 Jun 2025 16:36:20 +0530 Subject: [PATCH 06/13] Remove underscore prefixes from utility functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove underscore prefixes from detect_trainer_from_image_patterns() and detect_trainer() to follow established codebase conventions. Analysis shows no other utility functions in the codebase use underscore prefixes. Functions renamed: - _detect_trainer_from_image_patterns → detect_trainer_from_image_patterns - _detect_trainer → detect_trainer Update all function calls and tests accordingly. Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/utils/utils.py | 8 ++++---- python/kubeflow/trainer/utils/utils_test.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index 67144b2d9..0acb58da2 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -108,7 +108,7 @@ def get_runtime_trainer_container( return None -def _detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Trainer]: +def detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Trainer]: """ Detect trainer type based on image name patterns using regex. @@ -144,7 +144,7 @@ def _detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Train return None -def _detect_trainer( +def detect_trainer( trainer_container: models.IoK8sApiCoreV1Container, ) -> types.Trainer: """ @@ -168,7 +168,7 @@ def _detect_trainer( return copy.deepcopy(types.ALL_TRAINERS[image_name]) # 2. Use image pattern matching - trainer = _detect_trainer_from_image_patterns(image_name) + trainer = detect_trainer_from_image_patterns(image_name) if trainer: return trainer @@ -191,7 +191,7 @@ def get_runtime_trainer( raise Exception(f"Runtime doesn't have trainer container {replicated_jobs}") # Use the new detection logic with fallback - trainer = _detect_trainer(trainer_container) + trainer = detect_trainer(trainer_container) # Get the container devices. if devices := get_container_devices(trainer_container.resources): diff --git a/python/kubeflow/trainer/utils/utils_test.py b/python/kubeflow/trainer/utils/utils_test.py index 2e5b494bf..06e697c6c 100644 --- a/python/kubeflow/trainer/utils/utils_test.py +++ b/python/kubeflow/trainer/utils/utils_test.py @@ -83,7 +83,7 @@ def test_trainer_detection_from_image_patterns( self, image_name, expected_framework ): """Test trainer detection using image pattern matching with various case scenarios.""" - trainer = utils._detect_trainer_from_image_patterns(image_name) + trainer = utils.detect_trainer_from_image_patterns(image_name) if expected_framework is None: assert trainer is None @@ -124,7 +124,7 @@ def test_trainer_detection_precedence(self, image_name, expected_framework): trainer_container = Mock() trainer_container.image = image_name - trainer = utils._detect_trainer(trainer_container) + trainer = utils.detect_trainer(trainer_container) assert trainer is not None assert trainer.framework == expected_framework From 76eba6de0fc64c27252d869751dceffbdcc1d027 Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 30 Jun 2025 16:38:19 +0530 Subject: [PATCH 07/13] Require explicit 'pytorch' in image names for PyTorch detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove generic 'torch' pattern matching and require explicit 'pytorch' in image names for better framework distinction. This prevents ambiguity between PyTorch and other torch-related libraries. - Remove regex pattern: r'^torch(?!tune)' - Keep only: r'pytorch' for PyTorch detection - Update test case: 'torch-custom:latest' → 'pytorch-torch-custom:latest' - Add test case: 'torch-custom:latest' now returns None This ensures clearer separation between PyTorch and TorchTune images. Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/utils/utils.py | 6 +----- python/kubeflow/trainer/utils/utils_test.py | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index 0acb58da2..95db52228 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -133,14 +133,10 @@ def detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Traine if re.search(r"torchtune", image_name, re.IGNORECASE): return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCHTUNE]) - # PyTorch patterns (more specific to avoid matching torchtune) + # PyTorch patterns - require explicit "pytorch" in image name for clarity if re.search(r"pytorch", image_name, re.IGNORECASE): return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCH]) - # Generic torch patterns (but not torchtune) - if re.search(r"^torch(?!tune)", image_name, re.IGNORECASE): - return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCH]) - return None diff --git a/python/kubeflow/trainer/utils/utils_test.py b/python/kubeflow/trainer/utils/utils_test.py index 06e697c6c..742c73019 100644 --- a/python/kubeflow/trainer/utils/utils_test.py +++ b/python/kubeflow/trainer/utils/utils_test.py @@ -45,7 +45,7 @@ class TestTrainerDetection: # Edge cases - partial matches ("my-deepspeed-runtime:latest", types.Framework.DEEPSPEED), ("mlx-custom:latest", types.Framework.MLX), - ("torch-custom:latest", types.Framework.TORCH), + ("pytorch-torch-custom:latest", types.Framework.TORCH), ("pytorch-custom:latest", types.Framework.TORCH), ("torchtune-custom:latest", types.Framework.TORCHTUNE), # Edge cases - with numbers and special characters @@ -72,7 +72,11 @@ class TestTrainerDetection: "quay.io/myorg/torchtune/finetuning:latest", types.Framework.TORCHTUNE, ), - # Edge cases - no match + # Edge cases - no match (including generic torch without pytorch) + ( + "torch-custom:latest", + None, + ), # Generic torch should not match (requires pytorch) ("unknown-image:latest", None), ("", None), ("nginx:latest", None), From c0a7d937ab6324e27defe25b004a566393d3de9a Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 30 Jun 2025 16:39:28 +0530 Subject: [PATCH 08/13] Add explanatory comments for essential None checks Add detailed comments explaining why 'is not None' checks are necessary in ML policy processing: 1. For torch: prevents AttributeError when accessing None.actual_instance 2. For MPI: prevents setting accelerator_count to None 3. Semantically: only override when user explicitly provides values These checks prevent runtime errors and ensure correct behavior when ML policies have undefined num_proc_per_node values. Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/utils/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index 95db52228..643989fde 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -194,6 +194,10 @@ def get_runtime_trainer( _, trainer.accelerator_count = devices # Torch and MPI plugins override accelerator count. + # NOTE: The 'is not None' checks are essential because: + # 1. For torch: prevents AttributeError when accessing None.actual_instance + # 2. For MPI: prevents setting accelerator_count to None + # 3. Semantically: only override when user explicitly provides num_proc_per_node if ml_policy.torch and ml_policy.torch.num_proc_per_node is not None: num_proc = ml_policy.torch.num_proc_per_node.actual_instance if isinstance(num_proc, int): From baf87ba5fa4819e01384044e61a4479bc1bbc21c Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 30 Jun 2025 16:43:04 +0530 Subject: [PATCH 09/13] Remove redundant ALL_TRAINERS dictionary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminate ALL_TRAINERS and rely solely on regex pattern matching for trainer detection. This removes duplication between static mapping and TRAINER_CONFIGS while maintaining full functionality. - Remove ALL_TRAINERS from types.py - Simplify detect_trainer(): regex patterns → DEFAULT_TRAINER fallback - Update tests to verify official images work with regex patterns All official Kubeflow images correctly detected by regex, ensuring no breaking changes while reducing architectural complexity. The regex patterns now serve as the single source of truth. Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/types/types.py | 13 ------------ python/kubeflow/trainer/types/types_test.py | 2 +- python/kubeflow/trainer/utils/utils.py | 17 ++++++--------- python/kubeflow/trainer/utils/utils_test.py | 23 ++++++++++++++++++--- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/python/kubeflow/trainer/types/types.py b/python/kubeflow/trainer/types/types.py index bd5c5bbe4..ee4ea2ce3 100644 --- a/python/kubeflow/trainer/types/types.py +++ b/python/kubeflow/trainer/types/types.py @@ -266,19 +266,6 @@ class Initializer: ), } - -# The dict where key is the container image and value its representation. -# Each Trainer representation defines trainer parameters (e.g. type, framework, entrypoint). -# TODO (andreyvelich): We should allow user to overrides the default image names. -ALL_TRAINERS: Dict[str, Trainer] = { - # Custom Trainers. - "pytorch/pytorch": TRAINER_CONFIGS[Framework.TORCH], - "ghcr.io/kubeflow/trainer/mlx-runtime": TRAINER_CONFIGS[Framework.MLX], - "ghcr.io/kubeflow/trainer/deepspeed-runtime": TRAINER_CONFIGS[Framework.DEEPSPEED], - # Builtin Trainers. - "ghcr.io/kubeflow/trainer/torchtune-trainer": TRAINER_CONFIGS[Framework.TORCHTUNE], -} - # The default trainer configuration when runtime detection fails DEFAULT_TRAINER = TRAINER_CONFIGS[Framework.TORCH] diff --git a/python/kubeflow/trainer/types/types_test.py b/python/kubeflow/trainer/types/types_test.py index e43a42ebb..3730d9d0e 100644 --- a/python/kubeflow/trainer/types/types_test.py +++ b/python/kubeflow/trainer/types/types_test.py @@ -15,4 +15,4 @@ def test_centralized_trainer_configs(self): def test_default_trainer_uses_centralized_config(self): """Test that DEFAULT_TRAINER uses centralized configuration.""" assert types.DEFAULT_TRAINER == types.TRAINER_CONFIGS[types.Framework.TORCH] - assert types.DEFAULT_TRAINER.framework == types.Framework.TORCH \ No newline at end of file + assert types.DEFAULT_TRAINER.framework == types.Framework.TORCH diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index 643989fde..a9cf3bd79 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -144,12 +144,11 @@ def detect_trainer( trainer_container: models.IoK8sApiCoreV1Container, ) -> types.Trainer: """ - Detect trainer type with precedence logic. + Detect trainer type using pattern matching with fallback. - This method implements the precedence order: - 1. Check existing ALL_TRAINERS mapping (backward compatibility) - 2. Use image pattern matching - 3. Fall back to DEFAULT_TRAINER + This method implements the detection logic: + 1. Use image pattern matching to detect framework + 2. Fall back to DEFAULT_TRAINER if no patterns match Args: trainer_container: The trainer container object @@ -159,16 +158,12 @@ def detect_trainer( """ image_name = trainer_container.image.split(":")[0] - # 1. Check existing ALL_TRAINERS mapping (backward compatibility) - if image_name in types.ALL_TRAINERS: - return copy.deepcopy(types.ALL_TRAINERS[image_name]) - - # 2. Use image pattern matching + # 1. Use image pattern matching trainer = detect_trainer_from_image_patterns(image_name) if trainer: return trainer - # 3. Fall back to DEFAULT_TRAINER + # 2. Fall back to DEFAULT_TRAINER return copy.deepcopy(types.DEFAULT_TRAINER) diff --git a/python/kubeflow/trainer/utils/utils_test.py b/python/kubeflow/trainer/utils/utils_test.py index 742c73019..1e7b51676 100644 --- a/python/kubeflow/trainer/utils/utils_test.py +++ b/python/kubeflow/trainer/utils/utils_test.py @@ -97,7 +97,7 @@ def test_trainer_detection_from_image_patterns( @pytest.mark.parametrize( "image_name,expected_framework", [ - # Known images (should use ALL_TRAINERS mapping) + # Official Kubeflow images (should be detected by regex) ("pytorch/pytorch", types.Framework.TORCH), ("ghcr.io/kubeflow/trainer/mlx-runtime", types.Framework.MLX), ( @@ -123,7 +123,7 @@ def test_trainer_detection_from_image_patterns( ], ) def test_trainer_detection_precedence(self, image_name, expected_framework): - """Test the precedence order of trainer detection methods.""" + """Test the trainer detection logic with pattern matching and fallback.""" # Create mock trainer container trainer_container = Mock() trainer_container.image = image_name @@ -132,7 +132,24 @@ def test_trainer_detection_precedence(self, image_name, expected_framework): assert trainer is not None assert trainer.framework == expected_framework - + def test_official_kubeflow_images_detected_by_regex(self): + """Test that official Kubeflow trainer images are correctly detected by regex patterns.""" + # Official Kubeflow images that should be detected by regex patterns + official_images = [ + ("pytorch/pytorch", types.Framework.TORCH), + ("ghcr.io/kubeflow/trainer/mlx-runtime", types.Framework.MLX), + ("ghcr.io/kubeflow/trainer/deepspeed-runtime", types.Framework.DEEPSPEED), + ("ghcr.io/kubeflow/trainer/torchtune-trainer", types.Framework.TORCHTUNE), + ] + + for image_name, expected_framework in official_images: + trainer = utils.detect_trainer_from_image_patterns(image_name) + assert trainer is not None, ( + f"Failed to detect trainer for official Kubeflow image: {image_name}" + ) + assert trainer.framework == expected_framework, ( + f"Wrong framework detected for {image_name}: got {trainer.framework}, expected {expected_framework}" + ) class TestAcceleratorCountLogic: From b3aed485ba5ae693c12f5d9355ce0223c4e49be0 Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Mon, 30 Jun 2025 17:45:05 +0530 Subject: [PATCH 10/13] Remove uv and testing setup to focus PR on trainer detection logic - Remove uv.lock file - Remove test dependencies from pyproject.toml - Remove pytest configuration from pyproject.toml - Keep only core trainer detection improvements and tests This ensures the PR focuses solely on trainer detection enhancements. Signed-off-by: Krishnaswamy Subramanian --- python/pyproject.toml | 17 -- python/uv.lock | 438 ------------------------------------------ 2 files changed, 455 deletions(-) delete mode 100644 python/uv.lock diff --git a/python/pyproject.toml b/python/pyproject.toml index 7191d0b04..c0b5f5772 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -35,12 +35,6 @@ dependencies = [ "kubeflow_trainer_api@git+https://github.com/kubeflow/trainer.git@master#subdirectory=api/python_api" ] -[project.optional-dependencies] -test = [ - "pytest>=7.0.0", - "pytest-mock>=3.10.0", -] - [project.urls] Homepage = "https://github.com/kubeflow/trainer" Documentation = "https://www.kubeflow.org/docs/components/trainer/" @@ -54,14 +48,3 @@ path = "kubeflow/trainer/__init__.py" [tool.hatch.metadata] allow-direct-references = true - -[tool.pytest.ini_options] -addopts = [ - "--import-mode=importlib", - "-v", - "--tb=short", -] -testpaths = ["kubeflow"] -python_files = ["test_*.py", "*_test.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] diff --git a/python/uv.lock b/python/uv.lock deleted file mode 100644 index 313b3d385..000000000 --- a/python/uv.lock +++ /dev/null @@ -1,438 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.12" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "cachetools" -version = "5.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, -] - -[[package]] -name = "certifi" -version = "2025.6.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "durationpy" -version = "0.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, -] - -[[package]] -name = "google-auth" -version = "2.40.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachetools" }, - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "kubeflow" -source = { editable = "." } -dependencies = [ - { name = "kubeflow-trainer-api" }, - { name = "kubernetes" }, - { name = "pydantic" }, -] - -[package.optional-dependencies] -test = [ - { name = "pytest" }, - { name = "pytest-mock" }, -] - -[package.metadata] -requires-dist = [ - { name = "kubeflow-trainer-api", git = "https://github.com/kubeflow/trainer.git?subdirectory=api%2Fpython_api&rev=master" }, - { name = "kubernetes", specifier = ">=27.2.0" }, - { name = "pydantic", specifier = ">=2.10.0" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, - { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.10.0" }, -] -provides-extras = ["test"] - -[[package]] -name = "kubeflow-trainer-api" -version = "2.0.0" -source = { git = "https://github.com/kubeflow/trainer.git?subdirectory=api%2Fpython_api&rev=master#b71a69064fa033c2ca51f6a24dbe199b552cada9" } -dependencies = [ - { name = "pydantic" }, -] - -[[package]] -name = "kubernetes" -version = "33.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "durationpy" }, - { name = "google-auth" }, - { name = "oauthlib" }, - { name = "python-dateutil" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "requests-oauthlib" }, - { name = "six" }, - { name = "urllib3" }, - { name = "websocket-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/52/19ebe8004c243fdfa78268a96727c71e08f00ff6fe69a301d0b7fcbce3c2/kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993", size = 1036779, upload-time = "2025-06-09T21:57:58.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/43/d9bebfc3db7dea6ec80df5cb2aad8d274dd18ec2edd6c4f21f32c237cbbb/kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5", size = 1941335, upload-time = "2025-06-09T21:57:56.327Z" }, -] - -[[package]] -name = "oauthlib" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/8a/6ea75ff7acf89f43afb157604429af4661a9840b1f2cece602b6a13c1893/oauthlib-3.3.0.tar.gz", hash = "sha256:4e707cf88d7dfc22a8cce22ca736a2eef9967c1dd3845efc0703fc922353eeb2", size = 190292, upload-time = "2025-06-17T23:19:18.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/3d/760b1456010ed11ce87c0109007f0166078dfdada7597f0091ae76eb7305/oauthlib-3.3.0-py3-none-any.whl", hash = "sha256:a2b3a0a2a4ec2feb4b9110f56674a39b2cc2f23e14713f4ed20441dfba14e934", size = 165155, upload-time = "2025-06-17T23:19:16.771Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - -[[package]] -name = "pydantic" -version = "2.11.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, -] - -[[package]] -name = "pytest-mock" -version = "3.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, -] - -[[package]] -name = "requests" -version = "2.32.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, -] - -[[package]] -name = "urllib3" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, -] - -[[package]] -name = "websocket-client" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, -] From d3c00433c91cfac91d6ad24f8998bf5cc49c04fa Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Fri, 4 Jul 2025 12:04:23 +0530 Subject: [PATCH 11/13] Improve trainer detection API with default parameter and better encapsulation - Add optional default parameter to detect_trainer_from_image_patterns() - Handle copy.deepcopy() internally for better encapsulation - Remove boilerplate code from detect_trainer() function - Add comprehensive unit tests with proper separation of concerns - Maintain backward compatibility with existing behavior Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/utils/utils.py | 19 ++++++++------- python/kubeflow/trainer/utils/utils_test.py | 26 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index a9cf3bd79..88df0f4cc 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -108,7 +108,9 @@ def get_runtime_trainer_container( return None -def detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Trainer]: +def detect_trainer_from_image_patterns( + image_name: str, default: Optional[types.Trainer] = None +) -> Optional[types.Trainer]: """ Detect trainer type based on image name patterns using regex. @@ -117,9 +119,10 @@ def detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Traine Args: image_name: The container image name + default: Optional default trainer to return if no patterns match Returns: - Trainer object if detected, None otherwise + Trainer object if detected, default if provided, None otherwise """ # DeepSpeed patterns if re.search(r"deepspeed", image_name, re.IGNORECASE): @@ -137,6 +140,9 @@ def detect_trainer_from_image_patterns(image_name: str) -> Optional[types.Traine if re.search(r"pytorch", image_name, re.IGNORECASE): return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCH]) + # Handle deep copy internally + if default is not None: + return copy.deepcopy(default) return None @@ -158,13 +164,8 @@ def detect_trainer( """ image_name = trainer_container.image.split(":")[0] - # 1. Use image pattern matching - trainer = detect_trainer_from_image_patterns(image_name) - if trainer: - return trainer - - # 2. Fall back to DEFAULT_TRAINER - return copy.deepcopy(types.DEFAULT_TRAINER) + # Use image pattern matching with default fallback + return detect_trainer_from_image_patterns(image_name, types.DEFAULT_TRAINER) def get_runtime_trainer( diff --git a/python/kubeflow/trainer/utils/utils_test.py b/python/kubeflow/trainer/utils/utils_test.py index 1e7b51676..cc5497cdd 100644 --- a/python/kubeflow/trainer/utils/utils_test.py +++ b/python/kubeflow/trainer/utils/utils_test.py @@ -151,6 +151,32 @@ def test_official_kubeflow_images_detected_by_regex(self): f"Wrong framework detected for {image_name}: got {trainer.framework}, expected {expected_framework}" ) + def test_default_parameter_returns_none_when_no_default_provided(self): + """Test that function returns None when no default is provided and no pattern matches.""" + trainer = utils.detect_trainer_from_image_patterns("unknown-image:latest") + assert trainer is None + + def test_default_parameter_returns_deep_copy_of_default(self): + """Test that function returns a deep copy of the provided default when no pattern matches.""" + custom_trainer = types.TRAINER_CONFIGS[types.Framework.MLX] + trainer = utils.detect_trainer_from_image_patterns( + "unknown-image:latest", custom_trainer + ) + assert trainer is not None + assert trainer.framework == types.Framework.MLX + # Verify it's a deep copy (different object) + assert trainer is not custom_trainer + + def test_pattern_matching_overrides_default_parameter(self): + """Test that pattern matching takes precedence over the default parameter.""" + custom_trainer = types.TRAINER_CONFIGS[types.Framework.MLX] + trainer = utils.detect_trainer_from_image_patterns( + "deepspeed-custom:latest", custom_trainer + ) + assert trainer is not None + assert trainer.framework == types.Framework.DEEPSPEED # Pattern match wins + assert trainer.framework != types.Framework.MLX # Default is ignored + class TestAcceleratorCountLogic: """Test cases for accelerator count logic in get_runtime_trainer.""" From 48aa98ba15b8967ad0d9efbd95afde124ba4cb68 Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Wed, 23 Jul 2025 14:23:06 +0530 Subject: [PATCH 12/13] Rename detect_trainer_from_image_patterns to get_trainer_from_image Simplify trainer detection API by removing optional default parameter and always returning a Trainer object. The function now directly returns DEFAULT_TRAINER when no regex patterns match, eliminating the need for None handling in calling code. Changes: - Rename function to get_trainer_from_image for clarity - Remove optional default parameter from function signature - Always return types.Trainer instead of Optional[types.Trainer] - Update all test cases to expect DEFAULT_TRAINER for unknown images - Simplify detect_trainer() function logic Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/utils/utils.py | 21 +++------ python/kubeflow/trainer/utils/utils_test.py | 50 ++++++++++----------- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index 88df0f4cc..0df70d964 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -108,21 +108,15 @@ def get_runtime_trainer_container( return None -def detect_trainer_from_image_patterns( - image_name: str, default: Optional[types.Trainer] = None -) -> Optional[types.Trainer]: +def get_trainer_from_image(image_name: str) -> types.Trainer: """ Detect trainer type based on image name patterns using regex. - This method uses pattern matching on the image name to determine the likely trainer type. - Args: - image_name: The container image name - default: Optional default trainer to return if no patterns match - + image_name: The container image name. Returns: - Trainer object if detected, default if provided, None otherwise + Trainer: Trainer object if detected, otherwise the DEFAULT_TRAINER is returned. """ # DeepSpeed patterns if re.search(r"deepspeed", image_name, re.IGNORECASE): @@ -140,10 +134,7 @@ def detect_trainer_from_image_patterns( if re.search(r"pytorch", image_name, re.IGNORECASE): return copy.deepcopy(types.TRAINER_CONFIGS[types.Framework.TORCH]) - # Handle deep copy internally - if default is not None: - return copy.deepcopy(default) - return None + return copy.deepcopy(types.DEFAULT_TRAINER) def detect_trainer( @@ -163,9 +154,7 @@ def detect_trainer( Trainer object """ image_name = trainer_container.image.split(":")[0] - - # Use image pattern matching with default fallback - return detect_trainer_from_image_patterns(image_name, types.DEFAULT_TRAINER) + return get_trainer_from_image(image_name) def get_runtime_trainer( diff --git a/python/kubeflow/trainer/utils/utils_test.py b/python/kubeflow/trainer/utils/utils_test.py index cc5497cdd..b89ce6b4f 100644 --- a/python/kubeflow/trainer/utils/utils_test.py +++ b/python/kubeflow/trainer/utils/utils_test.py @@ -87,10 +87,11 @@ def test_trainer_detection_from_image_patterns( self, image_name, expected_framework ): """Test trainer detection using image pattern matching with various case scenarios.""" - trainer = utils.detect_trainer_from_image_patterns(image_name) + trainer = utils.get_trainer_from_image(image_name) if expected_framework is None: - assert trainer is None + # When no pattern matches, should return DEFAULT_TRAINER (PyTorch) + assert trainer.framework == types.Framework.TORCH else: assert trainer.framework == expected_framework @@ -143,7 +144,7 @@ def test_official_kubeflow_images_detected_by_regex(self): ] for image_name, expected_framework in official_images: - trainer = utils.detect_trainer_from_image_patterns(image_name) + trainer = utils.get_trainer_from_image(image_name) assert trainer is not None, ( f"Failed to detect trainer for official Kubeflow image: {image_name}" ) @@ -151,31 +152,30 @@ def test_official_kubeflow_images_detected_by_regex(self): f"Wrong framework detected for {image_name}: got {trainer.framework}, expected {expected_framework}" ) - def test_default_parameter_returns_none_when_no_default_provided(self): - """Test that function returns None when no default is provided and no pattern matches.""" - trainer = utils.detect_trainer_from_image_patterns("unknown-image:latest") - assert trainer is None - - def test_default_parameter_returns_deep_copy_of_default(self): - """Test that function returns a deep copy of the provided default when no pattern matches.""" - custom_trainer = types.TRAINER_CONFIGS[types.Framework.MLX] - trainer = utils.detect_trainer_from_image_patterns( - "unknown-image:latest", custom_trainer - ) + def test_returns_default_trainer_when_no_pattern_matches(self): + """Test that function returns DEFAULT_TRAINER when no pattern matches.""" + trainer = utils.get_trainer_from_image("unknown-image:latest") assert trainer is not None - assert trainer.framework == types.Framework.MLX - # Verify it's a deep copy (different object) - assert trainer is not custom_trainer - - def test_pattern_matching_overrides_default_parameter(self): - """Test that pattern matching takes precedence over the default parameter.""" - custom_trainer = types.TRAINER_CONFIGS[types.Framework.MLX] - trainer = utils.detect_trainer_from_image_patterns( - "deepspeed-custom:latest", custom_trainer - ) + assert trainer.framework == types.Framework.TORCH # DEFAULT_TRAINER is PyTorch + + def test_returns_deep_copy_of_default_trainer(self): + """Test that function returns a deep copy of DEFAULT_TRAINER when no pattern matches.""" + trainer1 = utils.get_trainer_from_image("unknown-image-1:latest") + trainer2 = utils.get_trainer_from_image("unknown-image-2:latest") + + assert trainer1 is not None + assert trainer2 is not None + assert trainer1.framework == types.Framework.TORCH + assert trainer2.framework == types.Framework.TORCH + # Verify they are different objects (deep copies) + assert trainer1 is not trainer2 + + def test_pattern_matching_takes_precedence_over_default(self): + """Test that pattern matching takes precedence over default fallback.""" + trainer = utils.get_trainer_from_image("deepspeed-custom:latest") assert trainer is not None assert trainer.framework == types.Framework.DEEPSPEED # Pattern match wins - assert trainer.framework != types.Framework.MLX # Default is ignored + assert trainer.framework != types.Framework.TORCH # Not the default class TestAcceleratorCountLogic: From fa5778b8eea84d7e4d7506f8c0c5adc59cb28d5b Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Wed, 23 Jul 2025 14:38:02 +0530 Subject: [PATCH 13/13] Fix accelerator count validation to handle zero values properly Changes: - For torch: check actual_instance value truthiness, not just object existence - For MPI: already correctly validates the direct value - Zero values (0) are now ignored (treated as None) - Negative values are trusted as explicit user input - Update test cases to reflect new behavior Signed-off-by: Krishnaswamy Subramanian --- python/kubeflow/trainer/utils/utils.py | 11 +++++------ python/kubeflow/trainer/utils/utils_test.py | 6 ++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/python/kubeflow/trainer/utils/utils.py b/python/kubeflow/trainer/utils/utils.py index 0df70d964..a8b7cd61f 100644 --- a/python/kubeflow/trainer/utils/utils.py +++ b/python/kubeflow/trainer/utils/utils.py @@ -179,15 +179,14 @@ def get_runtime_trainer( _, trainer.accelerator_count = devices # Torch and MPI plugins override accelerator count. - # NOTE: The 'is not None' checks are essential because: - # 1. For torch: prevents AttributeError when accessing None.actual_instance - # 2. For MPI: prevents setting accelerator_count to None - # 3. Semantically: only override when user explicitly provides num_proc_per_node - if ml_policy.torch and ml_policy.torch.num_proc_per_node is not None: + # NOTE: Using truthiness check handles None/0 automatically + if (ml_policy.torch and + ml_policy.torch.num_proc_per_node and + ml_policy.torch.num_proc_per_node.actual_instance): num_proc = ml_policy.torch.num_proc_per_node.actual_instance if isinstance(num_proc, int): trainer.accelerator_count = num_proc - elif ml_policy.mpi and ml_policy.mpi.num_proc_per_node is not None: + elif (ml_policy.mpi and ml_policy.mpi.num_proc_per_node): trainer.accelerator_count = ml_policy.mpi.num_proc_per_node # Multiply accelerator_count by the number of nodes. diff --git a/python/kubeflow/trainer/utils/utils_test.py b/python/kubeflow/trainer/utils/utils_test.py index b89ce6b4f..68f24e60c 100644 --- a/python/kubeflow/trainer/utils/utils_test.py +++ b/python/kubeflow/trainer/utils/utils_test.py @@ -203,6 +203,12 @@ class TestAcceleratorCountLogic: ({"mpi": {"num_proc_per_node": 2}}, 2), ({"mpi": {"num_proc_per_node": 16}}, 16), ({"mpi": {"num_proc_per_node": 1}}, 1), + # Zero values should be treated as None (ignored) + ({"torch": {"num_proc_per_node": 0}}, None), + ({"mpi": {"num_proc_per_node": 0}}, None), + # Negative values are trusted (user input) + ({"torch": {"num_proc_per_node": -1}}, -1), + ({"mpi": {"num_proc_per_node": -1}}, -1), # No policies ({}, None), ({"torch": {}}, None),