From 152d3b96a51173ff8b8896139c37656b8c3255b8 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Fri, 8 Mar 2024 17:31:05 -0500 Subject: [PATCH 01/17] update to pydantic 2 --- poetry.lock | 167 ++++++++++++++++++++++++++++++++++--------------- pyproject.toml | 2 +- 2 files changed, 119 insertions(+), 50 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6f0e90f..2573f35 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "atomicwrites" version = "1.4.1" @@ -188,55 +199,113 @@ files = [ [[package]] name = "pydantic" -version = "1.10.1" -description = "Data validation and settings management using python type hints" +version = "2.6.3" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221166d99726238f71adc4fa9f3e94063a10787574b966f86a774559e709ac5a"}, - {file = "pydantic-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a90e85d95fd968cd7cae122e0d3e0e1f6613bc88c1ff3fe838ac9785ea4b1c4c"}, - {file = "pydantic-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2157aaf5718c648eaec9e654a34179ae42ffc363dc3ad058538a4f3ecbd9341"}, - {file = "pydantic-1.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6142246fc9adb51cadaeb84fb52a86f3adad4c6a7b0938a5dd0b1356b0088217"}, - {file = "pydantic-1.10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:60dad97a09b6f44690c05467a4f397b62bfc2c839ac39102819d6979abc2be0d"}, - {file = "pydantic-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6f5bcb59d33ec46621dae76e714c53035087666cac80c81c9047a84f3ff93d0"}, - {file = "pydantic-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:522906820cd60e63c7960ba83078bf2d2ad2dd0870bf68248039bcb1ec3eb0a4"}, - {file = "pydantic-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d545c89d88bdd5559db17aeb5a61a26799903e4bd76114779b3bf1456690f6ce"}, - {file = "pydantic-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad2374b5b3b771dcc6e2f6e0d56632ab63b90e9808b7a73ad865397fcdb4b2cd"}, - {file = "pydantic-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90e02f61b7354ed330f294a437d0bffac9e21a5d46cb4cc3c89d220e497db7ac"}, - {file = "pydantic-1.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc5ffe7bd0b4778fa5b7a5f825c52d6cfea3ae2d9b52b05b9b1d97e36dee23a8"}, - {file = "pydantic-1.10.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7acb7b66ffd2bc046eaff0063df84c83fc3826722d5272adaeadf6252e17f691"}, - {file = "pydantic-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e6786ed5faa559dea5a77f6d2de9a08d18130de9344533535d945f34bdcd42e"}, - {file = "pydantic-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:c7bf8ff1d18186eb0cbe42bd9bfb4cbf7fde1fd01b8608925458990c21f202f0"}, - {file = "pydantic-1.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14a5babda137a294df7ad5f220986d79bbb87fdeb332c6ded61ce19da7f5f3bf"}, - {file = "pydantic-1.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5659cb9c6b3d27fc0067025c4f5a205f5e838232a4a929b412781117c2343d44"}, - {file = "pydantic-1.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d70fb91b03c32d2e857b071a22a5225e6b625ca82bd2cc8dd729d88e0bd200"}, - {file = "pydantic-1.10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9a93be313e40f12c6f2cb84533b226bbe23d0774872e38d83415e6890215e3a6"}, - {file = "pydantic-1.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d55aeb01bb7bd7c7e1bd904668a4a2ffcbb1c248e7ae9eb40a272fd7e67dd98b"}, - {file = "pydantic-1.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:43d41b6f13706488e854729955ba8f740e6ec375cd16b72b81dc24b9d84f0d15"}, - {file = "pydantic-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f31ffe0e38805a0e6410330f78147bb89193b136d7a5f79cae60d3e849b520a6"}, - {file = "pydantic-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8eee69eda7674977b079a21e7bf825b59d8bf15145300e8034ed3eb239ac444f"}, - {file = "pydantic-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f927bff6c319fc92e0a2cbeb2609b5c1cd562862f4b54ec905e353282b7c8b1"}, - {file = "pydantic-1.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1bc3f8fef6ba36977108505e90558911e7fbccb4e930805d5dd90891b56ff4"}, - {file = "pydantic-1.10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96ab6ce1346d14c6e581a69c333bdd1b492df9cf85ad31ad77a8aa42180b7e09"}, - {file = "pydantic-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:444cf220a12134da1cd42fe4f45edff622139e10177ce3d8ef2b4f41db1291b2"}, - {file = "pydantic-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:dbfbff83565b4514dd8cebc8b8c81a12247e89427ff997ad0a9da7b2b1065c12"}, - {file = "pydantic-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5327406f4bfd5aee784e7ad2a6a5fdd7171c19905bf34cb1994a1ba73a87c468"}, - {file = "pydantic-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1072eae28bf034a311764c130784e8065201a90edbca10f495c906737b3bd642"}, - {file = "pydantic-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce901335667a68dfbc10dd2ee6c0d676b89210d754441c2469fbc37baf7ee2ed"}, - {file = "pydantic-1.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54d6465cd2112441305faf5143a491b40de07a203116b5755a2108e36b25308d"}, - {file = "pydantic-1.10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2b5e5e7a0ec96704099e271911a1049321ba1afda92920df0769898a7e9a1298"}, - {file = "pydantic-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ae43704358304da45c1c3dd7056f173c618b252f91594bcb6d6f6b4c6c284dee"}, - {file = "pydantic-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:2d7da49229ffb1049779a5a6c1c50a26da164bd053cf8ee9042197dc08a98259"}, - {file = "pydantic-1.10.1-py3-none-any.whl", hash = "sha256:f8b10e59c035ff3dcc9791619d6e6c5141e0fa5cbe264e19e267b8d523b210bf"}, - {file = "pydantic-1.10.1.tar.gz", hash = "sha256:d41bb80347a8a2d51fbd6f1748b42aca14541315878447ba159617544712f770"}, + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, ] [package.dependencies] -typing-extensions = ">=4.1.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyparsing" @@ -335,13 +404,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.3.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] @@ -362,5 +431,5 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "269986645e36addffacf8f2bd6e4d8424318250277321441f23e962a3cce5ba0" +python-versions = "^3.11" +content-hash = "2797f407eb6a8abeb3bbc122c1bd9c355519a1fbc3f90bad84ea739b562b16b6" diff --git a/pyproject.toml b/pyproject.toml index 35333b2..bb0f4c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ repository = 'https://github.com/btoron/pyOFSC' python = "^3.11" requests = "^2.28.1" pytest = "^7.1.2" -pydantic = "^1.9.1" +pydantic = "^2.6.3" cachetools = "^5.3.1" [tool.poetry.dev-dependencies] From 6fbf58850975a99ce23915e51074119b5adeb456 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Mon, 11 Mar 2024 15:11:32 -0400 Subject: [PATCH 02/17] User tests updated --- .gitignore | 3 + ofsc/__init__.py | 12 +-- ofsc/core.py | 43 ++++---- ofsc/metadata.py | 129 ++++++++++------------ ofsc/models.py | 194 ++++++++++++++-------------------- poetry.lock | 35 +++++- pyproject.toml | 1 + tests/OFSC_activities_test.py | 14 +-- tests/OFSC_metadata_test.py | 42 -------- tests/OFSC_resources_test.py | 12 +-- tests/OFSC_test.py | 40 ++++--- tests/OFSC_users_test.py | 109 ------------------- tests/conftest.py | 26 ++++- tests/test_core_activities.py | 56 +++++++++- tests/test_core_users.py | 87 +++++++++++++++ tests/test_metadata.py | 46 ++++---- tests/test_model.py | 31 ++++-- 17 files changed, 442 insertions(+), 438 deletions(-) delete mode 100644 tests/OFSC_users_test.py create mode 100644 tests/test_core_users.py diff --git a/.gitignore b/.gitignore index 01857da..3ceb268 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +signature.png +**/log.txt +log.txt diff --git a/ofsc/__init__.py b/ofsc/__init__.py index 25fc474..4e0bdd8 100644 --- a/ofsc/__init__.py +++ b/ofsc/__init__.py @@ -75,18 +75,14 @@ def __getattr__(self, method_name): def wrapper(*args, **kwargs): if method_name in self._core_methods: - warn( - f"{method_name} was called without the API name (Core). This will be deprecated in OFSC 2.0", - DeprecationWarning, + raise NotImplementedError( + f"{method_name} was called without the API name (Core). This was deprecated in OFSC 2.0" ) - return getattr(self.core, method_name)(*args, **kwargs) if method_name in self._metadata_methods: - warn( - f"{method_name} was called without the API name (Metadata). This will be deprecated in OFSC 2.0", - DeprecationWarning, + raise NotImplementedError( + f"{method_name} was called without the API name (Metadata). This was deprecated in OFSC 2.0" ) - return getattr(self.metadata, method_name)(*args, **kwargs) raise Exception("method not found") return wrapper diff --git a/ofsc/core.py b/ofsc/core.py index 18c40ab..fdfab72 100644 --- a/ofsc/core.py +++ b/ofsc/core.py @@ -361,28 +361,6 @@ def get_daily_extract_file(self, date, filename, response_type=FULL_RESPONSE): else: return response.text - def get_file_property( - self, - activityId, - label, - mediaType="application/octet-stream", - response_type=FULL_RESPONSE, - ): - headers = self.headers - headers["Accept"] = mediaType - response = requests.get( - "https://api.etadirect.com/rest/ofscCore/v1/activities/{}/{}".format( - activityId, label - ), - headers=headers, - ) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - ## 202202 Helper functions def get_all_activities( self, root, date_from, date_to, activity_fields, initial_offset=0, limit=5000 @@ -494,5 +472,24 @@ def bulk_update(self, data: BulkUpdateRequest): self.baseUrl, "/rest/ofscCore/v1/activities/custom-actions/bulkUpdate", ) - response = requests.post(url, headers=self.headers, data=data.json()) + response = requests.post(url, headers=self.headers, data=data.model_dump_json()) + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_file_property( + self, + activityId, + label, + mediaType="application/octet-stream", + ): + url = urljoin( + self.baseUrl, + f"/rest/ofscCore/v1/activities/{activityId}/{label}", + ) + headers = self.headers + headers["Accept"] = mediaType + response = requests.get( + url, + headers=headers, + ) return response diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 3ec8097..f59e0cd 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -154,82 +154,6 @@ def create_or_replace_property(self, property: Property): return response # 202208 Skill management - def get_workskills(self, offset=0, limit=100, response_type=FULL_RESPONSE): - url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/workSkills") - params = {"offset": offset, "limit": limit} - response = requests.get( - url, - headers=self.headers, - params=params, - ) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def get_workskill(self, label: str, response_type=FULL_RESPONSE): - url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") - response = requests.get( - url, - headers=self.headers, - ) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def create_or_update_workskill(self, skill: Workskill, response_type=FULL_RESPONSE): - url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{skill.label}") - response = requests.put(url, headers=self.headers, data=skill.json()) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def delete_workskill(self, label: str, response_type=FULL_RESPONSE): - url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") - response = requests.delete(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - # Workskill conditions - def get_workskill_conditions(self, response_type=FULL_RESPONSE): - url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkillConditions") - response = requests.get( - url, - headers=self.headers, - ) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def replace_workskill_conditions( - self, data: WorskillConditionList, response_type=FULL_RESPONSE - ): - url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkillConditions") - content = '{"items":' + data.json(exclude_none=True) + "}" - headers = self.headers - headers["Content-Type"] = "application/json" - response = requests.put(url, headers=headers, data=content) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text # 202208 Workzones @wrap_return(response_type=JSON_RESPONSE, expected=[200]) @@ -274,3 +198,56 @@ def import_plugin(self, plugin: str): files = [("pluginFile", ("noname.xml", plugin, "text/xml"))] response = requests.post(url, headers=self.headers, files=files) return response + + @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + def get_workskills(self, offset=0, limit=100, response_type=FULL_RESPONSE): + url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/workSkills") + params = {"offset": offset, "limit": limit} + response = requests.get( + url, + headers=self.headers, + params=params, + ) + return response + + @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + def get_workskill(self, label: str, response_type=FULL_RESPONSE): + url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") + response = requests.get( + url, + headers=self.headers, + ) + return response + + @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + def create_or_update_workskill(self, skill: Workskill, response_type=FULL_RESPONSE): + url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{skill.label}") + response = requests.put(url, headers=self.headers, data=skill.model_dump_json()) + return response + + @wrap_return(response_type=FULL_RESPONSE, expected=[204]) + def delete_workskill(self, label: str, response_type=FULL_RESPONSE): + url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") + response = requests.delete(url, headers=self.headers) + return response + + # Workskill conditions + @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + def get_workskill_conditions(self, response_type=FULL_RESPONSE): + url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkillConditions") + response = requests.get( + url, + headers=self.headers, + ) + return response + + @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + def replace_workskill_conditions( + self, data: WorskillConditionList, response_type=FULL_RESPONSE + ): + url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkillConditions") + content = '{"items":' + data.model_dump_json(exclude_none=True) + "}" + headers = self.headers + headers["Content-Type"] = "application/json" + response = requests.put(url, headers=headers, data=content) + return response diff --git a/ofsc/models.py b/ofsc/models.py index d0cd706..f03dcf8 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -1,12 +1,20 @@ import base64 -import typing from enum import Enum from typing import Any, List, Optional from urllib.parse import urljoin import requests from cachetools import TTLCache, cached -from pydantic import BaseModel, Extra, validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + RootModel, + ValidationInfo, + field_validator, +) +from pydantic_settings import BaseSettings +from typing_extensions import Annotated from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, wrap_return @@ -16,8 +24,8 @@ class OFSConfig(BaseModel): secret: str companyName: str useToken: bool = False - root: Optional[str] - baseURL: Optional[str] + root: Optional[str] = None + baseURL: Optional[str] = None @property def basicAuthString(self): @@ -25,19 +33,17 @@ def basicAuthString(self): bytes(self.clientID + "@" + self.companyName + ":" + self.secret, "utf-8") ) - class Config: - validate_assignment = True + model_config = ConfigDict(validate_assignment=True) - @validator("baseURL") - def set_base_URL(cls, url, values): - print(values) - return url or f"https://{values['companyName']}.fs.ocs.oraclecloud.com" + @field_validator("baseURL") + def set_base_URL(cls, url, info: ValidationInfo): + return url or f"https://{info.data['companyName']}.fs.ocs.oraclecloud.com" class OFSOAuthRequest(BaseModel): - assertion: Optional[str] + assertion: Optional[str] = None grant_type: str = "client_credentials" - ofs_dynamic_scope: Optional[str] + ofs_dynamic_scope: Optional[str] = None class OFSApi: @@ -69,7 +75,7 @@ def token(self, auth: OFSOAuthRequest = OFSOAuthRequest()) -> requests.Response: headers["Content-Type"] = "application/x-www-form-urlencoded" url = urljoin(self.baseUrl, "/rest/oauthTokenService/v2/token") response = requests.post( - url, data=auth.dict(exclude_none=True), headers=headers + url, data=auth.model_dump(exclude_none=True), headers=headers ) return response @@ -77,9 +83,9 @@ def token(self, auth: OFSOAuthRequest = OFSOAuthRequest()) -> requests.Response: def headers(self): self._headers = {} if not self._config.useToken: - self._headers[ - "Authorization" - ] = "Basic " + self._config.basicAuthString.decode("utf-8") + self._headers["Authorization"] = ( + "Basic " + self._config.basicAuthString.decode("utf-8") + ) else: self._token = self.token().json()["access_token"] self._headers["Authorization"] = f"Bearer {self._token}" @@ -116,17 +122,15 @@ class EntityEnum(str, Enum): class Translation(BaseModel): language: str = "en" name: str - languageISO: Optional[str] - + languageISO: Optional[str] = None -class TranslationList(BaseModel): - __root__: List[Translation] +class TranslationList(RootModel[List[Translation]]): def __iter__(self): - return iter(self.__root__) + return iter(self.root) def __getitem__(self, item): - return self.__root__[item] + return self.root[item] class Workskill(BaseModel): @@ -134,21 +138,18 @@ class Workskill(BaseModel): active: bool = True name: str = "" sharing: SharingEnum - translations: Optional[TranslationList] + translations: Annotated[Optional[TranslationList], Field(validate_default=True)] = ( + None + ) - @validator("translations", always=True) + @field_validator("translations") def set_default(cls, field_value, values): - return field_value or [Translation(name=values["name"])] - - -class WorkskillList(BaseModel): - __root__: List[Workskill] + return field_value or TranslationList( + [Translation(name=values.data.get("name"))] + ) - def __iter__(self): - return iter(self.__root__) - def __getitem__(self, item): - return self.__root__[item] +WorkskillList = RootModel[List[Workskill]] class Condition(BaseModel): @@ -164,17 +165,10 @@ class WorkskillCondition(BaseModel): requiredLevel: int preferableLevel: int conditions: List[Condition] - dependencies: Any + dependencies: Any = None -class WorskillConditionList(BaseModel): - __root__: List[WorkskillCondition] - - def __iter__(self): - return iter(self.__root__) - - def __getitem__(self, item): - return self.__root__[item] +WorskillConditionList = RootModel[List[WorkskillCondition]] # Workzones @@ -186,29 +180,23 @@ class Workzone(BaseModel): keys: List[Any] -class WorkzoneList(BaseModel): - __root__: List[Workzone] - - def __iter__(self): - return iter(self.__root__) - - def __getitem__(self, item): - return self.__root__[item] +WorkzoneList = RootModel[List[Workzone]] class Property(BaseModel): label: str name: str type: str - entity: Optional[EntityEnum] - gui: Optional[str] - translations: TranslationList = [] + entity: Optional[EntityEnum] = None + gui: Optional[str] = None + translations: Annotated[TranslationList, Field(validate_default=True)] = [] - @validator("translations", always=True) + @field_validator("translations") def set_default(cls, field_value, values): - return field_value or [Translation(name=values["name"])] + return field_value or [Translation(name=values.name)] - @validator("gui") + @field_validator("gui") + @classmethod def gui_match(cls, v): if v not in [ "text", @@ -227,47 +215,30 @@ def gui_match(cls, v): raise ValueError(f"{v} is not a valid GUI value") return v - class Config: - extra = Extra.allow # or 'allow' str + model_config = ConfigDict(extra="allow") -class PropertyList(BaseModel): - __root__: List[Property] - - def __iter__(self): - return iter(self.__root__) - - def __getitem__(self, item): - return self.__root__[item] +PropertyList = RootModel[List[Property]] class Resource(BaseModel): - resourceId: Optional[str] - parentResourceId: Optional[str] + resourceId: Optional[str] = None + parentResourceId: Optional[str] = None resourceType: str name: str status: str = "active" organization: str = "default" language: str - languageISO: Optional[str] + languageISO: Optional[str] = None timeZone: str timeFormat: str = "24-hour" dateFormat: str = "mm/dd/yy" - email: Optional[str] - phone: Optional[str] + email: Optional[str] = None + phone: Optional[str] = None + model_config = ConfigDict(extra="allow") - class Config: - extra = Extra.allow # or 'allow' str - -class ResourceList(BaseModel): - __root__: List[Resource] - - def __iter__(self): - return iter(self.__root__) - - def __getitem__(self, item): - return self.__root__[item] +ResourceList = RootModel[List[Resource]] class ResourceType(BaseModel): @@ -275,40 +246,29 @@ class ResourceType(BaseModel): name: str active: bool role: str # TODO: change to enum - - class Config: - extra = Extra.allow # or 'allow' str + model_config = ConfigDict(extra="allow") -class ResourceTypeList(BaseModel): - __root__: List[ResourceType] - - def __iter__(self): - return iter(self.__root__) - - def __getitem__(self, item): - return self.__root__[item] +ResourceTypeList = RootModel[List[ResourceType]] # Core / Activities class BulkUpdateActivityItem(BaseModel): - activityId: Optional[int] - activityType: Optional[str] - date: Optional[str] - - class Config: - extra = Extra.allow # or 'allow' str + activityId: Optional[int] = None + activityType: Optional[str] = None + date: Optional[str] = None + model_config = ConfigDict(extra="allow") # CORE / BulkUpdaterequest class BulkUpdateParameters(BaseModel): - fallbackResource: Optional[str] - identifyActivityBy: Optional[str] - ifExistsThenDoNotUpdateFields: Optional[List[str]] - ifInFinalStatusThen: Optional[str] - inventoryPropertiesUpdateMode: Optional[str] + fallbackResource: Optional[str] = None + identifyActivityBy: Optional[str] = None + ifExistsThenDoNotUpdateFields: Optional[List[str]] = None + ifInFinalStatusThen: Optional[str] = None + inventoryPropertiesUpdateMode: Optional[str] = None class BulkUpdateRequest(BaseModel): @@ -317,28 +277,28 @@ class BulkUpdateRequest(BaseModel): class ActivityKeys(BaseModel): - activityId: Optional[int] - apptNumber: Optional[str] - customerNumber: Optional[str] + activityId: Optional[int] = None + apptNumber: Optional[str] = None + customerNumber: Optional[str] = None class BulkUpdateError(BaseModel): - errorDetail: Optional[str] - operation: Optional[str] + errorDetail: Optional[str] = None + operation: Optional[str] = None class BulkUpdateWarning(BaseModel): - code: Optional[int] - message: Optional[int] + code: Optional[int] = None + message: Optional[int] = None class BulkUpdateResult(BaseModel): - activityKeys: Optional[ActivityKeys] - errors: Optional[List[BulkUpdateError]] - operationsFailed: Optional[List[str]] - operationsPerformed: Optional[List[str]] - warnings: Optional[List[BulkUpdateWarning]] + activityKeys: Optional[ActivityKeys] = None + errors: Optional[List[BulkUpdateError]] = None + operationsFailed: Optional[List[str]] = None + operationsPerformed: Optional[List[str]] = None + warnings: Optional[List[BulkUpdateWarning]] = None class BulkUpdateResponse(BaseModel): - results: Optional[List[BulkUpdateResult]] + results: Optional[List[BulkUpdateResult]] = None diff --git a/poetry.lock b/poetry.lock index 2573f35..72140ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -307,6 +307,25 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.2.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, +] + +[package.dependencies] +pydantic = ">=2.3.0" +python-dotenv = ">=0.21.0" + +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -359,6 +378,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "requests" version = "2.31.0" @@ -432,4 +465,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "2797f407eb6a8abeb3bbc122c1bd9c355519a1fbc3f90bad84ea739b562b16b6" +content-hash = "aec42bf79ead76af1450c1397847d09fe8295c77ddb400369ce12ad9eabdd6c4" diff --git a/pyproject.toml b/pyproject.toml index bb0f4c7..6151501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ requests = "^2.28.1" pytest = "^7.1.2" pydantic = "^2.6.3" cachetools = "^5.3.1" +pydantic-settings = "^2.2.1" [tool.poetry.dev-dependencies] openpyxl = "^3.0.10" diff --git a/tests/OFSC_activities_test.py b/tests/OFSC_activities_test.py index 3acf63d..d2c6463 100644 --- a/tests/OFSC_activities_test.py +++ b/tests/OFSC_activities_test.py @@ -26,14 +26,14 @@ def setUp(self): secret=os.environ.get("OFSC_CLIENT_SECRET"), companyName=os.environ.get("OFSC_COMPANY"), ) - response = self.instance.get_activity(3954794, response_type=JSON_RESPONSE) + response = self.instance.core.get_activity(3954794, response_type=JSON_RESPONSE) self.assertIsNotNone(response["date"]) self.date = response["date"] # Test A.01 Get Activity Info (activity exists) def test_A01_get_activity(self): self.logger.info("...101: Get Activity Info (activity does exist)") - raw_response = self.instance.get_activity(3951935) + raw_response = self.instance.core.get_activity(3951935) response = json.loads(raw_response) self.logger.debug(response) self.assertEqual(response["customerNumber"], "019895700") @@ -43,7 +43,7 @@ def test_A02_get_activity(self): instance = self.instance logger = self.logger logger.info("...102: Get Activity Info (activity does not exist)") - raw_response = instance.get_activity(99999) + raw_response = instance.core.get_activity(99999) response = json.loads(raw_response) logger.debug(response) @@ -55,27 +55,27 @@ def test_A04_move_activity_between_buckets_no_error(self): logger = self.logger # Do a get resource to verify that the activity is in the right place - response = instance.get_activity(4224010, response_type=FULL_RESPONSE) + response = instance.core.get_activity(4224010, response_type=FULL_RESPONSE) logger.debug(response.json()) self.assertEqual(response.status_code, 200) original_resource = response.json()["resourceId"] logger.info("...104: Move activity (activity exists)") data = {"setResource": {"resourceId": "FLUSA"}} - response = instance.move_activity( + response = instance.core.move_activity( 4224010, json.dumps(data), response_type=FULL_RESPONSE ) self.assertEqual(response.status_code, 204) # Do a get resource to verify that the activity is in the right place - response = instance.get_activity(4224010, response_type=FULL_RESPONSE) + response = instance.core.get_activity(4224010, response_type=FULL_RESPONSE) logger.debug(response.json()) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["resourceId"], "FLUSA") # Return it to the previous place data["setResource"]["resourceId"] = original_resource - response = instance.move_activity( + response = instance.core.move_activity( 4224010, json.dumps(data), response_type=FULL_RESPONSE ) self.assertEqual(response.status_code, 204) diff --git a/tests/OFSC_metadata_test.py b/tests/OFSC_metadata_test.py index 4f264cc..cada932 100644 --- a/tests/OFSC_metadata_test.py +++ b/tests/OFSC_metadata_test.py @@ -27,48 +27,6 @@ def setUp(self): ) self.date = os.environ.get("OFSC_TEST_DATE") - # Test C.P.10 Get File Property 01 - def test_get_file_property_01(self): - self.logger.info("...C.P.01 Get File Property") - instance = self.instance - logger = self.logger - raw_response = instance.get_file_property( - activityId=3954865, - label="csign", - mediaType="*/*", - response_type=FULL_RESPONSE, - ) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.debug(self.pp.pformat(response)) - self.assertIsNotNone(response["mediaType"]) - self.assertEqual(response["mediaType"], "image/png") - self.assertEqual(response["name"], "signature.png") - - # Test C.P.10 Get File Property 02 - def test_get_file_property_02(self): - self.logger.info("...C.P.02 Get File Property content") - instance = self.instance - logger = self.logger - metadata_response = instance.get_file_property( - activityId=3954865, - label="csign", - mediaType="*/*", - response_type=FULL_RESPONSE, - ) - logging.debug(self.pp.pformat(metadata_response.json())) - response = metadata_response.json() - raw_response = instance.get_file_property( - activityId=3954865, - label="csign", - mediaType="image/png", - response_type=FULL_RESPONSE, - ) - with open(os.path.join(os.getcwd(), response["name"]), "wb") as fd: - fd.write(raw_response.content) - self.assertEqual(response["name"], "signature.png") - # TODO: Assert the size of the file - if __name__ == "__main__": unittest.main() diff --git a/tests/OFSC_resources_test.py b/tests/OFSC_resources_test.py index f8eae78..79c7acc 100644 --- a/tests/OFSC_resources_test.py +++ b/tests/OFSC_resources_test.py @@ -23,7 +23,7 @@ def setUp(self): secret=os.environ.get("OFSC_CLIENT_SECRET"), companyName=os.environ.get("OFSC_COMPANY"), ) - response = self.instance.get_activity(3954794, response_type=JSON_RESPONSE) + response = self.instance.core.get_activity(3954794, response_type=JSON_RESPONSE) self.assertIsNotNone(response["date"]) self.date = response["date"] @@ -31,7 +31,7 @@ def setUp(self): def test_R01_get_resource_route_nofields(self): instance = self.instance logger = self.logger - raw_response = instance.get_resource_route( + raw_response = instance.core.get_resource_route( 33001, date=self.date, response_type=FULL_RESPONSE ) logging.debug(self.pp.pformat(raw_response.json())) @@ -41,7 +41,7 @@ def test_R01_get_resource_route_nofields(self): def test_R02_get_resource_route_twofields(self): instance = self.instance logger = self.logger - raw_response = instance.get_resource_route( + raw_response = instance.core.get_resource_route( 33001, date=self.date, activityFields="activityId,activityType" ) response = json.loads(raw_response) @@ -51,7 +51,7 @@ def test_R02_get_resource_route_twofields(self): def test_R03_get_resource_descendants_noexpand(self): instance = self.instance logger = self.logger - raw_response = instance.get_resource_descendants("FLUSA") + raw_response = instance.core.get_resource_descendants("FLUSA") response = json.loads(raw_response) # print(response) self.assertEqual(response["totalResults"], 37) @@ -59,7 +59,7 @@ def test_R03_get_resource_descendants_noexpand(self): def test_R04_get_resource_descendants_expand(self): instance = self.instance logger = self.logger - raw_response = instance.get_resource_descendants( + raw_response = instance.core.get_resource_descendants( "FLUSA", workSchedules=True, workZones=True, workSkills=True ) response = json.loads(raw_response) @@ -69,7 +69,7 @@ def test_R04_get_resource_descendants_expand(self): def test_R05_get_resource_descendants_noexpand_fields(self): instance = self.instance logger = self.logger - raw_response = instance.get_resource_descendants( + raw_response = instance.core.get_resource_descendants( "FLUSA", resourceFields="resourceId,phone", response_type=FULL_RESPONSE ) # logging.debug(self.pp.pformat(raw_response.json())) diff --git a/tests/OFSC_test.py b/tests/OFSC_test.py index 246fef3..5681a0f 100644 --- a/tests/OFSC_test.py +++ b/tests/OFSC_test.py @@ -36,25 +36,25 @@ def move_activity_between_buckets_no_error(self): logger = self.logger # Do a get resource to verify that the activity is in the right place - response = instance.get_activity(self.aid, response_type=FULL_RESPONSE) + response = instance.core.get_activity(self.aid, response_type=FULL_RESPONSE) self.assertEqual(response.status_code, 200) original_resource = response.json()["resourceId"] logger.info("...104: Move activity (activity exists)") data = {"setResource": {"resourceId": "FLUSA"}} - response = instance.move_activity( + response = instance.core.move_activity( 4224010, json.dumps(data), response_type=FULL_RESPONSE ) self.assertEqual(response.status_code, 204) # Do a get resource to verify that the activity is in the right place - response = instance.get_activity(self.aid, response_type=FULL_RESPONSE) + response = instance.core.get_activity(self.aid, response_type=FULL_RESPONSE) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["resourceId"], "FLUSA") # Return it to the previous place data["setResource"]["resourceId"] = original_resource - response = instance.move_activity( + response = instance.core.move_activity( 4224010, json.dumps(data), response_type=FULL_RESPONSE ) self.assertEqual(response.status_code, 204) @@ -65,7 +65,7 @@ def test_004_get_events(self): logger = self.logger global pp, created_time created_subscription = self.create_subscription() - details = instance.get_subscription_details( + details = instance.core.get_subscription_details( created_subscription["subscriptionId"], response_type=JSON_RESPONSE ) # Moving activity @@ -76,7 +76,7 @@ def test_004_get_events(self): } logger.info("...210: Get Events") current_page = "" - raw_response = instance.get_events(params) + raw_response = instance.core.get_events(params) response = json.loads(raw_response) logger.info(self.pp.pformat(response)) self.assertTrue(response["found"]) @@ -85,7 +85,9 @@ def test_004_get_events(self): while next_page != current_page: current_page = next_page params2 = {"subscriptionId": details["subscriptionId"], "page": next_page} - raw_response = instance.get_events(params2, response_type=FULL_RESPONSE) + raw_response = instance.core.get_events( + params2, response_type=FULL_RESPONSE + ) response = raw_response.json() if response["items"]: events.extend(response["items"]) @@ -100,21 +102,23 @@ def test_004_get_events(self): def test_201_get_resource_no_expand(self): instance = self.instance logger = self.logger - raw_response = instance.get_resource(55001) + raw_response = instance.core.get_resource(55001) response = json.loads(raw_response) self.assertEqual(response["resourceInternalId"], 5000001) def test_202_get_resource_expand(self): instance = self.instance logger = self.logger - raw_response = instance.get_resource(55001, workSkills=True, workZones=True) + raw_response = instance.core.get_resource( + 55001, workSkills=True, workZones=True + ) response = json.loads(raw_response) self.assertEqual(response["resourceInternalId"], 5000001) def test_203_get_position_history(self): instance = self.instance logger = self.logger - raw_response = instance.get_position_history(33001, date=self.date) + raw_response = instance.core.get_position_history(33001, date=self.date) response = json.loads(raw_response) self.assertIsNotNone(response["totalResults"]) self.assertTrue(response["totalResults"] > 200) @@ -123,7 +127,7 @@ def test_203_get_position_history(self): def test_301_get_capacity_areas_simple(self): instance = self.instance logger = self.logger - raw_response = instance.get_capacity_areas(response_type=FULL_RESPONSE) + raw_response = instance.metadata.get_capacity_areas(response_type=FULL_RESPONSE) logging.debug(self.pp.pformat(raw_response.json())) response = raw_response.json() logger.info(self.pp.pformat(response)) @@ -134,7 +138,9 @@ def test_301_get_capacity_areas_simple(self): def test_302_get_capacity_area(self): instance = self.instance logger = self.logger - raw_response = instance.get_capacity_area("FLUSA", response_type=FULL_RESPONSE) + raw_response = instance.metadata.get_capacity_area( + "FLUSA", response_type=FULL_RESPONSE + ) logging.debug(self.pp.pformat(raw_response.json())) response = raw_response.json() logger.info(self.pp.pformat(response)) @@ -147,7 +153,9 @@ def test_302_get_capacity_area(self): def test_311_get_activity_type_groups(self): instance = self.instance logger = self.logger - raw_response = instance.get_activity_type_groups(response_type=FULL_RESPONSE) + raw_response = instance.metadata.get_activity_type_groups( + response_type=FULL_RESPONSE + ) logging.debug(self.pp.pformat(raw_response.json())) response = raw_response.json() logger.info(self.pp.pformat(response)) @@ -159,7 +167,7 @@ def test_311_get_activity_type_groups(self): def test_312_get_activity_type_group(self): instance = self.instance logger = self.logger - raw_response = instance.get_activity_type_group( + raw_response = instance.metadata.get_activity_type_group( "customer", response_type=FULL_RESPONSE ) logging.debug(self.pp.pformat(raw_response.json())) @@ -175,7 +183,7 @@ def test_312_get_activity_type_group(self): def test_313_get_activity_types(self): instance = self.instance logger = self.logger - raw_response = instance.get_activity_types(response_type=FULL_RESPONSE) + raw_response = instance.metadata.get_activity_types(response_type=FULL_RESPONSE) logging.debug(self.pp.pformat(raw_response.json())) response = raw_response.json() self.assertEqual(raw_response.status_code, 200) @@ -193,7 +201,7 @@ def test_313_get_activity_types(self): def test_313_get_activity_type(self): instance = self.instance logger = self.logger - raw_response = instance.get_activity_type( + raw_response = instance.metadata.get_activity_type( "ac_installation", response_type=FULL_RESPONSE ) logging.debug(self.pp.pformat(raw_response.json())) diff --git a/tests/OFSC_users_test.py b/tests/OFSC_users_test.py deleted file mode 100644 index d5fa027..0000000 --- a/tests/OFSC_users_test.py +++ /dev/null @@ -1,109 +0,0 @@ -import unittest - - -import sys, os -sys.path.append(os.path.abspath('.')) -from ofsc import OFSC, FULL_RESPONSE -import logging -import json -import argparse - - -import pprint - - -class ofscTest(unittest.TestCase): - - def setUp(self): - self.logger = logging.getLogger() - self.pp = pprint.PrettyPrinter(indent=4) - self.logger.setLevel(logging.DEBUG) - #todo add credentials to test run - logging.warning("Here {}".format(os.environ.get('OFSC_CLIENT_ID'))) - self.instance = OFSC(clientID=os.environ.get('OFSC_CLIENT_ID'), secret=os.environ.get('OFSC_CLIENT_SECRET'), companyName=os.environ.get('OFSC_COMPANY')) - self.date = os.environ.get('OFSC_TEST_DATE') - - # Test C.U.01 Get Users - def test_get_users(self): - self.logger.info("...C.U.01 Get Users") - instance = self.instance - logger = self.logger - raw_response = instance.get_users(response_type=FULL_RESPONSE) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.debug(self.pp.pformat(response)) - self.assertIsNotNone(response['totalResults']) - self.assertEqual(response['totalResults'], 306) - self.assertEqual(response['items'][0]['login'], 'admin') - - def test_get_user(self): - self.logger.info("...C.U.02 Get Specific User") - instance = self.instance - logger = self.logger - raw_response = instance.get_user(login="chris", response_type=FULL_RESPONSE) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.debug(self.pp.pformat(response)) - self.assertEqual(raw_response.status_code, 200) - self.assertIsNotNone(response['login']) - self.assertEqual(response['login'], 'chris') - self.assertEqual(response['resourceInternalIds'][0], 3000000) - - - def test_update_user(self): - self.logger.info("...C.U.03 Update Specific User") - instance = self.instance - logger = self.logger - raw_response = instance.get_user(login="chris", response_type=FULL_RESPONSE) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.info(self.pp.pformat(response)) - self.assertEqual(raw_response.status_code, 200) - self.assertIsNotNone(response['name']) - self.assertEqual(response['name'], 'Chris') - new_data = {} - new_data['name']='Changed' - raw_response = instance.update_user(login="chris", data=json.dumps(new_data), response_type=FULL_RESPONSE) - logging.info(self.pp.pformat(raw_response.text)) - response = raw_response.json() - self.assertEqual(raw_response.status_code, 200) - self.assertIsNotNone(response['name']) - self.assertEqual(response['name'], 'Changed') - new_data = {} - new_data['name']='Chris' - raw_response = instance.update_user(login="chris", data=json.dumps(new_data), response_type=FULL_RESPONSE) - logging.info(self.pp.pformat(raw_response.text)) - response = raw_response.json() - self.assertEqual(raw_response.status_code, 200) - self.assertIsNotNone(response['name']) - self.assertEqual(response['name'], 'Chris') - - def test_create_user(self): - self.logger.info("...C.U.04 Create User (not existent)") - instance = self.instance - logger = self.logger - new_data = { - "name": "Test Name", - "mainResourceId": "44042", - "language": "en", - "timeZone": "Arizona", - "userType": "technician", - "password": "123123123", - "resources": ["44008", "44035", "44042"] - } - raw_response = instance.create_user(login="test_user", data=json.dumps(new_data), response_type=FULL_RESPONSE) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.info(self.pp.pformat(response)) - self.assertEqual(raw_response.status_code, 200) - self.assertIsNotNone(response['name']) - self.assertEqual(response['name'], 'Test Name') - - raw_response = instance.delete_user(login="test_user", response_type=FULL_RESPONSE) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.info(self.pp.pformat(response)) - self.assertEqual(raw_response.status_code, 200) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/conftest.py b/tests/conftest.py index e6addfb..db66a96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,6 +71,28 @@ def demo_data(): "expected_items": 758, "expected_postalcode": "55001", } - } + }, + "24A WMP 02 Demo_Services.E360.Supremo.Chapter8.ESM . 2024-03-01 22:20": { + "get_all_activities": { + "bucket_id": "CAUSA", + "expected_id": 3960470, + "expected_items": 698, + "expected_postalcode": "55001", + }, + "metadata": { + "expected_workskills": 7, + "expected_workskill_conditions": 8, + "expected_resource_types": 10, + "expected_properties": 463, + }, + "get_file_property": { + "activity_id": 3954799, # Note: manual addition + }, + "get_users": { + "totalResults": 322, + }, + }, } - return demo_data["23B Service Update 1"] + return demo_data[ + "24A WMP 02 Demo_Services.E360.Supremo.Chapter8.ESM . 2024-03-01 22:20" + ] diff --git a/tests/test_core_activities.py b/tests/test_core_activities.py index 74d2d9b..95c3496 100644 --- a/tests/test_core_activities.py +++ b/tests/test_core_activities.py @@ -1,4 +1,5 @@ import logging +import os from datetime import date, timedelta import pytest @@ -86,7 +87,6 @@ def test_get_activities_offset(instance, current_date, demo_data, request_loggin def test_model_bulk_update_simple(instance, request_logging): - logging.info("...104. Bulk Update") data = { "updateParameters": { "identifyActivityBy": "apptNumber", @@ -151,8 +151,58 @@ def test_model_bulk_update_simple(instance, request_logging): } ], } - input = BulkUpdateRequest.parse_obj(data) + input = BulkUpdateRequest.model_validate(data) raw_response = instance.core.bulk_update(input, response_type=FULL_RESPONSE) assert raw_response.status_code == 200 response = raw_response.json() - output = BulkUpdateResponse.parse_obj(response) + output = BulkUpdateResponse.model_validate(response) + + +# Test C.P.10 Get File Property 01 +def test_get_file_property_01(instance, pp, demo_data): + activity_id = demo_data.get("get_file_property").get("activity_id") + # Get all properties from the activity + raw_response = instance.core.get_activity(activity_id, response_type=FULL_RESPONSE) + assert raw_response.status_code == 200, raw_response.json() + response = raw_response.json() + # verify that the file is there + assert response.get("csign") is not None + assert response.get("csign").get("links") is not None + logging.info(pp.pformat(response.get("csign").get("links")[0].get("href"))) + raw_response = instance.core.get_file_property( + activityId=activity_id, + label="csign", + mediaType="*/*", + response_type=FULL_RESPONSE, + ) + assert raw_response.status_code == 200, raw_response.json() + logging.info(pp.pformat(raw_response.json())) + response = raw_response.json() + logging.info(pp.pformat(response)) + assert response["mediaType"] is not None + assert response["mediaType"] == "image/png" + assert response["name"] == "signature.png" + + +# Test C.P.10 Get File Property 02 +def test_get_file_property_02(instance, pp, demo_data): + logging.info("...C.P.02 Get File Property content") + activity_id = demo_data.get("get_file_property").get("activity_id") + metadata_response = instance.core.get_file_property( + activityId=activity_id, + label="csign", + mediaType="*/*", + response_type=FULL_RESPONSE, + ) + logging.debug(pp.pformat(metadata_response.json())) + response = metadata_response.json() + raw_response = instance.core.get_file_property( + activityId=activity_id, + label="csign", + mediaType="image/png", + response_type=FULL_RESPONSE, + ) + with open(os.path.join(os.getcwd(), response["name"]), "wb") as fd: + fd.write(raw_response.content) + assert response["name"] == "signature.png" + # TODO: Assert the size of the file diff --git a/tests/test_core_users.py b/tests/test_core_users.py new file mode 100644 index 0000000..aa2482a --- /dev/null +++ b/tests/test_core_users.py @@ -0,0 +1,87 @@ +import os +import sys +import unittest + +sys.path.append(os.path.abspath(".")) +import argparse +import json +import logging +import pprint + +from ofsc import FULL_RESPONSE, OFSC + + +# Test C.U.01 Get Users +def test_get_users(instance, demo_data, pp): + raw_response = instance.core.get_users(response_type=FULL_RESPONSE) + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + logging.debug(pp.pformat(response)) + assert response["totalResults"] is not None + assert response["totalResults"] == demo_data.get("get_users").get("totalResults") + assert response["items"][0]["login"] == "admin" + + +def test_get_user(instance, demo_data, pp): + raw_response = instance.core.get_user(login="chris", response_type=FULL_RESPONSE) + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + logging.debug(pp.pformat(response)) + assert raw_response.status_code == 200 + assert response["login"] is not None + assert response["login"] == "chris" + assert response["resourceInternalIds"][0] == 3000000 + + +def test_update_user(instance, demo_data, pp): + raw_response = instance.core.get_user(login="chris", response_type=FULL_RESPONSE) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["name"] is not None + assert response["name"] == "Chris" + new_data = {} + new_data["name"] = "Changed" + raw_response = instance.core.update_user( + login="chris", data=json.dumps(new_data), response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["name"] is not None + assert response["name"] == "Changed" + new_data = {} + new_data["name"] = "Chris" + raw_response = instance.core.update_user( + login="chris", data=json.dumps(new_data), response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["name"] is not None + assert response["name"] == "Chris" + + +def test_create_user(instance, demo_data, pp): + new_data = { + "name": "Test Name", + "mainResourceId": "44042", + "language": "en", + "timeZone": "Arizona", + "userType": "technician", + "password": "123123123121212Abc!", + "resources": ["44008", "44035", "44042"], + } + raw_response = instance.core.create_user( + login="test_user", data=json.dumps(new_data), response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200, raw_response.json() + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + logging.info(pp.pformat(response)) + + assert response["name"] is not None + assert response["name"] == "Test Name" + + raw_response = instance.core.delete_user( + login="test_user", response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + logging.debug(pp.pformat(raw_response.json())) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 0e8467b..6fa8738 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -18,12 +18,13 @@ ) -def test_get_workskills(instance): +def test_get_workskills(instance, demo_data): logging.info("...Get all skills") metadata_response = instance.metadata.get_workskills(response_type=FULL_RESPONSE) response = metadata_response.json() + expected_workskills = demo_data.get("metadata").get("expected_workskills") assert response["totalResults"] is not None - assert response["totalResults"] == 6 # 22.B + assert response["totalResults"] == expected_workskills # 22.B assert response["items"][0]["label"] == "EST" assert response["items"][1]["name"] == "Residential" @@ -41,7 +42,7 @@ def test_get_workskill(instance): def test_create_workskill(instance, pp): logging.info("...create one skill") skill = Workskill(label="TEST", name="test", sharing=SharingEnum.maximal) - logging.warning(f"TEST.Create WorkSkill: IN: {skill.json()}") + logging.warning(f"TEST.Create WorkSkill: IN: {skill.model_dump_json()}") metadata_response = instance.metadata.create_or_update_workskill( skill=skill, response_type=FULL_RESPONSE ) @@ -55,9 +56,7 @@ def test_create_workskill(instance, pp): def test_delete_workskill(instance): - logging.info("...delete one skill") skill = Workskill(label="TEST", name="test", sharing=SharingEnum.maximal) - logging.warning(skill.json()) metadata_response = instance.metadata.create_or_update_workskill( skill=skill, response_type=FULL_RESPONSE ) @@ -71,36 +70,42 @@ def test_delete_workskill(instance): assert metadata_response.status_code == 204 -def test_get_workskill_conditions(instance, pp): +def test_get_workskill_conditions(instance, pp, demo_data): logging.info("... get workskill conditions") metadata_response = instance.metadata.get_workskill_conditions( response_type=FULL_RESPONSE ) + expected_workskill_conditions = demo_data.get("metadata").get( + "expected_workskill_conditions" + ) response = metadata_response.json() assert metadata_response.status_code == 200 logging.debug(pp.pformat(response)) assert response["totalResults"] is not None - assert response["totalResults"] == 7 + assert response["totalResults"] == expected_workskill_conditions for item in response["items"]: logging.debug(pp.pformat(item)) - ws_item = WorkskillCondition.parse_obj(item) + ws_item = WorkskillCondition.model_validate(item) logging.debug(pp.pformat(ws_item)) assert ws_item.label == item["label"] for condition in ws_item.conditions: assert type(condition) == Condition -def test_replace_workskill_conditions(instance, pp): +def test_replace_workskill_conditions(instance, pp, demo_data): logging.info("... replace workskill conditions") response = instance.metadata.get_workskill_conditions(response_type=JSON_RESPONSE) + expected_workskill_conditions = demo_data.get("metadata").get( + "expected_workskill_conditions" + ) assert response["totalResults"] is not None - assert response["totalResults"] == 7 - ws_list = WorskillConditionList.parse_obj(response["items"]) + assert response["totalResults"] == expected_workskill_conditions + ws_list = WorskillConditionList.model_validate(response["items"]) metadata_response = instance.metadata.replace_workskill_conditions(ws_list) logging.debug(pp.pformat(metadata_response.text)) assert metadata_response.status_code == 200 assert response["totalResults"] is not None - assert response["totalResults"] == 7 + assert response["totalResults"] == expected_workskill_conditions def test_get_workzones(instance): @@ -111,18 +116,20 @@ def test_get_workzones(instance): response = metadata_response.json() assert response["totalResults"] is not None assert response["totalResults"] == 18 # 22.B - assert response["items"][0]["workZoneLabel"] == "ALTAMONTE SPRINGS" + assert response["items"][0]["workZoneLabel"] == "ALTAMONTE_SPRINGS" assert response["items"][1]["workZoneName"] == "CASSELBERRY" -def test_get_resource_types(instance): +def test_get_resource_types(instance, demo_data): logging.info("...Get all Resource Types") metadata_response = instance.metadata.get_resource_types( response_type=FULL_RESPONSE ) response = metadata_response.json() assert response["totalResults"] is not None - assert response["totalResults"] == 9 # 22.D + assert response["totalResults"] == demo_data.get("metadata").get( + "expected_resource_types" + ) def test_get_property(instance): @@ -139,19 +146,20 @@ def test_get_property(instance): property = Property.parse_obj(response) -def test_get_properties(instance): +def test_get_properties(instance, demo_data): logging.info("...Get properties") metadata_response = instance.metadata.get_properties(response_type=FULL_RESPONSE) + expected_properties = demo_data.get("metadata").get("expected_properties") assert metadata_response.status_code == 200 response = metadata_response.json() assert response["totalResults"] - assert response["totalResults"] == 454 # 22.D + assert response["totalResults"] == expected_properties # 22.D assert response["items"][0]["label"] == "ITEM_NUMBER" def test_create_replace_property(instance: OFSC, request_logging, faker): logging.info("... Create property") - property = Property.parse_obj( + property = Property.model_validate( { "label": faker.pystr(), "type": "string", @@ -176,7 +184,7 @@ def test_create_replace_property(instance: OFSC, request_logging, faker): assert response["name"] == property.name assert response["type"] == property.type assert response["entity"] == property.entity - property = Property.parse_obj(response) + property = Property.model_validate(response) def test_import_plugin_file(instance: OFSC): diff --git a/tests/test_model.py b/tests/test_model.py index 7c36885..a8498f7 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -3,6 +3,9 @@ from dbm import dumb import pytest +from pydantic import ValidationError +from requests import Response + from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from ofsc.models import ( Condition, @@ -14,21 +17,19 @@ WorkskillList, WorskillConditionList, ) -from pydantic import ValidationError -from requests import Response def test_translation_model_base(): base = {"language": "en", "name": "Estimate", "languageISO": "en-US"} - obj = Translation.parse_obj(base) + obj = Translation.model_validate(base) assert obj.language == base["language"] assert obj.name == base["name"] def test_translation_model_base_invalid(): - base = {"language": "xx", "name": "Estimate", "languageISO": "en-US"} + base = {"language": "xx", "Noname": "NoEstimate", "languageISO": "en-US"} with pytest.raises(ValidationError) as validation: - obj = Translation.parse_obj(base) + obj = Translation.model_validate(base) def test_translationlist_model_base(): @@ -36,12 +37,23 @@ def test_translationlist_model_base(): {"language": "en", "name": "Estimate", "languageISO": "en-US"}, {"language": "es", "name": "Estimación"}, ] - objList = TranslationList.parse_obj(base) + objList = TranslationList.model_validate(base) for idx, obj in enumerate(objList): + assert type(obj) == Translation assert obj.language == base[idx]["language"] assert obj.name == base[idx]["name"] +def test_translationlist_model_json(): + base = [ + {"language": "en", "name": "Estimate", "languageISO": "en-US"}, + {"language": "es", "name": "Estimar"}, + ] + objList = TranslationList.model_validate(base) + assert json.loads(objList.model_dump_json())[0]["language"] == base[0]["language"] + assert json.loads(objList.model_dump_json())[1]["name"] == base[1]["name"] + + def test_workskill_model_base(): base = { "label": "EST", @@ -62,15 +74,16 @@ def test_workskill_model_base(): }, ], } - obj = Workskill.parse_obj(base) + obj = Workskill.model_validate(base) assert obj.label == base["label"] assert obj.active == base["active"] assert obj.name == base["name"] assert obj.sharing == base["sharing"] - assert obj.translations == TranslationList.parse_obj(base["translations"]) + assert obj.translations == TranslationList.model_validate(base["translations"]) + assert json.loads(obj.model_dump_json())["label"] == base["label"] def test_workskilllist_connected(instance): metadata_response = instance.metadata.get_workskills(response_type=JSON_RESPONSE) logging.warning(json.dumps(metadata_response, indent=4)) - objList = WorkskillList.parse_obj(metadata_response["items"]) + objList = WorkskillList.model_validate(metadata_response["items"]) From 32674b282f38b11ef3d77c488364d6981b60b02d Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Tue, 12 Mar 2024 08:24:34 -0400 Subject: [PATCH 03/17] Conversion to ofsc_2 --- ofsc/core.py | 12 +- tests/OFSC_metadata_test.py | 32 --- tests/OFSC_test.py | 219 ------------------- tests/conftest.py | 21 +- tests/metadata/test_activity_groups_types.py | 86 ++++++++ tests/metadata/test_capacity_areas.py | 33 +++ tests/metadata/test_properties.py | 60 +++++ tests/test_core_resources.py | 27 +++ tests/test_core_subscriptions.py | 72 ++++++ tests/test_metadata.py | 55 ----- 10 files changed, 301 insertions(+), 316 deletions(-) delete mode 100644 tests/OFSC_metadata_test.py delete mode 100644 tests/OFSC_test.py create mode 100644 tests/metadata/test_activity_groups_types.py create mode 100644 tests/metadata/test_capacity_areas.py create mode 100644 tests/metadata/test_properties.py diff --git a/ofsc/core.py b/ofsc/core.py index fdfab72..f877a83 100644 --- a/ofsc/core.py +++ b/ofsc/core.py @@ -79,7 +79,7 @@ def move_activity(self, activity_id, data, response_type=TEXT_RESPONSE): def get_events(self, params, response_type=TEXT_RESPONSE): url = urljoin(self.baseUrl, "rest/ofscCore/v1/events") response = requests.get( - "https://api.etadirect.com/rest/ofscCore/v1/events", + url, headers=self.headers, params=params, ) @@ -94,6 +94,7 @@ def get_events(self, params, response_type=TEXT_RESPONSE): # RESOURCE MANAGEMENT #### + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_resource( self, resource_id, @@ -101,7 +102,6 @@ def get_resource( workSkills=False, workZones=False, workSchedules=False, - response_type=TEXT_RESPONSE, ): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/resources/{}".format(str(resource_id)) @@ -130,13 +130,7 @@ def get_resource( data["expand"] = expand response = requests.get(url, params=data, headers=self.headers) - - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response # 202107 def create_resource_old(self, resourceId, data, response_type=TEXT_RESPONSE): diff --git a/tests/OFSC_metadata_test.py b/tests/OFSC_metadata_test.py deleted file mode 100644 index cada932..0000000 --- a/tests/OFSC_metadata_test.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -import sys -import unittest - -from ofsc.models import SharingEnum, Workskill - -sys.path.append(os.path.abspath(".")) -import argparse -import json -import logging -import pprint - -from ofsc import FULL_RESPONSE, OFSC - - -class ofscTest(unittest.TestCase): - def setUp(self): - self.logger = logging.getLogger() - self.pp = pprint.PrettyPrinter(indent=4) - self.logger.setLevel(logging.DEBUG) - # todo add credentials to test run - logging.warning("Here {}".format(os.environ.get("OFSC_CLIENT_ID"))) - self.instance = OFSC( - clientID=os.environ.get("OFSC_CLIENT_ID"), - secret=os.environ.get("OFSC_CLIENT_SECRET"), - companyName=os.environ.get("OFSC_COMPANY"), - ) - self.date = os.environ.get("OFSC_TEST_DATE") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/OFSC_test.py b/tests/OFSC_test.py deleted file mode 100644 index 5681a0f..0000000 --- a/tests/OFSC_test.py +++ /dev/null @@ -1,219 +0,0 @@ -import os -import sys -import unittest - -from ofsc.core import JSON_RESPONSE - -sys.path.append(os.path.abspath(".")) -import argparse -import json -import logging -import pprint - -from ofsc import FULL_RESPONSE, OFSC - - -class ofscTest(unittest.TestCase): - aid = 4224010 - - def setUp(self): - self.logger = logging.getLogger() - self.pp = pprint.PrettyPrinter(indent=4) - self.logger.setLevel(logging.DEBUG) - # todo add credentials to test run - logging.info("ClientID {}".format(os.environ.get("OFSC_CLIENT_ID"))) - self.instance = OFSC( - clientID=os.environ.get("OFSC_CLIENT_ID"), - secret=os.environ.get("OFSC_CLIENT_SECRET"), - companyName=os.environ.get("OFSC_COMPANY"), - root=os.environ.get("OFSC_ROOT"), - ) - self.logger.info(self.instance) - self.date = os.environ.get("OFSC_TEST_DATE") - - def move_activity_between_buckets_no_error(self): - instance = self.instance - logger = self.logger - - # Do a get resource to verify that the activity is in the right place - response = instance.core.get_activity(self.aid, response_type=FULL_RESPONSE) - self.assertEqual(response.status_code, 200) - original_resource = response.json()["resourceId"] - - logger.info("...104: Move activity (activity exists)") - data = {"setResource": {"resourceId": "FLUSA"}} - response = instance.core.move_activity( - 4224010, json.dumps(data), response_type=FULL_RESPONSE - ) - self.assertEqual(response.status_code, 204) - - # Do a get resource to verify that the activity is in the right place - response = instance.core.get_activity(self.aid, response_type=FULL_RESPONSE) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["resourceId"], "FLUSA") - - # Return it to the previous place - data["setResource"]["resourceId"] = original_resource - response = instance.core.move_activity( - 4224010, json.dumps(data), response_type=FULL_RESPONSE - ) - self.assertEqual(response.status_code, 204) - logger.info("...104: Move activity back") - - def test_004_get_events(self): - instance = self.instance - logger = self.logger - global pp, created_time - created_subscription = self.create_subscription() - details = instance.core.get_subscription_details( - created_subscription["subscriptionId"], response_type=JSON_RESPONSE - ) - # Moving activity - self.move_activity_between_buckets_no_error() - params = { - "subscriptionId": details["subscriptionId"], - "since": details["createdTime"], - } - logger.info("...210: Get Events") - current_page = "" - raw_response = instance.core.get_events(params) - response = json.loads(raw_response) - logger.info(self.pp.pformat(response)) - self.assertTrue(response["found"]) - next_page = response["nextPage"] - events = [] - while next_page != current_page: - current_page = next_page - params2 = {"subscriptionId": details["subscriptionId"], "page": next_page} - raw_response = instance.core.get_events( - params2, response_type=FULL_RESPONSE - ) - response = raw_response.json() - if response["items"]: - events.extend(response["items"]) - next_page = response["nextPage"] - self.assertGreaterEqual(len(events), 2) - for item in events: - logger.info(self.pp.pformat(item)) - if item["eventType"] == "activityMoved": - self.assertEqual(item["activityDetails"]["activityId"], self.aid) - self.delete_subscription(details["subscriptionId"]) - - def test_201_get_resource_no_expand(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_resource(55001) - response = json.loads(raw_response) - self.assertEqual(response["resourceInternalId"], 5000001) - - def test_202_get_resource_expand(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_resource( - 55001, workSkills=True, workZones=True - ) - response = json.loads(raw_response) - self.assertEqual(response["resourceInternalId"], 5000001) - - def test_203_get_position_history(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_position_history(33001, date=self.date) - response = json.loads(raw_response) - self.assertIsNotNone(response["totalResults"]) - self.assertTrue(response["totalResults"] > 200) - - # Capacity tests - def test_301_get_capacity_areas_simple(self): - instance = self.instance - logger = self.logger - raw_response = instance.metadata.get_capacity_areas(response_type=FULL_RESPONSE) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.info(self.pp.pformat(response)) - self.assertIsNotNone(response["items"]) - self.assertEqual(len(response["items"]), 2) - self.assertEqual(response["items"][0]["label"], "CAUSA") - - def test_302_get_capacity_area(self): - instance = self.instance - logger = self.logger - raw_response = instance.metadata.get_capacity_area( - "FLUSA", response_type=FULL_RESPONSE - ) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.info(self.pp.pformat(response)) - self.assertIsNotNone(response["label"]) - self.assertEqual(response["label"], "FLUSA") - self.assertIsNotNone(response["configuration"]) - self.assertIsNotNone(response["parentLabel"]) - self.assertEqual(response["parentLabel"], "SUNRISE") - - def test_311_get_activity_type_groups(self): - instance = self.instance - logger = self.logger - raw_response = instance.metadata.get_activity_type_groups( - response_type=FULL_RESPONSE - ) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.info(self.pp.pformat(response)) - self.assertIsNotNone(response["items"]) - self.assertEqual(len(response["items"]), 5) - self.assertEqual(response["totalResults"], 5) - self.assertEqual(response["items"][0]["label"], "customer") - - def test_312_get_activity_type_group(self): - instance = self.instance - logger = self.logger - raw_response = instance.metadata.get_activity_type_group( - "customer", response_type=FULL_RESPONSE - ) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - self.assertEqual(raw_response.status_code, 200) - logger.info(self.pp.pformat(response)) - self.assertIsNotNone(response["label"]) - self.assertEqual(response["label"], "customer") - self.assertIsNotNone(response["activityTypes"]) - self.assertEqual(len(response["activityTypes"]), 24) - self.assertEqual(response["activityTypes"][20]["label"], "hvac_emergency") - - def test_313_get_activity_types(self): - instance = self.instance - logger = self.logger - raw_response = instance.metadata.get_activity_types(response_type=FULL_RESPONSE) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - self.assertEqual(raw_response.status_code, 200) - logger.info(self.pp.pformat(response)) - self.assertIsNotNone(response["items"]) - self.assertEqual(len(response["items"]), 34) - self.assertEqual(response["totalResults"], 34) - self.assertEqual(response["items"][28]["label"], "crew_assignment") - self.assertEqual(response["items"][12]["label"], "06") - activityType = response["items"][12] - self.assertIsNotNone(activityType["features"]) - self.assertEqual(len(activityType["features"]), 27) - self.assertEqual(activityType["features"]["allowMoveBetweenResources"], True) - - def test_313_get_activity_type(self): - instance = self.instance - logger = self.logger - raw_response = instance.metadata.get_activity_type( - "ac_installation", response_type=FULL_RESPONSE - ) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - self.assertEqual(raw_response.status_code, 200) - logger.info(self.pp.pformat(response)) - self.assertIsNotNone(response["label"]) - self.assertEqual(response["label"], "ac_installation") - self.assertIsNotNone(response["features"]) - self.assertEqual(len(response["features"]), 27) - self.assertEqual(response["features"]["allowMoveBetweenResources"], True) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/conftest.py b/tests/conftest.py index db66a96..2d48d9e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ import requests from faker import Faker -from ofsc import OFSC +from ofsc import FULL_RESPONSE, OFSC @pytest.fixture(scope="module") @@ -35,6 +35,20 @@ def instance_with_token(): return instance +@pytest.fixture(scope="module") +def clear_subscriptions(instance): + response = instance.core.get_subscriptions(response_type=FULL_RESPONSE) + if response.status_code == 200 and response.json()["totalResults"] > 0: + for subscription in response.json()["items"]: + logging.info(subscription) + instance.core.delete_subscription(subscription["subscriptionId"]) + yield + response = instance.core.get_subscriptions(response_type=FULL_RESPONSE) + if response.status_code == 200 and response.json()["totalResults"] > 0: + for subscription in response.json()["items"]: + instance.core.delete_subscription(subscription["subscriptionId"]) + + @pytest.fixture def current_date(): return os.environ.get("OFSC_TEST_DATE") @@ -84,6 +98,10 @@ def demo_data(): "expected_workskill_conditions": 8, "expected_resource_types": 10, "expected_properties": 463, + "expected_activity_type_groups": 5, + "expected_activity_types": 35, + "expected_activity_types_customer": 25, + "expected_capacity_areas": ["CAUSA", "FLUSA", "South Florida"], }, "get_file_property": { "activity_id": 3954799, # Note: manual addition @@ -91,6 +109,7 @@ def demo_data(): "get_users": { "totalResults": 322, }, + "events": {"move_from": "FLUSA", "move_to": "CAUSA", "move_id": 4224268}, }, } return demo_data[ diff --git a/tests/metadata/test_activity_groups_types.py b/tests/metadata/test_activity_groups_types.py new file mode 100644 index 0000000..5e03ea1 --- /dev/null +++ b/tests/metadata/test_activity_groups_types.py @@ -0,0 +1,86 @@ +import json +import logging +from pathlib import Path + +from requests import Response + +from ofsc import OFSC +from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE +from ofsc.models import ( + Condition, + Property, + SharingEnum, + Translation, + TranslationList, + Workskill, + WorkskillCondition, + WorskillConditionList, +) + + +def test_get_activity_type_groups(instance, pp, demo_data): + expected_activity_type_groups = demo_data.get("metadata").get( + "expected_activity_type_groups" + ) + raw_response = instance.metadata.get_activity_type_groups( + response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + logging.info(pp.pformat(response)) + assert response["items"] is not None + assert len(response["items"]) == expected_activity_type_groups + assert response["totalResults"] == expected_activity_type_groups + assert response["items"][0]["label"] == "customer" + + +def test_get_activity_type_group(instance, demo_data, pp): + expected_activity_types = demo_data.get("metadata").get( + "expected_activity_types_customer" + ) + raw_response = instance.metadata.get_activity_type_group( + "customer", response_type=FULL_RESPONSE + ) + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + assert raw_response.status_code == 200 + logging.info(pp.pformat(response)) + assert response["label"] is not None + assert response["label"] == "customer" + assert response["activityTypes"] is not None + assert len(response["activityTypes"]) == expected_activity_types + assert response["activityTypes"][20]["label"] == "fitness_emergency" + + +def test_get_activity_types(instance, demo_data, pp): + expected_activity_types = demo_data.get("metadata").get("expected_activity_types") + raw_response = instance.metadata.get_activity_types(response_type=FULL_RESPONSE) + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + assert raw_response.status_code == 200 + logging.info(pp.pformat(response)) + assert response["items"] is not None + assert len(response["items"]) == expected_activity_types + assert response["totalResults"] == expected_activity_types + assert response["items"][28]["label"] == "crew_assignment" + assert response["items"][12]["label"] == "06" + activityType = response["items"][12] + assert activityType["features"] is not None + assert len(activityType["features"]) == 27 + assert activityType["features"]["allowMoveBetweenResources"] == True + + +def test_get_activity_type(instance, demo_data, pp): + raw_response = instance.metadata.get_activity_type( + "fitness_emergency", response_type=FULL_RESPONSE + ) + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + assert raw_response.status_code == 200 + logging.info(pp.pformat(response)) + assert response["label"] is not None + assert response["label"] == "fitness_emergency" + assert response["features"] is not None + assert len(response["features"]) == 27 + assert response["features"]["allowMoveBetweenResources"] == True diff --git a/tests/metadata/test_capacity_areas.py b/tests/metadata/test_capacity_areas.py new file mode 100644 index 0000000..06416bd --- /dev/null +++ b/tests/metadata/test_capacity_areas.py @@ -0,0 +1,33 @@ +import logging + +import pytest + +from ofsc.common import FULL_RESPONSE + + +# Capacity tests +def test_get_capacity_areas_simple(instance, pp, demo_data): + capacity_areas = demo_data.get("metadata").get("expected_capacity_areas") + raw_response = instance.metadata.get_capacity_areas(response_type=FULL_RESPONSE) + assert raw_response.status_code == 200 + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + logging.info(pp.pformat(response)) + assert response["items"] is not None + assert len(response["items"]) == len(capacity_areas) + assert response["items"][0]["label"] == "CAUSA" + + +def test_get_capacity_area(instance, pp): + raw_response = instance.metadata.get_capacity_area( + "FLUSA", response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + logging.info(pp.pformat(response)) + assert response["label"] is not None + assert response["label"] == "FLUSA" + assert response["configuration"] is not None + assert response["parentLabel"] is not None + assert response["parentLabel"] == "SUNRISE" diff --git a/tests/metadata/test_properties.py b/tests/metadata/test_properties.py new file mode 100644 index 0000000..d468696 --- /dev/null +++ b/tests/metadata/test_properties.py @@ -0,0 +1,60 @@ +import logging + +from ofsc import OFSC +from ofsc.common import FULL_RESPONSE +from ofsc.models import Property, Translation + + +def test_get_property(instance): + logging.info("...Get property info") + metadata_response = instance.metadata.get_property( + "XA_CASE_ACCOUNT", response_type=FULL_RESPONSE + ) + assert metadata_response.status_code == 200 + response = metadata_response.json() + logging.info(response) + assert response["label"] == "XA_CASE_ACCOUNT" + assert response["type"] == "string" + assert response["entity"] == "activity" + property = Property.parse_obj(response) + + +def test_get_properties(instance, demo_data): + logging.info("...Get properties") + metadata_response = instance.metadata.get_properties(response_type=FULL_RESPONSE) + expected_properties = demo_data.get("metadata").get("expected_properties") + assert metadata_response.status_code == 200 + response = metadata_response.json() + assert response["totalResults"] + assert response["totalResults"] == expected_properties # 22.D + assert response["items"][0]["label"] == "ITEM_NUMBER" + + +def test_create_replace_property(instance: OFSC, request_logging, faker): + logging.info("... Create property") + property = Property.model_validate( + { + "label": faker.pystr(), + "type": "string", + "entity": "activity", + "name": faker.pystr(), + "translations": [], + "gui": "text", + } + ) + property.translations.__root__.append(Translation(name=property.name)) + metadata_response = instance.metadata.create_or_replace_property( + property, response_type=FULL_RESPONSE + ) + logging.warning(metadata_response.json()) + assert metadata_response.status_code < 299, metadata_response.json() + + metadata_response = instance.metadata.get_property( + property.label, response_type=FULL_RESPONSE + ) + assert metadata_response.status_code < 299 + response = metadata_response.json() + assert response["name"] == property.name + assert response["type"] == property.type + assert response["entity"] == property.entity + property = Property.model_validate(response) diff --git a/tests/test_core_resources.py b/tests/test_core_resources.py index 9a1499e..e071d6e 100644 --- a/tests/test_core_resources.py +++ b/tests/test_core_resources.py @@ -58,3 +58,30 @@ def test_create_resource_from_obj_dict(instance, faker, request_logging): ) response = raw_response.json() assert raw_response.status_code == 200 + + +def test_get_resource_no_expand(instance, demo_data): + raw_response = instance.core.get_resource(55001, response_type=FULL_RESPONSE) + assert raw_response.status_code == 200 + logging.info(raw_response.json()) + response = raw_response.json() + assert response["resourceInternalId"] == 5000001 + + +def test_get_resource_expand(instance, demo_data, response_type=FULL_RESPONSE): + raw_response = instance.core.get_resource( + 55001, workSkills=True, workZones=True, response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["resourceInternalId"] == 5000001 + + +def test_get_position_history(instance, demo_data, current_date): + raw_response = instance.core.get_position_history( + 33001, date=current_date, response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["totalResults"] is not None + assert response["totalResults"] > 200 diff --git a/tests/test_core_subscriptions.py b/tests/test_core_subscriptions.py index 14c72ab..327b45a 100644 --- a/tests/test_core_subscriptions.py +++ b/tests/test_core_subscriptions.py @@ -1,5 +1,6 @@ import json import logging +import time from ofsc.common import FULL_RESPONSE @@ -46,3 +47,74 @@ def test_create_delete_subscription(instance): logging.info("...305: Delete Subscription") response = instance.core.delete_subscription(id, response_type=FULL_RESPONSE) assert response.status_code == 204 + + +def test_get_events(instance, pp, demo_data, clear_subscriptions): + move_data = demo_data.get("events") + + # Creating subscription + data = {"events": ["activityMoved"], "title": "Simple Subscription"} + raw_response = instance.core.create_subscription( + json.dumps(data), response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + response = raw_response.json() + assert "subscriptionId" in response.keys() + id = response["subscriptionId"] + + # Get creation time + params = {"subscriptionId": id} + raw_response = instance.core.get_subscription_details( + id, response_type=FULL_RESPONSE + ) + response = raw_response.json() + assert "subscriptionId" in response.keys() + assert response["subscriptionId"] == id + created_time = response["createdTime"] + logging.info(response) + + # Moving activity + data = {"setResource": {"resourceId": move_data["move_to"]}} + raw_response = instance.core.move_activity( + move_data["move_id"], json.dumps(data), response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 204, raw_response.json() + + params = { + "subscriptionId": id, + "since": created_time, + } + current_page = "" + raw_response = instance.core.get_events(params) + response = json.loads(raw_response) + assert response["found"] + next_page = response["nextPage"] + events = [] + time.sleep(3) + while next_page != current_page: + logging.info(f"Current page: {current_page}, Next page: {next_page}") + current_page = next_page + params2 = {"subscriptionId": id, "page": next_page} + raw_response = instance.core.get_events(params2, response_type=FULL_RESPONSE) + response = raw_response.json() + if response["items"]: + events.extend(response["items"]) + next_page = response["nextPage"] + logging.info( + f"Current page: {current_page}, Next page: {next_page}, {response}" + ) + assert len(events) >= 1 + for item in events: + if item["eventType"] == "activityMoved": + assert item["activityDetails"]["activityId"] == move_data["move_id"] + + # Moving activity back + data = {"setResource": {"resourceId": move_data["move_from"]}} + raw_response = instance.core.move_activity( + move_data["move_id"], json.dumps(data), response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 204, raw_response.json() + + # Deleting subscription + response = instance.core.delete_subscription(id, response_type=FULL_RESPONSE) + assert response.status_code == 204 diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 6fa8738..4411a5f 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -132,61 +132,6 @@ def test_get_resource_types(instance, demo_data): ) -def test_get_property(instance): - logging.info("...Get property info") - metadata_response = instance.metadata.get_property( - "XA_CASE_ACCOUNT", response_type=FULL_RESPONSE - ) - assert metadata_response.status_code == 200 - response = metadata_response.json() - logging.info(response) - assert response["label"] == "XA_CASE_ACCOUNT" - assert response["type"] == "string" - assert response["entity"] == "activity" - property = Property.parse_obj(response) - - -def test_get_properties(instance, demo_data): - logging.info("...Get properties") - metadata_response = instance.metadata.get_properties(response_type=FULL_RESPONSE) - expected_properties = demo_data.get("metadata").get("expected_properties") - assert metadata_response.status_code == 200 - response = metadata_response.json() - assert response["totalResults"] - assert response["totalResults"] == expected_properties # 22.D - assert response["items"][0]["label"] == "ITEM_NUMBER" - - -def test_create_replace_property(instance: OFSC, request_logging, faker): - logging.info("... Create property") - property = Property.model_validate( - { - "label": faker.pystr(), - "type": "string", - "entity": "activity", - "name": faker.pystr(), - "translations": [], - "gui": "text", - } - ) - property.translations.__root__.append(Translation(name=property.name)) - metadata_response = instance.metadata.create_or_replace_property( - property, response_type=FULL_RESPONSE - ) - logging.warning(metadata_response.json()) - assert metadata_response.status_code < 299, metadata_response.json() - - metadata_response = instance.metadata.get_property( - property.label, response_type=FULL_RESPONSE - ) - assert metadata_response.status_code < 299 - response = metadata_response.json() - assert response["name"] == property.name - assert response["type"] == property.type - assert response["entity"] == property.entity - property = Property.model_validate(response) - - def test_import_plugin_file(instance: OFSC): logging.info("... Import plugin via file") metadata_response = instance.metadata.import_plugin_file(Path("tests/test.xml")) From ca0c717c9810bdcb55e0e731c789bf5b03805529 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Tue, 12 Mar 2024 09:24:38 -0400 Subject: [PATCH 04/17] test simplifying --- ofsc/core.py | 79 +++++------------ tests/OFSC_activities_test.py | 85 ------------------ tests/OFSC_resources_test.py | 82 ------------------ tests/metadata/test_activity_groups_types.py | 8 +- tests/metadata/test_capacity_areas.py | 4 +- tests/metadata/test_properties.py | 7 +- tests/test_core_activities.py | 23 +++-- tests/test_core_resources.py | 90 +++++++++++++++----- tests/test_core_subscriptions.py | 5 -- tests/test_core_users.py | 2 +- tests/test_metadata.py | 12 +-- tests/test_model.py | 2 +- 12 files changed, 116 insertions(+), 283 deletions(-) delete mode 100644 tests/OFSC_activities_test.py delete mode 100644 tests/OFSC_resources_test.py diff --git a/ofsc/core.py b/ofsc/core.py index f877a83..7486095 100644 --- a/ofsc/core.py +++ b/ofsc/core.py @@ -22,18 +22,13 @@ def get_activities(self, params, response_type=TEXT_RESPONSE): else: return response.text - def get_activity(self, activity_id, response_type=TEXT_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_activity(self, activity_id): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/activities/{}".format(activity_id) ) response = requests.get(url, headers=self.headers) - # print (response.status_code) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response def update_activity(self, activity_id, data, response_type=TEXT_RESPONSE): url = urljoin( @@ -176,14 +171,9 @@ def get_position_history(self, resource_id, date, response_type=TEXT_RESPONSE): else: return response.text + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_resource_route( - self, - resource_id, - date, - activityFields=None, - offset=0, - limit=100, - response_type=TEXT_RESPONSE, + self, resource_id, date, activityFields=None, offset=0, limit=100 ): url = urljoin( self.baseUrl, @@ -193,14 +183,9 @@ def get_resource_route( if activityFields is not None: params["activityFields"] = activityFields response = requests.get(url, params=params, headers=self.headers) + return response - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_resource_descendants( self, resource_id, @@ -211,7 +196,6 @@ def get_resource_descendants( workSkills=False, workZones=False, workSchedules=False, - response_type=TEXT_RESPONSE, ): url = urljoin( self.baseUrl, @@ -248,62 +232,39 @@ def get_resource_descendants( logging.debug(json.dumps(params, indent=2)) response = requests.get(url, params=params, headers=self.headers) - - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ## 202104 User Management - def get_users(self, offset=0, limit=100, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_users(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/users") params = {} params["offset"] = offset params["limit"] = limit response = requests.get(url, params, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response - def get_user(self, login, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_user(self, login): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/users/{}".format(login)) response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response - def update_user(self, login, data, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def update_user(self, login, data): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/users/{}".format(login)) response = requests.patch(url, headers=self.headers, data=data) - # print (response.status_code) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ##202106 + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def create_user(self, login, data, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/users/{login}") response = requests.put(url, headers=self.headers, data=data) - # print (response.status_code) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ##202106 + def delete_user(self, login, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/users/{login}") response = requests.delete(url, headers=self.headers) diff --git a/tests/OFSC_activities_test.py b/tests/OFSC_activities_test.py deleted file mode 100644 index d2c6463..0000000 --- a/tests/OFSC_activities_test.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import sys -import unittest - -sys.path.append(os.path.abspath(".")) -import argparse -import json -import logging -import pprint -from datetime import date -from datetime import datetime as dt -from datetime import timedelta - -from ofsc import FULL_RESPONSE, JSON_RESPONSE, OFSC - - -class ofscActivitiesTest(unittest.TestCase): - def setUp(self): - self.logger = logging.getLogger() - self.pp = pprint.PrettyPrinter(indent=4) - self.logger.setLevel(logging.DEBUG) - # todo add credentials to test run - logging.warning("Here {}".format(os.environ.get("OFSC_CLIENT_ID"))) - self.instance = OFSC( - clientID=os.environ.get("OFSC_CLIENT_ID"), - secret=os.environ.get("OFSC_CLIENT_SECRET"), - companyName=os.environ.get("OFSC_COMPANY"), - ) - response = self.instance.core.get_activity(3954794, response_type=JSON_RESPONSE) - self.assertIsNotNone(response["date"]) - self.date = response["date"] - - # Test A.01 Get Activity Info (activity exists) - def test_A01_get_activity(self): - self.logger.info("...101: Get Activity Info (activity does exist)") - raw_response = self.instance.core.get_activity(3951935) - response = json.loads(raw_response) - self.logger.debug(response) - self.assertEqual(response["customerNumber"], "019895700") - - # Test A.02 Get Activity Info (activity does not exist) - def test_A02_get_activity(self): - instance = self.instance - logger = self.logger - logger.info("...102: Get Activity Info (activity does not exist)") - raw_response = instance.core.get_activity(99999) - response = json.loads(raw_response) - - logger.debug(response) - self.assertEqual(response["status"], "404") - - # Test A.04 Move activity (between buckets, no error) - def test_A04_move_activity_between_buckets_no_error(self): - instance = self.instance - logger = self.logger - - # Do a get resource to verify that the activity is in the right place - response = instance.core.get_activity(4224010, response_type=FULL_RESPONSE) - logger.debug(response.json()) - self.assertEqual(response.status_code, 200) - original_resource = response.json()["resourceId"] - - logger.info("...104: Move activity (activity exists)") - data = {"setResource": {"resourceId": "FLUSA"}} - response = instance.core.move_activity( - 4224010, json.dumps(data), response_type=FULL_RESPONSE - ) - self.assertEqual(response.status_code, 204) - - # Do a get resource to verify that the activity is in the right place - response = instance.core.get_activity(4224010, response_type=FULL_RESPONSE) - logger.debug(response.json()) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["resourceId"], "FLUSA") - - # Return it to the previous place - data["setResource"]["resourceId"] = original_resource - response = instance.core.move_activity( - 4224010, json.dumps(data), response_type=FULL_RESPONSE - ) - self.assertEqual(response.status_code, 204) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/OFSC_resources_test.py b/tests/OFSC_resources_test.py deleted file mode 100644 index 79c7acc..0000000 --- a/tests/OFSC_resources_test.py +++ /dev/null @@ -1,82 +0,0 @@ -import os -import sys -import unittest - -sys.path.append(os.path.abspath(".")) -import argparse -import json -import logging -import pprint - -from ofsc import FULL_RESPONSE, JSON_RESPONSE, OFSC - - -class ofscTest(unittest.TestCase): - def setUp(self): - self.logger = logging.getLogger() - self.pp = pprint.PrettyPrinter(indent=4) - self.logger.setLevel(logging.DEBUG) - # todo add credentials to test run - logging.warning("Here {}".format(os.environ.get("OFSC_CLIENT_ID"))) - self.instance = OFSC( - clientID=os.environ.get("OFSC_CLIENT_ID"), - secret=os.environ.get("OFSC_CLIENT_SECRET"), - companyName=os.environ.get("OFSC_COMPANY"), - ) - response = self.instance.core.get_activity(3954794, response_type=JSON_RESPONSE) - self.assertIsNotNone(response["date"]) - self.date = response["date"] - - # Test R.0.1 - def test_R01_get_resource_route_nofields(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_resource_route( - 33001, date=self.date, response_type=FULL_RESPONSE - ) - logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - self.assertEqual(response["totalResults"], 13) - - def test_R02_get_resource_route_twofields(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_resource_route( - 33001, date=self.date, activityFields="activityId,activityType" - ) - response = json.loads(raw_response) - # print(response) - self.assertEqual(response["totalResults"], 13) - - def test_R03_get_resource_descendants_noexpand(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_resource_descendants("FLUSA") - response = json.loads(raw_response) - # print(response) - self.assertEqual(response["totalResults"], 37) - - def test_R04_get_resource_descendants_expand(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_resource_descendants( - "FLUSA", workSchedules=True, workZones=True, workSkills=True - ) - response = json.loads(raw_response) - # print(response) - self.assertEqual(response["totalResults"], 37) - - def test_R05_get_resource_descendants_noexpand_fields(self): - instance = self.instance - logger = self.logger - raw_response = instance.core.get_resource_descendants( - "FLUSA", resourceFields="resourceId,phone", response_type=FULL_RESPONSE - ) - # logging.debug(self.pp.pformat(raw_response.json())) - response = raw_response.json() - logger.info(self.pp.pformat(response)) - self.assertEqual(response["totalResults"], 37) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/metadata/test_activity_groups_types.py b/tests/metadata/test_activity_groups_types.py index 5e03ea1..c0359cf 100644 --- a/tests/metadata/test_activity_groups_types.py +++ b/tests/metadata/test_activity_groups_types.py @@ -28,7 +28,7 @@ def test_get_activity_type_groups(instance, pp, demo_data): assert raw_response.status_code == 200 logging.debug(pp.pformat(raw_response.json())) response = raw_response.json() - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert response["items"] is not None assert len(response["items"]) == expected_activity_type_groups assert response["totalResults"] == expected_activity_type_groups @@ -45,7 +45,7 @@ def test_get_activity_type_group(instance, demo_data, pp): logging.debug(pp.pformat(raw_response.json())) response = raw_response.json() assert raw_response.status_code == 200 - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert response["label"] is not None assert response["label"] == "customer" assert response["activityTypes"] is not None @@ -59,7 +59,7 @@ def test_get_activity_types(instance, demo_data, pp): logging.debug(pp.pformat(raw_response.json())) response = raw_response.json() assert raw_response.status_code == 200 - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert response["items"] is not None assert len(response["items"]) == expected_activity_types assert response["totalResults"] == expected_activity_types @@ -78,7 +78,7 @@ def test_get_activity_type(instance, demo_data, pp): logging.debug(pp.pformat(raw_response.json())) response = raw_response.json() assert raw_response.status_code == 200 - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert response["label"] is not None assert response["label"] == "fitness_emergency" assert response["features"] is not None diff --git a/tests/metadata/test_capacity_areas.py b/tests/metadata/test_capacity_areas.py index 06416bd..e46e17b 100644 --- a/tests/metadata/test_capacity_areas.py +++ b/tests/metadata/test_capacity_areas.py @@ -12,7 +12,7 @@ def test_get_capacity_areas_simple(instance, pp, demo_data): assert raw_response.status_code == 200 logging.debug(pp.pformat(raw_response.json())) response = raw_response.json() - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert response["items"] is not None assert len(response["items"]) == len(capacity_areas) assert response["items"][0]["label"] == "CAUSA" @@ -25,7 +25,7 @@ def test_get_capacity_area(instance, pp): assert raw_response.status_code == 200 logging.debug(pp.pformat(raw_response.json())) response = raw_response.json() - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert response["label"] is not None assert response["label"] == "FLUSA" assert response["configuration"] is not None diff --git a/tests/metadata/test_properties.py b/tests/metadata/test_properties.py index d468696..a784e30 100644 --- a/tests/metadata/test_properties.py +++ b/tests/metadata/test_properties.py @@ -6,13 +6,12 @@ def test_get_property(instance): - logging.info("...Get property info") metadata_response = instance.metadata.get_property( "XA_CASE_ACCOUNT", response_type=FULL_RESPONSE ) assert metadata_response.status_code == 200 response = metadata_response.json() - logging.info(response) + logging.debug(response) assert response["label"] == "XA_CASE_ACCOUNT" assert response["type"] == "string" assert response["entity"] == "activity" @@ -20,7 +19,6 @@ def test_get_property(instance): def test_get_properties(instance, demo_data): - logging.info("...Get properties") metadata_response = instance.metadata.get_properties(response_type=FULL_RESPONSE) expected_properties = demo_data.get("metadata").get("expected_properties") assert metadata_response.status_code == 200 @@ -31,7 +29,6 @@ def test_get_properties(instance, demo_data): def test_create_replace_property(instance: OFSC, request_logging, faker): - logging.info("... Create property") property = Property.model_validate( { "label": faker.pystr(), @@ -46,7 +43,7 @@ def test_create_replace_property(instance: OFSC, request_logging, faker): metadata_response = instance.metadata.create_or_replace_property( property, response_type=FULL_RESPONSE ) - logging.warning(metadata_response.json()) + logging.debug(metadata_response.json()) assert metadata_response.status_code < 299, metadata_response.json() metadata_response = instance.metadata.get_property( diff --git a/tests/test_core_activities.py b/tests/test_core_activities.py index 95c3496..f66dc03 100644 --- a/tests/test_core_activities.py +++ b/tests/test_core_activities.py @@ -8,8 +8,21 @@ from ofsc.models import BulkUpdateRequest, BulkUpdateResponse +# Test A.01 Get Activity Info (activity exists) +def test_get_activity(instance): + raw_response = instance.core.get_activity(3951935, response_type=FULL_RESPONSE) + response = raw_response.json() + logging.debug(response) + assert response["customerNumber"] == "019895700" + + +# Test A.02 Get Activity Info (activity does not exist) +def test_get_activity_error(instance): + raw_response = instance.core.get_activity(99999, response_type=FULL_RESPONSE) + assert raw_response.status_code == 404 + + def test_search_activities_001(instance): - logging.info("...101: Search Activities (activity exists)") params = { "searchInField": "customerPhone", "searchForValue": "555760757294", @@ -17,17 +30,16 @@ def test_search_activities_001(instance): "dateTo": "2099-01-01", } response = instance.core.search_activities(params, response_type=FULL_RESPONSE) - logging.info(response.json()) + logging.debug(response.json()) assert response.status_code == 200 assert response.json()["totalResults"] == 2 # 202206 Modified in demo 22B # test A.06 Get Activities def test_get_activities_no_offset(instance, current_date, demo_data, request_logging): - logging.info("...102: Get activities (no offset)") start = date.fromisoformat(current_date) - timedelta(days=5) end = start + timedelta(days=20) - logging.info(f"{start} {end}") + logging.debug(f"{start} {end}") params = { "dateFrom": start.strftime("%Y-%m-%d"), "dateTo": end.strftime("%Y-%m-%d"), @@ -56,10 +68,9 @@ def test_get_activities_no_offset(instance, current_date, demo_data, request_log def test_get_activities_offset(instance, current_date, demo_data, request_logging): - logging.info("...103: Get activities (offset)") start = date.fromisoformat(current_date) - timedelta(days=5) end = start + timedelta(days=20) - logging.info(f"{start} {end}") + logging.debug(f"{start} {end}") params = { "dateFrom": start.strftime("%Y-%m-%d"), "dateTo": end.strftime("%Y-%m-%d"), diff --git a/tests/test_core_resources.py b/tests/test_core_resources.py index e071d6e..f6622b0 100644 --- a/tests/test_core_resources.py +++ b/tests/test_core_resources.py @@ -1,11 +1,14 @@ import json import logging +import pytest + from ofsc.common import FULL_RESPONSE -def test_create_resource(instance, faker, request_logging): - new_data = { +@pytest.fixture +def new_data(faker): + return { "parentResourceId": "SUNRISE", "resourceType": "BK", "name": faker.name(), @@ -13,6 +16,9 @@ def test_create_resource(instance, faker, request_logging): "timeZone": "Arizona", "externalId": faker.pystr(), } + + +def test_create_resource(instance, new_data, request_logging): raw_response = instance.core.create_resource( resourceId=new_data["externalId"], data=json.dumps(new_data), @@ -24,15 +30,7 @@ def test_create_resource(instance, faker, request_logging): assert response["name"] == new_data["name"] -def test_create_resource_dict(instance, faker, request_logging): - new_data = { - "parentResourceId": "SUNRISE", - "resourceType": "BK", - "name": faker.name(), - "language": "en", - "timeZone": "Arizona", - "externalId": faker.pystr(), - } +def test_create_resource_dict(instance, new_data, request_logging): raw_response = instance.core.create_resource( resourceId=new_data["externalId"], data=new_data, @@ -42,15 +40,7 @@ def test_create_resource_dict(instance, faker, request_logging): assert raw_response.status_code >= 299 -def test_create_resource_from_obj_dict(instance, faker, request_logging): - new_data = { - "parentResourceId": "SUNRISE", - "resourceType": "BK", - "name": faker.name(), - "language": "en", - "timeZone": "Arizona", - "externalId": faker.pystr(), - } +def test_create_resource_from_obj_dict(instance, new_data, request_logging): raw_response = instance.core.create_resource_from_obj( resourceId=new_data["externalId"], data=new_data, @@ -63,12 +53,12 @@ def test_create_resource_from_obj_dict(instance, faker, request_logging): def test_get_resource_no_expand(instance, demo_data): raw_response = instance.core.get_resource(55001, response_type=FULL_RESPONSE) assert raw_response.status_code == 200 - logging.info(raw_response.json()) + logging.debug(raw_response.json()) response = raw_response.json() assert response["resourceInternalId"] == 5000001 -def test_get_resource_expand(instance, demo_data, response_type=FULL_RESPONSE): +def test_get_resource_expand(instance, demo_data): raw_response = instance.core.get_resource( 55001, workSkills=True, workZones=True, response_type=FULL_RESPONSE ) @@ -85,3 +75,59 @@ def test_get_position_history(instance, demo_data, current_date): response = raw_response.json() assert response["totalResults"] is not None assert response["totalResults"] > 200 + + +def test_get_resource_route_nofields(instance, pp, demo_data, current_date): + raw_response = instance.core.get_resource_route( + 33001, date=current_date, response_type=FULL_RESPONSE + ) + logging.debug(pp.pformat(raw_response.json())) + assert raw_response.status_code == 200 + logging.debug(pp.pformat(raw_response.json())) + response = raw_response.json() + assert response["totalResults"] == 13 + + +def test_get_resource_route_twofields(instance, current_date, pp): + raw_response = instance.core.get_resource_route( + 33001, + date=current_date, + activityFields="activityId,activityType", + response_type=FULL_RESPONSE, + ) + logging.debug(pp.pformat(raw_response.json())) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["totalResults"] == 13 + + +def test_get_resource_descendants_noexpand(instance): + raw_response = instance.core.get_resource_descendants( + "FLUSA", response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["totalResults"] == 37 + + +def test_get_resource_descendants_expand(instance): + raw_response = instance.core.get_resource_descendants( + "FLUSA", + workSchedules=True, + workZones=True, + workSkills=True, + response_type=FULL_RESPONSE, + ) + assert raw_response.status_code == 200 + response = raw_response.json() + assert response["totalResults"] == 37 + + +def test_get_resource_descendants_noexpand_fields(instance, pp): + raw_response = instance.core.get_resource_descendants( + "FLUSA", resourceFields="resourceId,phone", response_type=FULL_RESPONSE + ) + assert raw_response.status_code == 200 + response = raw_response.json() + logging.debug(pp.pformat(response)) + assert response["totalResults"] == 37 diff --git a/tests/test_core_subscriptions.py b/tests/test_core_subscriptions.py index 327b45a..1825100 100644 --- a/tests/test_core_subscriptions.py +++ b/tests/test_core_subscriptions.py @@ -6,7 +6,6 @@ def test_get_subscriptions(instance): - logging.info("...301: Get Subscriptions") raw_response = instance.core.get_subscriptions(response_type=FULL_RESPONSE) assert raw_response.status_code == 200 response = raw_response.json() @@ -14,7 +13,6 @@ def test_get_subscriptions(instance): def test_get_subscriptions_with_token(instance_with_token): - logging.info("...302: Get Subscriptions using token") raw_response = instance_with_token.core.get_subscriptions( response_type=FULL_RESPONSE ) @@ -25,7 +23,6 @@ def test_get_subscriptions_with_token(instance_with_token): def test_create_delete_subscription(instance): data = {"events": ["activityMoved"], "title": "Simple Subscription"} - logging.info("...303: Create Subscription") raw_response = instance.core.create_subscription( json.dumps(data), response_type=FULL_RESPONSE ) @@ -34,7 +31,6 @@ def test_create_delete_subscription(instance): assert "subscriptionId" in response.keys() id = response["subscriptionId"] - logging.info("...304: Subscription details") raw_response = instance.core.get_subscription_details( id, response_type=FULL_RESPONSE ) @@ -44,7 +40,6 @@ def test_create_delete_subscription(instance): assert response["subscriptionId"] == id assert response["events"] == data["events"] - logging.info("...305: Delete Subscription") response = instance.core.delete_subscription(id, response_type=FULL_RESPONSE) assert response.status_code == 204 diff --git a/tests/test_core_users.py b/tests/test_core_users.py index aa2482a..64fe999 100644 --- a/tests/test_core_users.py +++ b/tests/test_core_users.py @@ -75,7 +75,7 @@ def test_create_user(instance, demo_data, pp): assert raw_response.status_code == 200, raw_response.json() logging.debug(pp.pformat(raw_response.json())) response = raw_response.json() - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert response["name"] is not None assert response["name"] == "Test Name" diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 4411a5f..1bc01b8 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -19,7 +19,6 @@ def test_get_workskills(instance, demo_data): - logging.info("...Get all skills") metadata_response = instance.metadata.get_workskills(response_type=FULL_RESPONSE) response = metadata_response.json() expected_workskills = demo_data.get("metadata").get("expected_workskills") @@ -30,7 +29,6 @@ def test_get_workskills(instance, demo_data): def test_get_workskill(instance): - logging.info("...Get one skill") metadata_response = instance.metadata.get_workskill( label="RES", response_type=FULL_RESPONSE ) @@ -40,14 +38,12 @@ def test_get_workskill(instance): def test_create_workskill(instance, pp): - logging.info("...create one skill") skill = Workskill(label="TEST", name="test", sharing=SharingEnum.maximal) - logging.warning(f"TEST.Create WorkSkill: IN: {skill.model_dump_json()}") metadata_response = instance.metadata.create_or_update_workskill( skill=skill, response_type=FULL_RESPONSE ) response = metadata_response.json() - logging.info(pp.pformat(response)) + logging.debug(pp.pformat(response)) assert metadata_response.status_code < 299, response assert response["label"] == skill.label assert response["name"] == skill.name @@ -71,7 +67,6 @@ def test_delete_workskill(instance): def test_get_workskill_conditions(instance, pp, demo_data): - logging.info("... get workskill conditions") metadata_response = instance.metadata.get_workskill_conditions( response_type=FULL_RESPONSE ) @@ -93,7 +88,6 @@ def test_get_workskill_conditions(instance, pp, demo_data): def test_replace_workskill_conditions(instance, pp, demo_data): - logging.info("... replace workskill conditions") response = instance.metadata.get_workskill_conditions(response_type=JSON_RESPONSE) expected_workskill_conditions = demo_data.get("metadata").get( "expected_workskill_conditions" @@ -109,7 +103,6 @@ def test_replace_workskill_conditions(instance, pp, demo_data): def test_get_workzones(instance): - logging.info("...Get all workzones") metadata_response = instance.metadata.get_workzones( offset=0, limit=1000, response_type=FULL_RESPONSE ) @@ -121,7 +114,6 @@ def test_get_workzones(instance): def test_get_resource_types(instance, demo_data): - logging.info("...Get all Resource Types") metadata_response = instance.metadata.get_resource_types( response_type=FULL_RESPONSE ) @@ -133,13 +125,11 @@ def test_get_resource_types(instance, demo_data): def test_import_plugin_file(instance: OFSC): - logging.info("... Import plugin via file") metadata_response = instance.metadata.import_plugin_file(Path("tests/test.xml")) assert metadata_response.status_code == 204 def test_import_plugin(instance: OFSC): - logging.info("... Import plugin") metadata_response = instance.metadata.import_plugin( Path("tests/test.xml").read_text() ) diff --git a/tests/test_model.py b/tests/test_model.py index a8498f7..204b386 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -85,5 +85,5 @@ def test_workskill_model_base(): def test_workskilllist_connected(instance): metadata_response = instance.metadata.get_workskills(response_type=JSON_RESPONSE) - logging.warning(json.dumps(metadata_response, indent=4)) + logging.debug(json.dumps(metadata_response, indent=4)) objList = WorkskillList.model_validate(metadata_response["items"]) From 11ad9dccfcb4a86c8628746e8737d9d4100ab1d2 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Tue, 12 Mar 2024 10:08:02 -0400 Subject: [PATCH 05/17] Complete wrapping on core --- ofsc/core.py | 125 ++++++++++++++----------------------------------- ofsc/models.py | 42 ++++++++++++++--- 2 files changed, 70 insertions(+), 97 deletions(-) diff --git a/ofsc/core.py b/ofsc/core.py index 7486095..7ecc96d 100644 --- a/ofsc/core.py +++ b/ofsc/core.py @@ -12,15 +12,11 @@ class OFSCore(OFSApi): # OFSC Function Library - def get_activities(self, params, response_type=TEXT_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_activities(self, params): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/activities") response = requests.get(url, headers=self.headers, params=params) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_activity(self, activity_id): @@ -30,60 +26,41 @@ def get_activity(self, activity_id): response = requests.get(url, headers=self.headers) return response - def update_activity(self, activity_id, data, response_type=TEXT_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def update_activity(self, activity_id, data): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/activities/{}".format(activity_id) ) response = requests.patch(url, headers=self.headers, data=data) - # print (response.status_code) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response # 202107 Added ssearch - def search_activities(self, params, response_type=TEXT_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def search_activities(self, params): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/activities/custom-actions/search" ) response = requests.get(url, headers=self.headers, params=params) - # print (response.status_code) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def move_activity(self, activity_id, data, response_type=TEXT_RESPONSE): + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def move_activity(self, activity_id, data): url = urljoin( self.baseUrl, f"/rest/ofscCore/v1/activities/{activity_id}/custom-actions/move", ) response = requests.post(url, headers=self.headers, data=data) - # print (response.status_code) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def get_events(self, params, response_type=TEXT_RESPONSE): - url = urljoin(self.baseUrl, "rest/ofscCore/v1/events") + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_events(self, params): + url = urljoin(self.baseUrl, "/rest/ofscCore/v1/events") response = requests.get( url, headers=self.headers, params=params, ) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ##### # RESOURCE MANAGEMENT @@ -127,19 +104,6 @@ def get_resource( response = requests.get(url, params=data, headers=self.headers) return response - # 202107 - def create_resource_old(self, resourceId, data, response_type=TEXT_RESPONSE): - url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/resources/{resourceId}") - - response = requests.put(url, headers=self.headers, data=data) - # print (response.status_code) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - # 202209 Resource Types @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def create_resource(self, resourceId, data): @@ -155,7 +119,8 @@ def create_resource_from_obj(self, resourceId, data): response = requests.put(url, headers=self.headers, data=json.dumps(data)) return response - def get_position_history(self, resource_id, date, response_type=TEXT_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_position_history(self, resource_id, date): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/resources/{}/positionHistory".format(str(resource_id)), @@ -163,13 +128,7 @@ def get_position_history(self, resource_id, date, response_type=TEXT_RESPONSE): params = {} params["date"] = date response = requests.get(url, params=params, headers=self.headers) - - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_resource_route( @@ -258,50 +217,39 @@ def update_user(self, login, data): ##202106 @wrap_return(response_type=JSON_RESPONSE, expected=[200]) - def create_user(self, login, data, response_type=FULL_RESPONSE): + def create_user(self, login, data): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/users/{login}") response = requests.put(url, headers=self.headers, data=data) return response ##202106 - def delete_user(self, login, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def delete_user(self, login): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/users/{login}") response = requests.delete(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ##202105 Daily Extract - NOT TESTED - def get_daily_extract_dates(self, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_daily_extract_dates(self): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/folders/dailyExtract/folders/") response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ##202105 Daily Extract - NOT TESTED - def get_daily_extract_files(self, date, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_daily_extract_files(self, date): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/folders/dailyExtract/folders/{}/files".format(date), ) response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ##202105 Daily Extract - NOT TESTED - def get_daily_extract_file(self, date, filename, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_daily_extract_file(self, date, filename): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/folders/dailyExtract/folders/{}/files/{}".format( @@ -309,12 +257,7 @@ def get_daily_extract_file(self, date, filename, response_type=FULL_RESPONSE): ), ) response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ## 202202 Helper functions def get_all_activities( diff --git a/ofsc/models.py b/ofsc/models.py index f03dcf8..3faa1be 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -149,7 +149,12 @@ def set_default(cls, field_value, values): ) -WorkskillList = RootModel[List[Workskill]] +class WorkskillList(RootModel[List[Workskill]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] class Condition(BaseModel): @@ -168,7 +173,12 @@ class WorkskillCondition(BaseModel): dependencies: Any = None -WorskillConditionList = RootModel[List[WorkskillCondition]] +class WorskillConditionList(RootModel[List[WorkskillCondition]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] # Workzones @@ -180,7 +190,12 @@ class Workzone(BaseModel): keys: List[Any] -WorkzoneList = RootModel[List[Workzone]] +class WorkzoneList(RootModel[List[Workzone]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] class Property(BaseModel): @@ -218,7 +233,12 @@ def gui_match(cls, v): model_config = ConfigDict(extra="allow") -PropertyList = RootModel[List[Property]] +class PropertyList(RootModel[List[Property]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] class Resource(BaseModel): @@ -238,7 +258,12 @@ class Resource(BaseModel): model_config = ConfigDict(extra="allow") -ResourceList = RootModel[List[Resource]] +class ResourceList(RootModel[List[Resource]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] class ResourceType(BaseModel): @@ -249,7 +274,12 @@ class ResourceType(BaseModel): model_config = ConfigDict(extra="allow") -ResourceTypeList = RootModel[List[ResourceType]] +class ResourceTypeList(RootModel[List[ResourceType]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] # Core / Activities From 046d18cc772a4ca8a44995f79c8addcd6155786b Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Tue, 12 Mar 2024 10:15:33 -0400 Subject: [PATCH 06/17] wrapped metadata functions --- ofsc/metadata.py | 90 ++++++++++++++---------------------------- tests/test_metadata.py | 10 +++-- 2 files changed, 36 insertions(+), 64 deletions(-) diff --git a/ofsc/metadata.py b/ofsc/metadata.py index f59e0cd..feca2f8 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -35,13 +35,13 @@ class OFSMetadata(OFSApi): ] capacityHeaders = capacityAreasFields.split(",") + additionalCapacityFields + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_capacity_areas( self, expand="parent", fields=capacityAreasFields, status="active", queryType="area", - response_type=FULL_RESPONSE, ): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/capacityAreas") params = {} @@ -50,79 +50,53 @@ def get_capacity_areas( params["status"] = status params["type"] = queryType response = requests.get(url, params=params, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_capacity_area(self, label, response_type=FULL_RESPONSE): encoded_label = urllib.parse.quote_plus(label) url = urljoin( self.baseUrl, "/rest/ofscMetadata/v1/capacityAreas/{}".format(encoded_label) ) response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def get_activity_type_groups( - self, offset=0, limit=100, response_type=FULL_RESPONSE - ): + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_activity_type_groups(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypeGroups") response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def get_activity_type_group(self, label, response_type=FULL_RESPONSE): + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_activity_type_group(self, label): encoded_label = urllib.parse.quote_plus(label) url = urljoin( self.baseUrl, "/rest/ofscMetadata/v1/activityTypeGroups/{}".format(encoded_label), ) response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ## 202205 Activity Type + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_activity_types(self, offset=0, limit=100, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypes") response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text - - def get_activity_type(self, label, response_type=FULL_RESPONSE): + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_activity_type(self, label): encoded_label = urllib.parse.quote_plus(label) url = urljoin( self.baseUrl, "/rest/ofscMetadata/v1/activityTypes/{}".format(encoded_label) ) response = requests.get(url, headers=self.headers) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response ## 202202 Properties and file properties - def get_properties(self, offset=0, limit=100, response_type=FULL_RESPONSE): + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_properties(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/properties") params = {"offset": offset, "limit": limit} response = requests.get( @@ -130,12 +104,7 @@ def get_properties(self, offset=0, limit=100, response_type=FULL_RESPONSE): headers=self.headers, params=params, ) - if response_type == FULL_RESPONSE: - return response - elif response_type == JSON_RESPONSE: - return response.json() - else: - return response.text + return response # 202209 Get Property @wrap_return(response_type=JSON_RESPONSE, expected=[200]) @@ -179,18 +148,17 @@ def get_resource_types(self): return response # 202212 Import plugin - @wrap_return(response_type=FULL_RESPONSE, expected=[204]) + @wrap_return(response_type=JSON_RESPONSE, expected=[204]) def import_plugin_file(self, plugin: Path): url = urljoin( self.baseUrl, f"/rest/ofscMetadata/v1/plugins/custom-actions/import" ) - headers = self.headers files = [("pluginFile", (plugin.name, plugin.read_text(), "text/xml"))] response = requests.post(url, headers=self.headers, files=files) return response # 202212 Import plugin - @wrap_return(response_type=FULL_RESPONSE, expected=[204]) + @wrap_return(response_type=JSON_RESPONSE, expected=[204]) def import_plugin(self, plugin: str): url = urljoin( self.baseUrl, f"/rest/ofscMetadata/v1/plugins/custom-actions/import" @@ -199,7 +167,7 @@ def import_plugin(self, plugin: str): response = requests.post(url, headers=self.headers, files=files) return response - @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_workskills(self, offset=0, limit=100, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/workSkills") params = {"offset": offset, "limit": limit} @@ -210,7 +178,7 @@ def get_workskills(self, offset=0, limit=100, response_type=FULL_RESPONSE): ) return response - @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_workskill(self, label: str, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") response = requests.get( @@ -219,20 +187,20 @@ def get_workskill(self, label: str, response_type=FULL_RESPONSE): ) return response - @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def create_or_update_workskill(self, skill: Workskill, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{skill.label}") response = requests.put(url, headers=self.headers, data=skill.model_dump_json()) return response - @wrap_return(response_type=FULL_RESPONSE, expected=[204]) + @wrap_return(response_type=JSON_RESPONSE, expected=[204]) def delete_workskill(self, label: str, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") response = requests.delete(url, headers=self.headers) return response # Workskill conditions - @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_workskill_conditions(self, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkillConditions") response = requests.get( @@ -241,7 +209,7 @@ def get_workskill_conditions(self, response_type=FULL_RESPONSE): ) return response - @wrap_return(response_type=FULL_RESPONSE, expected=[200]) + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def replace_workskill_conditions( self, data: WorskillConditionList, response_type=FULL_RESPONSE ): diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 1bc01b8..d7121d7 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -95,7 +95,9 @@ def test_replace_workskill_conditions(instance, pp, demo_data): assert response["totalResults"] is not None assert response["totalResults"] == expected_workskill_conditions ws_list = WorskillConditionList.model_validate(response["items"]) - metadata_response = instance.metadata.replace_workskill_conditions(ws_list) + metadata_response = instance.metadata.replace_workskill_conditions( + ws_list, response_type=FULL_RESPONSE + ) logging.debug(pp.pformat(metadata_response.text)) assert metadata_response.status_code == 200 assert response["totalResults"] is not None @@ -125,12 +127,14 @@ def test_get_resource_types(instance, demo_data): def test_import_plugin_file(instance: OFSC): - metadata_response = instance.metadata.import_plugin_file(Path("tests/test.xml")) + metadata_response = instance.metadata.import_plugin_file( + Path("tests/test.xml"), response_type=FULL_RESPONSE + ) assert metadata_response.status_code == 204 def test_import_plugin(instance: OFSC): metadata_response = instance.metadata.import_plugin( - Path("tests/test.xml").read_text() + Path("tests/test.xml").read_text(), response_type=FULL_RESPONSE ) assert metadata_response.status_code == 204 From 41f7fe4a27fd6dfffc32719a4e7b958bcf9a4f56 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Tue, 12 Mar 2024 14:03:37 -0400 Subject: [PATCH 07/17] conversion to models --- README.md | 14 +++++++++++--- ofsc/__init__.py | 23 +++++++++++++---------- ofsc/common.py | 20 +++++++++++++++++--- ofsc/exceptions.py | 12 ++++++++++++ ofsc/metadata.py | 10 +++++++--- ofsc/models.py | 22 ++++++++++++++++++++++ tests/test_base.py | 33 +++++++++++++++++++++++++++++++-- tests/test_model.py | 21 +++++++++++++++++++++ 8 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 ofsc/exceptions.py diff --git a/README.md b/README.md index 695ad2c..cc7875a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A simple Python wrapper for Oracle OFS REST API Starting with OFS 1.17 we are adding models for the most common entities. All models should be imported from `ofsc.models`. All existing create functions will be transitioned to models. In OFS 2.0 all functions will use models -The models are based on the Pydantic BaseModel, so it is possible to build an entity using the `parse_obj` or `parse_file` static methods. +The models are based on the Pydantic BaseModel, so it is possible to build an entity using the `model_validate` static methods. Currently implemented: - Workskill @@ -102,10 +102,11 @@ OFS REST API Version | PyOFSC 21D| 1.15 22B| 1.16, 1.17 22D| 1.18 +24A| 2.0 ## Deprecation Warning -Starting in OFSC 2.0 (estimated for December 2022) all functions will have to be called using the API name (Core or Metadata). See the examples. +Starting in OFSC 2.0 all functions will have to be called using the API name (Core or Metadata). See the examples. Instead of @@ -117,4 +118,11 @@ It will be required to use the right API module: instance = OFSC(..) list_of_activites = instance.core.get_activities(...) -During the transition period a DeprecationWarning will be raised if the functions are used in the old way \ No newline at end of file +During the transition period a DeprecationWarning will be raised if the functions are used in the old way + +## What's new in OFSC 2.0 + +- All metadata functions now use models, when available +- All functions are now using the API name (Core or Metadata) +- All functions return a python object by default. If there is an available model it will be used, otherwise a dict will be returned (see `response_type` parameter and `auto_model` parameter) +- Errors during API calls can raise exceptions and will by default when returning an object (see `auto_raise` parameter) \ No newline at end of file diff --git a/ofsc/__init__.py b/ofsc/__init__.py index 4e0bdd8..3c5faa2 100644 --- a/ofsc/__init__.py +++ b/ofsc/__init__.py @@ -1,11 +1,4 @@ -import base64 -import logging -from functools import wraps -from http import client -from urllib import response -from warnings import warn - -from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE, wrap_return +from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from .core import OFSCore from .metadata import OFSMetadata from .models import OFSConfig @@ -13,10 +6,18 @@ class OFSC: - # 202308 The API portal was deprecated, so the default URL becomes {companyname}.fs.ocs.oraclecloud.com + # the default URL becomes {companyname}.fs.ocs.oraclecloud.com def __init__( - self, clientID, companyName, secret, root=None, baseUrl=None, useToken=False + self, + clientID, + companyName, + secret, + root=None, + baseUrl=None, + useToken=False, + auto_raise=True, + auto_model=True, ): self._config = OFSConfig( baseURL=baseUrl, @@ -25,6 +26,8 @@ def __init__( companyName=companyName, root=root, useToken=useToken, + auto_raise=auto_raise, # 20240401: This is a new feature that will raise an exception if the API returns an error + auto_model=auto_model, # 20240401: This is a new feature that will return a pydantic model if the API returns a 200 ) self._core = OFSCore(config=self._config) self._metadata = OFSMetadata(config=self._config) diff --git a/ofsc/common.py b/ofsc/common.py index 61ed432..cdea3d2 100644 --- a/ofsc/common.py +++ b/ofsc/common.py @@ -1,6 +1,10 @@ import logging from functools import wraps +import requests + +from .exceptions import OFSAPIException + TEXT_RESPONSE = 1 FULL_RESPONSE = 2 JSON_RESPONSE = 3 @@ -8,17 +12,19 @@ def wrap_return(*a, **kw): """ - Decorator @return_as wraps the function + Decorator @wrap_return wraps the function and decides the return type and if we launch an exception """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): + config = args[0].config # Pre: response_type = kwargs.get( "response_type", kw.get("response_type", FULL_RESPONSE) ) + expected_codes = kw.get("expected_codes", [200]) kwargs.pop("response_type", None) response = func(*args, **kwargs) # post: @@ -27,10 +33,18 @@ def wrapper(*args, **kwargs): if response_type == FULL_RESPONSE: return response elif response_type == JSON_RESPONSE: - return response.json() + if response.status_code in expected_codes: + return response.json() + else: + if not config.auto_raise: + return response.json() + # Check if response.statyus code is between 400 and 499 + if 400 <= response.status_code < 500: + raise OFSAPIException(**response.json()) + elif 500 <= response.status_code < 600: + raise OFSAPIException(**response.json()) else: return response.text - return result return wrapper diff --git a/ofsc/exceptions.py b/ofsc/exceptions.py new file mode 100644 index 0000000..12a8d29 --- /dev/null +++ b/ofsc/exceptions.py @@ -0,0 +1,12 @@ +import logging + + +class OFSAPIException(Exception): + def __init__(self, *args: object, **kwargs) -> None: + super().__init__(*args) + for key, value in kwargs.items(): + match key: + case "status": + setattr(self, "status_code", int(value)) + case _: + setattr(self, key, value) diff --git a/ofsc/metadata.py b/ofsc/metadata.py index feca2f8..5ee1603 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -10,6 +10,8 @@ from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE, wrap_return from .models import ( + ActivityTypeGroup, + ActivityTypeGroupList, OFSApi, OFSConfig, Property, @@ -53,7 +55,7 @@ def get_capacity_areas( return response @wrap_return(response_type=JSON_RESPONSE, expected=[200]) - def get_capacity_area(self, label, response_type=FULL_RESPONSE): + def get_capacity_area(self, label: str): encoded_label = urllib.parse.quote_plus(label) url = urljoin( self.baseUrl, "/rest/ofscMetadata/v1/capacityAreas/{}".format(encoded_label) @@ -61,13 +63,15 @@ def get_capacity_area(self, label, response_type=FULL_RESPONSE): response = requests.get(url, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return( + response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroupList + ) def get_activity_type_groups(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypeGroups") response = requests.get(url, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroup) def get_activity_type_group(self, label): encoded_label = urllib.parse.quote_plus(label) url = urljoin( diff --git a/ofsc/models.py b/ofsc/models.py index 3faa1be..7c25806 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -26,6 +26,8 @@ class OFSConfig(BaseModel): useToken: bool = False root: Optional[str] = None baseURL: Optional[str] = None + auto_raise: bool = True + auto_model: bool = True @property def basicAuthString(self): @@ -46,6 +48,13 @@ class OFSOAuthRequest(BaseModel): ofs_dynamic_scope: Optional[str] = None +class OFSAPIError(BaseModel): + type: str + title: str + status: int + detail: str + + class OFSApi: def __init__(self, config: OFSConfig) -> None: self._config = config @@ -332,3 +341,16 @@ class BulkUpdateResult(BaseModel): class BulkUpdateResponse(BaseModel): results: Optional[List[BulkUpdateResult]] = None + + +class ActivityTypeGroup(BaseModel): + label: str + translations: TranslationList + + +class ActivityTypeGroupList(RootModel[List[ActivityTypeGroup]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] diff --git a/tests/test_base.py b/tests/test_base.py index e710593..bfe364a 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -3,10 +3,10 @@ import requests from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE +from ofsc.exceptions import OFSAPIException -def test_wrapper(instance): - logging.info("...301: Testing wrapper") +def test_wrapper_generic(instance): raw_response = instance.core.get_subscriptions(response_type=FULL_RESPONSE) assert isinstance(raw_response, requests.Response) assert raw_response.status_code == 200 @@ -19,3 +19,32 @@ def test_wrapper(instance): assert isinstance(text_response, str) default_response = instance.core.get_subscriptions() assert isinstance(default_response, dict) + + +def test_wrapper_with_error(instance, pp): + instance.core.config.auto_raise = False + raw_response = instance.core.get_activity("123456", response_type=FULL_RESPONSE) + assert isinstance(raw_response, requests.Response) + assert raw_response.status_code == 404 + raw_response = instance.core.get_activity("123456", response_type=JSON_RESPONSE) + assert isinstance(raw_response, dict) + assert raw_response["status"] == "404" + instance.core.config.auto_raise = True + raw_response = instance.core.get_activity("123456", response_type=FULL_RESPONSE) + assert isinstance(raw_response, requests.Response) + assert raw_response.status_code == 404 + + # Validate that the next line raises an exception + try: + instance.core.get_activity("123456", response_type=JSON_RESPONSE) + except Exception as e: + assert isinstance(e, OFSAPIException) + # log exception fields + assert e.status_code == 404 + + +def test_wrapper_with_model(instance, demo_data): + raw_response = instance.metadata.get_workskills(response_type=FULL_RESPONSE) + assert isinstance(raw_response, requests.Response) + response = raw_response.json() + assert "totalResults" in response.keys() diff --git a/tests/test_model.py b/tests/test_model.py index 204b386..8671ca4 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -8,6 +8,8 @@ from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from ofsc.models import ( + ActivityTypeGroup, + ActivityTypeGroupList, Condition, SharingEnum, Translation, @@ -87,3 +89,22 @@ def test_workskilllist_connected(instance): metadata_response = instance.metadata.get_workskills(response_type=JSON_RESPONSE) logging.debug(json.dumps(metadata_response, indent=4)) objList = WorkskillList.model_validate(metadata_response["items"]) + + +def test_activity_type_group_model(instance): + metadata_response = instance.metadata.get_activity_type_groups( + response_type=JSON_RESPONSE + ) + logging.debug(json.dumps(metadata_response, indent=4)) + objList = ActivityTypeGroupList.model_validate(metadata_response["items"]) + ## Iterate through the list and validate each item + for idx, obj in enumerate(objList): + assert type(obj) == ActivityTypeGroup + assert obj.label == metadata_response["items"][idx]["label"] + new_obj = ActivityTypeGroup.model_validate( + instance.metadata.get_activity_type_group( + label=obj.label, response_type=JSON_RESPONSE + ) + ) + assert new_obj.label == obj.label + assert new_obj.translations == obj.translations From 7e7a345f8740f9f2373aa5b8cc3ad43f666c9e61 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Tue, 12 Mar 2024 15:04:59 -0400 Subject: [PATCH 08/17] activityTypeGroups model finished --- README.md | 70 ++++++++++++++++--------------- ofsc/__init__.py | 22 ++++++++-- ofsc/common.py | 38 +++++++++++++---- ofsc/metadata.py | 40 ++++++++++-------- tests/metadata/test_properties.py | 2 +- tests/test_base.py | 28 +++++++++++-- tests/test_model.py | 1 + 7 files changed, 131 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index cc7875a..be1c3e9 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@ Starting with OFS 1.17 we are adding models for the most common entities. All mo The models are based on the Pydantic BaseModel, so it is possible to build an entity using the `model_validate` static methods. Currently implemented: +- ActivityTypeGroup +- Property - Workskill - WorkSkillCondition - Workzone -- Property + Experimental: - Resource @@ -35,53 +37,53 @@ Experimental: ### Core / Events - get_subscriptions(self, response_type=TEXT_RESPONSE) - create_subscription(self, data, response_type=TEXT_RESPONSE) - delete_subscription(self, subscription_id, response_type=FULL_RESPONSE) - get_subscription_details(self, subscription_id, response_type=TEXT_RESPONSE) - get_events(self, params, response_type=TEXT_RESPONSE) + get_subscriptions(self, response_type=JSON_RESPONSE) + create_subscription(self, data, response_type=JSON_RESPONSE) + delete_subscription(self, subscription_id, response_type=JSON_RESPONSE) + get_subscription_details(self, subscription_id, response_type=JSON_RESPONSE) + get_events(self, params, response_type=JSON_RESPONSE) ### Core / Resources - create_resource(self, resourceId, data, response_type=TEXT_RESPONSE) - get_resource(self, resource_id, inventories=False, workSkills=False, workZones=False, workSchedules=False , response_type=TEXT_RESPONSE) - get_position_history(self, resource_id,date,response_type=TEXT_RESPONSE) - get_resource_route(self, resource_id, date, activityFields = None, offset=0, limit=100, response_type=TEXT_RESPONSE) - get_resource_descendants(self, resource_id, resourceFields=None, offset=0, limit=100, inventories=False, workSkills=False, workZones=False, workSchedules=False, response_type=TEXT_RESPONSE) + create_resource(self, resourceId, data, response_type=JSON_RESPONSE) + get_resource(self, resource_id, inventories=False, workSkills=False, workZones=False, workSchedules=False , response_type=JSON_RESPONSE) + get_position_history(self, resource_id,date,response_type=JSON_RESPONSE) + get_resource_route(self, resource_id, date, activityFields = None, offset=0, limit=100, response_type=JSON_RESPONSE) + get_resource_descendants(self, resource_id, resourceFields=None, offset=0, limit=100, inventories=False, workSkills=False, workZones=False, workSchedules=False, response_type=JSON_RESPONSE) ### Core / Users - get_users(self, offset=0, limit=100, response_type=FULL_RESPONSE) - get_user(self, login, response_type=FULL_RESPONSE): - update_user (self, login, data, response_type=TEXT_RESPONSE) - create_user(self, login, data, response_type=FULL_RESPONSE) - delete_user(self, login, response_type=FULL_RESPONSE) + get_users(self, offset=0, limit=100, response_type=JSON_RESPONSE) + get_user(self, login, response_type=JSON_RESPONSE): + update_user (self, login, data, response_type=JSON_RESPONSE) + create_user(self, login, data, response_type=JSON_RESPONSE) + delete_user(self, login, response_type=JSON_RESPONSE) ### Core / Daily Extract - get_daily_extract_dates(self, response_type=FULL_RESPONSE) - get_daily_extract_files(self, date, response_type=FULL_RESPONSE) - get_daily_extract_file(self, date, filename, response_type=FULL_RESPONSE) + get_daily_extract_dates(self, response_type=JSON_RESPONSE) + get_daily_extract_files(self, date, response_type=JSON_RESPONSE) + get_daily_extract_file(self, date, filename, response_type=JSON_RESPONSE) ### Metadata / Capacity - get_capacity_areas (self, expand="parent", fields=capacityAreasFields, status="active", queryType="area", response_type=FULL_RESPONSE) - get_capacity_area (self,label, response_type=FULL_RESPONSE) + get_capacity_areas (self, expand="parent", fields=capacityAreasFields, status="active", queryType="area", response_type=JSON_RESPONSE) + get_capacity_area (self,label, response_type=JSON_RESPONSE) ### Metadata / Activity Types - get_activity_type_groups (self, expand="parent", offset=0, limit=100, response_type=FULL_RESPONSE) - get_activity_type_group (self,label, response_type=FULL_RESPONSE) - get_activity_types(self, offset=0, limit=100, response_type=FULL_RESPONSE) - get_activity_type (self, label, response_type=FULL_RESPONSE) + get_activity_type_groups (self, expand="parent", offset=0, limit=100, response_type=JSON_RESPONSE) + get_activity_type_group (self,label, response_type=JSON_RESPONSE) + get_activity_types(self, offset=0, limit=100, response_type=JSON_RESPONSE) + get_activity_type (self, label, response_type=JSON_RESPONSE) ### Metadata / Properties - get_properties (self, offset=0, limit=100, response_type=FULL_RESPONSE) + get_properties (self, offset=0, limit=100, response_type=JSON_RESPONSE) get_property(self, label: str, response_type=JSON_RESPONSE) create_or_replace_property(self, property: Property, response_type=JSON_RESPONSE) ### Metadata / Workskills - get_workskills (self, offset=0, limit=100, response_type=FULL_RESPONSE) - get_workskill(self, label: str, response_type=FULL_RESPONSE) - create_or_update_workskill(self, skill: Workskill, response_type=FULL_RESPONSE) - delete_workskill(self, label: str, response_type=FULL_RESPONSE) - get_workskill_conditions(self, response_type=FULL_RESPONSE) - replace_workskill_conditions(self, data: WorskillConditionList, response_type=FULL_RESPONSE + get_workskills (self, offset=0, limit=100, response_type=JSON_RESPONSE) + get_workskill(self, label: str, response_type=JSON_RESPONSE) + create_or_update_workskill(self, skill: Workskill, response_type=JSON_RESPONSE) + delete_workskill(self, label: str, response_type=JSON_RESPONSE) + get_workskill_conditions(self, response_type=JSON_RESPONSE) + replace_workskill_conditions(self, data: WorskillConditionList, response_type=JSON_RESPONSE) ### Metadata / Plugins import_plugin(self, plugin: str) @@ -91,7 +93,7 @@ Experimental: get_resource_types(self, response_type=JSON_RESPONSE): ### Metadata / workzones - get_workzones(self, response_type=FULL_RESPONSE) + get_workzones(self, response_type=JSON_RESPONSE) ## Test History @@ -106,7 +108,7 @@ OFS REST API Version | PyOFSC ## Deprecation Warning -Starting in OFSC 2.0 all functions will have to be called using the API name (Core or Metadata). See the examples. +Starting in OFSC 2.0 all functions have to be called using the API name (Core or Metadata). See the examples. Instead of diff --git a/ofsc/__init__.py b/ofsc/__init__.py index 3c5faa2..0820006 100644 --- a/ofsc/__init__.py +++ b/ofsc/__init__.py @@ -1,3 +1,5 @@ +import logging + from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from .core import OFSCore from .metadata import OFSMetadata @@ -16,9 +18,10 @@ def __init__( root=None, baseUrl=None, useToken=False, - auto_raise=True, - auto_model=True, + enable_auto_raise=True, + enable_auto_model=True, ): + self._config = OFSConfig( baseURL=baseUrl, clientID=clientID, @@ -26,8 +29,8 @@ def __init__( companyName=companyName, root=root, useToken=useToken, - auto_raise=auto_raise, # 20240401: This is a new feature that will raise an exception if the API returns an error - auto_model=auto_model, # 20240401: This is a new feature that will return a pydantic model if the API returns a 200 + auto_raise=enable_auto_raise, # 20240401: This is a new feature that will raise an exception if the API returns an error + auto_model=enable_auto_model, # 20240401: This is a new feature that will return a pydantic model if the API returns a 200 ) self._core = OFSCore(config=self._config) self._metadata = OFSMetadata(config=self._config) @@ -65,6 +68,17 @@ def oauth2(self) -> OFSOauth2: self._oauth = OFSOauth2(config=self._config) return self._oauth + @property + def auto_model(self): + return self._config.auto_model + + @auto_model.setter + def auto_model(self, value): + self._config.auto_model = value + self._core.config.auto_model = value + self._metadata.config.auto_model = value + self._oauth.config.auto_model = value + def __str__(self) -> str: return f"baseURL={self._config.baseURL}" diff --git a/ofsc/common.py b/ofsc/common.py index cdea3d2..1cae772 100644 --- a/ofsc/common.py +++ b/ofsc/common.py @@ -10,7 +10,7 @@ JSON_RESPONSE = 3 -def wrap_return(*a, **kw): +def wrap_return(*decorator_args, **decorator_kwargs): """ Decorator @wrap_return wraps the function and decides the return type and if we launch an exception @@ -18,23 +18,43 @@ def wrap_return(*a, **kw): def decorator(func): @wraps(func) - def wrapper(*args, **kwargs): - config = args[0].config + def wrapper(*func_args, **func_kwargs): + logging.debug( + f"{func_args=}, {func_kwargs=}, {decorator_args=}, {decorator_kwargs=}" + ) + config = func_args[0].config # Pre: - response_type = kwargs.get( - "response_type", kw.get("response_type", FULL_RESPONSE) + response_type = func_kwargs.get( + "response_type", decorator_kwargs.get("response_type", FULL_RESPONSE) ) - expected_codes = kw.get("expected_codes", [200]) - kwargs.pop("response_type", None) - response = func(*args, **kwargs) + func_kwargs.pop("response_type", None) + expected_codes = decorator_kwargs.get("expected_codes", [200]) + model = func_kwargs.get("model", decorator_kwargs.get("model", None)) + func_kwargs.pop("model", None) + + response = func(*func_args, **func_kwargs) # post: logging.debug(response) if response_type == FULL_RESPONSE: return response elif response_type == JSON_RESPONSE: + logging.debug( + f"{response_type=}, {config.auto_model=}, {model=} {func_args= } {func_kwargs=}" + ) if response.status_code in expected_codes: - return response.json() + match response.status_code: + case 204: + return response.text + case _: + data_response = response.json() + if config.auto_model and model is not None: + if data_response.get("items"): + return model.model_validate(data_response["items"]) + else: + return model.model_validate(data_response) + else: + return data_response else: if not config.auto_raise: return response.json() diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 5ee1603..8fc8cca 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -63,24 +63,6 @@ def get_capacity_area(self, label: str): response = requests.get(url, headers=self.headers) return response - @wrap_return( - response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroupList - ) - def get_activity_type_groups(self, offset=0, limit=100): - url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypeGroups") - response = requests.get(url, headers=self.headers) - return response - - @wrap_return(response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroup) - def get_activity_type_group(self, label): - encoded_label = urllib.parse.quote_plus(label) - url = urljoin( - self.baseUrl, - "/rest/ofscMetadata/v1/activityTypeGroups/{}".format(encoded_label), - ) - response = requests.get(url, headers=self.headers) - return response - ## 202205 Activity Type @wrap_return(response_type=JSON_RESPONSE, expected=[200]) def get_activity_types(self, offset=0, limit=100, response_type=FULL_RESPONSE): @@ -223,3 +205,25 @@ def replace_workskill_conditions( headers["Content-Type"] = "application/json" response = requests.put(url, headers=headers, data=content) return response + + ##### + # Migration to OFS 2.0 model format + + # 202402 Metadata - Activity Type Groups + @wrap_return( + response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroupList + ) + def get_activity_type_groups(self, offset=0, limit=100): + url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypeGroups") + response = requests.get(url, headers=self.headers) + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroup) + def get_activity_type_group(self, label): + encoded_label = urllib.parse.quote_plus(label) + url = urljoin( + self.baseUrl, + f"/rest/ofscMetadata/v1/activityTypeGroups/{encoded_label}", + ) + response = requests.get(url, headers=self.headers) + return response diff --git a/tests/metadata/test_properties.py b/tests/metadata/test_properties.py index a784e30..da2e72f 100644 --- a/tests/metadata/test_properties.py +++ b/tests/metadata/test_properties.py @@ -15,7 +15,7 @@ def test_get_property(instance): assert response["label"] == "XA_CASE_ACCOUNT" assert response["type"] == "string" assert response["entity"] == "activity" - property = Property.parse_obj(response) + property = Property.model_validate(response) def test_get_properties(instance, demo_data): diff --git a/tests/test_base.py b/tests/test_base.py index bfe364a..a4211bd 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -4,6 +4,7 @@ from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from ofsc.exceptions import OFSAPIException +from ofsc.models import ActivityTypeGroup, ActivityTypeGroupList def test_wrapper_generic(instance): @@ -43,8 +44,27 @@ def test_wrapper_with_error(instance, pp): assert e.status_code == 404 -def test_wrapper_with_model(instance, demo_data): - raw_response = instance.metadata.get_workskills(response_type=FULL_RESPONSE) +def test_wrapper_with_model_list(instance, demo_data): + instance.core.config.auto_model = True + raw_response = instance.metadata.get_activity_type_groups( + response_type=FULL_RESPONSE + ) assert isinstance(raw_response, requests.Response) - response = raw_response.json() - assert "totalResults" in response.keys() + assert raw_response.status_code == 200 + + json_response = instance.metadata.get_activity_type_groups() + assert isinstance(json_response, ActivityTypeGroupList) + + +def test_wrapper_with_model_single(instance): + instance.core.config.auto_model = True + raw_response = instance.metadata.get_activity_type_group("customer") + assert isinstance(raw_response, ActivityTypeGroup) + + +def test_wrapper_without_model(instance): + instance.auto_model = False + raw_response = instance.metadata.get_activity_type_group("customer") + assert isinstance(raw_response, dict) + assert "label" in raw_response.keys() + assert "name" in raw_response.keys() diff --git a/tests/test_model.py b/tests/test_model.py index 8671ca4..bf1dd01 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -92,6 +92,7 @@ def test_workskilllist_connected(instance): def test_activity_type_group_model(instance): + instance.core.config.auto_model = False metadata_response = instance.metadata.get_activity_type_groups( response_type=JSON_RESPONSE ) From ab189e0711178542ee968eefbaf18c09d3b35fb0 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Tue, 12 Mar 2024 15:40:39 -0400 Subject: [PATCH 09/17] added ActivityType models --- README.md | 14 ++++++---- ofsc/models.py | 68 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_model.py | 34 +++++++++++++++++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index be1c3e9..3006665 100644 --- a/README.md +++ b/README.md @@ -61,17 +61,19 @@ Experimental: get_daily_extract_dates(self, response_type=JSON_RESPONSE) get_daily_extract_files(self, date, response_type=JSON_RESPONSE) get_daily_extract_file(self, date, filename, response_type=JSON_RESPONSE) - -### Metadata / Capacity - get_capacity_areas (self, expand="parent", fields=capacityAreasFields, status="active", queryType="area", response_type=JSON_RESPONSE) - get_capacity_area (self,label, response_type=JSON_RESPONSE) - -### Metadata / Activity Types +\ +### Metadata / Activity Type Groups get_activity_type_groups (self, expand="parent", offset=0, limit=100, response_type=JSON_RESPONSE) get_activity_type_group (self,label, response_type=JSON_RESPONSE) + +### Metadata / Activity Types get_activity_types(self, offset=0, limit=100, response_type=JSON_RESPONSE) get_activity_type (self, label, response_type=JSON_RESPONSE) +### Metadata / Capacity + get_capacity_areas (self, expand="parent", fields=capacityAreasFields, status="active", queryType="area", response_type=JSON_RESPONSE) + get_capacity_area (self,label, response_type=JSON_RESPONSE) + ### Metadata / Properties get_properties (self, offset=0, limit=100, response_type=JSON_RESPONSE) get_property(self, label: str, response_type=JSON_RESPONSE) diff --git a/ofsc/models.py b/ofsc/models.py index 7c25806..1cef1fc 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -354,3 +354,71 @@ def __iter__(self): def __getitem__(self, item): return self.root[item] + + +class ActivityTypeColors(BaseModel): + cancelled: Annotated[Optional[str], Field(alias="cancelled")] + completed: Annotated[Optional[str], Field(alias="completed")] + notdone: Annotated[Optional[str], Field(alias="notdone")] + notOrdered: Annotated[Optional[str], Field(alias="notOrdered")] + pending: Annotated[Optional[str], Field(alias="pending")] + started: Annotated[Optional[str], Field(alias="started")] + suspended: Annotated[Optional[str], Field(alias="suspended")] + warning: Annotated[Optional[str], Field(alias="warning")] + + +class ActivityTypeFeatures(BaseModel): + model_config = ConfigDict(extra="allow") + allowCreationInBuckets: Optional[bool] = False + allowMassActivities: Optional[bool] = False + allowMoveBetweenResources: Optional[bool] = False + allowNonScheduled: Optional[bool] = False + allowRepeatingActivities: Optional[bool] = False + allowReschedule: Optional[bool] = False + allowToCreateFromIncomingInterface: Optional[bool] = False + allowToSearch: Optional[bool] = False + calculateActivityDurationUsingStatistics: Optional[bool] = False + calculateDeliveryWindow: Optional[bool] = False + calculateTravel: Optional[bool] = False + disableLocationTracking: Optional[bool] = False + enableDayBeforeTrigger: Optional[bool] = False + enableNotStartedTrigger: Optional[bool] = False + enableReminderAndChangeTriggers: Optional[bool] = False + enableSwWarningTrigger: Optional[bool] = False + isSegmentingEnabled: Optional[bool] = False + isTeamworkAvailable: Optional[bool] = False + slaAndServiceWindowUseCustomerTimeZone: Optional[bool] = False + supportOfInventory: Optional[bool] = False + supportOfLinks: Optional[bool] = False + supportOfNotOrderedActivities: Optional[bool] = False + supportOfPreferredResources: Optional[bool] = False + supportOfRequiredInventory: Optional[bool] = False + supportOfTimeSlots: Optional[bool] = False + supportOfWorkSkills: Optional[bool] = False + supportOfWorkZones: Optional[bool] = False + + +class ActivityTypeTimeSlots(BaseModel): + label: str + + +class ActivityType(BaseModel): + active: bool + colors: Optional[ActivityTypeColors] + defaultDuration: int + features: Optional[ActivityTypeFeatures] + groupLabel: Optional[str] + label: str + name: str + segmentMaxDuration: Optional[int] = None + segmentMinDuration: Optional[int] = None + timeSlots: Optional[List[ActivityTypeTimeSlots]] = None + translations: TranslationList + + +class ActivityTypeList(RootModel[List[ActivityType]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] diff --git a/tests/test_model.py b/tests/test_model.py index bf1dd01..22f9a3b 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -8,8 +8,10 @@ from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from ofsc.models import ( + ActivityType, ActivityTypeGroup, ActivityTypeGroupList, + ActivityTypeList, Condition, SharingEnum, Translation, @@ -109,3 +111,35 @@ def test_activity_type_group_model(instance): ) assert new_obj.label == obj.label assert new_obj.translations == obj.translations + + +def test_activity_type_model_list(instance): + instance.core.config.auto_model = False + metadata_response = instance.metadata.get_activity_types( + response_type=JSON_RESPONSE + ) + logging.debug(json.dumps(metadata_response, indent=4)) + objList = ActivityTypeList.model_validate(metadata_response["items"]) + ## Iterate through the list and validate each item + for idx, obj in enumerate(objList): + assert type(obj) == ActivityType + assert obj.label == metadata_response["items"][idx]["label"] + new_obj = ActivityType.model_validate( + instance.metadata.get_activity_type( + label=obj.label, response_type=JSON_RESPONSE + ) + ) + assert new_obj.label == obj.label + + +def test_activity_type_model_simple(instance): + instance.core.config.auto_model = False + metadata_response = instance.metadata.get_activity_type( + label="01", response_type=JSON_RESPONSE + ) + logging.debug(json.dumps(metadata_response, indent=4)) + obj = ActivityType.model_validate(metadata_response) + assert obj.label == metadata_response["label"] + assert obj.translations == TranslationList.model_validate( + metadata_response["translations"] + ) From 4a5c997838fbe7578d44cfe7660e094cbc766214 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Wed, 13 Mar 2024 09:41:01 -0400 Subject: [PATCH 10/17] Activity Groups and Types completed --- ofsc/common.py | 5 +- ofsc/metadata.py | 42 +++-- ofsc/models.py | 24 ++- tests/metadata/test_activity_groups_types.py | 67 ++++++++ tests/test_base.py | 8 +- tests/test_model.py | 164 +++++++++++++------ 6 files changed, 233 insertions(+), 77 deletions(-) diff --git a/ofsc/common.py b/ofsc/common.py index 1cae772..7809c59 100644 --- a/ofsc/common.py +++ b/ofsc/common.py @@ -49,10 +49,7 @@ def wrapper(*func_args, **func_kwargs): case _: data_response = response.json() if config.auto_model and model is not None: - if data_response.get("items"): - return model.model_validate(data_response["items"]) - else: - return model.model_validate(data_response) + return model.model_validate(data_response) else: return data_response else: diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 8fc8cca..2dbd73a 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -12,6 +12,8 @@ from .models import ( ActivityTypeGroup, ActivityTypeGroupList, + ActivityTypeGroupListResponse, + ActivityTypeListResponse, OFSApi, OFSConfig, Property, @@ -63,22 +65,6 @@ def get_capacity_area(self, label: str): response = requests.get(url, headers=self.headers) return response - ## 202205 Activity Type - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) - def get_activity_types(self, offset=0, limit=100, response_type=FULL_RESPONSE): - url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypes") - response = requests.get(url, headers=self.headers) - return response - - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) - def get_activity_type(self, label): - encoded_label = urllib.parse.quote_plus(label) - url = urljoin( - self.baseUrl, "/rest/ofscMetadata/v1/activityTypes/{}".format(encoded_label) - ) - response = requests.get(url, headers=self.headers) - return response - ## 202202 Properties and file properties @wrap_return(response_type=JSON_RESPONSE, expected=[200]) @@ -211,11 +197,12 @@ def replace_workskill_conditions( # 202402 Metadata - Activity Type Groups @wrap_return( - response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroupList + response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroupListResponse ) def get_activity_type_groups(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypeGroups") - response = requests.get(url, headers=self.headers) + params = {"offset": offset, "limit": limit} + response = requests.get(url, headers=self.headers, params=params) return response @wrap_return(response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroup) @@ -227,3 +214,22 @@ def get_activity_type_group(self, label): ) response = requests.get(url, headers=self.headers) return response + + ## 202402 Activity Type + @wrap_return( + response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeListResponse + ) + def get_activity_types(self, offset=0, limit=100): + url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypes") + params = {"offset": offset, "limit": limit} + response = requests.get(url, headers=self.headers, params=params) + return response + + @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + def get_activity_type(self, label): + encoded_label = urllib.parse.quote_plus(label) + url = urljoin( + self.baseUrl, "/rest/ofscMetadata/v1/activityTypes/{}".format(encoded_label) + ) + response = requests.get(url, headers=self.headers) + return response diff --git a/ofsc/models.py b/ofsc/models.py index 1cef1fc..3a4452a 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -1,6 +1,6 @@ import base64 from enum import Enum -from typing import Any, List, Optional +from typing import Any, Generic, List, Optional, TypeVar from urllib.parse import urljoin import requests @@ -18,6 +18,14 @@ from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, wrap_return +T = TypeVar("T") + + +class OFSResponseList(BaseModel, Generic[T]): + totalResults: int + items: List[T] + hasMore: Annotated[bool, Field(alias="hasMore")] + class OFSConfig(BaseModel): clientID: str @@ -345,8 +353,14 @@ class BulkUpdateResponse(BaseModel): class ActivityTypeGroup(BaseModel): label: str + name: str + _activityTypes: Annotated[Optional[List[dict]], "activityTypes"] = [] translations: TranslationList + @property + def activityTypes(self): + return [_activityType["label"] for _activityType in self._activityTypes] + class ActivityTypeGroupList(RootModel[List[ActivityTypeGroup]]): def __iter__(self): @@ -356,6 +370,10 @@ def __getitem__(self, item): return self.root[item] +class ActivityTypeGroupListResponse(OFSResponseList[ActivityTypeGroup]): + pass + + class ActivityTypeColors(BaseModel): cancelled: Annotated[Optional[str], Field(alias="cancelled")] completed: Annotated[Optional[str], Field(alias="completed")] @@ -422,3 +440,7 @@ def __iter__(self): def __getitem__(self, item): return self.root[item] + + +class ActivityTypeListResponse(OFSResponseList[ActivityType]): + pass diff --git a/tests/metadata/test_activity_groups_types.py b/tests/metadata/test_activity_groups_types.py index c0359cf..b0e1bc1 100644 --- a/tests/metadata/test_activity_groups_types.py +++ b/tests/metadata/test_activity_groups_types.py @@ -7,6 +7,11 @@ from ofsc import OFSC from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from ofsc.models import ( + ActivityType, + ActivityTypeGroup, + ActivityTypeGroupListResponse, + ActivityTypeList, + ActivityTypeListResponse, Condition, Property, SharingEnum, @@ -18,6 +23,18 @@ ) +def test_activity_type_group_model(instance): + instance.core.config.auto_model = True + metadata_response = instance.metadata.get_activity_type_groups( + response_type=JSON_RESPONSE + ) + assert isinstance( + metadata_response, ActivityTypeGroupListResponse + ), f"Response is {type(metadata_response)}" + for item in metadata_response.items: + assert isinstance(item, ActivityTypeGroup) + + def test_get_activity_type_groups(instance, pp, demo_data): expected_activity_type_groups = demo_data.get("metadata").get( "expected_activity_type_groups" @@ -71,6 +88,24 @@ def test_get_activity_types(instance, demo_data, pp): assert activityType["features"]["allowMoveBetweenResources"] == True +def test_get_activity_types_with_model(instance, demo_data, pp): + instance.auto_model = True + expected_activity_types = demo_data.get("metadata").get("expected_activity_types") + raw_response = instance.metadata.get_activity_types(offset=0, limit=30) + logging.debug(pp.pformat(raw_response)) + response = raw_response + assert isinstance(response, ActivityTypeListResponse) + + assert response.items is not None + assert len(response.items) == 30 + assert response.totalResults == expected_activity_types + assert response.items[28].label == "crew_assignment" + assert response.items[12].label == "06" + activityType = response.items[12] + assert activityType.features is not None + assert activityType.features.allowMoveBetweenResources == True + + def test_get_activity_type(instance, demo_data, pp): raw_response = instance.metadata.get_activity_type( "fitness_emergency", response_type=FULL_RESPONSE @@ -84,3 +119,35 @@ def test_get_activity_type(instance, demo_data, pp): assert response["features"] is not None assert len(response["features"]) == 27 assert response["features"]["allowMoveBetweenResources"] == True + + +def test_activity_type_model_list(instance): + instance.core.config.auto_model = False + metadata_response = instance.metadata.get_activity_types( + response_type=JSON_RESPONSE + ) + logging.debug(json.dumps(metadata_response, indent=4)) + objList = ActivityTypeList.model_validate(metadata_response["items"]) + ## Iterate through the list and validate each item + for idx, obj in enumerate(objList): + assert type(obj) == ActivityType + assert obj.label == metadata_response["items"][idx]["label"] + new_obj = ActivityType.model_validate( + instance.metadata.get_activity_type( + label=obj.label, response_type=JSON_RESPONSE + ) + ) + assert new_obj.label == obj.label + + +def test_activity_type_model_simple(instance): + instance.core.config.auto_model = False + metadata_response = instance.metadata.get_activity_type( + label="01", response_type=JSON_RESPONSE + ) + logging.debug(json.dumps(metadata_response, indent=4)) + obj = ActivityType.model_validate(metadata_response) + assert obj.label == metadata_response["label"] + assert obj.translations == TranslationList.model_validate( + metadata_response["translations"] + ) diff --git a/tests/test_base.py b/tests/test_base.py index a4211bd..1452331 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -4,7 +4,11 @@ from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE from ofsc.exceptions import OFSAPIException -from ofsc.models import ActivityTypeGroup, ActivityTypeGroupList +from ofsc.models import ( + ActivityTypeGroup, + ActivityTypeGroupList, + ActivityTypeGroupListResponse, +) def test_wrapper_generic(instance): @@ -53,7 +57,7 @@ def test_wrapper_with_model_list(instance, demo_data): assert raw_response.status_code == 200 json_response = instance.metadata.get_activity_type_groups() - assert isinstance(json_response, ActivityTypeGroupList) + assert isinstance(json_response, ActivityTypeGroupListResponse) def test_wrapper_with_model_single(instance): diff --git a/tests/test_model.py b/tests/test_model.py index 22f9a3b..29236ac 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -11,6 +11,7 @@ ActivityType, ActivityTypeGroup, ActivityTypeGroupList, + ActivityTypeGroupListResponse, ActivityTypeList, Condition, SharingEnum, @@ -58,6 +59,117 @@ def test_translationlist_model_json(): assert json.loads(objList.model_dump_json())[1]["name"] == base[1]["name"] +# Activity Type Groups +def test_activity_type_group_model_base(): + base = { + "label": "customer", + "name": "Customer", + "activityTypes": [ + {"label": "4"}, + {"label": "5"}, + {"label": "6"}, + {"label": "7"}, + {"label": "8"}, + {"label": "installation"}, + {"label": "Testing"}, + {"label": "Multiday"}, + {"label": "SDI"}, + ], + "translations": [ + {"language": "en", "name": "Customer", "languageISO": "en-US"}, + {"language": "es", "name": "Cliente", "languageISO": "es-ES"}, + ], + "links": [ + { + "rel": "canonical", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/activityTypeGroups/customer", + }, + { + "rel": "describedby", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/metadata-catalog/activityTypeGroups", + }, + ], + } + obj = ActivityTypeGroup.model_validate(base) + assert obj.label == base["label"] + + +def test_activity_type_model_base(): + base = { + "label": "6", + "name": "Phone Install/Upgrade", + "active": True, + "groupLabel": "customer", + "defaultDuration": 48, + "timeSlots": [ + {"label": "08-10"}, + {"label": "10-12"}, + {"label": "13-15"}, + {"label": "15-17"}, + {"label": "all-day"}, + ], + "colors": { + "pending": "FFDE00", + "started": "5DBE3F", + "suspended": "99FFFF", + "cancelled": "80FF80", + "notdone": "60CECE", + "notOrdered": "FFCC99", + "warning": "FFAAAA", + "completed": "79B6EB", + }, + "features": { + "isTeamworkAvailable": False, + "isSegmentingEnabled": False, + "allowMoveBetweenResources": False, + "allowCreationInBuckets": False, + "allowReschedule": True, + "supportOfNotOrderedActivities": True, + "allowNonScheduled": True, + "supportOfWorkZones": True, + "supportOfWorkSkills": True, + "supportOfTimeSlots": True, + "supportOfInventory": True, + "supportOfLinks": True, + "supportOfPreferredResources": True, + "allowMassActivities": True, + "allowRepeatingActivities": True, + "calculateTravel": True, + "calculateActivityDurationUsingStatistics": True, + "allowToSearch": True, + "allowToCreateFromIncomingInterface": True, + "enableDayBeforeTrigger": True, + "enableReminderAndChangeTriggers": True, + "enableNotStartedTrigger": True, + "enableSwWarningTrigger": True, + "calculateDeliveryWindow": True, + "slaAndServiceWindowUseCustomerTimeZone": True, + "supportOfRequiredInventory": True, + "disableLocationTracking": False, + }, + "translations": [ + {"language": "en", "name": "Phone Install/Upgrade", "languageISO": "en-US"}, + { + "language": "es", + "name": "Install/Upgrade: Telefono", + "languageISO": "es-ES", + }, + ], + "links": [ + { + "rel": "canonical", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/activityTypes/6", + }, + { + "rel": "describedby", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/metadata-catalog/activityTypes", + }, + ], + } + obj = ActivityType.model_validate(base) + assert obj.label == base["label"] + + def test_workskill_model_base(): base = { "label": "EST", @@ -91,55 +203,3 @@ def test_workskilllist_connected(instance): metadata_response = instance.metadata.get_workskills(response_type=JSON_RESPONSE) logging.debug(json.dumps(metadata_response, indent=4)) objList = WorkskillList.model_validate(metadata_response["items"]) - - -def test_activity_type_group_model(instance): - instance.core.config.auto_model = False - metadata_response = instance.metadata.get_activity_type_groups( - response_type=JSON_RESPONSE - ) - logging.debug(json.dumps(metadata_response, indent=4)) - objList = ActivityTypeGroupList.model_validate(metadata_response["items"]) - ## Iterate through the list and validate each item - for idx, obj in enumerate(objList): - assert type(obj) == ActivityTypeGroup - assert obj.label == metadata_response["items"][idx]["label"] - new_obj = ActivityTypeGroup.model_validate( - instance.metadata.get_activity_type_group( - label=obj.label, response_type=JSON_RESPONSE - ) - ) - assert new_obj.label == obj.label - assert new_obj.translations == obj.translations - - -def test_activity_type_model_list(instance): - instance.core.config.auto_model = False - metadata_response = instance.metadata.get_activity_types( - response_type=JSON_RESPONSE - ) - logging.debug(json.dumps(metadata_response, indent=4)) - objList = ActivityTypeList.model_validate(metadata_response["items"]) - ## Iterate through the list and validate each item - for idx, obj in enumerate(objList): - assert type(obj) == ActivityType - assert obj.label == metadata_response["items"][idx]["label"] - new_obj = ActivityType.model_validate( - instance.metadata.get_activity_type( - label=obj.label, response_type=JSON_RESPONSE - ) - ) - assert new_obj.label == obj.label - - -def test_activity_type_model_simple(instance): - instance.core.config.auto_model = False - metadata_response = instance.metadata.get_activity_type( - label="01", response_type=JSON_RESPONSE - ) - logging.debug(json.dumps(metadata_response, indent=4)) - obj = ActivityType.model_validate(metadata_response) - assert obj.label == metadata_response["label"] - assert obj.translations == TranslationList.model_validate( - metadata_response["translations"] - ) From 8b9a0908e04e069ef12b45bb3d6b493fd2284000 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Wed, 13 Mar 2024 10:23:05 -0400 Subject: [PATCH 11/17] renamed JSON_RESPONSE to OBJ_RESPONSE --- README.md | 7 +-- examples/get_capacity_areas.py | 4 +- examples/get_users_simple.py | 10 ++-- examples/get_work_skill_conditions.py | 10 ++-- examples/get_workzones.py | 10 ++-- ofsc/__init__.py | 2 +- ofsc/common.py | 6 +-- ofsc/core.py | 54 ++++++++++---------- ofsc/metadata.py | 40 +++++++-------- ofsc/models.py | 2 +- ofsc/oauth.py | 4 +- tests/metadata/test_activity_groups_types.py | 32 +++++++----- tests/test_base.py | 8 +-- tests/test_metadata.py | 4 +- tests/test_model.py | 4 +- 15 files changed, 102 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 3006665..a3ba77d 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ A simple Python wrapper for Oracle OFS REST API ## Models -Starting with OFS 1.17 we are adding models for the most common entities. All models should be imported from `ofsc.models`. All existing create functions will be transitioned to models. In OFS 2.0 all functions will use models +Starting with OFS 1.17 we added models for the most common entities and metadata. All models should be imported from `ofsc.models`. All existing create functions will be transitioned to models. In OFS 2.0 all functions will use models The models are based on the Pydantic BaseModel, so it is possible to build an entity using the `model_validate` static methods. Currently implemented: - ActivityTypeGroup +- ActivityType - Property - Workskill - WorkSkillCondition @@ -94,7 +95,7 @@ Experimental: ### Metadata / Resource Types get_resource_types(self, response_type=JSON_RESPONSE): -### Metadata / workzones +### Metadata / Workzones get_workzones(self, response_type=JSON_RESPONSE) ## Test History @@ -110,7 +111,7 @@ OFS REST API Version | PyOFSC ## Deprecation Warning -Starting in OFSC 2.0 all functions have to be called using the API name (Core or Metadata). See the examples. +Starting in OFSC 2.0 all functions are called using the API name (Core or Metadata). See the examples. Instead of diff --git a/examples/get_capacity_areas.py b/examples/get_capacity_areas.py index 2d82075..036b642 100644 --- a/examples/get_capacity_areas.py +++ b/examples/get_capacity_areas.py @@ -4,10 +4,10 @@ import logging import pprint +from config import Config from flatten_dict import flatten -from ofsc import FULL_RESPONSE, JSON_RESPONSE, OFSC -from config import Config +from ofsc import FULL_RESPONSE, OBJ_RESPONSE, OFSC capacityAreasFields = "label,name,type,status,parent.name,parent.label" diff --git a/examples/get_users_simple.py b/examples/get_users_simple.py index 19e5400..0ea2b0d 100755 --- a/examples/get_users_simple.py +++ b/examples/get_users_simple.py @@ -2,11 +2,11 @@ import argparse import logging -import ofsc -from ofsc import FULL_RESPONSE, JSON_RESPONSE, OFSC - from config import Config +import ofsc +from ofsc import FULL_RESPONSE, OBJ_RESPONSE, OFSC + def init_script(): # Parse arguments @@ -38,7 +38,7 @@ def init_script(): def get_users(instance): - response = instance.core.get_users(offset=0, limit=100, response_type=JSON_RESPONSE) + response = instance.core.get_users(offset=0, limit=100, response_type=OBJ_RESPONSE) total_results = response["totalResults"] offset = response["offset"] final_items_list = response["items"] @@ -50,7 +50,7 @@ def get_users(instance): ) offset = offset + 100 response_json = instance.core.get_users( - offset=offset, response_type=JSON_RESPONSE + offset=offset, response_type=OBJ_RESPONSE ) total_results = response_json["totalResults"] items = response_json["items"] diff --git a/examples/get_work_skill_conditions.py b/examples/get_work_skill_conditions.py index f826d2f..372b0ca 100644 --- a/examples/get_work_skill_conditions.py +++ b/examples/get_work_skill_conditions.py @@ -4,12 +4,12 @@ from logging import basicConfig, debug, info, warning from typing import AnyStr, List -import ofsc -from ofsc import FULL_RESPONSE, JSON_RESPONSE, OFSC -from ofsc.models import WorkskillCondition, WorskillConditionList +from config import Config from openpyxl import Workbook -from config import Config +import ofsc +from ofsc import FULL_RESPONSE, OBJ_RESPONSE, OFSC +from ofsc.models import WorkskillCondition, WorskillConditionList def init_script(): @@ -44,7 +44,7 @@ def init_script(): def get_workskill_list(): - response = instance.metadata.get_workskill_conditions(response_type=JSON_RESPONSE) + response = instance.metadata.get_workskill_conditions(response_type=OBJ_RESPONSE) ws_list = WorskillConditionList.parse_obj(response["items"]) return ws_list diff --git a/examples/get_workzones.py b/examples/get_workzones.py index d9c5e6e..88370b5 100644 --- a/examples/get_workzones.py +++ b/examples/get_workzones.py @@ -5,12 +5,12 @@ from logging import basicConfig, debug, info, warning from typing import AnyStr, List -import ofsc -from ofsc import FULL_RESPONSE, JSON_RESPONSE, OFSC -from ofsc.models import Workzone, WorkzoneList +from config import Config from openpyxl import Workbook -from config import Config +import ofsc +from ofsc import FULL_RESPONSE, OBJ_RESPONSE, OFSC +from ofsc.models import Workzone, WorkzoneList def init_script(): @@ -45,7 +45,7 @@ def init_script(): def get_workzone_list(): - response = instance.metadata.get_workzones(response_type=JSON_RESPONSE) + response = instance.metadata.get_workzones(response_type=OBJ_RESPONSE) return WorkzoneList.parse_obj(response["items"]) diff --git a/ofsc/__init__.py b/ofsc/__init__.py index 0820006..d4c33f5 100644 --- a/ofsc/__init__.py +++ b/ofsc/__init__.py @@ -1,6 +1,6 @@ import logging -from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE +from .common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE from .core import OFSCore from .metadata import OFSMetadata from .models import OFSConfig diff --git a/ofsc/common.py b/ofsc/common.py index 7809c59..26de2b8 100644 --- a/ofsc/common.py +++ b/ofsc/common.py @@ -7,7 +7,7 @@ TEXT_RESPONSE = 1 FULL_RESPONSE = 2 -JSON_RESPONSE = 3 +OBJ_RESPONSE = 3 def wrap_return(*decorator_args, **decorator_kwargs): @@ -25,7 +25,7 @@ def wrapper(*func_args, **func_kwargs): config = func_args[0].config # Pre: response_type = func_kwargs.get( - "response_type", decorator_kwargs.get("response_type", FULL_RESPONSE) + "response_type", decorator_kwargs.get("response_type", OBJ_RESPONSE) ) func_kwargs.pop("response_type", None) expected_codes = decorator_kwargs.get("expected_codes", [200]) @@ -38,7 +38,7 @@ def wrapper(*func_args, **func_kwargs): if response_type == FULL_RESPONSE: return response - elif response_type == JSON_RESPONSE: + elif response_type == OBJ_RESPONSE: logging.debug( f"{response_type=}, {config.auto_model=}, {model=} {func_args= } {func_kwargs=}" ) diff --git a/ofsc/core.py b/ofsc/core.py index 7ecc96d..2692e9d 100644 --- a/ofsc/core.py +++ b/ofsc/core.py @@ -6,19 +6,19 @@ import requests -from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE, wrap_return +from .common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE, wrap_return from .models import BulkUpdateRequest, OFSApi, OFSConfig class OFSCore(OFSApi): # OFSC Function Library - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_activities(self, params): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/activities") response = requests.get(url, headers=self.headers, params=params) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_activity(self, activity_id): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/activities/{}".format(activity_id) @@ -26,7 +26,7 @@ def get_activity(self, activity_id): response = requests.get(url, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def update_activity(self, activity_id, data): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/activities/{}".format(activity_id) @@ -35,7 +35,7 @@ def update_activity(self, activity_id, data): return response # 202107 Added ssearch - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def search_activities(self, params): url = urljoin( self.baseUrl, "/rest/ofscCore/v1/activities/custom-actions/search" @@ -43,7 +43,7 @@ def search_activities(self, params): response = requests.get(url, headers=self.headers, params=params) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def move_activity(self, activity_id, data): url = urljoin( self.baseUrl, @@ -52,7 +52,7 @@ def move_activity(self, activity_id, data): response = requests.post(url, headers=self.headers, data=data) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_events(self, params): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/events") response = requests.get( @@ -66,7 +66,7 @@ def get_events(self, params): # RESOURCE MANAGEMENT #### - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_resource( self, resource_id, @@ -105,21 +105,21 @@ def get_resource( return response # 202209 Resource Types - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def create_resource(self, resourceId, data): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/resources/{resourceId}") logging.debug(f"OFSC.Create_Resource: {data} {type(data)}") response = requests.put(url, headers=self.headers, data=data) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def create_resource_from_obj(self, resourceId, data): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/resources/{resourceId}") logging.debug(f"OFSC.Create_Resource: {data} {type(data)}") response = requests.put(url, headers=self.headers, data=json.dumps(data)) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_position_history(self, resource_id, date): url = urljoin( self.baseUrl, @@ -130,7 +130,7 @@ def get_position_history(self, resource_id, date): response = requests.get(url, params=params, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_resource_route( self, resource_id, date, activityFields=None, offset=0, limit=100 ): @@ -144,7 +144,7 @@ def get_resource_route( response = requests.get(url, params=params, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_resource_descendants( self, resource_id, @@ -194,7 +194,7 @@ def get_resource_descendants( return response ## 202104 User Management - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_users(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/users") params = {} @@ -203,20 +203,20 @@ def get_users(self, offset=0, limit=100): response = requests.get(url, params, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_user(self, login): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/users/{}".format(login)) response = requests.get(url, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def update_user(self, login, data): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/users/{}".format(login)) response = requests.patch(url, headers=self.headers, data=data) return response ##202106 - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def create_user(self, login, data): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/users/{login}") response = requests.put(url, headers=self.headers, data=data) @@ -224,21 +224,21 @@ def create_user(self, login, data): ##202106 - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def delete_user(self, login): url = urljoin(self.baseUrl, f"/rest/ofscCore/v1/users/{login}") response = requests.delete(url, headers=self.headers) return response ##202105 Daily Extract - NOT TESTED - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_daily_extract_dates(self): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/folders/dailyExtract/folders/") response = requests.get(url, headers=self.headers) return response ##202105 Daily Extract - NOT TESTED - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_daily_extract_files(self, date): url = urljoin( self.baseUrl, @@ -248,7 +248,7 @@ def get_daily_extract_files(self, date): return response ##202105 Daily Extract - NOT TESTED - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_daily_extract_file(self, date, filename): url = urljoin( self.baseUrl, @@ -331,19 +331,19 @@ def get_all_properties(self, initial_offset=0, limit=100): ### # 1. Subscriptions Management. Using wrapper ### - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_subscriptions(self): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/events/subscriptions") response = requests.get(url, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def create_subscription(self, data): url = urljoin(self.baseUrl, "/rest/ofscCore/v1/events/subscriptions") response = requests.post(url, headers=self.headers, data=data) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[204]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[204]) def delete_subscription(self, subscription_id): url = urljoin( self.baseUrl, f"/rest/ofscCore/v1/events/subscriptions/{subscription_id}" @@ -351,7 +351,7 @@ def delete_subscription(self, subscription_id): response = requests.delete(url, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_subscription_details(self, subscription_id): url = urljoin( self.baseUrl, @@ -364,7 +364,7 @@ def get_subscription_details(self, subscription_id): # 2. Core / Activities ### - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def bulk_update(self, data: BulkUpdateRequest): url = urljoin( self.baseUrl, @@ -373,7 +373,7 @@ def bulk_update(self, data: BulkUpdateRequest): response = requests.post(url, headers=self.headers, data=data.model_dump_json()) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_file_property( self, activityId, diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 2dbd73a..31404f3 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -8,7 +8,7 @@ import requests -from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE, wrap_return +from .common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE, wrap_return from .models import ( ActivityTypeGroup, ActivityTypeGroupList, @@ -39,7 +39,7 @@ class OFSMetadata(OFSApi): ] capacityHeaders = capacityAreasFields.split(",") + additionalCapacityFields - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_capacity_areas( self, expand="parent", @@ -56,7 +56,7 @@ def get_capacity_areas( response = requests.get(url, params=params, headers=self.headers) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_capacity_area(self, label: str): encoded_label = urllib.parse.quote_plus(label) url = urljoin( @@ -67,7 +67,7 @@ def get_capacity_area(self, label: str): ## 202202 Properties and file properties - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_properties(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/properties") params = {"offset": offset, "limit": limit} @@ -79,14 +79,14 @@ def get_properties(self, offset=0, limit=100): return response # 202209 Get Property - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_property(self, label: str): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/properties/{label}") response = requests.get(url, headers=self.headers) return response # 202209 Create Property - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def create_or_replace_property(self, property: Property): url = urljoin( self.baseUrl, f"/rest/ofscMetadata/v1/properties/{property.label}" @@ -97,7 +97,7 @@ def create_or_replace_property(self, property: Property): # 202208 Skill management # 202208 Workzones - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_workzones( self, offset=0, @@ -113,14 +113,14 @@ def get_workzones( return response # 202209 Resource Types - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_resource_types(self): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/resourceTypes") response = requests.get(url, headers=self.headers) return response # 202212 Import plugin - @wrap_return(response_type=JSON_RESPONSE, expected=[204]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[204]) def import_plugin_file(self, plugin: Path): url = urljoin( self.baseUrl, f"/rest/ofscMetadata/v1/plugins/custom-actions/import" @@ -130,7 +130,7 @@ def import_plugin_file(self, plugin: Path): return response # 202212 Import plugin - @wrap_return(response_type=JSON_RESPONSE, expected=[204]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[204]) def import_plugin(self, plugin: str): url = urljoin( self.baseUrl, f"/rest/ofscMetadata/v1/plugins/custom-actions/import" @@ -139,7 +139,7 @@ def import_plugin(self, plugin: str): response = requests.post(url, headers=self.headers, files=files) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_workskills(self, offset=0, limit=100, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/workSkills") params = {"offset": offset, "limit": limit} @@ -150,7 +150,7 @@ def get_workskills(self, offset=0, limit=100, response_type=FULL_RESPONSE): ) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_workskill(self, label: str, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") response = requests.get( @@ -159,20 +159,20 @@ def get_workskill(self, label: str, response_type=FULL_RESPONSE): ) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def create_or_update_workskill(self, skill: Workskill, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{skill.label}") response = requests.put(url, headers=self.headers, data=skill.model_dump_json()) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[204]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[204]) def delete_workskill(self, label: str, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkills/{label}") response = requests.delete(url, headers=self.headers) return response # Workskill conditions - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_workskill_conditions(self, response_type=FULL_RESPONSE): url = urljoin(self.baseUrl, f"/rest/ofscMetadata/v1/workSkillConditions") response = requests.get( @@ -181,7 +181,7 @@ def get_workskill_conditions(self, response_type=FULL_RESPONSE): ) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def replace_workskill_conditions( self, data: WorskillConditionList, response_type=FULL_RESPONSE ): @@ -197,7 +197,7 @@ def replace_workskill_conditions( # 202402 Metadata - Activity Type Groups @wrap_return( - response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroupListResponse + response_type=OBJ_RESPONSE, expected=[200], model=ActivityTypeGroupListResponse ) def get_activity_type_groups(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypeGroups") @@ -205,7 +205,7 @@ def get_activity_type_groups(self, offset=0, limit=100): response = requests.get(url, headers=self.headers, params=params) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeGroup) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200], model=ActivityTypeGroup) def get_activity_type_group(self, label): encoded_label = urllib.parse.quote_plus(label) url = urljoin( @@ -217,7 +217,7 @@ def get_activity_type_group(self, label): ## 202402 Activity Type @wrap_return( - response_type=JSON_RESPONSE, expected=[200], model=ActivityTypeListResponse + response_type=OBJ_RESPONSE, expected=[200], model=ActivityTypeListResponse ) def get_activity_types(self, offset=0, limit=100): url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/activityTypes") @@ -225,7 +225,7 @@ def get_activity_types(self, offset=0, limit=100): response = requests.get(url, headers=self.headers, params=params) return response - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_activity_type(self, label): encoded_label = urllib.parse.quote_plus(label) url = urljoin( diff --git a/ofsc/models.py b/ofsc/models.py index 3a4452a..9fc3cc2 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -16,7 +16,7 @@ from pydantic_settings import BaseSettings from typing_extensions import Annotated -from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, wrap_return +from ofsc.common import FULL_RESPONSE, OBJ_RESPONSE, wrap_return T = TypeVar("T") diff --git a/ofsc/oauth.py b/ofsc/oauth.py index 4d6083a..03b9115 100644 --- a/ofsc/oauth.py +++ b/ofsc/oauth.py @@ -6,12 +6,12 @@ import requests -from .common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE, wrap_return +from .common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE, wrap_return from .models import OFSApi, OFSConfig, OFSOAuthRequest class OFSOauth2(OFSApi): - @wrap_return(response_type=JSON_RESPONSE, expected=[200]) + @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) def get_token( self, params: OFSOAuthRequest = OFSOAuthRequest() ) -> requests.Response: diff --git a/tests/metadata/test_activity_groups_types.py b/tests/metadata/test_activity_groups_types.py index b0e1bc1..2c4d1de 100644 --- a/tests/metadata/test_activity_groups_types.py +++ b/tests/metadata/test_activity_groups_types.py @@ -5,7 +5,7 @@ from requests import Response from ofsc import OFSC -from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE +from ofsc.common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE from ofsc.models import ( ActivityType, ActivityTypeGroup, @@ -26,7 +26,7 @@ def test_activity_type_group_model(instance): instance.core.config.auto_model = True metadata_response = instance.metadata.get_activity_type_groups( - response_type=JSON_RESPONSE + response_type=OBJ_RESPONSE ) assert isinstance( metadata_response, ActivityTypeGroupListResponse @@ -70,7 +70,10 @@ def test_get_activity_type_group(instance, demo_data, pp): assert response["activityTypes"][20]["label"] == "fitness_emergency" -def test_get_activity_types(instance, demo_data, pp): +# Activity Types + + +def test_get_activity_types_auto_model_full(instance, demo_data, pp): expected_activity_types = demo_data.get("metadata").get("expected_activity_types") raw_response = instance.metadata.get_activity_types(response_type=FULL_RESPONSE) logging.debug(pp.pformat(raw_response.json())) @@ -88,16 +91,16 @@ def test_get_activity_types(instance, demo_data, pp): assert activityType["features"]["allowMoveBetweenResources"] == True -def test_get_activity_types_with_model(instance, demo_data, pp): +def test_get_activity_types_auto_model_obj(instance, demo_data, pp): instance.auto_model = True expected_activity_types = demo_data.get("metadata").get("expected_activity_types") - raw_response = instance.metadata.get_activity_types(offset=0, limit=30) - logging.debug(pp.pformat(raw_response)) - response = raw_response + response = instance.metadata.get_activity_types(offset=0, limit=30) + logging.debug(pp.pformat(response)) assert isinstance(response, ActivityTypeListResponse) assert response.items is not None assert len(response.items) == 30 + assert isinstance(response.items[0], ActivityType) assert response.totalResults == expected_activity_types assert response.items[28].label == "crew_assignment" assert response.items[12].label == "06" @@ -106,7 +109,7 @@ def test_get_activity_types_with_model(instance, demo_data, pp): assert activityType.features.allowMoveBetweenResources == True -def test_get_activity_type(instance, demo_data, pp): +def test_get_activity_type_auto_model_full(instance, demo_data, pp): raw_response = instance.metadata.get_activity_type( "fitness_emergency", response_type=FULL_RESPONSE ) @@ -121,11 +124,13 @@ def test_get_activity_type(instance, demo_data, pp): assert response["features"]["allowMoveBetweenResources"] == True -def test_activity_type_model_list(instance): +def test_activity_types_no_model_list(instance): + limit = 10 instance.core.config.auto_model = False metadata_response = instance.metadata.get_activity_types( - response_type=JSON_RESPONSE + response_type=OBJ_RESPONSE, offset=0, limit=limit ) + assert isinstance(metadata_response, dict) logging.debug(json.dumps(metadata_response, indent=4)) objList = ActivityTypeList.model_validate(metadata_response["items"]) ## Iterate through the list and validate each item @@ -134,17 +139,18 @@ def test_activity_type_model_list(instance): assert obj.label == metadata_response["items"][idx]["label"] new_obj = ActivityType.model_validate( instance.metadata.get_activity_type( - label=obj.label, response_type=JSON_RESPONSE + label=obj.label, response_type=OBJ_RESPONSE ) ) assert new_obj.label == obj.label -def test_activity_type_model_simple(instance): +def test_activity_type_no_model_simple(instance): instance.core.config.auto_model = False metadata_response = instance.metadata.get_activity_type( - label="01", response_type=JSON_RESPONSE + label="01", response_type=OBJ_RESPONSE ) + assert isinstance(metadata_response, dict) logging.debug(json.dumps(metadata_response, indent=4)) obj = ActivityType.model_validate(metadata_response) assert obj.label == metadata_response["label"] diff --git a/tests/test_base.py b/tests/test_base.py index 1452331..f5ecbe5 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -2,7 +2,7 @@ import requests -from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE +from ofsc.common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE from ofsc.exceptions import OFSAPIException from ofsc.models import ( ActivityTypeGroup, @@ -17,7 +17,7 @@ def test_wrapper_generic(instance): assert raw_response.status_code == 200 response = raw_response.json() assert "totalResults" in response.keys() - json_response = instance.core.get_subscriptions(response_type=JSON_RESPONSE) + json_response = instance.core.get_subscriptions(response_type=OBJ_RESPONSE) assert isinstance(json_response, dict) assert "totalResults" in json_response.keys() text_response = instance.core.get_subscriptions(response_type=TEXT_RESPONSE) @@ -31,7 +31,7 @@ def test_wrapper_with_error(instance, pp): raw_response = instance.core.get_activity("123456", response_type=FULL_RESPONSE) assert isinstance(raw_response, requests.Response) assert raw_response.status_code == 404 - raw_response = instance.core.get_activity("123456", response_type=JSON_RESPONSE) + raw_response = instance.core.get_activity("123456", response_type=OBJ_RESPONSE) assert isinstance(raw_response, dict) assert raw_response["status"] == "404" instance.core.config.auto_raise = True @@ -41,7 +41,7 @@ def test_wrapper_with_error(instance, pp): # Validate that the next line raises an exception try: - instance.core.get_activity("123456", response_type=JSON_RESPONSE) + instance.core.get_activity("123456", response_type=OBJ_RESPONSE) except Exception as e: assert isinstance(e, OFSAPIException) # log exception fields diff --git a/tests/test_metadata.py b/tests/test_metadata.py index d7121d7..1267db3 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -5,7 +5,7 @@ from requests import Response from ofsc import OFSC -from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE +from ofsc.common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE from ofsc.models import ( Condition, Property, @@ -88,7 +88,7 @@ def test_get_workskill_conditions(instance, pp, demo_data): def test_replace_workskill_conditions(instance, pp, demo_data): - response = instance.metadata.get_workskill_conditions(response_type=JSON_RESPONSE) + response = instance.metadata.get_workskill_conditions(response_type=OBJ_RESPONSE) expected_workskill_conditions = demo_data.get("metadata").get( "expected_workskill_conditions" ) diff --git a/tests/test_model.py b/tests/test_model.py index 29236ac..ac76da6 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -6,7 +6,7 @@ from pydantic import ValidationError from requests import Response -from ofsc.common import FULL_RESPONSE, JSON_RESPONSE, TEXT_RESPONSE +from ofsc.common import FULL_RESPONSE, OBJ_RESPONSE, TEXT_RESPONSE from ofsc.models import ( ActivityType, ActivityTypeGroup, @@ -200,6 +200,6 @@ def test_workskill_model_base(): def test_workskilllist_connected(instance): - metadata_response = instance.metadata.get_workskills(response_type=JSON_RESPONSE) + metadata_response = instance.metadata.get_workskills(response_type=OBJ_RESPONSE) logging.debug(json.dumps(metadata_response, indent=4)) objList = WorkskillList.model_validate(metadata_response["items"]) From 8a1b05d2112b7313d3d1350d5a42a3023979da1e Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Thu, 14 Mar 2024 10:51:15 -0400 Subject: [PATCH 12/17] capacity areas list finished --- README.md | 3 +- ofsc/common.py | 1 + ofsc/metadata.py | 87 ++++++++++--------- ofsc/models.py | 66 ++++++++++++++- tests/conftest.py | 28 ++++++- tests/metadata/test_capacity_areas.py | 69 ++++++++++++++- tests/test_model.py | 116 +++++++++++++++++++++++++- 7 files changed, 323 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index a3ba77d..bf8f4d5 100644 --- a/README.md +++ b/README.md @@ -130,4 +130,5 @@ During the transition period a DeprecationWarning will be raised if the function - All metadata functions now use models, when available - All functions are now using the API name (Core or Metadata) - All functions return a python object by default. If there is an available model it will be used, otherwise a dict will be returned (see `response_type` parameter and `auto_model` parameter) -- Errors during API calls can raise exceptions and will by default when returning an object (see `auto_raise` parameter) \ No newline at end of file +- Errors during API calls can raise exceptions and will by default when returning an object (see `auto_raise` parameter) +- JSON_RESPONSE and TEXT_RESPONSE are now deprecated. Use `response_type` parameter to control the response type \ No newline at end of file diff --git a/ofsc/common.py b/ofsc/common.py index 26de2b8..7405113 100644 --- a/ofsc/common.py +++ b/ofsc/common.py @@ -57,6 +57,7 @@ def wrapper(*func_args, **func_kwargs): return response.json() # Check if response.statyus code is between 400 and 499 if 400 <= response.status_code < 500: + logging.error(response.json()) raise OFSAPIException(**response.json()) elif 500 <= response.status_code < 600: raise OFSAPIException(**response.json()) diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 31404f3..b54173e 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -14,6 +14,8 @@ ActivityTypeGroupList, ActivityTypeGroupListResponse, ActivityTypeListResponse, + CapacityArea, + CapacityAreaListResponse, OFSApi, OFSConfig, Property, @@ -25,46 +27,6 @@ class OFSMetadata(OFSApi): - capacityAreasFields = "label,name,type,status,parent.name,parent.label" - additionalCapacityFields = [ - "parentLabel", - "configuration.isTimeSlotBase", - "configuration.byCapacityCategory", - "configuration.byDay", - "configuration.byTimeSlot", - "configuration.isAllowCloseOnWorkzoneLevel", - "configuration.definitionLevel.day", - "configuration.definitionLevel.timeSlot", - "configuration.definitionLevel.capacityCategory", - ] - capacityHeaders = capacityAreasFields.split(",") + additionalCapacityFields - - @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) - def get_capacity_areas( - self, - expand="parent", - fields=capacityAreasFields, - status="active", - queryType="area", - ): - url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/capacityAreas") - params = {} - params["expand"] = expand - params["fields"] = fields - params["status"] = status - params["type"] = queryType - response = requests.get(url, params=params, headers=self.headers) - return response - - @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) - def get_capacity_area(self, label: str): - encoded_label = urllib.parse.quote_plus(label) - url = urljoin( - self.baseUrl, "/rest/ofscMetadata/v1/capacityAreas/{}".format(encoded_label) - ) - response = requests.get(url, headers=self.headers) - return response - ## 202202 Properties and file properties @wrap_return(response_type=OBJ_RESPONSE, expected=[200]) @@ -233,3 +195,48 @@ def get_activity_type(self, label): ) response = requests.get(url, headers=self.headers) return response + + # region Capacity Areas + capacityAreasFields = [ + "label", + "name", + "type", + "status", + "parent.name", + "parent.label", + ] + + @wrap_return( + response_type=OBJ_RESPONSE, expected=[200], model=CapacityAreaListResponse + ) + def get_capacity_areas( + self, + expandParent: bool = False, + fields: list[str] = ["label"], + activeOnly: bool = False, + areasOnly: bool = False, + ): + url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/capacityAreas") + assert isinstance(fields, list) + params = { + "expand": None if not expandParent else "parent", + "fields": ( + ",".join(fields) if fields else ",".join(self.capacityAreasFields) + ), + "status": None if not activeOnly else "active", + "type": None if not areasOnly else "area", + } + logging.warning(f"{params=}") + response = requests.get(url, params=params, headers=self.headers) + return response + + @wrap_return(response_type=OBJ_RESPONSE, expected=[200], model=CapacityArea) + def get_capacity_area(self, label: str): + encoded_label = urllib.parse.quote_plus(label) + url = urljoin( + self.baseUrl, f"/rest/ofscMetadata/v1/capacityAreas/{encoded_label}" + ) + response = requests.get(url, headers=self.headers) + return response + + # endregion diff --git a/ofsc/models.py b/ofsc/models.py index 9fc3cc2..6320277 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -1,4 +1,5 @@ import base64 +import logging from enum import Enum from typing import Any, Generic, List, Optional, TypeVar from urllib.parse import urljoin @@ -12,6 +13,7 @@ RootModel, ValidationInfo, field_validator, + model_validator, ) from pydantic_settings import BaseSettings from typing_extensions import Annotated @@ -22,9 +24,16 @@ class OFSResponseList(BaseModel, Generic[T]): - totalResults: int + items: List[T] - hasMore: Annotated[bool, Field(alias="hasMore")] + hasMore: Annotated[Optional[bool], Field(alias="hasMore")] = False + totalResults: int = -1 + + @model_validator(mode="after") + def check_coherence(self): + if self.totalResults != len(self.items) and self.hasMore is False: + self.totalResults = len(self.items) + return self class OFSConfig(BaseModel): @@ -351,6 +360,9 @@ class BulkUpdateResponse(BaseModel): results: Optional[List[BulkUpdateResult]] = None +# region Activity Type Groups + + class ActivityTypeGroup(BaseModel): label: str name: str @@ -374,6 +386,11 @@ class ActivityTypeGroupListResponse(OFSResponseList[ActivityTypeGroup]): pass +# endregion + +# region Activity Types + + class ActivityTypeColors(BaseModel): cancelled: Annotated[Optional[str], Field(alias="cancelled")] completed: Annotated[Optional[str], Field(alias="completed")] @@ -444,3 +461,48 @@ def __getitem__(self, item): class ActivityTypeListResponse(OFSResponseList[ActivityType]): pass + + +# endregion + +# region Capacity Areas + + +class CapacityAreaParent(BaseModel): + label: str + name: Optional[str] = None + + +class CapacityAreaConfiguration(BaseModel): + isTimeSlotBase: bool + byCapacityCategory: str + byDay: str + byTimeSlot: str + isAllowCloseOnWorkzoneLevel: bool + definitionLevel: List[str] + + +class CapacityArea(BaseModel): + label: str + name: Optional[str] = None + type: Optional[str] = "area" + status: Optional[str] = "active" + configuration: CapacityAreaConfiguration = None + parentLabel: Optional[str] = None + parent: Annotated[Optional[CapacityAreaParent], Field(alias="parent")] = None + status: str + translations: Annotated[Optional[TranslationList], Field(alias="translations")] = ( + None + ) + + +class CapacityAreaList(RootModel[List[CapacityArea]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] + + +class CapacityAreaListResponse(OFSResponseList[CapacityArea]): + pass diff --git a/tests/conftest.py b/tests/conftest.py index 2d48d9e..3deecd9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,7 +101,33 @@ def demo_data(): "expected_activity_type_groups": 5, "expected_activity_types": 35, "expected_activity_types_customer": 25, - "expected_capacity_areas": ["CAUSA", "FLUSA", "South Florida"], + "expected_capacity_areas": [ + { + "label": "CAUSA", + "status": "active", + "type": "area", + "parentLabel": "SUNRISE", + }, + { + "label": "FLUSA", + "status": "active", + "type": "area", + "parentLabel": "SUNRISE", + }, + { + "label": "South Florida", + "status": "active", + "type": "area", + "parentLabel": "FLUSA", + }, + {"label": "SUNRISE", "status": "active", "type": "group"}, + { + "label": "routing_old", + "status": "inactive", + "type": "area", + "parentLabel": "FLUSA", + }, + ], }, "get_file_property": { "activity_id": 3954799, # Note: manual addition diff --git a/tests/metadata/test_capacity_areas.py b/tests/metadata/test_capacity_areas.py index e46e17b..c2e045f 100644 --- a/tests/metadata/test_capacity_areas.py +++ b/tests/metadata/test_capacity_areas.py @@ -3,10 +3,11 @@ import pytest from ofsc.common import FULL_RESPONSE +from ofsc.models import CapacityAreaListResponse # Capacity tests -def test_get_capacity_areas_simple(instance, pp, demo_data): +def test_get_capacity_areas_no_model_simple(instance, pp, demo_data): capacity_areas = demo_data.get("metadata").get("expected_capacity_areas") raw_response = instance.metadata.get_capacity_areas(response_type=FULL_RESPONSE) assert raw_response.status_code == 200 @@ -14,10 +15,74 @@ def test_get_capacity_areas_simple(instance, pp, demo_data): response = raw_response.json() logging.debug(pp.pformat(response)) assert response["items"] is not None - assert len(response["items"]) == len(capacity_areas) + assert len(response["items"]) == len( + capacity_areas + ), f"Received {[i['label'] for i in response['items']]}" assert response["items"][0]["label"] == "CAUSA" +def test_get_capacity_areas_model_no_parameters(instance, pp, demo_data): + capacity_areas = demo_data.get("metadata").get("expected_capacity_areas") + metadata_response = instance.metadata.get_capacity_areas() + assert isinstance( + metadata_response, CapacityAreaListResponse + ), f"Expected a CapacityAreaListResponse received {type(metadata_response)}" + assert len(metadata_response.items) == len(capacity_areas) + assert metadata_response.hasMore is False + assert metadata_response.totalResults == len(capacity_areas) + + +def test_get_capacity_areas_model_with_parameters(instance, pp, demo_data): + capacity_areas = demo_data.get("metadata").get("expected_capacity_areas") + metadata_response = instance.metadata.get_capacity_areas( + activeOnly=False, + areasOnly=True, + expandParent=True, + fields=["label", "status", "parent.label"], + ) + assert isinstance( + metadata_response, CapacityAreaListResponse + ), f"Expected a CapacityAreaListResponse received {type(metadata_response)}" + expected_result = len([area for area in capacity_areas if area["type"] == "area"]) + assert len(metadata_response.items) == expected_result + assert metadata_response.hasMore is False + assert metadata_response.totalResults == expected_result + + metadata_response = instance.metadata.get_capacity_areas( + activeOnly=False, + areasOnly=True, + expandParent=False, + fields=["label", "status", "parent.label"], + ) + assert isinstance( + metadata_response, CapacityAreaListResponse + ), f"Expected a CapacityAreaListResponse received {type(metadata_response)}" + expected_result = len([area for area in capacity_areas if area["type"] == "area"]) + assert len(metadata_response.items) == expected_result + assert metadata_response.hasMore is False + assert metadata_response.totalResults == expected_result + + metadata_response = instance.metadata.get_capacity_areas( + activeOnly=True, + areasOnly=True, + expandParent=False, + fields=["label", "status", "parent.label"], + ) + assert isinstance( + metadata_response, CapacityAreaListResponse + ), f"Expected a CapacityAreaListResponse received {type(metadata_response)}" + expected_result = len( + [ + area + for area in capacity_areas + if (area["type"] == "area" and area["status"] == "active") + ] + ) + assert len(metadata_response.items) == expected_result + assert metadata_response.hasMore is False + assert metadata_response.totalResults == expected_result + + def test_get_capacity_area(instance, pp): raw_response = instance.metadata.get_capacity_area( "FLUSA", response_type=FULL_RESPONSE diff --git a/tests/test_model.py b/tests/test_model.py index ac76da6..9e847b3 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -13,6 +13,8 @@ ActivityTypeGroupList, ActivityTypeGroupListResponse, ActivityTypeList, + CapacityArea, + CapacityAreaListResponse, Condition, SharingEnum, Translation, @@ -59,7 +61,7 @@ def test_translationlist_model_json(): assert json.loads(objList.model_dump_json())[1]["name"] == base[1]["name"] -# Activity Type Groups +# region Activity Type Groups def test_activity_type_group_model_base(): base = { "label": "customer", @@ -170,6 +172,118 @@ def test_activity_type_model_base(): assert obj.label == base["label"] +# endregion + +# region Capacity Areas + + +def test_capacity_area_model_base(): + base = { + "label": "CapacityArea", + "name": "Capacity Area", + "type": "area", + "status": "active", + "workZones": { + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityAreas/CapacityArea/workZones" + }, + "organizations": { + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityAreas/CapacityArea/organizations" + }, + "capacityCategories": { + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityAreas/CapacityArea/capacityCategories" + }, + "timeIntervals": { + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityAreas/CapacityArea/timeIntervals" + }, + "timeSlots": { + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityAreas/CapacityArea/timeSlots" + }, + "parentLabel": "66000", + "configuration": { + "definitionLevel": ["day"], + "isAllowCloseOnWorkzoneLevel": False, + "byDay": "percentIncludeOtherActivities", + "byCapacityCategory": "minutes", + "byTimeSlot": "minutes", + "isTimeSlotBase": False, + }, + } + obj = CapacityArea.model_validate(base) + assert obj.label == base["label"] + + +def test_capacity_area_list_model_base(): + base = { + "items": [ + { + "label": "22", + "name": "Sunrise Enterprise", + "type": "group", + "status": "active", + }, + { + "label": "ASIA", + "name": "Asia", + "type": "area", + "status": "active", + "parent": {"label": "22"}, + }, + { + "label": "EUROPE", + "name": "Europe", + "type": "area", + "status": "active", + "parent": {"label": "22"}, + }, + { + "label": "66000", + "name": "Newfoundland", + "type": "group", + "status": "active", + "parent": {"label": "22"}, + }, + { + "label": "CapacityArea", + "name": "Capacity Area", + "type": "area", + "status": "active", + "parent": {"label": "66000"}, + }, + { + "label": "routing", + "name": "Planning", + "type": "area", + "status": "active", + "parent": {"label": "22"}, + }, + { + "label": "S??o Jos??", + "name": "S??o Jos?? dos Campos", + "type": "area", + "status": "active", + "parent": {"label": "22"}, + }, + { + "label": "Texasin", + "name": "Texas inventories", + "type": "group", + "status": "active", + "parent": {"label": "22"}, + }, + { + "label": "routing_bucket_T", + "name": "Texas City", + "type": "area", + "status": "active", + "parent": {"label": "Texasin"}, + }, + ] + } + + obj = CapacityAreaListResponse.model_validate(base) + + +# endregion def test_workskill_model_base(): base = { "label": "EST", From acd0970b183031796a2dcbe231c811691bada5df Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Thu, 14 Mar 2024 11:10:30 -0400 Subject: [PATCH 13/17] capacity areas finished --- ofsc/metadata.py | 1 - ofsc/models.py | 1 + tests/metadata/test_capacity_areas.py | 12 +++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ofsc/metadata.py b/ofsc/metadata.py index b54173e..6df2d03 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -226,7 +226,6 @@ def get_capacity_areas( "status": None if not activeOnly else "active", "type": None if not areasOnly else "area", } - logging.warning(f"{params=}") response = requests.get(url, params=params, headers=self.headers) return response diff --git a/ofsc/models.py b/ofsc/models.py index 6320277..56cc61e 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -494,6 +494,7 @@ class CapacityArea(BaseModel): translations: Annotated[Optional[TranslationList], Field(alias="translations")] = ( None ) + # Note: as of 24A the additional fields returned are just HREFs so we won't include them here class CapacityAreaList(RootModel[List[CapacityArea]]): diff --git a/tests/metadata/test_capacity_areas.py b/tests/metadata/test_capacity_areas.py index c2e045f..5aeba2c 100644 --- a/tests/metadata/test_capacity_areas.py +++ b/tests/metadata/test_capacity_areas.py @@ -83,7 +83,7 @@ def test_get_capacity_areas_model_with_parameters(instance, pp, demo_data): assert metadata_response.totalResults == expected_result -def test_get_capacity_area(instance, pp): +def test_get_capacity_area_no_model(instance, pp): raw_response = instance.metadata.get_capacity_area( "FLUSA", response_type=FULL_RESPONSE ) @@ -96,3 +96,13 @@ def test_get_capacity_area(instance, pp): assert response["configuration"] is not None assert response["parentLabel"] is not None assert response["parentLabel"] == "SUNRISE" + + +def test_get_capacity_area_model(instance, pp, demo_data): + metadata_response = instance.metadata.get_capacity_area("FLUSA") + assert metadata_response.label == "FLUSA" + assert metadata_response.configuration is not None + assert metadata_response.parentLabel is not None + assert metadata_response.parentLabel == "SUNRISE" + assert metadata_response.status == "active" + assert metadata_response.type == "area" From 72839c83f62471d3ef709eb5c048ec1ba98a6b61 Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Wed, 20 Mar 2024 08:04:49 -0400 Subject: [PATCH 14/17] Added capacity catgegories (read-only) --- ofsc/metadata.py | 24 ++++++ ofsc/models.py | 42 +++++++++ tests/conftest.py | 5 ++ tests/metadata/test_capacity_categories.py | 28 ++++++ tests/test_model.py | 99 ++++++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 tests/metadata/test_capacity_categories.py diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 6df2d03..4c58def 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -16,6 +16,8 @@ ActivityTypeListResponse, CapacityArea, CapacityAreaListResponse, + CapacityCategory, + CapacityCategoryListResponse, OFSApi, OFSConfig, Property, @@ -239,3 +241,25 @@ def get_capacity_area(self, label: str): return response # endregion + + # region 202402 Metadata - Capacity Categories + @wrap_return( + response_type=OBJ_RESPONSE, expected=[200], model=CapacityCategoryListResponse + ) + def get_capacity_categories(self, offset=0, limit=100): + url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/capacityCategories") + params = {"offset": offset, "limit": limit} + response = requests.get(url, headers=self.headers, params=params) + return response + + @wrap_return(response_type=OBJ_RESPONSE, expected=[200], model=CapacityCategory) + def get_capacity_category(self, label: str): + encoded_label = urllib.parse.quote_plus(label) + url = urljoin( + self.baseUrl, f"/rest/ofscMetadata/v1/capacityCategories/{encoded_label}" + ) + response = requests.get(url, headers=self.headers) + return response + + +# endregion diff --git a/ofsc/models.py b/ofsc/models.py index 56cc61e..2ceddbf 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -24,8 +24,11 @@ class OFSResponseList(BaseModel, Generic[T]): + model_config = ConfigDict(extra="allow") items: List[T] + offset: Annotated[Optional[int], Field(alias="offset")] = None + limit: Annotated[Optional[int], Field(alias="limit")] = None hasMore: Annotated[Optional[bool], Field(alias="hasMore")] = False totalResults: int = -1 @@ -507,3 +510,42 @@ def __getitem__(self, item): class CapacityAreaListResponse(OFSResponseList[CapacityArea]): pass + + +# endregion +# region 202403 Capacity Categories +class Item(BaseModel): + label: str + name: Optional[str] = None + + +class ItemList(RootModel[List[Item]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] + + +class CapacityCategory(BaseModel): + label: str + name: str + timeSlots: Optional[ItemList] = None + translations: Annotated[Optional[TranslationList], Field(alias="translations")] = ( + None + ) + workSkillGroups: Optional[ItemList] = None + workSkills: Optional[ItemList] = None + active: bool + model_config = ConfigDict(extra="allow") + + +class CapacityCategoryListResponse(OFSResponseList[CapacityCategory]): + pass + + +# endregion +# region 202404 Metadata - Time Slots +# endregion +# region 202404 Metadata - Workzones +# endregion diff --git a/tests/conftest.py b/tests/conftest.py index 3deecd9..e122d30 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,6 +128,11 @@ def demo_data(): "parentLabel": "FLUSA", }, ], + "expected_capacity_categories": { + "EST": {"label": "EST", "name": "Estimate"}, + "RES": {"label": "RES", "name": "Residential"}, + "COM": {"label": "COM", "name": "Commercial"}, + }, }, "get_file_property": { "activity_id": 3954799, # Note: manual addition diff --git a/tests/metadata/test_capacity_categories.py b/tests/metadata/test_capacity_categories.py new file mode 100644 index 0000000..f4b6b02 --- /dev/null +++ b/tests/metadata/test_capacity_categories.py @@ -0,0 +1,28 @@ +from ofsc.models import CapacityCategory, CapacityCategoryListResponse + + +def test_get_capacity_categories_model_no_parameters(instance, pp, demo_data): + capacity_categories = demo_data.get("metadata").get("expected_capacity_categories") + metadata_response = instance.metadata.get_capacity_categories() + assert isinstance( + metadata_response, CapacityCategoryListResponse + ), f"Expected a CapacityCategoryListResponse received {type(metadata_response)}" + assert len(metadata_response.items) == len( + capacity_categories.keys() + ), f"Expected {len(capacity_categories.keys())} received {metadata_response.totalResults}" + assert metadata_response.hasMore is False + assert metadata_response.totalResults == len(capacity_categories.keys()) + for category in metadata_response.items: + assert isinstance(category, CapacityCategory) + assert category.label in capacity_categories.keys() + + +def test_get_capacity_category(instance, pp, demo_data): + capacity_categories = demo_data.get("metadata").get("expected_capacity_categories") + for category in capacity_categories.keys(): + metadata_response = instance.metadata.get_capacity_category(category) + assert isinstance( + metadata_response, CapacityCategory + ), f"Expected a CapacityCategory received {type(metadata_response)}" + assert metadata_response.label == category + assert metadata_response.name == capacity_categories[category].get("name") diff --git a/tests/test_model.py b/tests/test_model.py index 9e847b3..61c07f1 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -15,7 +15,10 @@ ActivityTypeList, CapacityArea, CapacityAreaListResponse, + CapacityCategory, + CapacityCategoryListResponse, Condition, + ItemList, SharingEnum, Translation, TranslationList, @@ -284,6 +287,7 @@ def test_capacity_area_list_model_base(): # endregion +# region Workskills def test_workskill_model_base(): base = { "label": "EST", @@ -317,3 +321,98 @@ def test_workskilllist_connected(instance): metadata_response = instance.metadata.get_workskills(response_type=OBJ_RESPONSE) logging.debug(json.dumps(metadata_response, indent=4)) objList = WorkskillList.model_validate(metadata_response["items"]) + + +# endregion +# region Capacity Categories +capacityCategoryList = { + "hasMore": True, + "totalResults": 8, + "limit": 1, + "offset": 2, + "items": [ + { + "label": "UP", + "name": "Upgrade", + "active": True, + "workSkills": [{"label": "UP", "ratio": 1, "startDate": "2000-01-01"}], + "workSkillGroups": [], + "timeSlots": [ + {"label": "08-10"}, + {"label": "10-12"}, + {"label": "13-15"}, + {"label": "15-17"}, + ], + "translations": [ + {"language": "en", "name": "Upgrade", "languageISO": "en-US"}, + {"language": "es", "name": "Upgrade", "languageISO": "es-ES"}, + {"language": "fr", "name": "Upgrade", "languageISO": "fr-FR"}, + {"language": "nl", "name": "Upgrade", "languageISO": "nl-NL"}, + {"language": "de", "name": "Upgrade", "languageISO": "de-DE"}, + {"language": "ro", "name": "Upgrade", "languageISO": "ro-RO"}, + { + "language": "ru", + "name": "????????????????????????", + "languageISO": "ru-RU", + }, + {"language": "br", "name": "Upgrade", "languageISO": "pt-BR"}, + ], + "links": [ + { + "rel": "canonical", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityCategories/UP", + }, + { + "rel": "describedby", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/metadata-catalog/capacityCategories", + }, + ], + } + ], + "links": [ + { + "rel": "canonical", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityCategories?limit=1&offset=2", + }, + { + "rel": "prev", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityCategories?offset=1", + }, + { + "rel": "next", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/capacityCategories?offset=3", + }, + { + "rel": "describedby", + "href": "https://.fs.ocs.oraclecloud.com/rest/ofscMetadata/v1/metadata-catalog/capacityCategories", + }, + ], +} + + +def test_capacity_category_model_list(): + objList = CapacityCategoryListResponse.model_validate(capacityCategoryList) + assert objList.hasMore == capacityCategoryList["hasMore"] + assert objList.totalResults == capacityCategoryList["totalResults"] + assert objList.limit == capacityCategoryList["limit"] + assert objList.offset == capacityCategoryList["offset"] + assert len(objList.items) == len(capacityCategoryList["items"]) + assert objList.links == capacityCategoryList["links"] + for idx, item in enumerate(objList.items): + assert item.label == capacityCategoryList["items"][idx]["label"] + assert item.name == capacityCategoryList["items"][idx]["name"] + assert item.active == capacityCategoryList["items"][idx]["active"] + assert item.timeSlots == ItemList.model_validate( + capacityCategoryList["items"][idx]["timeSlots"] + ) + assert item.translations == TranslationList.model_validate( + capacityCategoryList["items"][idx]["translations"] + ) + assert item.links == capacityCategoryList["items"][idx]["links"] + # assert item.workSkills == capacityCategoryList["items"][idx]["workSkills"] + assert item.workSkillGroups == ItemList.model_validate( + capacityCategoryList["items"][idx]["workSkillGroups"] + ) + + +# endregion From c18f3cfcc607600e22751ebf3c20236ff67409af Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Wed, 15 May 2024 12:59:07 -0400 Subject: [PATCH 15/17] Added inventory types --- README.md | 5 + ofsc/metadata.py | 27 +- ofsc/models.py | 31 ++ poetry.lock | 449 +++++++++++++------------ pyproject.toml | 4 +- tests/conftest.py | 7 + tests/metadata/test_inventory_types.py | 40 +++ tests/metadata/test_properties.py | 6 +- tests/test_model.py | 10 + 9 files changed, 356 insertions(+), 223 deletions(-) create mode 100644 tests/metadata/test_inventory_types.py diff --git a/README.md b/README.md index bf8f4d5..2aa5662 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,11 @@ Experimental: get_capacity_areas (self, expand="parent", fields=capacityAreasFields, status="active", queryType="area", response_type=JSON_RESPONSE) get_capacity_area (self,label, response_type=JSON_RESPONSE) +### Metadata / Inventory + get_inventory_types (self, offset=0, limit=100, response_type=JSON_RESPONSE) + get_inventory_type (self, label, response_type=JSON_RESPONSE) + create_or_replace_inventory_type(self, inventory: InventoryType, response_type=JSON_RESPONSE) + ### Metadata / Properties get_properties (self, offset=0, limit=100, response_type=JSON_RESPONSE) get_property(self, label: str, response_type=JSON_RESPONSE) diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 4c58def..6b1f803 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -18,6 +18,8 @@ CapacityAreaListResponse, CapacityCategory, CapacityCategoryListResponse, + InventoryType, + InventoryTypeListResponse, OFSApi, OFSConfig, Property, @@ -55,7 +57,9 @@ def create_or_replace_property(self, property: Property): url = urljoin( self.baseUrl, f"/rest/ofscMetadata/v1/properties/{property.label}" ) - response = requests.put(url, headers=self.headers, data=property.json()) + response = requests.put( + url, headers=self.headers, data=property.model_dump_json() + ) return response # 202208 Skill management @@ -261,5 +265,24 @@ def get_capacity_category(self, label: str): response = requests.get(url, headers=self.headers) return response + # endregion + + # region 202405 Inventory Types + @wrap_return( + response_type=OBJ_RESPONSE, expected=[200], model=InventoryTypeListResponse + ) + def get_inventory_types(self): + url = urljoin(self.baseUrl, "/rest/ofscMetadata/v1/inventoryTypes") + response = requests.get(url, headers=self.headers) + return response + + @wrap_return(response_type=OBJ_RESPONSE, expected=[200], model=InventoryType) + def get_inventory_type(self, label: str): + encoded_label = urllib.parse.quote_plus(label) + url = urljoin( + self.baseUrl, f"/rest/ofscMetadata/v1/inventoryTypes/{encoded_label}" + ) + response = requests.get(url, headers=self.headers) + return response -# endregion + # endregion diff --git a/ofsc/models.py b/ofsc/models.py index 2ceddbf..c8f4697 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -161,6 +161,9 @@ def __iter__(self): def __getitem__(self, item): return self.root[item] + def map(self): + return {translation.language: translation for translation in self.root} + class Workskill(BaseModel): label: str @@ -545,6 +548,34 @@ class CapacityCategoryListResponse(OFSResponseList[CapacityCategory]): # endregion + +# region 202405 Inventory Types + + +class InventoryType(BaseModel): + label: str + translations: Annotated[Optional[TranslationList], Field(alias="translations")] = ( + None + ) + active: bool = True + model_property: Optional[str] = None + non_serialized: bool = False + quantityPrecision: Optional[int] = 0 + model_config = ConfigDict(extra="allow") + + +class InventoryTypeList(RootModel[List[InventoryType]]): + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] + + +class InventoryTypeListResponse(OFSResponseList[InventoryType]): + pass + + # region 202404 Metadata - Time Slots # endregion # region 202404 Metadata - Workzones diff --git a/poetry.lock b/poetry.lock index 72140ee..f43c2eb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -11,78 +11,136 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] - -[[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.5" -files = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] - -[package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] - [[package]] name = "cachetools" -version = "5.3.1" +version = "5.3.3" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, ] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[package.extras] -unicode-backport = ["unicodedata2"] - [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] @@ -98,13 +156,13 @@ files = [ [[package]] name = "faker" -version = "14.2.0" +version = "14.2.1" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.6" files = [ - {file = "Faker-14.2.0-py3-none-any.whl", hash = "sha256:e02c55a5b0586caaf913cc6c254b3de178e08b031c5922e590fd033ebbdbfd02"}, - {file = "Faker-14.2.0.tar.gz", hash = "sha256:6db56e2c43a2b74250d1c332ef25fef7dc07dcb6c5fab5329dd7b4467b8ed7b9"}, + {file = "Faker-14.2.1-py3-none-any.whl", hash = "sha256:2e28aaea60456857d4ce95dd12aed767769537ad23d13d51a545cd40a654e9d9"}, + {file = "Faker-14.2.1.tar.gz", hash = "sha256:daad7badb4fd916bd047b28c8459ef4689e4fe6acf61f6dfebee8cc602e4d009"}, ] [package.dependencies] @@ -112,35 +170,35 @@ python-dateutil = ">=2.4" [[package]] name = "idna" -version = "3.3" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "openpyxl" -version = "3.0.10" +version = "3.1.2" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" optional = false python-versions = ">=3.6" files = [ - {file = "openpyxl-3.0.10-py2.py3-none-any.whl", hash = "sha256:0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355"}, - {file = "openpyxl-3.0.10.tar.gz", hash = "sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449"}, + {file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"}, + {file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"}, ] [package.dependencies] @@ -148,69 +206,54 @@ et-xmlfile = "*" [[package]] name = "packaging" -version = "21.3" +version = "24.0" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - [[package]] name = "pluggy" -version = "1.0.0" +version = "0.13.1" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] [[package]] name = "pyarmor" -version = "7.6.1" +version = "7.7.4" description = "A tool used to obfuscate python scripts, bind obfuscated scripts to fixed machine or expire obfuscated scripts." optional = false python-versions = "*" files = [ - {file = "pyarmor-7.6.1-py2.py3-none-any.whl", hash = "sha256:d637538cba2e4b85795e34dd63403932a176cd10bfe4401f6109ff9aafa36455"}, - {file = "pyarmor-7.6.1.zip", hash = "sha256:ea78a13a936496d124701ae2d8a9fea5f5fb90f2aa0fa7bacb9e890994ee594e"}, + {file = "pyarmor-7.7.4-py2.py3-none-any.whl", hash = "sha256:e29e2b05683919ee72a62adb602e21fc0f933f01d57aade6d7e98d9b7563b088"}, + {file = "pyarmor-7.7.4.zip", hash = "sha256:8a78756be546e7174f631cbfe248cd096218eeebaaf07b994eda2281157db11d"}, ] [[package]] name = "pydantic" -version = "2.6.3" +version = "2.7.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, - {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.3" +pydantic-core = "2.18.2" typing-extensions = ">=4.6.1" [package.extras] @@ -218,90 +261,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.3" -description = "" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, ] [package.dependencies] @@ -326,53 +369,35 @@ python-dotenv = ">=0.21.0" toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pytest" -version = "7.1.2" +version = "7.4.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -424,45 +449,35 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "urllib3" -version = "1.26.18" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.8" files = [ - {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, - {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "aec42bf79ead76af1450c1397847d09fe8295c77ddb400369ce12ad9eabdd6c4" +content-hash = "b769f60fc37c35bd75d1d70af3c611b8d23bbce792723672cdad62e93f75a415" diff --git a/pyproject.toml b/pyproject.toml index 6151501..04be337 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ofsc" -version = "1.20.3" +version = "2.0.0" license = "MIT" description = "Python wrapper for Oracle Field Service API" authors = ["Borja Toron "] @@ -11,7 +11,7 @@ repository = 'https://github.com/btoron/pyOFSC' [tool.poetry.dependencies] python = "^3.11" requests = "^2.28.1" -pytest = "^7.1.2" +pytest = "7.4" pydantic = "^2.6.3" cachetools = "^5.3.1" pydantic-settings = "^2.2.1" diff --git a/tests/conftest.py b/tests/conftest.py index e122d30..501e6cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -133,6 +133,13 @@ def demo_data(): "RES": {"label": "RES", "name": "Residential"}, "COM": {"label": "COM", "name": "Commercial"}, }, + "expected_inventory_types": { + "count": 23, + "demo": { + "label": "FIT5000", + "status": "active", + }, + }, }, "get_file_property": { "activity_id": 3954799, # Note: manual addition diff --git a/tests/metadata/test_inventory_types.py b/tests/metadata/test_inventory_types.py new file mode 100644 index 0000000..d288522 --- /dev/null +++ b/tests/metadata/test_inventory_types.py @@ -0,0 +1,40 @@ +import logging + +from ofsc.common import FULL_RESPONSE, OBJ_RESPONSE +from ofsc.models import InventoryType, InventoryTypeListResponse + + +def test_inventory_types_model(instance): + instance.core.config.auto_model = True + metadata_response = instance.metadata.get_inventory_types( + response_type=OBJ_RESPONSE + ) + assert isinstance( + metadata_response, InventoryTypeListResponse + ), f"Response is {type(metadata_response)}" + for item in metadata_response.items: + assert isinstance(item, InventoryType) + + +def test_inventory_types_demo(instance, demo_data): + metadata_response = instance.metadata.get_inventory_types( + response_type=OBJ_RESPONSE + ) + assert metadata_response.items, "No inventory types found" + assert metadata_response.totalResults > 0, "No inventory types found" + assert len(metadata_response.items) == demo_data.get("metadata").get( + "expected_inventory_types" + ).get( + "count" + ), f"Expected {demo_data.get('metadata').get('expected_inventory_types').get('count')} inventory types, got {len(metadata_response.items)}" + + +def test_inventory_types_create_replace(instance, demo_data, request_logging): + data = demo_data.get("metadata").get("expected_inventory_types").get("demo") + inv_type = instance.metadata.get_inventory_type( + data.get("label"), response_type=OBJ_RESPONSE + ) + assert isinstance(inv_type, InventoryType) + assert inv_type.label == data.get("label") + logging.warning(inv_type.model_dump_json()) + assert False diff --git a/tests/metadata/test_properties.py b/tests/metadata/test_properties.py index da2e72f..353e2fd 100644 --- a/tests/metadata/test_properties.py +++ b/tests/metadata/test_properties.py @@ -2,7 +2,7 @@ from ofsc import OFSC from ofsc.common import FULL_RESPONSE -from ofsc.models import Property, Translation +from ofsc.models import Property, Translation, TranslationList def test_get_property(instance): @@ -39,7 +39,8 @@ def test_create_replace_property(instance: OFSC, request_logging, faker): "gui": "text", } ) - property.translations.__root__.append(Translation(name=property.name)) + en_name = Translation(name=property.name) + property.translations = TranslationList([en_name]) metadata_response = instance.metadata.create_or_replace_property( property, response_type=FULL_RESPONSE ) @@ -54,4 +55,5 @@ def test_create_replace_property(instance: OFSC, request_logging, faker): assert response["name"] == property.name assert response["type"] == property.type assert response["entity"] == property.entity + assert response.get("translations")[0]["name"] == property.translations[0].name property = Property.model_validate(response) diff --git a/tests/test_model.py b/tests/test_model.py index 61c07f1..e0cd397 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -64,6 +64,16 @@ def test_translationlist_model_json(): assert json.loads(objList.model_dump_json())[1]["name"] == base[1]["name"] +def test_translation_map(): + base = [ + {"language": "en", "name": "Estimate", "languageISO": "en-US"}, + {"language": "es", "name": "Estimar"}, + ] + ## Map the list into a dictionary with the language as the key + objMap = TranslationList.model_validate(base).map() + assert objMap.get("en").name == "Estimate" + + # region Activity Type Groups def test_activity_type_group_model_base(): base = { From 753fc43ab0879dbf2ba09016a277cc1e29bc505c Mon Sep 17 00:00:00 2001 From: Borja Toron Date: Wed, 15 May 2024 14:10:40 -0400 Subject: [PATCH 16/17] Fixed issue with non ascii char --- ofsc/metadata.py | 2 +- ofsc/models.py | 4 +++- pyproject.toml | 2 +- tests/metadata/test_properties.py | 33 ++++++++++++++++++++++++++++++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/ofsc/metadata.py b/ofsc/metadata.py index 6b1f803..5fcb9e4 100644 --- a/ofsc/metadata.py +++ b/ofsc/metadata.py @@ -58,7 +58,7 @@ def create_or_replace_property(self, property: Property): self.baseUrl, f"/rest/ofscMetadata/v1/properties/{property.label}" ) response = requests.put( - url, headers=self.headers, data=property.model_dump_json() + url, headers=self.headers, data=property.model_dump_json().encode("utf-8") ) return response diff --git a/ofsc/models.py b/ofsc/models.py index c8f4697..5a971fd 100644 --- a/ofsc/models.py +++ b/ofsc/models.py @@ -111,6 +111,8 @@ def token(self, auth: OFSOAuthRequest = OFSOAuthRequest()) -> requests.Response: @property def headers(self): self._headers = {} + self._headers["Content-Type"] = "application/json;charset=UTF-8" + if not self._config.useToken: self._headers["Authorization"] = ( "Basic " + self._config.basicAuthString.decode("utf-8") @@ -262,7 +264,7 @@ def gui_match(cls, v): raise ValueError(f"{v} is not a valid GUI value") return v - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="ignore") class PropertyList(RootModel[List[Property]]): diff --git a/pyproject.toml b/pyproject.toml index 04be337..fa654fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ofsc" -version = "2.0.0" +version = "2.0.4" license = "MIT" description = "Python wrapper for Oracle Field Service API" authors = ["Borja Toron "] diff --git a/tests/metadata/test_properties.py b/tests/metadata/test_properties.py index 353e2fd..a69dc30 100644 --- a/tests/metadata/test_properties.py +++ b/tests/metadata/test_properties.py @@ -28,7 +28,7 @@ def test_get_properties(instance, demo_data): assert response["items"][0]["label"] == "ITEM_NUMBER" -def test_create_replace_property(instance: OFSC, request_logging, faker): +def test_create_replace_property(instance: OFSC, faker): property = Property.model_validate( { "label": faker.pystr(), @@ -57,3 +57,34 @@ def test_create_replace_property(instance: OFSC, request_logging, faker): assert response["entity"] == property.entity assert response.get("translations")[0]["name"] == property.translations[0].name property = Property.model_validate(response) + + +def test_create_replace_property_noansi(instance: OFSC, request_logging, faker): + property = Property.model_validate( + { + "label": faker.pystr(), + "type": "string", + "entity": "activity", + "name": "césped", + "translations": [], + "gui": "text", + } + ) + en_name = Translation(name=property.name) + property.translations = TranslationList([en_name]) + metadata_response = instance.metadata.create_or_replace_property( + property, response_type=FULL_RESPONSE + ) + logging.debug(metadata_response.json()) + assert metadata_response.status_code < 299, metadata_response.json() + + metadata_response = instance.metadata.get_property( + property.label, response_type=FULL_RESPONSE + ) + assert metadata_response.status_code < 299 + response = metadata_response.json() + assert response["name"] == property.name + assert response["type"] == property.type + assert response["entity"] == property.entity + assert response.get("translations")[0]["name"] == property.translations[0].name + property = Property.model_validate(response) From 5bff73c4a1105852055daeca0e58d6e949efa549 Mon Sep 17 00:00:00 2001 From: Borja Toron-Antons Date: Wed, 23 Oct 2024 20:31:23 -0500 Subject: [PATCH 17/17] Updated poetry.lock --- poetry.lock | 466 ++++++++++++++++++++++++++++------------------------ 1 file changed, 250 insertions(+), 216 deletions(-) diff --git a/poetry.lock b/poetry.lock index f43c2eb..f57b1cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,134 +2,149 @@ [[package]] name = "annotated-types" -version = "0.6.0" +version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "cachetools" -version = "5.3.3" +version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -170,15 +185,18 @@ python-dateutil = ">=2.4" [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -192,13 +210,13 @@ files = [ [[package]] name = "openpyxl" -version = "3.1.2" +version = "3.1.5" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"}, - {file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"}, + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, ] [package.dependencies] @@ -206,28 +224,29 @@ et-xmlfile = "*" [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "pluggy" -version = "0.13.1" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "pyarmor" @@ -242,109 +261,123 @@ files = [ [[package]] name = "pydantic" -version = "2.7.1" +version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, - {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.18.2" -typing-extensions = ">=4.6.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.18.2" +version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, - {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, - {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, - {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, - {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, - {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, - {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, - {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, - {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, - {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, - {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, - {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, - {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, - {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -352,20 +385,21 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.2.1" +version = "2.6.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, - {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, + {file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"}, + {file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"}, ] [package.dependencies] -pydantic = ">=2.3.0" +pydantic = ">=2.7.0" python-dotenv = ">=0.21.0" [package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] @@ -419,13 +453,13 @@ cli = ["click (>=5.0)"] [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -451,24 +485,24 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras]