diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ba19e28..0ad2ab60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index fe912239..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "python.linting.enabled": true, - "python.linting.pylintPath": "pylint", - "editor.formatOnSave": true, - "python.formatting.provider": "yapf", - "python.linting.pylintEnabled": true, -} \ No newline at end of file diff --git a/Pipfile b/Pipfile index 8f634a08..e14cf569 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,9 @@ verify_ssl = true name = "pypi" [packages] # Packages required to run the application -microsoft-kiota-http = "*" +microsoft-kiota-abstractions = "==0.1.0" +microsoft-kiota-http = "==0.2.0" +microsoft-kiota-authentication-azure = "==0.1.0" httpx = {version = "==0.23.0", extras = ["http2"]} [dev-packages] # Packages required to develop the application @@ -17,7 +19,8 @@ pytest = "==7.1.3" pytest-cov = "==4.0.0" types-python-dateutil = "*" toml = "==0.10.2" -pytest-asyncio = "==0.19.0" +pytest-trio = "==0.7.0" pytest-mock = "==3.10.0" asyncmock = "==0.4.2" azure-identity = "*" +trio = "==0.22.0" diff --git a/Pipfile.lock b/Pipfile.lock index 65c876a2..8e5dcb7e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "adadae68a10208010ac5ad153524834f5a09579e7380a7b11c1453b26d0352b6" + "sha256": "bf905ef0b9652ce4a74de976c703ca85f10d9a3ca25515f2f304aa36a36b8dc4" }, "pipfile-spec": 6, "requires": {}, @@ -14,13 +14,122 @@ ] }, "default": { + "aiohttp": { + "hashes": [ + "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3", + "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782", + "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75", + "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf", + "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7", + "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675", + "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1", + "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785", + "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4", + "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf", + "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5", + "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15", + "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca", + "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8", + "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac", + "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8", + "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef", + "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516", + "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700", + "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2", + "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8", + "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0", + "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676", + "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad", + "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155", + "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db", + "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd", + "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091", + "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602", + "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411", + "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93", + "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd", + "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec", + "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51", + "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7", + "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17", + "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d", + "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00", + "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923", + "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440", + "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32", + "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e", + "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1", + "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724", + "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a", + "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8", + "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2", + "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33", + "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b", + "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2", + "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632", + "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b", + "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2", + "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316", + "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74", + "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96", + "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866", + "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44", + "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950", + "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa", + "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c", + "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a", + "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd", + "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd", + "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9", + "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421", + "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2", + "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922", + "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4", + "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237", + "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642", + "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578" + ], + "markers": "python_version >= '3.6'", + "version": "==3.8.1" + }, + "aiosignal": { + "hashes": [ + "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", + "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" + }, "anyio": { "hashes": [ - "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", - "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" + "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421", + "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.1" + "version": "==3.6.2" + }, + "async-timeout": { + "hashes": [ + "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", + "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.2" + }, + "attrs": { + "hashes": [ + "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", + "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + ], + "markers": "python_version >= '3.5'", + "version": "==22.1.0" + }, + "azure-core": { + "hashes": [ + "sha256:3d70e9ec64de92dfae330c15bc69085caceb2d83813ef6c01cc45326f2a4be83", + "sha256:88d2db5cf9a135a7287dc45fdde6b96f9ca62c9567512a3bb3e20e322ce7deb2" + ], + "version": "==1.21.1" }, "certifi": { "hashes": [ @@ -30,6 +139,79 @@ "markers": "python_version >= '3.6'", "version": "==2022.9.24" }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==2.1.1" + }, + "frozenlist": { + "hashes": [ + "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e", + "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04", + "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944", + "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845", + "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f", + "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f", + "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb", + "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2", + "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3", + "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f", + "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a", + "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131", + "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1", + "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa", + "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8", + "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b", + "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2", + "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e", + "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b", + "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b", + "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10", + "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170", + "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8", + "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b", + "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989", + "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd", + "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03", + "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519", + "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189", + "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b", + "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca", + "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff", + "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96", + "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d", + "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5", + "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2", + "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204", + "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a", + "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8", + "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141", + "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792", + "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9", + "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c", + "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c", + "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221", + "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d", + "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc", + "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f", + "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9", + "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef", + "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3", + "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab", + "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b", + "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db", + "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988", + "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6", + "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc", + "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83", + "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, "h11": { "hashes": [ "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", @@ -85,7 +267,7 @@ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==3.4" }, "microsoft-kiota-abstractions": { @@ -93,15 +275,97 @@ "sha256:b21e4182ceac16ba0cbc8e37d4487b8fed3b4724692b42faa3427a278328b4d4", "sha256:e0597e6691a7bae7977ac46ea625a921ac25df578ea6bacaacadebbcd77ac555" ], + "index": "pypi", + "version": "==0.1.0" + }, + "microsoft-kiota-authentication-azure": { + "hashes": [ + "sha256:1d8654e75b8691744b00e97dc126d4f0c67af299c84b382c5397982802aa5d3a", + "sha256:b5f37aa4a374205322ec025dcf6530adf1b54c98d79888b13dc96d7b8252d6ec" + ], + "index": "pypi", "version": "==0.1.0" }, "microsoft-kiota-http": { "hashes": [ - "sha256:634653aa4ea3ea3c0de2a7e300893b02265085c8ee392c609fa1248b1d7a2959", - "sha256:c27de018315995a8d54853a932f20a4775d6164f89988e5cd3822ac14f42ab44" + "sha256:12392f4d270001dfeba0464e602ef17922e9990cd6db1e26746b1c9d5263cbbb", + "sha256:34f61b0b732df4875f8f7058907af8f22fa6bc919aff9dc70dd7a806288ee7ab" ], "index": "pypi", - "version": "==0.1.0" + "version": "==0.2.0" + }, + "multidict": { + "hashes": [ + "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", + "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c", + "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672", + "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51", + "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032", + "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2", + "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b", + "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80", + "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88", + "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a", + "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d", + "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389", + "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c", + "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9", + "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c", + "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516", + "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b", + "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43", + "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee", + "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227", + "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d", + "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae", + "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7", + "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4", + "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9", + "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f", + "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013", + "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9", + "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e", + "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693", + "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a", + "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15", + "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb", + "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96", + "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87", + "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376", + "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658", + "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0", + "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071", + "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360", + "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc", + "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3", + "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba", + "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8", + "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9", + "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2", + "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3", + "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68", + "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8", + "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d", + "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49", + "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608", + "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57", + "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86", + "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20", + "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293", + "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849", + "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937", + "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.2" + }, + "requests": { + "hashes": [ + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + ], + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.1" }, "rfc3986": { "extras": [ @@ -113,6 +377,14 @@ ], "version": "==1.5.0" }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, "sniffio": { "hashes": [ "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", @@ -128,16 +400,97 @@ ], "markers": "python_version >= '3.6'", "version": "==4.1.1" + }, + "urllib3": { + "hashes": [ + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" + }, + "yarl": { + "hashes": [ + "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb", + "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3", + "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035", + "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453", + "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d", + "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a", + "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231", + "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f", + "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae", + "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b", + "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3", + "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507", + "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd", + "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae", + "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe", + "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c", + "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4", + "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64", + "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357", + "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54", + "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461", + "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4", + "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497", + "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0", + "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1", + "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957", + "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350", + "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780", + "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843", + "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548", + "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6", + "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40", + "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee", + "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b", + "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6", + "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0", + "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e", + "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880", + "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc", + "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e", + "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead", + "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28", + "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf", + "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd", + "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae", + "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0", + "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0", + "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae", + "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda", + "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546", + "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802", + "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be", + "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07", + "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936", + "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272", + "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc", + "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a", + "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28", + "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b" + ], + "markers": "python_version >= '3.7'", + "version": "==1.8.1" } }, "develop": { "astroid": { "hashes": [ - "sha256:2df4f9980c4511474687895cbfdb8558293c1a826d9118bb09233d7c2bff1c83", - "sha256:867a756bbf35b7bc07b35bfa6522acd01f91ad9919df675e8428072869792dce" + "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83", + "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f" ], "markers": "python_full_version >= '3.7.2'", - "version": "==2.12.11" + "version": "==2.12.12" + }, + "async-generator": { + "hashes": [ + "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", + "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" + ], + "markers": "python_version >= '3.5'", + "version": "==1.10" }, "asyncmock": { "hashes": [ @@ -157,11 +510,10 @@ }, "azure-core": { "hashes": [ - "sha256:578ea3ae56bca48880c96797871b6c954b5ae78d10d54360182c7604dc837f25", - "sha256:b0036a0d256329e08d1278dff7df36be30031d2ec9b16c691bc61e4732f71fe0" + "sha256:3d70e9ec64de92dfae330c15bc69085caceb2d83813ef6c01cc45326f2a4be83", + "sha256:88d2db5cf9a135a7287dc45fdde6b96f9ca62c9567512a3bb3e20e322ce7deb2" ], - "markers": "python_version >= '3.7'", - "version": "==1.26.0" + "version": "==1.21.1" }, "azure-identity": { "hashes": [ @@ -246,6 +598,7 @@ "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" ], + "markers": "os_name == 'nt' and implementation_name != 'pypy'", "version": "==1.15.1" }, "charset-normalizer": { @@ -253,16 +606,16 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.1.1" }, "colorama": { "hashes": [ - "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", - "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "markers": "sys_platform == 'win32' and sys_platform == 'win32'", - "version": "==0.4.5" + "markers": "sys_platform == 'win32'", + "version": "==0.4.6" }, "coverage": { "extras": [ @@ -325,42 +678,43 @@ }, "cryptography": { "hashes": [ - "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", - "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", - "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", - "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", - "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", - "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", - "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", - "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", - "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", - "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", - "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", - "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", - "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", - "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", - "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", - "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", - "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", - "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", - "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", - "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", - "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", - "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", - "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", - "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", - "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", - "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" - ], - "version": "==38.0.1" + "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d", + "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd", + "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146", + "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7", + "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436", + "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0", + "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828", + "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b", + "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55", + "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36", + "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50", + "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2", + "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a", + "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8", + "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0", + "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548", + "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320", + "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748", + "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249", + "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959", + "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f", + "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0", + "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd", + "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220", + "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", + "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" + ], + "markers": "python_version >= '3.6'", + "version": "==38.0.3" }, "dill": { "hashes": [ - "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302", - "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86" + "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", + "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.3.5.1" + "markers": "python_version >= '3.7'", + "version": "==0.3.6" }, "docutils": { "hashes": [ @@ -370,6 +724,14 @@ "markers": "python_version >= '3.7'", "version": "==0.19" }, + "exceptiongroup": { + "hashes": [ + "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41", + "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad" + ], + "markers": "python_version < '3.11'", + "version": "==1.0.0" + }, "flit": { "hashes": [ "sha256:06a93a6737fa9380ba85fe8d7f28efb6c93c4f4ee9c7d00cc3375a81f33b91a4", @@ -391,7 +753,7 @@ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==3.4" }, "iniconfig": { @@ -411,46 +773,28 @@ }, "lazy-object-proxy": { "hashes": [ - "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", - "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", - "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", - "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", - "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", - "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", - "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", - "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", - "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", - "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", - "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", - "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", - "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", - "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", - "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", - "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", - "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", - "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", - "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", - "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", - "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", - "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", - "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", - "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", - "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", - "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", - "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", - "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", - "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", - "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", - "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", - "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", - "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", - "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", - "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", - "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", - "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" + "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada", + "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d", + "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7", + "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe", + "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd", + "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c", + "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858", + "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288", + "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec", + "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f", + "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891", + "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c", + "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25", + "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156", + "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8", + "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f", + "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e", + "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0", + "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b" ], - "markers": "python_version >= '3.6'", - "version": "==1.7.1" + "markers": "python_version >= '3.7'", + "version": "==1.8.0" }, "mccabe": { "hashes": [ @@ -519,6 +863,14 @@ ], "version": "==0.4.3" }, + "outcome": { + "hashes": [ + "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672", + "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", @@ -545,11 +897,11 @@ }, "portalocker": { "hashes": [ - "sha256:400bae275366e7b840d4baad0654c6ec5994e07c40c423d78e9e1340279b8352", - "sha256:ae8e9cc2660da04bf41fa1a0eef7e300bb5e4a5869adfb1a6d8551632b559b2b" + "sha256:102ed1f2badd8dec9af3d732ef70e94b215b85ba45a8d7ff3c0003f19b442f4e", + "sha256:964f6830fb42a74b5d32bce99ed37d8308c1d7d44ddf18f3dd89f4680de97b39" ], "markers": "python_version >= '3.5' and platform_system == 'Windows'", - "version": "==2.5.1" + "version": "==2.6.0" }, "py": { "hashes": [ @@ -572,11 +924,11 @@ "crypto" ], "hashes": [ - "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80", - "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b" + "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd", + "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14" ], "markers": "python_version >= '3.7'", - "version": "==2.5.0" + "version": "==2.6.0" }, "pylint": { "hashes": [ @@ -626,6 +978,13 @@ "index": "pypi", "version": "==3.10.0" }, + "pytest-trio": { + "hashes": [ + "sha256:c01b741819aec2c419555f28944e132d3c711dae1e673d63260809bf92c30c31" + ], + "index": "pypi", + "version": "==0.7.0" + }, "pywin32": { "hashes": [ "sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc", @@ -651,7 +1010,7 @@ "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], - "markers": "python_version >= '3.7' and python_version < '4.0'", + "markers": "python_version >= '3.7' and python_version < '4'", "version": "==2.28.1" }, "six": { @@ -662,6 +1021,21 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, + "sniffio": { + "hashes": [ + "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", + "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", @@ -675,6 +1049,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], + "markers": "python_version >= '3.7'", "version": "==2.0.1" }, "tomli-w": { @@ -687,33 +1062,41 @@ }, "tomlkit": { "hashes": [ - "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c", - "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7" + "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b", + "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==0.11.5" + "markers": "python_version >= '3.6'", + "version": "==0.11.6" + }, + "trio": { + "hashes": [ + "sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf", + "sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0" + ], + "index": "pypi", + "version": "==0.22.0" }, "types-cryptography": { "hashes": [ - "sha256:913b3e66a502edbf4bfc3bb45e33ab476040c56942164a7ff37bd1f0ef8ef783", - "sha256:b85c45fd4d3d92e8b18e9a5ee2da84517e8fff658e3ef5755c885b1c2a27c1fe" + "sha256:b9f8aa93d9e4d7ff920961cdce3dc7cca479946890924d68356b66f15f4f35e3", + "sha256:f108e7a7161eedd9fe391e1a8ae5a98d40c1da2f0418bc0812a9119990272314" ], - "version": "==3.3.23" + "version": "==3.3.23.1" }, "types-python-dateutil": { "hashes": [ - "sha256:6284df1e4783d8fc6e587f0317a81333856b872a6669a282f8a325342bce7fa8", - "sha256:bfd3eb39c7253aea4ba23b10f69b017d30b013662bb4be4ab48b20bbd763f309" + "sha256:3f4dbe465e7e0c6581db11fd7a4855d1355b78712b3f292bd399cd332247e9c0", + "sha256:e6e32ce18f37765b08c46622287bc8d8136dc0c562d9ad5b8fd158c59963d7a7" ], "index": "pypi", - "version": "==2.8.19" + "version": "==2.8.19.2" }, "typing-extensions": { "hashes": [ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" }, "urllib3": { @@ -721,7 +1104,7 @@ "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4.0'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", "version": "==1.26.12" }, "wrapt": { diff --git a/msgraph/core/__init__.py b/msgraph/core/__init__.py index e726d590..f0b08984 100644 --- a/msgraph/core/__init__.py +++ b/msgraph/core/__init__.py @@ -2,9 +2,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from ._client_factory import HTTPClientFactory from ._constants import SDK_VERSION from ._enums import APIVersion, NationalClouds -from ._graph_client import GraphClient +from .base_graph_request_adapter import BaseGraphRequestAdapter +from .graph_client_factory import GraphClientFactory __version__ = SDK_VERSION diff --git a/msgraph/core/_client_factory.py b/msgraph/core/_client_factory.py deleted file mode 100644 index a316fa59..00000000 --- a/msgraph/core/_client_factory.py +++ /dev/null @@ -1,94 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import functools - -from requests import Session - -from ._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from ._enums import APIVersion, NationalClouds -from .middleware.abc_token_credential import TokenCredential -from .middleware.authorization import AuthorizationHandler -from .middleware.middleware import BaseMiddleware, MiddlewarePipeline -from .middleware.retry import RetryHandler -from .middleware.telemetry import TelemetryHandler - - -class HTTPClientFactory: - """Constructs native HTTP Client(session) instances configured with either custom or default - pipeline of middleware. - - :func: Class constructor accepts a user provided session object and kwargs to configure the - request handling behaviour of the client - :keyword enum api_version: The Microsoft Graph API version to be used, for example - `APIVersion.v1` (default). This value is used in setting the base url for all requests for - that session. - :class:`~msgraphcore.enums.APIVersion` defines valid API versions. - :keyword enum cloud: a supported Microsoft Graph cloud endpoint. - Defaults to `NationalClouds.Global` - :class:`~msgraphcore.enums.NationalClouds` defines supported sovereign clouds. - :keyword tuple timeout: Default connection and read timeout values for all session requests. - Specify a tuple in the form of Tuple(connect_timeout, read_timeout) if you would like to set - the values separately. If you specify a single value for the timeout, the timeout value will - be applied to both the connect and the read timeouts. - :keyword obj session: A custom Session instance from the python requests library - """ - def __init__(self, **kwargs): - """Class constructor that accepts a user provided session object and kwargs - to configure the request handling behaviour of the client""" - self.api_version = kwargs.get('api_version', APIVersion.v1) - self.endpoint = kwargs.get('cloud', NationalClouds.Global) - self.timeout = kwargs.get('timeout', (DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT)) - self.session = kwargs.get('session', Session()) - - self._set_base_url() - self._set_default_timeout() - - def create_with_default_middleware(self, credential: TokenCredential, **kwargs) -> Session: - """Applies the default middleware chain to the HTTP Client - - :param credential: TokenCredential used to acquire an access token for the Microsoft - Graph API. Created through one of the credential classes from `azure.identity` - """ - middleware = [ - AuthorizationHandler(credential, **kwargs), - RetryHandler(**kwargs), - TelemetryHandler(), - ] - self._register(middleware) - return self.session - - def create_with_custom_middleware(self, middleware: [BaseMiddleware]) -> Session: - """Applies a custom middleware chain to the HTTP Client - - :param list middleware: Custom middleware(HTTPAdapter) list that will be used to create - a middleware pipeline. The middleware should be arranged in the order in which they will - modify the request. - """ - if not middleware: - raise ValueError("Please provide a list of custom middleware") - self._register(middleware) - return self.session - - def _set_base_url(self): - """Helper method to set the base url""" - base_url = self.endpoint + '/' + self.api_version - self.session.base_url = base_url - - def _set_default_timeout(self): - """Helper method to set a default timeout for the session - Reference: https://github.com/psf/requests/issues/2011 - """ - self.session.request = functools.partial(self.session.request, timeout=self.timeout) - - def _register(self, middleware: [BaseMiddleware]) -> None: - """ - Helper method that constructs a middleware_pipeline with the specified middleware - """ - if middleware: - middleware_pipeline = MiddlewarePipeline() - for ware in middleware: - middleware_pipeline.add_middleware(ware) - - self.session.mount('https://', middleware_pipeline) diff --git a/msgraph/core/_graph_client.py b/msgraph/core/_graph_client.py deleted file mode 100644 index 8b70c379..00000000 --- a/msgraph/core/_graph_client.py +++ /dev/null @@ -1,176 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json - -from requests import Request, Session - -from ._client_factory import HTTPClientFactory -from .middleware.request_context import RequestContext - -# These are middleware options that can be configured per request. -# Supports options for default middleware as well as custom middleware. -supported_options = [ - # Auth Options - 'scopes', - - # Retry Options - 'max_retries', - 'retry_backoff_factor', - 'retry_backoff_max', - 'retry_time_limit', - 'retry_on_status_codes', - - # Custom middleware options - 'custom_option', -] - - -def collect_options(func): - """Collect middleware options into a middleware control dict and pass it as a header""" - def wrapper(*args, **kwargs): - - middleware_control = dict() - - for option in supported_options: - value = kwargs.pop(option, None) - if value: - middleware_control.update({option: value}) - - if 'headers' in kwargs.keys(): - kwargs['headers'].update({'middleware_control': json.dumps(middleware_control)}) - else: - kwargs['headers'] = {'middleware_control': json.dumps(middleware_control)} - - return func(*args, **kwargs) - - return wrapper - - -class GraphClient: - """Constructs a custom HTTPClient to be used for requests against Microsoft Graph - - :keyword credential: TokenCredential used to acquire an access token for the Microsoft - Graph API. Created through one of the credential classes from `azure.identity` - :keyword list middleware: Custom middleware(HTTPAdapter) list that will be used to create - a middleware pipeline. The middleware should be arranged in the order in which they will - modify the request. - :keyword enum api_version: The Microsoft Graph API version to be used, for example - `APIVersion.v1` (default). This value is used in setting the base url for all requests for - that session. - :class:`~msgraphcore.enums.APIVersion` defines valid API versions. - :keyword enum cloud: a supported Microsoft Graph cloud endpoint. - Defaults to `NationalClouds.Global` - :class:`~msgraphcore.enums.NationalClouds` defines supported sovereign clouds. - :keyword tuple timeout: Default connection and read timeout values for all session requests. - Specify a tuple in the form of Tuple(connect_timeout, read_timeout) if you would like to set - the values separately. If you specify a single value for the timeout, the timeout value will - be applied to both the connect and the read timeouts. - :keyword obj session: A custom Session instance from the python requests library - """ - __instance = None - - def __new__(cls, *args, **kwargs): - if not GraphClient.__instance: - GraphClient.__instance = object.__new__(cls) - return GraphClient.__instance - - def __init__(self, **kwargs): - """ - Class constructor that accepts a session object and kwargs to - be passed to the HTTPClientFactory - """ - self.graph_session = self.get_graph_session(**kwargs) - - @collect_options - def get(self, url: str, **kwargs): - r"""Sends a GET request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.get(self._graph_url(url), **kwargs) - - def options(self, url, **kwargs): - r"""Sends a OPTIONS request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - - return self.graph_session.options(self._graph_url(url), **kwargs) - - def head(self, url, **kwargs): - r"""Sends a HEAD request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - - return self.graph_session.head(self._graph_url(url), **kwargs) - - def post(self, url, data=None, json=None, **kwargs): - r"""Sends a POST request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param json: (optional) json to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.post(self._graph_url(url), data=data, json=json, **kwargs) - - def put(self, url, data=None, **kwargs): - r"""Sends a PUT request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - - return self.graph_session.put(self._graph_url(url), data=data, **kwargs) - - def patch(self, url, data=None, **kwargs): - r"""Sends a PATCH request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, list of tuples, bytes, or file-like - object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.patch(self._graph_url(url), data=data, **kwargs) - - def delete(self, url, **kwargs): - r"""Sends a DELETE request. Returns :class:`Response` object. - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :rtype: requests.Response - """ - return self.graph_session.delete(self._graph_url(url), **kwargs) - - def _graph_url(self, url: str) -> str: - """Appends BASE_URL to user provided path - :param url: user provided path - :return: graph_url - """ - return self.graph_session.base_url + url if (url[0] == '/') else url - - @staticmethod - def get_graph_session(**kwargs): - """Method to always return a single instance of a HTTP Client""" - - credential = kwargs.pop('credential', None) - middleware = kwargs.pop('middleware', None) - - if credential and middleware: - raise ValueError( - "Invalid parameters! Both TokenCredential and middleware cannot be passed" - ) - if not credential and not middleware: - raise ValueError("Invalid parameters!. Missing TokenCredential or middleware") - - if credential: - return HTTPClientFactory(**kwargs).create_with_default_middleware(credential, **kwargs) - return HTTPClientFactory(**kwargs).create_with_custom_middleware(middleware) diff --git a/msgraph/core/base_graph_request_adapter.py b/msgraph/core/base_graph_request_adapter.py new file mode 100644 index 00000000..2acfdc10 --- /dev/null +++ b/msgraph/core/base_graph_request_adapter.py @@ -0,0 +1,29 @@ +import httpx +from kiota_abstractions.authentication import AuthenticationProvider +from kiota_abstractions.serialization import ( + ParseNodeFactory, + ParseNodeFactoryRegistry, + SerializationWriterFactory, + SerializationWriterFactoryRegistry, +) +from kiota_http.httpx_request_adapter import HttpxRequestAdapter + +from .graph_client_factory import GraphClientFactory + + +class BaseGraphRequestAdapter(HttpxRequestAdapter): + + def __init__( + self, + authentication_provider: AuthenticationProvider, + parse_node_factory: ParseNodeFactory = ParseNodeFactoryRegistry(), + serialization_writer_factory: + SerializationWriterFactory = SerializationWriterFactoryRegistry(), + http_client: httpx.AsyncClient = GraphClientFactory.create_with_default_middleware() + ) -> None: + super().__init__( + authentication_provider=authentication_provider, + parse_node_factory=parse_node_factory, + serialization_writer_factory=serialization_writer_factory, + http_client=http_client + ) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py new file mode 100644 index 00000000..6cbbbcf7 --- /dev/null +++ b/msgraph/core/graph_client_factory.py @@ -0,0 +1,78 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from __future__ import annotations + +from typing import List, Optional + +import httpx +from kiota_http.kiota_client_factory import KiotaClientFactory +from kiota_http.middleware import AsyncKiotaTransport +from kiota_http.middleware.middleware import BaseMiddleware + +from ._enums import APIVersion, NationalClouds +from .middleware import GraphTelemetryHandler + + +class GraphClientFactory(KiotaClientFactory): + """Constructs httpx.AsyncClient instances configured with either custom or default + pipeline of graph specific middleware. + """ + + @staticmethod + def create_with_default_middleware( + api_version: APIVersion = APIVersion.v1, + host: NationalClouds = NationalClouds.Global + ) -> httpx.AsyncClient: + """Constructs native HTTP AsyncClient(httpx.AsyncClient) instances configured with + a custom transport loaded with a default pipeline of middleware. + Returns: + httpx.AsycClient: An instance of the AsyncClient object + """ + client = KiotaClientFactory.get_default_client() + client.base_url = GraphClientFactory._get_base_url(host, api_version) + current_transport = client._transport + + middleware = KiotaClientFactory.get_default_middleware() + middleware.append(GraphTelemetryHandler()) + middleware_pipeline = KiotaClientFactory.create_middleware_pipeline( + middleware, current_transport + ) + + client._transport = AsyncKiotaTransport( + transport=current_transport, pipeline=middleware_pipeline + ) + return client + + @staticmethod + def create_with_custom_middleware( + middleware: Optional[List[BaseMiddleware]], + api_version: APIVersion = APIVersion.v1, + host: NationalClouds = NationalClouds.Global, + ) -> httpx.AsyncClient: + """Applies a custom middleware chain to the HTTP Client + + Args: + middleware(List[BaseMiddleware]): Custom middleware list that will be used to create + a middleware pipeline. The middleware should be arranged in the order in which they will + modify the request. + """ + client = KiotaClientFactory.get_default_client() + client.base_url = GraphClientFactory._get_base_url(host, api_version) + current_transport = client._transport + + middleware_pipeline = KiotaClientFactory.create_middleware_pipeline( + middleware, current_transport + ) + + client._transport = AsyncKiotaTransport( + transport=current_transport, pipeline=middleware_pipeline + ) + return client + + @staticmethod + def _get_base_url(host: str, api_version: APIVersion) -> str: + """Helper method to set the complete base url""" + base_url = f'{host}/{api_version}' + return base_url diff --git a/msgraph/core/middleware/__init__.py b/msgraph/core/middleware/__init__.py index b74cfa3b..508a0819 100644 --- a/msgraph/core/middleware/__init__.py +++ b/msgraph/core/middleware/__init__.py @@ -2,3 +2,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +from .request_context import GraphRequestContext +from .telemetry import GraphTelemetryHandler diff --git a/msgraph/core/middleware/abc_token_credential.py b/msgraph/core/middleware/abc_token_credential.py deleted file mode 100644 index 724394b9..00000000 --- a/msgraph/core/middleware/abc_token_credential.py +++ /dev/null @@ -1,11 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from abc import ABC, abstractmethod - - -class TokenCredential(ABC): - @abstractmethod - def get_token(self, *scopes, **kwargs): - pass diff --git a/msgraph/core/middleware/authorization.py b/msgraph/core/middleware/authorization.py deleted file mode 100644 index 43884d67..00000000 --- a/msgraph/core/middleware/authorization.py +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from .._enums import FeatureUsageFlag -from .abc_token_credential import TokenCredential -from .middleware import BaseMiddleware - - -class AuthorizationHandler(BaseMiddleware): - def __init__(self, credential: TokenCredential, **kwargs): - super().__init__() - self.credential = credential - self.scopes = kwargs.get("scopes", ['.default']) - self.retry_count = 0 - - def send(self, request, **kwargs): - context = request.context - request.headers.update( - {'Authorization': 'Bearer {}'.format(self._get_access_token(context))} - ) - context.set_feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED - response = super().send(request, **kwargs) - - # Token might have expired just before transmission, retry the request one more time - if response.status_code == 401 and self.retry_count < 2: - self.retry_count += 1 - return self.send(request, **kwargs) - return response - - def _get_access_token(self, context): - return self.credential.get_token(*self.get_scopes(context))[0] - - def get_scopes(self, context): - # Checks if there are any options for this middleware - return context.middleware_control.get('scopes', self.scopes) diff --git a/msgraph/core/middleware/middleware.py b/msgraph/core/middleware/middleware.py deleted file mode 100644 index ac31134f..00000000 --- a/msgraph/core/middleware/middleware.py +++ /dev/null @@ -1,66 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json -import ssl - -from requests.adapters import HTTPAdapter -from urllib3 import PoolManager - -from .request_context import RequestContext - - -class MiddlewarePipeline(HTTPAdapter): - """MiddlewarePipeline, entry point of middleware - The pipeline is implemented as a linked-list, read more about - it here https://buffered.dev/middleware-python-requests/ - """ - def __init__(self): - super().__init__() - self._current_middleware = None - self._first_middleware = None - self.poolmanager = PoolManager(ssl_version=ssl.PROTOCOL_TLSv1_2) - - def add_middleware(self, middleware): - if self._middleware_present(): - self._current_middleware.next = middleware - self._current_middleware = middleware - else: - self._first_middleware = middleware - self._current_middleware = self._first_middleware - - def send(self, request, **kwargs): - - middleware_control_json = request.headers.pop('middleware_control', None) - if middleware_control_json: - middleware_control = json.loads(middleware_control_json) - else: - middleware_control = dict() - - request.context = RequestContext(middleware_control, request.headers) - - if self._middleware_present(): - return self._first_middleware.send(request, **kwargs) - # No middleware in pipeline, call superclass' send - return super().send(request, **kwargs) - - def _middleware_present(self): - return self._current_middleware - - -class BaseMiddleware(HTTPAdapter): - """Base class for middleware - - Handles moving a Request to the next middleware in the pipeline. - If the current middleware is the last one in the pipeline, it - makes a network request - """ - def __init__(self): - super().__init__() - self.next = None - - def send(self, request, **kwargs): - if self.next is None: - return super().send(request, **kwargs) - return self.next.send(request, **kwargs) diff --git a/msgraph/core/middleware/request_context.py b/msgraph/core/middleware/request_context.py index ff44b23a..1f520a15 100644 --- a/msgraph/core/middleware/request_context.py +++ b/msgraph/core/middleware/request_context.py @@ -4,16 +4,19 @@ # ------------------------------------ import uuid +import httpx + from .._enums import FeatureUsageFlag -class RequestContext: - """A request context contains data that is persisted throughout the request and - includes a ClientRequestId property, MiddlewareControl property to control behavior - of middleware as well as a FeatureUsage  property to keep track of middleware used +class GraphRequestContext: + """A request context contains data that is persisted throughout the request and + includes a ClientRequestId property, MiddlewareControl property to control behavior + of middleware as well as a FeatureUsage property to keep track of middleware used in making the request. """ - def __init__(self, middleware_control, headers): + + def __init__(self, middleware_control: dict, headers: httpx.Headers): """Constructor for request context instances Args: @@ -26,12 +29,12 @@ def __init__(self, middleware_control, headers): """ self.middleware_control = middleware_control self.client_request_id = headers.get('client-request-id', str(uuid.uuid4())) - self._feature_usage = FeatureUsageFlag.NONE + self._feature_usage: int = FeatureUsageFlag.NONE @property def feature_usage(self): return hex(self._feature_usage) @feature_usage.setter - def set_feature_usage(self, flag: FeatureUsageFlag): + def feature_usage(self, flag: FeatureUsageFlag) -> None: self._feature_usage = self._feature_usage | flag diff --git a/msgraph/core/middleware/retry.py b/msgraph/core/middleware/retry.py deleted file mode 100644 index 814b27a8..00000000 --- a/msgraph/core/middleware/retry.py +++ /dev/null @@ -1,221 +0,0 @@ -import datetime -import random -import time -from email.utils import parsedate_to_datetime - -from .._enums import FeatureUsageFlag -from .middleware import BaseMiddleware - - -class RetryHandler(BaseMiddleware): - """ - TransportAdapter that allows us to specify the retry policy for all requests - - Retry configuration. - - :param int max_retries: - Maximum number of retries to allow. Takes precedence over other counts. - Set to ``0`` to fail on the first retry. - :param iterable retry_on_status_codes: - A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``allowed_methods`` - and the response status code is in ``RETRY STATUS CODES``. - :param float retry_backoff_factor: - A backoff factor to apply between attempts after the second try - (most errors are resolved immediately by a second try without a - delay). - The request will sleep for:: - {backoff factor} * (2 ** ({retry number} - 1)) - seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep - for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer - than :attr:`RetryHandler.MAXIMUM_BACKOFF`. - By default, backoff is set to 0.5. - :param int retry_time_limit: - The maximum cumulative time in seconds that total retries should take. - The cumulative retry time and retry-after value for each request retry - will be evaluated against this value; if the cumulative retry time plus - the retry-after value is greater than the retry_time_limit, the failed - response will be immediately returned, else the request retry continues. - """ - - DEFAULT_MAX_RETRIES = 3 - MAX_RETRIES = 10 - DEFAULT_DELAY = 3 - MAX_DELAY = 180 - DEFAULT_BACKOFF_FACTOR = 0.5 - MAXIMUM_BACKOFF = 120 - _DEFAULT_RETRY_STATUS_CODES = {429, 503, 504} - - def __init__(self, **kwargs): - super().__init__() - self.max_retries: int = min( - kwargs.pop('max_retries', self.DEFAULT_MAX_RETRIES), self.MAX_RETRIES - ) - self.backoff_factor: float = kwargs.pop('retry_backoff_factor', self.DEFAULT_BACKOFF_FACTOR) - self.backoff_max: int = kwargs.pop('retry_backoff_max', self.MAXIMUM_BACKOFF) - self.timeout: int = kwargs.pop('retry_time_limit', self.MAX_DELAY) - - status_codes: [int] = kwargs.pop('retry_on_status_codes', []) - - self._retry_on_status_codes: set = set(status_codes) | self._DEFAULT_RETRY_STATUS_CODES - self._allowed_methods: set = frozenset( - ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] - ) - self._respect_retry_after_header: bool = True - - @classmethod - def disable_retries(cls): - """ - Disable retries by setting retry_total to zero. - retry_total takes precedence over all other counts. - """ - return cls(max_retries=0) - - def get_retry_options(self, middleware_control): - """ - Check if request specific configs have been passed and override any session defaults - Then configure retry settings into the form of a dict. - """ - if middleware_control: - return { - 'total': - min(middleware_control.get('max_retries', self.max_retries), self.MAX_RETRIES), - 'backoff': - middleware_control.get('retry_backoff_factor', self.backoff_factor), - 'max_backoff': - middleware_control.get('retry_backoff_max', self.backoff_max), - 'timeout': - middleware_control.get('retry_time_limit', self.timeout), - 'retry_codes': - set(middleware_control.get('retry_on_status_codes', self._retry_on_status_codes)) - | set(self._DEFAULT_RETRY_STATUS_CODES), - 'methods': - self._allowed_methods, - } - return { - 'total': self.max_retries, - 'backoff': self.backoff_factor, - 'max_backoff': self.backoff_max, - 'timeout': self.timeout, - 'retry_codes': self._retry_on_status_codes, - 'methods': self._allowed_methods, - } - - def send(self, request, **kwargs): - """ - Sends the http request object to the next middleware or retries the request if necessary. - """ - retry_options = self.get_retry_options(request.context.middleware_control) - absolute_time_limit = min(retry_options['timeout'], self.MAX_DELAY) - response = None - retry_count = 0 - retry_valid = True - - while retry_valid: - start_time = time.time() - if retry_count > 0: - request.headers.update({'retry-attempt': '{}'.format(retry_count)}) - response = super().send(request, **kwargs) - # Check if the request needs to be retried based on the response method - # and status code - if self.should_retry(retry_options, response): - # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_options, retry_count) - - # Get the delay time between retries - delay = self.get_delay_time(retry_options, retry_count, response) - - if retry_valid and delay < absolute_time_limit: - time.sleep(delay) - end_time = time.time() - absolute_time_limit -= (end_time - start_time) - # increment the count for retries - retry_count += 1 - - continue - break - return response - - def should_retry(self, retry_options, response): - """ - Determines whether the request should be retried - Checks if the request method is in allowed methods - Checks if the response status code is in retryable status codes. - """ - if not self._is_method_retryable(retry_options, response.request): - return False - if not self._is_request_payload_buffered(response): - return False - return retry_options['total'] and response.status_code in retry_options['retry_codes'] - - def _is_method_retryable(self, retry_options, request): - """ - Checks if a given request should be retried upon, depending on - whether the HTTP method is in the set of allowed methods - """ - if request.method.upper() not in retry_options['methods']: - return False - return True - - def _is_request_payload_buffered(self, response): - """ - Checks if the request payload is buffered/rewindable. - Payloads with forward only streams will return false and have the responses - returned without any retry attempt. - """ - if response.request.method.upper() in frozenset(['HEAD', 'GET', 'DELETE', 'OPTIONS']): - return True - if response.request.headers.get('Content-Type') == "application/octet-stream": - return False - return True - - def check_retry_valid(self, retry_options, retry_count): - """ - Check that the max retries limit has not been hit - """ - if retry_count < retry_options['total']: - return True - return False - - def get_delay_time(self, retry_options, retry_count, response=None): - """ - Get the time in seconds to delay between retry attempts. - Respects a retry-after header in the response if provided - If no retry-after response header, it defaults to exponential backoff - """ - retry_after = self._get_retry_after(response) - if retry_after: - return retry_after - return self._get_delay_time_exp_backoff(retry_options, retry_count) - - def _get_delay_time_exp_backoff(self, retry_options, retry_count): - """ - Get time in seconds to delay between retry attempts based on an exponential - backoff value. - """ - exp_backoff_value = retry_options['backoff'] * +(2**(retry_count - 1)) - backoff_value = exp_backoff_value + (random.randint(0, 1000) / 1000) - - backoff = min(retry_options['max_backoff'], backoff_value) - return backoff - - def _get_retry_after(self, response): - """ - Check if retry-after is specified in the response header and get the value - """ - retry_after = response.headers.get("retry-after") - if retry_after: - return self._parse_retry_after(retry_after) - return None - - def _parse_retry_after(self, retry_after): - """ - Helper to parse Retry-After and get value in seconds. - """ - try: - delay = int(retry_after) - except ValueError: - # Not an integer? Try HTTP date - retry_date = parsedate_to_datetime(retry_after) - delay = (retry_date - datetime.datetime.now(retry_date.tzinfo)).total_seconds() - return max(0, delay) diff --git a/msgraph/core/middleware/telemetry.py b/msgraph/core/middleware/telemetry.py index 9db9b707..01d66f9a 100644 --- a/msgraph/core/middleware/telemetry.py +++ b/msgraph/core/middleware/telemetry.py @@ -1,17 +1,29 @@ +import http +import json import platform +import httpx +from kiota_http.middleware import AsyncKiotaTransport, BaseMiddleware, RedirectHandler, RetryHandler from urllib3.util import parse_url from .._constants import SDK_VERSION -from .._enums import NationalClouds -from .middleware import BaseMiddleware +from .._enums import FeatureUsageFlag, NationalClouds +from .request_context import GraphRequestContext -class TelemetryHandler(BaseMiddleware): +class GraphRequest(httpx.Request): + context: GraphRequestContext + + +class GraphTelemetryHandler(BaseMiddleware): """Middleware component that attaches metadata to a Graph request in order to help the SDK team improve the developer experience. """ - def send(self, request, **kwargs): + + async def send(self, request: GraphRequest, transport: AsyncKiotaTransport): + """Adds telemetry headers and sends the http request. + """ + self.set_request_context_and_feature_usage(request, transport) if self.is_graph_url(request.url): self._add_client_request_id_header(request) @@ -19,26 +31,42 @@ def send(self, request, **kwargs): self._add_host_os_header(request) self._add_runtime_environment_header(request) - response = super().send(request, **kwargs) + response = await super().send(request, transport) return response + def set_request_context_and_feature_usage( + self, request: GraphRequest, transport: AsyncKiotaTransport + ) -> GraphRequest: + + request_options = {} + options = request.headers.pop('request_options', None) + if options: + request_options = json.loads(options) + + request.context = GraphRequestContext(request_options, request.headers) + middleware = transport.pipeline._first_middleware + while middleware: + if isinstance(middleware, RedirectHandler): + request.context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + if isinstance(middleware, RetryHandler): + request.context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED + + middleware = middleware.next + + return request + def is_graph_url(self, url): """Check if the request is made to a graph endpoint. We do not add telemetry headers to non-graph endpoints""" endpoints = set(item.value for item in NationalClouds) - base_url = parse_url(url) - endpoint = "{}://{}".format( - base_url.scheme, - base_url.netloc, - ) + base_url = parse_url(str(url)) + endpoint = f"{base_url.scheme}://{base_url.netloc}" return endpoint in endpoints def _add_client_request_id_header(self, request) -> None: """Add a client-request-id header with GUID value to request""" - request.headers.update( - {'client-request-id': '{}'.format(request.context.client_request_id)} - ) + request.headers.update({'client-request-id': f'{request.context.client_request_id}'}) def _append_sdk_version_header(self, request) -> None: """Add SdkVersion request header to each request to identify the language and diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f77d5e91 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,54 @@ +import httpx +import pytest +from kiota_abstractions.authentication import AnonymousAuthenticationProvider +from kiota_authentication_azure.azure_identity_access_token_provider import ( + AzureIdentityAccessTokenProvider, +) + +from msgraph.core import APIVersion, NationalClouds +from msgraph.core.graph_client_factory import GraphClientFactory +from msgraph.core.middleware import GraphRequestContext + +BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 + + +class MockAuthenticationProvider(AnonymousAuthenticationProvider): + + async def get_authorization_token(self, request: httpx.Request) -> str: + """Returns a string representing a dummy token + Args: + request (GraphRequest): Graph request object + """ + request.headers['Authorization'] = 'Sample token' + return + + +@pytest.fixture +def mock_auth_provider(): + return MockAuthenticationProvider() + + +@pytest.fixture +def mock_transport(): + client = GraphClientFactory.create_with_default_middleware() + return client._transport + + +@pytest.fixture +def mock_request(): + req = httpx.Request('GET', "https://example.org") + return req + + +@pytest.fixture +def mock_graph_request(): + req = httpx.Request('GET', BASE_URL) + req.context = GraphRequestContext({}, req.headers) + return req + + +@pytest.fixture +def mock_response(): + return httpx.Response( + json={'message': 'Success!'}, status_code=200, headers={"Content-Type": "application/json"} + ) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index b74cfa3b..00000000 --- a/tests/integration/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ diff --git a/tests/integration/test_graphclient.py b/tests/integration/test_graphclient.py deleted file mode 100644 index 3772a9d7..00000000 --- a/tests/integration/test_graphclient.py +++ /dev/null @@ -1,91 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from azure.identity import EnvironmentCredential -from requests import Session - -from msgraph.core import APIVersion, GraphClient -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_graph_client_with_default_middleware(): - """ - Test that a graph client uses default middleware if none are provided - """ - # credential = _CustomTokenCredential() - client = GraphClient(credential=EnvironmentCredential()) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_graph_client_with_user_provided_session(): - """ - Test that the graph client works with a user provided session object - """ - - session = Session() - client = GraphClient(session=session, credential=EnvironmentCredential()) - response = client.get('https://graph.microsoft.com/v1.0/users', ) - assert response.status_code == 200 - - -def test_graph_client_with_custom_settings(): - """ - Test that the graph client works with user provided configuration - """ - credential = EnvironmentCredential() - client = GraphClient(api_version=APIVersion.beta, credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', ) - assert response.status_code == 200 - - -def test_graph_client_with_custom_middleware(): - """ - Test client factory works with user provided middleware - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = GraphClient(middleware=middleware) - response = client.get('https://graph.microsoft.com/v1.0/users', ) - assert response.status_code == 200 - - -def test_graph_client_adds_context_to_request(): - """ - Test the graph client adds a context object to a request - """ - credential = EnvironmentCredential() - scopes = ['https://graph.microsoft.com/.default'] - client = GraphClient(credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes) - assert response.status_code == 200 - assert hasattr(response.request, 'context') - - -def test_graph_client_picks_options_from_kwargs(): - """ - Test the graph client picks middleware options from kwargs and sets them in the context - """ - credential = EnvironmentCredential() - scopes = ['https://graph.microsoft.com/.default'] - client = GraphClient(credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes) - assert response.status_code == 200 - assert 'scopes' in response.request.context.middleware_control.keys() - assert response.request.context.middleware_control['scopes'] == scopes - - -def test_graph_client_allows_passing_optional_kwargs(): - """ - Test the graph client allows passing optional kwargs native to the requests library - such as stream, proxy and cert. - """ - credential = EnvironmentCredential() - scopes = ['https://graph.microsoft.com/.default'] - client = GraphClient(credential=credential) - response = client.get('https://graph.microsoft.com/v1.0/users', scopes=scopes, stream=True) - assert response.status_code == 200 diff --git a/tests/integration/test_http_client_factory.py b/tests/integration/test_http_client_factory.py deleted file mode 100644 index ea4cbb0e..00000000 --- a/tests/integration/test_http_client_factory.py +++ /dev/null @@ -1,86 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from azure.identity import EnvironmentCredential -from requests import Session - -from msgraph.core import APIVersion, HTTPClientFactory -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_client_factory_with_default_middleware(): - """ - Test that a client created from client factory with default middleware - works as expected. - """ - credential = EnvironmentCredential() - client = HTTPClientFactory().create_with_default_middleware(credential) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_client_factory_with_user_provided_session(): - """ - Test that the client works with a user provided session object - """ - - session = Session() - credential = EnvironmentCredential() - client = HTTPClientFactory(session=session).create_with_default_middleware(credential) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_client_factory_with_custom_settings(): - """ - Test that the client works with user provided configuration - """ - credential = EnvironmentCredential() - client = HTTPClientFactory(api_version=APIVersion.beta - ).create_with_default_middleware(credential) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_client_factory_with_custom_middleware(): - """ - Test client factory works with user provided middleware - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - - -def test_context_object_is_attached_to_requests_from_client_factory(): - """ - Test that requests from a native HTTP client have a context object attached - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - assert hasattr(response.request, 'context') - - -def test_middleware_control_is_empty_for_requests_from_client_factory(): - """ - Test that requests from a native HTTP client have no middlware options in the middleware - control - """ - credential = EnvironmentCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware) - response = client.get('https://graph.microsoft.com/v1.0/users') - assert response.status_code == 200 - assert response.request.context.middleware_control == {} diff --git a/tests/integration/test_retry.py b/tests/integration/test_retry.py deleted file mode 100644 index af0f8322..00000000 --- a/tests/integration/test_retry.py +++ /dev/null @@ -1,91 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from azure.identity import EnvironmentCredential - -from msgraph.core import GraphClient - - -@pytest.fixture -def graph_client(): - scopes = ['https://graph.microsoft.com/.default'] - credential = EnvironmentCredential() - client = GraphClient(credential=credential, scopes=scopes) - return client - - -def test_no_retry_success_response(graph_client): - """ - Test that a request with valid http header and a success response is not retried - """ - response = graph_client.get('https://graph.microsoft.com/v1.0/users') - - assert response.status_code == 200 - with pytest.raises(KeyError): - response.request.headers["retry-attempt"] - - -def test_valid_retry_429(graph_client): - """ - Test that a request with valid http header and 503 response is retried - """ - response = graph_client.get('https://httpbin.org/status/429') - - assert response.status_code == 429 - assert response.request.headers["retry-attempt"] == "3" - - -def test_valid_retry_503(graph_client): - """ - Test that a request with valid http header and 503 response is retried - """ - response = graph_client.get('https://httpbin.org/status/503') - - assert response.status_code == 503 - assert response.request.headers["retry-attempt"] == "3" - - -def test_valid_retry_504(graph_client): - """ - Test that a request with valid http header and 503 response is retried - """ - response = graph_client.get('https://httpbin.org/status/504') - - assert response.status_code == 504 - assert response.request.headers["retry-attempt"] == "3" - - -def test_request_specific_options_override_default(graph_client): - """ - Test that retry options passed to the request take precedence over - the default options. - """ - response_1 = graph_client.get('https://httpbin.org/status/429') - response_2 = graph_client.get('https://httpbin.org/status/503', max_retries=2) - response_3 = graph_client.get('https://httpbin.org/status/504') - response_4 = graph_client.get('https://httpbin.org/status/429', max_retries=1) - - assert response_1.status_code == 429 - assert response_1.request.headers["Retry-Attempt"] == "3" - assert response_2.status_code == 503 - assert response_2.request.headers["Retry-Attempt"] == "2" - assert response_3.status_code == 504 - assert response_3.request.headers["Retry-Attempt"] == "3" - assert response_4.status_code == 429 - assert response_4.request.headers["Retry-Attempt"] == "1" - - -def test_retries_time_limit(graph_client): - """ - Test that the cumulative retry time plus the retry-after values does not exceed the - provided retries time limit - """ - - response = graph_client.get('https://httpbin.org/status/503', retry_time_limit=0.1) - - assert response.status_code == 503 - headers = response.request.headers - with pytest.raises(KeyError): - response.request.headers["retry-attempt"] diff --git a/tests/integration/test_telemetry.py b/tests/integration/test_telemetry.py deleted file mode 100644 index 6346ecef..00000000 --- a/tests/integration/test_telemetry.py +++ /dev/null @@ -1,72 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import platform -import re -import uuid - -import pytest -from azure.identity import EnvironmentCredential - -from msgraph.core import SDK_VERSION, APIVersion, GraphClient, NationalClouds - -BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 - - -@pytest.fixture -def graph_client(): - scopes = ['https://graph.microsoft.com/.default'] - credential = EnvironmentCredential() - client = GraphClient(credential=credential, scopes=scopes) - return client - - -def test_telemetry_handler(graph_client): - """ - Test telemetry handler updates the graph request with the requisite headers - """ - response = graph_client.get('https://graph.microsoft.com/v1.0/users') - system = platform.system() - version = platform.version() - host_os = f'{system} {version}' - python_version = platform.python_version() - runtime_environment = f'Python/{python_version}' - - assert response.status_code == 200 - assert response.request.headers["client-request-id"] - assert response.request.headers["sdkVersion"].startswith('graph-python-core/' + SDK_VERSION) - assert response.request.headers["HostOs"] == host_os - assert response.request.headers["RuntimeEnvironment"] == runtime_environment - - -def test_telemetry_handler_non_graph_url(graph_client): - """ - Test telemetry handler does not updates the request headers for non-graph requests - """ - response = graph_client.get('https://httpbin.org/status/200') - - assert response.status_code == 200 - with pytest.raises(KeyError): - response.request.headers["client-request-id"] - response.request.headers["sdkVersion"] - response.request.headers["HostOs"] - response.request.headers["RuntimeEnvironment"] - - -def test_custom_client_request_id(graph_client): - """ - Test customer provided client request id overrides default value - """ - custom_id = str(uuid.uuid4()) - response = graph_client.get( - 'https://httpbin.org/status/200', headers={"client-request-id": custom_id} - ) - - assert response.status_code == 200 - assert response.request.context.client_request_id == custom_id - with pytest.raises(KeyError): - response.request.headers["client-request-id"] - response.request.headers["sdkVersion"] - response.request.headers["HostOs"] - response.request.headers["RuntimeEnvironment"] diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index b74cfa3b..00000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ diff --git a/tests/unit/test_auth_handler.py b/tests/unit/test_auth_handler.py deleted file mode 100644 index 50b40dcd..00000000 --- a/tests/unit/test_auth_handler.py +++ /dev/null @@ -1,35 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest - -from msgraph.core.middleware.authorization import AuthorizationHandler -from msgraph.core.middleware.request_context import RequestContext - - -def test_context_options_override_default_scopes(): - """ Test scopes found in the request context override default scopes""" - default_scopes = ['.default'] - middleware_control = { - 'scopes': ['email.read'], - } - request_context = RequestContext(middleware_control, {}) - - auth_handler = AuthorizationHandler(None, scopes=default_scopes) - - auth_handler_scopes = auth_handler.get_scopes(request_context) - assert auth_handler_scopes == middleware_control['scopes'] - - -def test_auth_handler_get_scopes_does_not_overwrite_default_scopes(): - default_scopes = ['.default'] - middleware_control = { - 'scopes': ['email.read'], - } - request_context = RequestContext(middleware_control, {}) - auth_handler = AuthorizationHandler(None, scopes=default_scopes) - - auth_handler_scopes = auth_handler.get_scopes(request_context) - - assert auth_handler.scopes == default_scopes diff --git a/tests/unit/test_base_graph_request_adapter.py b/tests/unit/test_base_graph_request_adapter.py new file mode 100644 index 00000000..3b9b1487 --- /dev/null +++ b/tests/unit/test_base_graph_request_adapter.py @@ -0,0 +1,25 @@ +import httpx +import pytest +from asyncmock import AsyncMock +from kiota_abstractions.serialization import ( + ParseNodeFactoryRegistry, + SerializationWriterFactoryRegistry, +) + +from msgraph.core.base_graph_request_adapter import BaseGraphRequestAdapter + + +def test_create_graph_request_adapter(mock_auth_provider): + request_adapter = BaseGraphRequestAdapter(mock_auth_provider) + assert request_adapter._authentication_provider is mock_auth_provider + assert isinstance(request_adapter._parse_node_factory, ParseNodeFactoryRegistry) + assert isinstance( + request_adapter._serialization_writer_factory, SerializationWriterFactoryRegistry + ) + assert isinstance(request_adapter._http_client, httpx.AsyncClient) + assert request_adapter.base_url == '' + + +def test_create_request_adapter_no_auth_provider(): + with pytest.raises(TypeError): + BaseGraphRequestAdapter(None) diff --git a/tests/unit/test_client_factory.py b/tests/unit/test_client_factory.py deleted file mode 100644 index db414b45..00000000 --- a/tests/unit/test_client_factory.py +++ /dev/null @@ -1,78 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -from requests import Session -from requests.adapters import HTTPAdapter - -from msgraph.core import APIVersion, HTTPClientFactory, NationalClouds -from msgraph.core._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_initialize_with_default_config(): - """Test creation of HTTP Client will use the default configuration - if none are passed""" - client = HTTPClientFactory() - - assert client.api_version == APIVersion.v1 - assert client.endpoint == NationalClouds.Global - assert client.timeout == (DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT) - assert isinstance(client.session, Session) - - -def test_initialize_with_custom_config(): - """Test creation of HTTP Client will use custom configuration if they are passed""" - client = HTTPClientFactory(api_version=APIVersion.beta, timeout=(5, 5)) - - assert client.api_version == APIVersion.beta - assert client.endpoint == NationalClouds.Global - assert client.timeout == (5, 5) - assert isinstance(client.session, Session) - - -def test_create_with_default_middleware(): - """Test creation of HTTP Client using default middleware""" - credential = _CustomTokenCredential() - client = HTTPClientFactory().create_with_default_middleware(credential=credential) - middleware = client.get_adapter('https://') - - assert isinstance(middleware, HTTPAdapter) - - -def test_create_with_custom_middleware(): - """Test creation of HTTP Clients with custom middleware""" - credential = _CustomTokenCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory().create_with_custom_middleware(middleware=middleware) - custom_middleware = client.get_adapter('https://') - - assert isinstance(custom_middleware, HTTPAdapter) - - -def test_get_base_url(): - """ - Test base url is formed by combining the national cloud endpoint with - Api version - """ - client = HTTPClientFactory(api_version=APIVersion.beta, cloud=NationalClouds.Germany) - assert client.session.base_url == client.endpoint + '/' + client.api_version - - -def test_register_middleware(): - credential = _CustomTokenCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory() - client._register(middleware) - - assert isinstance(client.session.get_adapter('https://'), HTTPAdapter) - - -class _CustomTokenCredential: - def get_token(self, scopes): - return ['{token:https://graph.microsoft.com/}'] diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py deleted file mode 100644 index b908fa5b..00000000 --- a/tests/unit/test_graph_client.py +++ /dev/null @@ -1,96 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import pytest -import responses -from requests import Session -from requests.adapters import HTTPAdapter - -from msgraph.core import APIVersion, GraphClient, NationalClouds -from msgraph.core.middleware.authorization import AuthorizationHandler - - -def test_graph_client_with_default_middleware(): - """ - Test creating a graph client with default middleware works as expected - """ - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - - assert isinstance(client.graph_session, Session) - assert isinstance(client.graph_session.get_adapter('https://'), HTTPAdapter) - assert client.graph_session.base_url == NationalClouds.Global + '/' + APIVersion.v1 - - -def test_graph_client_with_custom_middleware(): - """ - Test creating a graph client with custom middleware works as expected - """ - credential = _CustomTokenCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = GraphClient(middleware=middleware) - - assert isinstance(client.graph_session, Session) - assert isinstance(client.graph_session.get_adapter('https://'), HTTPAdapter) - assert client.graph_session.base_url == NationalClouds.Global + '/' + APIVersion.v1 - - -def test_graph_client_with_custom_configuration(): - """ - Test creating a graph client with custom middleware works as expected - """ - credential = _CustomTokenCredential() - client = GraphClient( - credential=credential, api_version=APIVersion.beta, cloud=NationalClouds.China - ) - - assert client.graph_session.base_url == NationalClouds.China + '/' + APIVersion.beta - - -def test_graph_client_uses_same_session(): - """ - Test graph client is a singleton class and uses the same session - """ - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - - client2 = GraphClient(credential=credential) - assert client is client2 - - -@responses.activate -def test_graph_client_builds_graph_urls(): - """ - Test that the graph client builds full urls if supplied with partial - """ - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - graph_url = client.graph_session.base_url + '/me' - - responses.add(responses.GET, graph_url, status=200) - - client.get('/me', headers={}) - assert graph_url == responses.calls[0].request.url - - -@responses.activate -def test_does_not_build_graph_urls_for_full_urls(): - """ - Test that the graph client builds full urls if supplied with partial - """ - other_url = 'https://microsoft.com/' - responses.add(responses.GET, other_url, status=200) - - credential = _CustomTokenCredential() - client = GraphClient(credential=credential) - client.get(other_url, headers={}) - request_url = responses.calls[0].request.url - assert other_url == request_url - - -class _CustomTokenCredential: - def get_token(self, scopes): - return ['{token:https://graph.microsoft.com/}'] diff --git a/tests/unit/test_graph_client_factory.py b/tests/unit/test_graph_client_factory.py new file mode 100644 index 00000000..f6746fbd --- /dev/null +++ b/tests/unit/test_graph_client_factory.py @@ -0,0 +1,58 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import httpx +import pytest +from kiota_http.middleware import AsyncKiotaTransport, MiddlewarePipeline, RedirectHandler + +from msgraph.core import APIVersion, GraphClientFactory, NationalClouds +from msgraph.core.middleware.telemetry import GraphTelemetryHandler + + +def test_create_with_default_middleware(): + """Test creation of GraphClient using default middleware""" + client = GraphClientFactory.create_with_default_middleware() + + assert isinstance(client, httpx.AsyncClient) + assert isinstance(client._transport, AsyncKiotaTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline, MiddlewarePipeline) + assert isinstance(pipeline._first_middleware, RedirectHandler) + assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) + + +def test_create_with_custom_middleware(): + """Test creation of HTTP Clients with custom middleware""" + middleware = [ + GraphTelemetryHandler(), + ] + client = GraphClientFactory.create_with_custom_middleware(middleware=middleware) + + assert isinstance(client, httpx.AsyncClient) + assert isinstance(client._transport, AsyncKiotaTransport) + pipeline = client._transport.pipeline + assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) + + +def test_graph_client_factory_with_custom_configuration(): + """ + Test creating a graph client with custom url overrides the default + """ + graph_client = GraphClientFactory.create_with_default_middleware( + api_version=APIVersion.beta, host=NationalClouds.China + ) + assert isinstance(graph_client, httpx.AsyncClient) + assert str(graph_client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' + + +def test_get_base_url(): + """ + Test base url is formed by combining the national cloud endpoint with + Api version + """ + url = GraphClientFactory._get_base_url( + host=NationalClouds.Germany, + api_version=APIVersion.beta, + ) + assert url == f'{NationalClouds.Germany}/{APIVersion.beta}' diff --git a/tests/unit/test_graph_telemetry_handler.py b/tests/unit/test_graph_telemetry_handler.py new file mode 100644 index 00000000..b0a24720 --- /dev/null +++ b/tests/unit/test_graph_telemetry_handler.py @@ -0,0 +1,119 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import platform +import re +import uuid + +import httpx +import pytest + +from msgraph.core import SDK_VERSION, APIVersion, NationalClouds +from msgraph.core._enums import FeatureUsageFlag +from msgraph.core.middleware import GraphRequestContext, GraphTelemetryHandler + +BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 + + +def test_set_request_context_and_feature_usage(mock_request, mock_transport): + telemetry_handler = GraphTelemetryHandler() + telemetry_handler.set_request_context_and_feature_usage(mock_request, mock_transport) + + assert hasattr(mock_request, 'context') + assert mock_request.context.feature_usage == hex( + FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + ) + + +def test_is_graph_url(mock_graph_request): + """ + Test method that checks whether a request url is a graph endpoint + """ + telemetry_handler = GraphTelemetryHandler() + assert telemetry_handler.is_graph_url(mock_graph_request.url) + + +def test_is_not_graph_url(mock_request): + """ + Test method that checks whether a request url is a graph endpoint with a + non-graph url. + """ + telemetry_handler = GraphTelemetryHandler() + assert not telemetry_handler.is_graph_url(mock_request.url) + + +def test_add_client_request_id_header(mock_graph_request): + """ + Test that client_request_id is added to the request headers + """ + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_client_request_id_header(mock_graph_request) + + assert 'client-request-id' in mock_graph_request.headers + assert _is_valid_uuid(mock_graph_request.headers.get('client-request-id')) + + +def test_custom_client_request_id_header(): + """ + Test that a custom client request id is used, if provided + """ + custom_id = str(uuid.uuid4()) + request = httpx.Request('GET', BASE_URL) + request.context = GraphRequestContext({}, {'client-request-id': custom_id}) + + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_client_request_id_header(request) + + assert 'client-request-id' in request.headers + assert _is_valid_uuid(request.headers.get('client-request-id')) + assert request.headers.get('client-request-id') == custom_id + + +def test_append_sdk_version_header(mock_graph_request): + """ + Test that sdkVersion is added to the request headers + """ + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._append_sdk_version_header(mock_graph_request) + + assert 'sdkVersion' in mock_graph_request.headers + assert mock_graph_request.headers.get('sdkVersion' + ).startswith('graph-python-core/' + SDK_VERSION) + + +def test_add_host_os_header(mock_graph_request): + """ + Test that HostOs is added to the request headers + """ + system = platform.system() + version = platform.version() + host_os = f'{system} {version}' + + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_host_os_header(mock_graph_request) + + assert 'HostOs' in mock_graph_request.headers + assert mock_graph_request.headers.get('HostOs') == host_os + + +def test_add_runtime_environment_header(mock_graph_request): + """ + Test that RuntimeEnvironment is added to the request headers + """ + python_version = platform.python_version() + runtime_environment = f'Python/{python_version}' + + telemetry_handler = GraphTelemetryHandler() + telemetry_handler._add_runtime_environment_header(mock_graph_request) + + assert 'RuntimeEnvironment' in mock_graph_request.headers + assert mock_graph_request.headers.get('RuntimeEnvironment') == runtime_environment + + +def _is_valid_uuid(guid): + regex = "^[{]?[0-9a-fA-F]{8}" + "-([0-9a-fA-F]{4}-)" + "{3}[0-9a-fA-F]{12}[}]?$" + pattern = re.compile(regex) + if re.search(pattern, guid): + return True + return False diff --git a/tests/unit/test_middleware_pipeline.py b/tests/unit/test_middleware_pipeline.py deleted file mode 100644 index e4560c37..00000000 --- a/tests/unit/test_middleware_pipeline.py +++ /dev/null @@ -1,89 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from collections import OrderedDict -from unittest import TestCase - -from msgraph.core.middleware.middleware import BaseMiddleware, MiddlewarePipeline - - -class MiddlewarePipelineTest(TestCase): - def test_adds_middlewares_in_order(self): - middleware_pipeline = MiddlewarePipeline() - middleware_pipeline.add_middleware(MockRequestMiddleware1()) - middleware_pipeline.add_middleware(MockRequestMiddleware2()) - - first_middleware = middleware_pipeline._first_middleware - second_middleware = middleware_pipeline._first_middleware.next - - self.assertIsInstance(first_middleware, MockRequestMiddleware1) - self.assertIsInstance(second_middleware, MockRequestMiddleware2) - - def test_request_object_is_modified_in_order(self): - middleware_pipeline = MiddlewarePipeline() - middleware_pipeline.add_middleware(MockRequestMiddleware1()) - middleware_pipeline.add_middleware(MockRequestMiddleware2()) - - request = OrderedDict() - request.headers = {} - result = middleware_pipeline.send(request) - - second, _ = result.popitem() - first, _ = result.popitem() - - self.assertEqual(second, 'middleware2') - self.assertEqual(first, 'middleware1') - - def test_response_object_is_modified_in_reverse_order(self): - middleware_pipeline = MiddlewarePipeline() - middleware_pipeline.add_middleware( - MockResponseMiddleware1() - ) # returns world as the response - middleware_pipeline.add_middleware( - MockResponseMiddleware2() - ) # returns hello as the response - - # Responses are passed through the list of middlewares in reverse order. - # This will return hello world - request = OrderedDict() - request.headers = {} - resp = middleware_pipeline.send(request) - - self.assertEqual(resp, 'Hello World') - - -class MockRequestMiddleware1(BaseMiddleware): - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - request['middleware1'] = 1 - return super().send(request, **kwargs) - - -class MockRequestMiddleware2(BaseMiddleware): - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - request['middleware2'] = 2 - return request - - -class MockResponseMiddleware1(BaseMiddleware): - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - resp = super().send(request, **kwargs) - resp += 'World' - return resp - - -class MockResponseMiddleware2(BaseMiddleware): - def __init__(self): - super().__init__() - - def send(self, request, **kwargs): - return 'Hello ' diff --git a/tests/unit/test_retry_handler.py b/tests/unit/test_retry_handler.py deleted file mode 100644 index 9201801c..00000000 --- a/tests/unit/test_retry_handler.py +++ /dev/null @@ -1,192 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from email.utils import formatdate -from time import time - -import pytest -import requests -import responses - -from msgraph.core import APIVersion, NationalClouds -from msgraph.core.middleware.retry import RetryHandler - -BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 - - -def test_no_config(): - """ - Test that default values are used if no custom confguration is passed - """ - retry_handler = RetryHandler() - assert retry_handler.max_retries == retry_handler.DEFAULT_MAX_RETRIES - assert retry_handler.timeout == retry_handler.MAX_DELAY - assert retry_handler.backoff_max == retry_handler.MAXIMUM_BACKOFF - assert retry_handler.backoff_factor == retry_handler.DEFAULT_BACKOFF_FACTOR - assert retry_handler._allowed_methods == frozenset( - ['HEAD', 'GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS'] - ) - assert retry_handler._respect_retry_after_header - assert retry_handler._retry_on_status_codes == retry_handler._DEFAULT_RETRY_STATUS_CODES - - -def test_custom_config(): - """ - Test that default configuration is overrriden if custom configuration is provided - """ - retry_handler = RetryHandler( - max_retries=10, - retry_backoff_factor=0.2, - retry_backoff_max=200, - retry_time_limit=100, - retry_on_status_codes=[502, 503] - ) - - assert retry_handler.max_retries == 10 - assert retry_handler.timeout == 100 - assert retry_handler.backoff_max == 200 - assert retry_handler.backoff_factor == 0.2 - assert retry_handler._retry_on_status_codes == {429, 502, 503, 504} - - -def test_disable_retries(): - """ - Test that when disable_retries class method is called, total retries are set to zero - """ - retry_handler = RetryHandler() - retry_handler = retry_handler.disable_retries() - assert retry_handler.max_retries == 0 - retry_options = retry_handler.get_retry_options({}) - assert not retry_handler.check_retry_valid(retry_options, 0) - - -@responses.activate -def test_method_retryable_with_valid_method(): - """ - Test if method is retryable with a retryable request method. - """ - responses.add(responses.GET, BASE_URL, status=502) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert retry_handler._is_method_retryable(settings, response.request) - - -@responses.activate -def test_should_retry_valid(): - """ - Test the should_retry method with a valid HTTP method and response code - """ - responses.add(responses.GET, BASE_URL, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert retry_handler.should_retry(settings, response) - - -@responses.activate -def test_should_retry_invalid(): - """ - Test the should_retry method with an valid HTTP response code - """ - responses.add(responses.GET, BASE_URL, status=502) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert not retry_handler.should_retry(settings, response) - - -@responses.activate -def test_is_request_payload_buffered_valid(): - """ - Test for _is_request_payload_buffered helper method. - Should return true request payload is buffered/rewindable. - """ - responses.add(responses.GET, BASE_URL, status=429) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._is_request_payload_buffered(response) - - -@responses.activate -def test_is_request_payload_buffered_invalid(): - """ - Test for _is_request_payload_buffered helper method. - Should return false if request payload is forward streamed. - """ - responses.add(responses.POST, BASE_URL, status=429) - response = requests.post(BASE_URL, headers={'Content-Type': "application/octet-stream"}) - - retry_handler = RetryHandler() - - assert not retry_handler._is_request_payload_buffered(response) - - -def test_check_retry_valid(): - """ - Test that a retry is valid if the maximum number of retries has not been reached - """ - retry_handler = RetryHandler() - settings = retry_handler.get_retry_options({}) - - assert retry_handler.check_retry_valid(settings, 0) - - -def test_check_retry_valid_no_retries(): - """ - Test that a retry is not valid if maximum number of retries has been reached - """ - retry_handler = RetryHandler(max_retries=2) - settings = retry_handler.get_retry_options({}) - - assert not retry_handler.check_retry_valid(settings, 2) - - -@responses.activate -def test_get_retry_after(): - """ - Test the _get_retry_after method with an integer value for retry header. - """ - responses.add(responses.GET, BASE_URL, headers={'Retry-After': "120"}, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._get_retry_after(response) == 120 - - -@responses.activate -def test_get_retry_after_no_header(): - """ - Test the _get_retry_after method with no Retry-After header. - """ - responses.add(responses.GET, BASE_URL, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._get_retry_after(response) is None - - -@responses.activate -def test_get_retry_after_http_date(): - """ - Test the _get_retry_after method with a http date as Retry-After value. - """ - timevalue = time() + 120 - http_date = formatdate(timeval=timevalue, localtime=False, usegmt=True) - responses.add(responses.GET, BASE_URL, headers={'retry-after': f'{http_date}'}, status=503) - response = requests.get(BASE_URL) - - retry_handler = RetryHandler() - - assert retry_handler._get_retry_after(response) < 120 diff --git a/tests/unit/test_telemetry_handler.py b/tests/unit/test_telemetry_handler.py deleted file mode 100644 index 0d58eb93..00000000 --- a/tests/unit/test_telemetry_handler.py +++ /dev/null @@ -1,146 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import platform -import re -import uuid - -import pytest -import requests -import responses - -from msgraph.core import SDK_VERSION, APIVersion, GraphClient, NationalClouds -from msgraph.core.middleware.request_context import RequestContext -from msgraph.core.middleware.telemetry import TelemetryHandler - -BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 - - -@responses.activate -def test_is_graph_url(): - """ - Test method that checks whether a request url is a graph endpoint - """ - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - - telemetry_handler = TelemetryHandler() - assert telemetry_handler.is_graph_url(request.url) - - -@responses.activate -def test_is_not_graph_url(): - """ - Test method that checks whether a request url is a graph endpoint with a - non-graph url - """ - responses.add(responses.GET, 'https://httpbin.org/status/200') - response = requests.get('https://httpbin.org/status/200') - request = response.request - - telemetry_handler = TelemetryHandler() - assert not telemetry_handler.is_graph_url(request.url) - - -@responses.activate -def test_add_client_request_id_header(): - """ - Test that client_request_id is added to the request headers - """ - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_client_request_id_header(request) - - assert 'client-request-id' in request.headers - assert _is_valid_uuid(request.headers.get('client-request-id')) - - -@responses.activate -def test_custom_client_request_id_header(): - """ - Test that a custom client request id is used, if provided - """ - custom_id = str(uuid.uuid4()) - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {'client-request-id': custom_id}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_client_request_id_header(request) - - assert 'client-request-id' in request.headers - assert _is_valid_uuid(request.headers.get('client-request-id')) - assert request.headers.get('client-request-id') == custom_id - - -@responses.activate -def test_append_sdk_version_header(): - """ - Test that sdkVersion is added to the request headers - """ - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._append_sdk_version_header(request) - - assert 'sdkVersion' in request.headers - assert request.headers.get('sdkVersion').startswith('graph-python-core/' + SDK_VERSION) - - -@responses.activate -def test_add_host_os_header(): - """ - Test that HostOs is added to the request headers - """ - system = platform.system() - version = platform.version() - host_os = f'{system} {version}' - - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_host_os_header(request) - - assert 'HostOs' in request.headers - assert request.headers.get('HostOs') == host_os - - -@responses.activate -def test_add_runtime_environment_header(): - """ - Test that RuntimeEnvironment is added to the request headers - """ - python_version = platform.python_version() - runtime_environment = f'Python/{python_version}' - - responses.add(responses.GET, BASE_URL) - response = requests.get(BASE_URL) - request = response.request - request.context = RequestContext({}, {}) - - telemetry_handler = TelemetryHandler() - telemetry_handler._add_runtime_environment_header(request) - - assert 'RuntimeEnvironment' in request.headers - assert request.headers.get('RuntimeEnvironment') == runtime_environment - - -def _is_valid_uuid(guid): - regex = "^[{]?[0-9a-fA-F]{8}" + "-([0-9a-fA-F]{4}-)" + "{3}[0-9a-fA-F]{12}[}]?$" - pattern = re.compile(regex) - if re.search(pattern, guid): - return True - return False