diff --git a/changes/3493.enhance.md b/changes/3493.enhance.md new file mode 100644 index 00000000000..98917b4399f --- /dev/null +++ b/changes/3493.enhance.md @@ -0,0 +1 @@ +Add skeleton vFolder handler Interface of manager \ No newline at end of file diff --git a/python.lock b/python.lock index b7cb52a22f5..eaf6884ebce 100644 --- a/python.lock +++ b/python.lock @@ -69,7 +69,7 @@ // "prometheus-client~=0.21.1", // "psutil~=6.0", // "pycryptodome>=3.20.0", -// "pydantic~=2.9.2", +// "pydantic[email]~=2.9.2", // "pyhumps~=3.8.0", // "pyroscope-io~=0.8.8", // "pytest-aiohttp~=1.0.5", @@ -799,13 +799,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", - "url": "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl" + "hash": "c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", + "url": "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", - "url": "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz" + "hash": "1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", + "url": "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz" } ], "project_name": "attrs", @@ -852,7 +852,7 @@ "towncrier<24.7; extra == \"docs\"" ], "requires_python": ">=3.8", - "version": "24.3.0" + "version": "25.1.0" }, { "artifacts": [ @@ -1047,43 +1047,48 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "f9843a5d06f501d66ada06f5a5417f671823af2cf319e36ceefa1bafaaaaa953", - "url": "https://files.pythonhosted.org/packages/79/97/4697aa8050e306d6139815996adeb263ddc83024399a188e8b42587665db/boto3-1.36.3-py3-none-any.whl" + "hash": "20d97739cea1b0f549e9096c453ac727a350da28bd0451098714260b655a85ea", + "url": "https://files.pythonhosted.org/packages/3a/d9/d0e741995fedf458e99f71856ae725c201f4cbd69ba6c92fd7498fe71a16/boto3-1.36.13-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "c8031aa1c4a7c331081b2d86c49a362654b86e0b89d0a41fa166a68b226f4aba", + "url": "https://files.pythonhosted.org/packages/b1/de/3c35089f97f6068beb852b51b9eede70e8f7e39a6c8ddff68f3bcabafe3e/boto3-1.36.13.tar.gz" } ], "project_name": "boto3", "requires_dists": [ - "botocore<1.37.0,>=1.36.3", + "botocore<1.37.0,>=1.36.13", "botocore[crt]<2.0a0,>=1.21.0; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", "s3transfer<0.12.0,>=0.11.0" ], "requires_python": ">=3.8", - "version": "1.36.3" + "version": "1.36.13" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "536ab828e6f90dbb000e3702ac45fd76642113ae2db1b7b1373ad24104e89255", - "url": "https://files.pythonhosted.org/packages/9f/14/f952fed35b9c04aa66453b5fb5d1262a5a9f5dfdcb396d387c1ff0c6da41/botocore-1.36.3-py3-none-any.whl" + "hash": "d644a814440bf8d55f4e29b1c0e6f021e2573b7784e0c91f55f4d9d689e08005", + "url": "https://files.pythonhosted.org/packages/24/4d/dc5d65588601cec5f243a73c8c16bc22e485d2845eccb2f924ba883df4e4/botocore-1.36.13-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "775b835e979da5c96548ed1a0b798101a145aec3cd46541d62e27dda5a94d7f8", - "url": "https://files.pythonhosted.org/packages/3a/61/69eb06a803c83e0da733b60b2bc65880c18ef2dee19ee10cf8732794a3c1/botocore-1.36.3.tar.gz" + "hash": "50a3ff292f8dfdde21074b5c916afe847b01e074ab16d9c9fe71b34960c77134", + "url": "https://files.pythonhosted.org/packages/7e/0b/87dcaaa03a7b5bf3e06abfeccb2af328a436a97fd7b6015f174f1350a284/botocore-1.36.13.tar.gz" } ], "project_name": "botocore", "requires_dists": [ - "awscrt==0.23.4; extra == \"crt\"", + "awscrt==0.23.8; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", "python-dateutil<3.0.0,>=2.1", "urllib3!=2.2.0,<3,>=1.25.4; python_version >= \"3.10\"", "urllib3<1.27,>=1.25.4; python_version < \"3.10\"" ], "requires_python": ">=3.8", - "version": "1.36.3" + "version": "1.36.13" }, { "artifacts": [ @@ -1182,19 +1187,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", - "url": "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl" + "hash": "ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", + "url": "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", - "url": "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz" + "hash": "3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "url": "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz" } ], "project_name": "certifi", "requires_dists": [], "requires_python": ">=3.6", - "version": "2024.12.14" + "version": "2025.1.31" }, { "artifacts": [ @@ -1526,6 +1531,46 @@ "requires_python": "<3.13,>=3.7", "version": "0.5.14" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", + "url": "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", + "url": "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz" + } + ], + "project_name": "dnspython", + "requires_dists": [ + "aioquic>=1.0.0; extra == \"doq\"", + "black>=23.1.0; extra == \"dev\"", + "coverage>=7.0; extra == \"dev\"", + "cryptography>=43; extra == \"dnssec\"", + "flake8>=7; extra == \"dev\"", + "h2>=4.1.0; extra == \"doh\"", + "httpcore>=1.0.0; extra == \"doh\"", + "httpx>=0.26.0; extra == \"doh\"", + "hypercorn>=0.16.0; extra == \"dev\"", + "idna>=3.7; extra == \"idna\"", + "mypy>=1.8; extra == \"dev\"", + "pylint>=3; extra == \"dev\"", + "pytest-cov>=4.1.0; extra == \"dev\"", + "pytest>=7.4; extra == \"dev\"", + "quart-trio>=0.11.0; extra == \"dev\"", + "sphinx-rtd-theme>=2.0.0; extra == \"dev\"", + "sphinx>=7.2.0; extra == \"dev\"", + "trio>=0.23; extra == \"trio\"", + "twine>=4.0.0; extra == \"dev\"", + "wheel>=0.42.0; extra == \"dev\"", + "wmi>=1.5.1; extra == \"wmi\"" + ], + "requires_python": ">=3.9", + "version": "2.7.0" + }, { "artifacts": [ { @@ -1547,6 +1592,27 @@ "requires_python": ">=3.8", "version": "1.6.6" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", + "url": "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", + "url": "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz" + } + ], + "project_name": "email-validator", + "requires_dists": [ + "dnspython>=2.0.0", + "idna>=2.0.0" + ], + "requires_python": ">=3.8", + "version": "2.2.0" + }, { "artifacts": [ { @@ -1717,13 +1783,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0", - "url": "https://files.pythonhosted.org/packages/8d/8d/4d5d5f9f500499f7bd4c93903b43e8d6976f3fc6f064637ded1a85d09b07/google_auth-2.37.0-py2.py3-none-any.whl" + "hash": "e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", + "url": "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00", - "url": "https://files.pythonhosted.org/packages/46/af/b25763b9d35dfc2c6f9c3ec34d8d3f1ba760af3a7b7e8d5c5f0579522c45/google_auth-2.37.0.tar.gz" + "hash": "8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", + "url": "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz" } ], "project_name": "google-auth", @@ -1743,7 +1809,7 @@ "rsa<5,>=3.1.4" ], "requires_python": ">=3.7", - "version": "2.37.0" + "version": "2.38.0" }, { "artifacts": [ @@ -1793,13 +1859,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a", - "url": "https://files.pythonhosted.org/packages/e3/dc/078bd6b304de790618ebb95e2aedaadb78f4527ac43a9ad8815f006636b6/graphql_core-3.2.5-py3-none-any.whl" + "hash": "78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", + "url": "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5", - "url": "https://files.pythonhosted.org/packages/2e/b5/ebc6fe3852e2d2fdaf682dddfc366934f3d2c9ef9b6d1b0e6ca348d936ba/graphql_core-3.2.5.tar.gz" + "hash": "c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", + "url": "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz" } ], "project_name": "graphql-core", @@ -1807,7 +1873,7 @@ "typing-extensions<5,>=4; python_version < \"3.10\"" ], "requires_python": "<4,>=3.6", - "version": "3.2.5" + "version": "3.2.6" }, { "artifacts": [ @@ -2461,13 +2527,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627", - "url": "https://files.pythonhosted.org/packages/1e/bf/7a6a36ce2e4cafdfb202752be68850e22607fccd692847c45c1ae3c17ba6/Mako-1.3.8-py3-none-any.whl" + "hash": "95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", + "url": "https://files.pythonhosted.org/packages/cd/83/de0a49e7de540513f53ab5d2e105321dedeb08a8f5850f0208decf4390ec/Mako-1.3.9-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8", - "url": "https://files.pythonhosted.org/packages/5f/d9/8518279534ed7dace1795d5a47e49d5299dd0994eed1053996402a8902f9/mako-1.3.8.tar.gz" + "hash": "b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac", + "url": "https://files.pythonhosted.org/packages/62/4f/ddb1965901bc388958db9f0c991255b2c469349a741ae8c9cd8a562d70a6/mako-1.3.9.tar.gz" } ], "project_name": "mako", @@ -2478,7 +2544,7 @@ "pytest; extra == \"testing\"" ], "requires_python": ">=3.8", - "version": "1.3.8" + "version": "1.3.9" }, { "artifacts": [ @@ -2581,13 +2647,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "ec5d00d873ce473b7f2ffcb7104286a376c354cab0c2fa12f5573dab03e87210", - "url": "https://files.pythonhosted.org/packages/8e/25/5b300f0400078d9783fbe44d30fedd849a130fc3aff01f18278c12342b6f/marshmallow-3.25.1-py3-none-any.whl" + "hash": "3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", + "url": "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "f4debda3bb11153d81ac34b0d582bf23053055ee11e791b54b4b35493468040a", - "url": "https://files.pythonhosted.org/packages/b8/85/43b8e95251312e8d0d3389263e87e368a5a015db475e140d5dd8cb8dcb47/marshmallow-3.25.1.tar.gz" + "hash": "e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", + "url": "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz" } ], "project_name": "marshmallow", @@ -2606,7 +2672,7 @@ "tox; extra == \"dev\"" ], "requires_python": ">=3.9", - "version": "3.25.1" + "version": "3.26.1" }, { "artifacts": [ @@ -3047,34 +3113,34 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "0aebecb809cae990f8129ada5ca273d9d670b76d9bfc9b1809f0a9c02b7dbf41", - "url": "https://files.pythonhosted.org/packages/33/90/f198a61df8381fb43ae0fe81b3d2718e8dcc51ae8502c7657ab9381fbc4f/protobuf-4.25.5-py3-none-any.whl" + "hash": "07972021c8e30b870cfc0863409d033af940213e0e7f64e27fe017b929d2c9f7", + "url": "https://files.pythonhosted.org/packages/71/eb/be11a1244d0e58ee04c17a1f939b100199063e26ecca8262c04827fe0bf5/protobuf-4.25.6-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "fe14e16c22be926d3abfcb500e60cab068baf10b542b8c858fa27e098123e331", - "url": "https://files.pythonhosted.org/packages/05/a6/094a2640be576d760baa34c902dcb8199d89bce9ed7dd7a6af74dcbbd62d/protobuf-4.25.5-cp37-abi3-manylinux2014_x86_64.whl" + "hash": "f8cfbae7c5afd0d0eaccbe73267339bff605a2315860bb1ba08eb66670a9a91f", + "url": "https://files.pythonhosted.org/packages/48/d5/cccc7e82bbda9909ced3e7a441a24205ea07fea4ce23a772743c0c7611fa/protobuf-4.25.6.tar.gz" }, { "algorithm": "sha256", - "hash": "b2fde3d805354df675ea4c7c6338c1aecd254dfc9925e88c6d31a2bcb97eb173", - "url": "https://files.pythonhosted.org/packages/51/49/d110f0a43beb365758a252203c43eaaad169fe7749da918869a8c991f726/protobuf-4.25.5-cp37-abi3-macosx_10_9_universal2.whl" + "hash": "5dd800da412ba7f6f26d2c08868a5023ce624e1fdb28bccca2dc957191e81fb5", + "url": "https://files.pythonhosted.org/packages/64/d5/7dbeb69b74fa88f297c6d8f11b7c9cef0c2e2fb1fdf155c2ca5775cfa998/protobuf-4.25.6-cp37-abi3-manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584", - "url": "https://files.pythonhosted.org/packages/67/dd/48d5fdb68ec74d70fabcc252e434492e56f70944d9f17b6a15e3746d2295/protobuf-4.25.5.tar.gz" + "hash": "6d4381f2417606d7e01750e2729fe6fbcda3f9883aa0c32b51d23012bded6c91", + "url": "https://files.pythonhosted.org/packages/b7/03/361e87cc824452376c2abcef0eabd18da78a7439479ec6541cf29076a4dc/protobuf-4.25.6-cp37-abi3-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "919ad92d9b0310070f8356c24b855c98df2b8bd207ebc1c0c6fcc9ab1e007f3d", - "url": "https://files.pythonhosted.org/packages/c6/ab/0f384ca0bc6054b1a7b6009000ab75d28a5506e4459378b81280ae7fd358/protobuf-4.25.5-cp37-abi3-manylinux2014_aarch64.whl" + "hash": "4434ff8bb5576f9e0c78f47c41cdf3a152c0b44de475784cd3fd170aef16205a", + "url": "https://files.pythonhosted.org/packages/d4/f0/6d5c100f6b18d973e86646aa5fc09bc12ee88a28684a56fd95511bceee68/protobuf-4.25.6-cp37-abi3-manylinux2014_x86_64.whl" } ], "project_name": "protobuf", "requires_dists": [], "requires_python": ">=3.8", - "version": "4.25.5" + "version": "4.25.6" }, { "artifacts": [ @@ -3591,13 +3657,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075", - "url": "https://files.pythonhosted.org/packages/61/d8/defa05ae50dcd6019a95527200d3b3980043df5aa445d40cb0ef9f7f98ab/pytest_asyncio-0.25.2-py3-none-any.whl" + "hash": "9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", + "url": "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f", - "url": "https://files.pythonhosted.org/packages/72/df/adcc0d60f1053d74717d21d58c0048479e9cab51464ce0d2965b086bd0e2/pytest_asyncio-0.25.2.tar.gz" + "hash": "fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", + "url": "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz" } ], "project_name": "pytest-asyncio", @@ -3609,7 +3675,7 @@ "sphinx>=5.3; extra == \"docs\"" ], "requires_python": ">=3.9", - "version": "0.25.2" + "version": "0.25.3" }, { "artifacts": [ @@ -3759,53 +3825,53 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", - "url": "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "91e2bfb8e9a29f709d51b208dd5f441dc98eb412c8fe75c24ea464734ccdb48e", + "url": "https://files.pythonhosted.org/packages/d9/c4/b3edb7d0ae82ad6fb1a8cdb191a4113c427a01e85139906f3b655b07f4f8/pyzmq-26.2.1-cp312-cp312-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", - "url": "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl" + "hash": "17d72a74e5e9ff3829deb72897a175333d3ef5b5413948cae3cf7ebf0b02ecca", + "url": "https://files.pythonhosted.org/packages/5a/e3/8d0382cb59feb111c252b54e8728257416a38ffcb2243c4e4775a3c990fe/pyzmq-26.2.1.tar.gz" }, { "algorithm": "sha256", - "hash": "ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", - "url": "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl" + "hash": "a6549ecb0041dafa55b5932dcbb6c68293e0bd5980b5b99f5ebb05f9a3b8a8f3", + "url": "https://files.pythonhosted.org/packages/9c/b9/260a74786f162c7f521f5f891584a51d5a42fd15f5dcaa5c9226b2865fcc/pyzmq-26.2.1-cp312-cp312-macosx_10_15_universal2.whl" }, { "algorithm": "sha256", - "hash": "e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", - "url": "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl" + "hash": "2d88ba221a07fc2c5581565f1d0fe8038c15711ae79b80d9462e080a1ac30435", + "url": "https://files.pythonhosted.org/packages/a1/d1/6fda77a034d02034367b040973fd3861d945a5347e607bd2e98c99f20599/pyzmq-26.2.1-cp312-cp312-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", - "url": "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl" + "hash": "1c84c1297ff9f1cd2440da4d57237cb74be21fdfe7d01a10810acba04e79371a", + "url": "https://files.pythonhosted.org/packages/ad/81/48f7fd8a71c427412e739ce576fc1ee14f3dc34527ca9b0076e471676183/pyzmq-26.2.1-cp312-cp312-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", - "url": "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "0250c94561f388db51fd0213cdccbd0b9ef50fd3c57ce1ac937bf3034d92d72e", + "url": "https://files.pythonhosted.org/packages/bf/73/8a0757e4b68f5a8ccb90ddadbb76c6a5f880266cdb18be38c99bcdc17aaa/pyzmq-26.2.1-cp312-cp312-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", - "url": "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "786dd8a81b969c2081b31b17b326d3a499ddd1856e06d6d79ad41011a25148da", + "url": "https://files.pythonhosted.org/packages/c3/25/0b4824596f261a3cc512ab152448b383047ff5f143a6906a36876415981c/pyzmq-26.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", - "url": "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "46d4ebafc27081a7f73a0f151d0c38d4291656aa134344ec1f3d0199ebfbb6d4", + "url": "https://files.pythonhosted.org/packages/c7/d8/818f15c6ef36b5450e435cbb0d3a51599fc884a5d2b27b46b9c00af68ef1/pyzmq-26.2.1-cp312-cp312-musllinux_1_1_i686.whl" }, { "algorithm": "sha256", - "hash": "17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", - "url": "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl" + "hash": "36ee4297d9e4b34b5dc1dd7ab5d5ea2cbba8511517ef44104d2915a917a56dc8", + "url": "https://files.pythonhosted.org/packages/cf/de/f02ec973cd33155bb772bae33ace774acc7cc71b87b25c4829068bec35de/pyzmq-26.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", - "url": "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz" + "hash": "c2a9cb17fd83b7a3a3009901aca828feaf20aa2451a8a487b035455a86549c09", + "url": "https://files.pythonhosted.org/packages/d1/80/8fc583085f85ac91682744efc916888dd9f11f9f75a31aef1b78a5486c6c/pyzmq-26.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl" } ], "project_name": "pyzmq", @@ -3813,7 +3879,7 @@ "cffi; implementation_name == \"pypy\"" ], "requires_python": ">=3.7", - "version": "26.2.0" + "version": "26.2.1" }, { "artifacts": [ @@ -3973,13 +4039,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "8fa0aa48177be1f3425176dfe1ab85dcd3d962df603c3dbfc585e6bf857ef0ff", - "url": "https://files.pythonhosted.org/packages/5f/ce/22673f4a85ccc640735b4f8d12178a0f41b5d3c6eda7f33756d10ce56901/s3transfer-0.11.1-py3-none-any.whl" + "hash": "be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc", + "url": "https://files.pythonhosted.org/packages/1b/ac/e7dc469e49048dc57f62e0c555d2ee3117fa30813d2a1a2962cce3a2a82a/s3transfer-0.11.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "3f25c900a367c8b7f7d8f9c34edc87e300bde424f779dc9f0a8ae4f9df9264f6", - "url": "https://files.pythonhosted.org/packages/1a/aa/fdd958c626b00e3f046d4004363e7f1a2aba4354f78d65ceb3b217fa5eb8/s3transfer-0.11.1.tar.gz" + "hash": "3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f", + "url": "https://files.pythonhosted.org/packages/62/45/2323b5928f86fd29f9afdcef4659f68fa73eaa5356912b774227f5cf46b5/s3transfer-0.11.2.tar.gz" } ], "project_name": "s3transfer", @@ -3988,7 +4054,7 @@ "botocore[crt]<2.0a.0,>=1.36.0; extra == \"crt\"" ], "requires_python": ">=3.8", - "version": "0.11.1" + "version": "0.11.2" }, { "artifacts": [ @@ -5151,7 +5217,7 @@ "prometheus-client~=0.21.1", "psutil~=6.0", "pycryptodome>=3.20.0", - "pydantic~=2.9.2", + "pydantic[email]~=2.9.2", "pyhumps~=3.8.0", "pyroscope-io~=0.8.8", "pytest-aiohttp~=1.0.5", diff --git a/requirements.txt b/requirements.txt index ef5064bc3ce..1e438b1c1b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,7 +58,7 @@ python-json-logger~=3.2.0 pyzmq~=26.2 PyJWT~=2.0 PyYAML~=6.0 -pydantic~=2.9.2 +pydantic[email]~=2.9.2 packaging>=24.1 hiredis>=3.0.0 redis[hiredis]==4.5.5 diff --git a/src/ai/backend/manager/api/vfolders/BUILD b/src/ai/backend/manager/api/vfolders/BUILD new file mode 100644 index 00000000000..c1ffc1a1410 --- /dev/null +++ b/src/ai/backend/manager/api/vfolders/BUILD @@ -0,0 +1 @@ +python_sources(name="src") \ No newline at end of file diff --git a/src/ai/backend/manager/api/vfolders/__init__.py b/src/ai/backend/manager/api/vfolders/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/ai/backend/manager/api/vfolders/api_schemas.py b/src/ai/backend/manager/api/vfolders/api_schemas.py new file mode 100644 index 00000000000..b01937c7c26 --- /dev/null +++ b/src/ai/backend/manager/api/vfolders/api_schemas.py @@ -0,0 +1,159 @@ +import uuid +from typing import Any, Mapping, Optional, Self + +from aiohttp import web +from pydantic import AliasChoices, BaseModel, Field + +from ai.backend.common import typed_validators as tv +from ai.backend.common.api_handlers import BaseResponseModel, MiddlewareParam +from ai.backend.common.types import VFolderUsageMode +from ai.backend.manager.api.vfolders.dtos import ( + Keypair, + UserIdentity, + VFolderCreateRequirements, + VFolderList, + VFolderListItem, + VFolderMetadata, +) +from ai.backend.manager.models import ( + VFolderOperationStatus, + VFolderPermission, +) + + +class VFolderCreateRequest(BaseModel): + name: tv.VFolderName = Field( + description="Name of the vfolder", + ) + folder_host: Optional[str] = Field( + validation_alias=AliasChoices("host", "folder_host"), + default=None, + ) + usage_mode: VFolderUsageMode = Field(default=VFolderUsageMode.GENERAL) + permission: VFolderPermission = Field(default=VFolderPermission.READ_WRITE) + unmanaged_path: Optional[str] = Field( + validation_alias=AliasChoices("unmanaged_path", "unmanagedPath"), + default=None, + ) + group_id: Optional[uuid.UUID] = Field( + validation_alias=AliasChoices("group", "groupId", "group_id"), + default=None, + ) + cloneable: bool = Field( + default=False, + ) + + def to_dto(self) -> VFolderCreateRequirements: + return VFolderCreateRequirements( + name=self.name, + folder_host=self.folder_host, + usage_mode=self.usage_mode, + permission=self.permission, + group_id=self.group_id, + cloneable=self.cloneable, + unmanaged_path=self.unmanaged_path, + ) + + +class VFolderListRequest(BaseModel): + group_id: Optional[uuid.UUID] = Field( + default=None, validation_alias=AliasChoices("group_id", "groupId") + ) + + +class RenameVFolderId(BaseModel): + vfolder_id: uuid.UUID = Field(validation_alias=AliasChoices("vfolder_id", "vfolderId", "id")) + + +class VFolderNewName(BaseModel): + new_name: tv.VFolderName = Field( + description="Name of the vfolder", + ) + + +class VFolderDeleteRequest(BaseModel): + vfolder_id: uuid.UUID = Field( + validation_alias=AliasChoices("vfolder_id", "vfolderId", "id"), + description="Target vfolder id to soft-delete, to go to trash bin", + ) + + +class VFolderCreateResponse(BaseResponseModel): + id: str + name: str + quota_scope_id: str + host: str + usage_mode: VFolderUsageMode + permission: str + max_size: int = 0 # migrated to quota scopes, no longer valid + creator: str + ownership_type: str + user: Optional[str] + group: Optional[str] + cloneable: bool + status: VFolderOperationStatus = Field(default=VFolderOperationStatus.READY) + + @classmethod + def from_vfolder_metadata(cls, data: VFolderMetadata): + return cls( + id=data.id, + name=data.name, + quota_scope_id=str(data.quota_scope_id), + host=data.host, + usage_mode=data.usage_mode, + permission=data.permission, + max_size=data.max_size, + creator=data.creator, + ownership_type=data.ownership_type, + user=data.user, + group=data.group, + cloneable=data.cloneable, + status=data.status, + ) + + +class VFolderListResponse(BaseResponseModel): + root: list[VFolderListItem] = Field(default_factory=list) + + @classmethod + def from_dataclass(cls, vfolder_list: VFolderList) -> Self: + return cls(root=vfolder_list.entries) + + +class UserIdentityModel(MiddlewareParam): + user_uuid: uuid.UUID + user_role: str + user_email: str + domain_name: str + + @classmethod + def from_request(cls, request: web.Request) -> Self: + return cls( + user_uuid=request["user"]["uuid"], + user_role=request["user"]["role"], + user_email=request["user"]["email"], + domain_name=request["user"]["domain_name"], + ) + + def to_dto(self) -> UserIdentity: + return UserIdentity( + user_uuid=self.user_uuid, + user_role=self.user_role, + user_email=self.user_email, + domain_name=self.domain_name, + ) + + +class KeypairModel(MiddlewareParam): + access_key: str + resource_policy: Mapping[str, Any] + + @classmethod + def from_request(cls, request: web.Request) -> Self: + return cls( + access_key=request["keypair"]["access_key"], + resource_policy=request["keypair"]["resource_policy"], + ) + + def to_dto(self) -> Keypair: + return Keypair(access_key=self.access_key, resource_policy=self.resource_policy) diff --git a/src/ai/backend/manager/api/vfolders/dtos.py b/src/ai/backend/manager/api/vfolders/dtos.py new file mode 100644 index 00000000000..b32e8fe3d97 --- /dev/null +++ b/src/ai/backend/manager/api/vfolders/dtos.py @@ -0,0 +1,101 @@ +import uuid +from dataclasses import dataclass +from typing import Any, Mapping, Optional + +from ai.backend.common.types import QuotaScopeID, VFolderUsageMode +from ai.backend.manager.models import ( + ProjectType, + VFolderOperationStatus, + VFolderOwnershipType, + VFolderPermission, +) + + +@dataclass +class UserIdentity: + user_uuid: uuid.UUID + user_role: str + user_email: str + domain_name: str + + +@dataclass +class Keypair: + access_key: str + resource_policy: Mapping[str, Any] + + +@dataclass +class UserScopeInput: + requester_id: uuid.UUID + is_authorized: bool + is_superadmin: bool + delegate_email: Optional[str] = None + + +@dataclass +class VFolderCreateRequirements: + name: str + folder_host: Optional[str] + usage_mode: VFolderUsageMode + permission: VFolderPermission + group_id: Optional[uuid.UUID] + cloneable: bool + unmanaged_path: Optional[str] + + +@dataclass +class VFolderMetadata: + id: str + name: str + quota_scope_id: QuotaScopeID + host: str + usage_mode: VFolderUsageMode + created_at: str + permission: VFolderPermission + max_size: int # migrated to quota scopes, no longer valid + creator: str + ownership_type: VFolderOwnershipType + user: Optional[str] + group: Optional[str] + cloneable: bool + status: VFolderOperationStatus + + +@dataclass +class VFolderListItem: + id: str + name: str + quota_scope_id: str + host: str + usage_mode: VFolderUsageMode + created_at: str + permission: VFolderPermission + max_size: int + creator: str + ownership_type: VFolderOwnershipType + user: Optional[str] + group: Optional[str] + cloneable: bool + status: VFolderOperationStatus + is_owner: bool + user_email: str + group_name: str + type: str # legacy + max_files: int + cur_size: int + + +@dataclass +class VFolderList: + entries: list[VFolderListItem] + + +@dataclass +class VFolderCapabilityInfo: + max_vfolder_count: int + max_quota_scope_size: int + ownership_type: str + quota_scope_id: QuotaScopeID + group_uuid: Optional[uuid.UUID] = None + group_type: Optional[ProjectType] = None diff --git a/src/ai/backend/manager/api/vfolders/handlers.py b/src/ai/backend/manager/api/vfolders/handlers.py new file mode 100644 index 00000000000..2f119c347d4 --- /dev/null +++ b/src/ai/backend/manager/api/vfolders/handlers.py @@ -0,0 +1,148 @@ +import uuid +from typing import Optional, Protocol + +from ai.backend.common.api_handlers import ( + APIResponse, + BodyParam, + PathParam, + QueryParam, + api_handler, +) +from ai.backend.manager.api.vfolders.api_schemas import ( + KeypairModel, + RenameVFolderId, + UserIdentityModel, + VFolderCreateRequest, + VFolderCreateResponse, + VFolderDeleteRequest, + VFolderListRequest, + VFolderListResponse, + VFolderNewName, +) +from ai.backend.manager.api.vfolders.dtos import ( + Keypair, + UserIdentity, + VFolderCreateRequirements, + VFolderList, + VFolderMetadata, +) + + +class VFolderServiceProtocol(Protocol): + async def create_vfolder_in_personal( + self, + user_identity: UserIdentity, + keypair: Keypair, + vfolder_create_requirements: VFolderCreateRequirements, + ) -> VFolderMetadata: ... + + async def create_vfolder_in_group( + self, + user_identity: UserIdentity, + keypair: Keypair, + vfolder_create_requirements: VFolderCreateRequirements, + ) -> VFolderMetadata: ... + + async def get_vfolders( + self, user_identity: UserIdentity, group_id: Optional[uuid.UUID] + ) -> VFolderList: ... + + async def rename_vfolder( + self, user_identity: UserIdentity, keypair: Keypair, vfolder_id: uuid.UUID, new_name: str + ) -> None: ... + + async def delete_vfolder( + self, + vfolder_id: str, + user_identity: UserIdentity, + keypair: Keypair, + ) -> None: ... + + +class VFolderHandler: + def __init__(self, vfolder_service: VFolderServiceProtocol): + self.vfolder_service = vfolder_service + + @api_handler + async def create_vfolder( + self, + keypair: KeypairModel, + user_identity: UserIdentityModel, + body: BodyParam[VFolderCreateRequest], + ) -> APIResponse: + parsed_body = body.parsed + create_requirements: VFolderCreateRequirements = parsed_body.to_dto() + + vfolder_metadata: VFolderMetadata + if create_requirements.group_id: + vfolder_metadata = await self.vfolder_service.create_vfolder_in_group( + user_identity=user_identity.to_dto(), + keypair=keypair.to_dto(), + vfolder_create_requirements=create_requirements, + ) + else: + vfolder_metadata = await self.vfolder_service.create_vfolder_in_personal( + user_identity=user_identity.to_dto(), + keypair=keypair.to_dto(), + vfolder_create_requirements=create_requirements, + ) + + return APIResponse.build( + status_code=200, + response_model=VFolderCreateResponse.from_vfolder_metadata(vfolder_metadata), + ) + + @api_handler + async def list_vfolders( + self, user_identity: UserIdentityModel, query: QueryParam[VFolderListRequest] + ) -> APIResponse: + parsed_query = query.parsed + + vfolder_list: VFolderList = await self.vfolder_service.get_vfolders( + user_identity=user_identity.to_dto(), group_id=parsed_query.group_id + ) + + return APIResponse.build( + status_code=200, + response_model=VFolderListResponse.from_dataclass(vfolder_list=vfolder_list), + ) + + @api_handler + async def rename_vfolder( + self, + keypair: KeypairModel, + user_identity: UserIdentityModel, + path: PathParam[RenameVFolderId], + body: BodyParam[VFolderNewName], + ) -> APIResponse: + parsed_path = path.parsed + parsed_body = body.parsed + + vfolder_id: uuid.UUID = parsed_path.vfolder_id + new_name: str = parsed_body.new_name + + await self.vfolder_service.rename_vfolder( + user_identity=user_identity.to_dto(), + keypair=keypair.to_dto(), + vfolder_id=vfolder_id, + new_name=new_name, + ) + + return APIResponse.no_content(status_code=201) + + @api_handler + async def delete_vfolder( + self, + keypair: KeypairModel, + user_identity: UserIdentityModel, + path: PathParam[VFolderDeleteRequest], + ) -> APIResponse: + parsed_path = path.parsed + + await self.vfolder_service.delete_vfolder( + user_identity=user_identity.to_dto(), + keypair=keypair.to_dto(), + vfolder_id=str(parsed_path.vfolder_id), + ) + + return APIResponse.no_content(status_code=204) diff --git a/tests/manager/api/vfolders/BUILD b/tests/manager/api/vfolders/BUILD new file mode 100644 index 00000000000..0cde4bd6b82 --- /dev/null +++ b/tests/manager/api/vfolders/BUILD @@ -0,0 +1,8 @@ +python_test_utils( + sources=[ + "conftest.py", + ], + dependencies=["tests/manager:fixtures", "src/ai/backend/manager/api/vfolders:src"], +) + +python_tests(name="tests") diff --git a/tests/manager/api/vfolders/conftest.py b/tests/manager/api/vfolders/conftest.py new file mode 100644 index 00000000000..af80682fe1d --- /dev/null +++ b/tests/manager/api/vfolders/conftest.py @@ -0,0 +1,142 @@ +import uuid +from datetime import datetime +from typing import Optional +from unittest.mock import MagicMock + +import pytest + +from ai.backend.common.types import QuotaScopeID, QuotaScopeType, VFolderUsageMode +from ai.backend.manager.api.vfolders.api_schemas import ( + VFolderCreateRequirements, + VFolderList, + VFolderListItem, + VFolderMetadata, +) +from ai.backend.manager.api.vfolders.dtos import ( + Keypair, + UserIdentity, +) +from ai.backend.manager.api.vfolders.handlers import VFolderServiceProtocol +from ai.backend.manager.models import ( + VFolderOperationStatus, + VFolderOwnershipType, + VFolderPermission, +) + + +@pytest.fixture +def mock_authenticated_request(): + mock_request = MagicMock() + mock_request.__getitem__.side_effect = { + "user": { + "uuid": uuid.uuid4(), + "role": "user", + "email": "test@email.com", + "domain_name": "default", + }, + "keypair": { + "access_key": "TESTKEY", + "resource_policy": {"allowed_vfolder_hosts": ["local"]}, + }, + }.get + + vfolder_id = str(uuid.uuid4()) + mock_request.match_info = {"vfolder_id": vfolder_id} + return mock_request + + +@pytest.fixture +def mock_vfolder_service(): + return MockVFolderService() + + +class MockVFolderService(VFolderServiceProtocol): + async def create_vfolder_in_personal( + self, + user_identity: UserIdentity, + keypair: Keypair, + vfolder_create_requirements: VFolderCreateRequirements, + ) -> VFolderMetadata: + return VFolderMetadata( + id=str(uuid.uuid4()), + name=vfolder_create_requirements.name, + quota_scope_id=QuotaScopeID(QuotaScopeType.USER, uuid.uuid4()), + host="local", + usage_mode=vfolder_create_requirements.usage_mode, + created_at=datetime.now().isoformat(), + permission=vfolder_create_requirements.permission, + max_size=0, + creator=str(user_identity.user_uuid), + ownership_type=VFolderOwnershipType.USER, + user=str(user_identity.user_uuid), + group=None, + cloneable=vfolder_create_requirements.cloneable, + status=VFolderOperationStatus.READY, + ) + + async def create_vfolder_in_group( + self, + user_identity: UserIdentity, + keypair: Keypair, + vfolder_create_requirements: VFolderCreateRequirements, + ) -> VFolderMetadata: + return VFolderMetadata( + id=str(uuid.uuid4()), + name=vfolder_create_requirements.name, + quota_scope_id=QuotaScopeID(QuotaScopeType.USER, uuid.uuid4()), + host="local", + usage_mode=vfolder_create_requirements.usage_mode, + created_at=datetime.now().isoformat(), + permission=vfolder_create_requirements.permission, + max_size=0, + creator=str(user_identity.user_uuid), + ownership_type=VFolderOwnershipType.GROUP, + user=None, + group=str(vfolder_create_requirements.group_id), + cloneable=vfolder_create_requirements.cloneable, + status=VFolderOperationStatus.READY, + ) + + async def get_vfolders( + self, user_identity: UserIdentity, group_id: Optional[uuid.UUID] + ) -> VFolderList: + test_vfolder = VFolderListItem( + id=str(uuid.uuid4()), + name="test-folder", + quota_scope_id=str(uuid.uuid4()), + host="local", + usage_mode=VFolderUsageMode.GENERAL, + created_at=datetime.now().isoformat(), + permission=VFolderPermission.READ_WRITE, + max_size=0, + creator=str(user_identity.user_uuid), + ownership_type=VFolderOwnershipType.USER, + user=str(user_identity.user_uuid), + group=None, + cloneable=False, + status=VFolderOperationStatus.READY, + is_owner=True, + user_email="test@test.com", + group_name="test-group", + type="user", + max_files=1000, + cur_size=0, + ) + return VFolderList(entries=[test_vfolder]) + + async def rename_vfolder( + self, + user_identity: UserIdentity, + keypair: Keypair, + vfolder_id: uuid.UUID, + new_name: str, + ) -> None: + pass + + async def delete_vfolder( + self, + vfolder_id: str, + user_identity: UserIdentity, + keypair: Keypair, + ) -> None: + pass diff --git a/tests/manager/api/vfolders/test_handlers.py b/tests/manager/api/vfolders/test_handlers.py new file mode 100644 index 00000000000..af866dc56ba --- /dev/null +++ b/tests/manager/api/vfolders/test_handlers.py @@ -0,0 +1,158 @@ +import json +import uuid +from typing import Any +from unittest.mock import AsyncMock + +import pytest + +from ai.backend.common.exception import InvalidAPIParameters +from ai.backend.common.types import VFolderUsageMode +from ai.backend.manager.api.vfolders.handlers import VFolderHandler +from ai.backend.manager.models import ( + VFolderOwnershipType, + VFolderPermission, +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "folder_data, expected_ownership", + [ + ( + { + "name": "test-folder", + "folder_host": "test-host", + "usage_mode": VFolderUsageMode.GENERAL, + "permission": VFolderPermission.READ_WRITE, + }, + VFolderOwnershipType.USER, + ), + ( + { + "name": "test-folder", + "folder_host": "test-host", + "usage_mode": VFolderUsageMode.GENERAL, + "permission": VFolderPermission.READ_WRITE, + "group": str(uuid.uuid4()), + }, + VFolderOwnershipType.GROUP, + ), + ], +) +async def test_create_vfolder_success( + mock_vfolder_service, + mock_authenticated_request, + folder_data: dict[str, Any], + expected_ownership: VFolderOwnershipType, +): + mock_authenticated_request.can_read_body = True + mock_authenticated_request.json = AsyncMock(return_value=folder_data) + + handler = VFolderHandler(vfolder_service=mock_vfolder_service) + response = await handler.create_vfolder(mock_authenticated_request) + + response_data = json.loads(response.text) + assert response_data["name"] == folder_data["name"] + assert response_data["ownership_type"] == expected_ownership + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "list_params,expected_count", + [ + ({"all": True}, 1), + ({"all": False, "group_id": str(uuid.uuid4())}, 1), + ({"all": False, "owner_user_email": "test@test.com"}, 1), + ], +) +async def test_list_vfolders( + mock_vfolder_service, + mock_authenticated_request, + list_params: dict[str, Any], + expected_count: int, +): + mock_authenticated_request.query = list_params + + handler = VFolderHandler(vfolder_service=mock_vfolder_service) + response = await handler.list_vfolders(mock_authenticated_request) + + response_data = json.loads(response.text) + assert len(response_data["root"]) == expected_count + if response_data["root"]: + assert response_data["root"][0]["user_email"] == "test@test.com" + + +@pytest.mark.asyncio +async def test_rename_vfolder_success( + mock_vfolder_service, + mock_authenticated_request, +): + mock_authenticated_request.can_read_body = True + mock_authenticated_request.json = AsyncMock(return_value={"new_name": "new-folder-name"}) + + handler = VFolderHandler(vfolder_service=mock_vfolder_service) + response = await handler.rename_vfolder(mock_authenticated_request) + + assert response.status == 201 + + +@pytest.mark.asyncio +async def test_delete_vfolder_success( + mock_vfolder_service, + mock_authenticated_request, +): + handler = VFolderHandler(vfolder_service=mock_vfolder_service) + response = await handler.delete_vfolder(mock_authenticated_request) + + assert response.status == 204 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "invalid_data", + [ + {"name": ".bashrc", "folder_host": "test-host"}, # matches ^\.[a-z0-9]+rc$ pattern + { + "name": ".python_profile", + "folder_host": "test-host", + }, # matches ^\.[a-z0-9]+_profile$ pattern + {"name": "test" * 50, "folder_host": "test-host"}, # exceeds 64 char limit + {"name": "/etc", "folder_host": "test-host"}, # in RESERVED_VFOLDERS + {"name": "/tmp", "folder_host": "test-host"}, + ], +) +async def test_create_vfolder_invalid_input( + mock_vfolder_service, + mock_authenticated_request, + invalid_data: dict[str, Any], +): + mock_authenticated_request.can_read_body = True + mock_authenticated_request.json = AsyncMock(return_value=invalid_data) + + handler = VFolderHandler(vfolder_service=mock_vfolder_service) + with pytest.raises(InvalidAPIParameters): + await handler.create_vfolder(mock_authenticated_request) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "invalid_name", + [ + ".bashrc", # matches ^\.[a-z0-9]+rc$ pattern + ".bash_profile", # matches ^\.[a-z0-9]+_profile$ pattern + ".python_profile", # matches ^\.[a-z0-9]+_profile$ pattern + "test" * 50, # exceeds 64 char limit + "/etc", # in RESERVED_VFOLDERS + ], +) +async def test_rename_vfolder_invalid_name( + mock_vfolder_service, + mock_authenticated_request, + invalid_name: str, +): + mock_authenticated_request.can_read_body = True + mock_authenticated_request.json = AsyncMock(return_value={"new_name": invalid_name}) + + handler = VFolderHandler(vfolder_service=mock_vfolder_service) + with pytest.raises(InvalidAPIParameters): + await handler.rename_vfolder(mock_authenticated_request)