From a87b64a069ad287d8746cfd0e924607976024c46 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Mon, 17 Oct 2022 18:05:51 +0300 Subject: [PATCH 01/40] Add kiota core dependencies --- Pipfile | 4 +- Pipfile.lock | 370 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 360 insertions(+), 14 deletions(-) diff --git a/Pipfile b/Pipfile index 8f634a08..eed51a53 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,9 @@ verify_ssl = true name = "pypi" [packages] # Packages required to run the application +microsoft-kiota-abstractions = '*' microsoft-kiota-http = "*" +microsoft-kiota-authentication-azure = '*' httpx = {version = "==0.23.0", extras = ["http2"]} [dev-packages] # Packages required to develop the application @@ -20,4 +22,4 @@ toml = "==0.10.2" pytest-asyncio = "==0.19.0" pytest-mock = "==3.10.0" asyncmock = "==0.4.2" -azure-identity = "*" +azure-identity = "*" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index 65c876a2..99e2c295 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "adadae68a10208010ac5ad153524834f5a09579e7380a7b11c1453b26d0352b6" + "sha256": "1686db8ab300430bfaaf36688f3e5a76bc4648883b26e2c17757ca03cba5c12e" }, "pipfile-spec": 6, "requires": {}, @@ -14,6 +14,92 @@ ] }, "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", @@ -22,6 +108,29 @@ "markers": "python_full_version >= '3.6.2'", "version": "==3.6.1" }, + "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": [ "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", @@ -30,6 +139,79 @@ "markers": "python_version >= '3.6'", "version": "==2022.9.24" }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_version >= '3.6'", + "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", @@ -93,6 +275,15 @@ "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": { @@ -103,6 +294,79 @@ "index": "pypi", "version": "==0.1.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": [ "idna2008" @@ -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,6 +400,79 @@ ], "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": { @@ -157,11 +502,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": [ @@ -651,7 +995,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": { @@ -695,18 +1039,18 @@ }, "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": [ @@ -721,7 +1065,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": { From 1f7dcab41b78841f6dadae6e90c75c125cf56053 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Mon, 17 Oct 2022 18:08:15 +0300 Subject: [PATCH 02/40] Update auth handler to new middleware format --- msgraph/core/middleware/__init__.py | 3 ++ msgraph/core/middleware/authorization.py | 36 +++++++++--------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/msgraph/core/middleware/__init__.py b/msgraph/core/middleware/__init__.py index b74cfa3b..a9186b8c 100644 --- a/msgraph/core/middleware/__init__.py +++ b/msgraph/core/middleware/__init__.py @@ -2,3 +2,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +from .authorization import AuthorizationHandler +from .middleware import GraphMiddlewarePipeline +from .telemetry import TelemetryHandler diff --git a/msgraph/core/middleware/authorization.py b/msgraph/core/middleware/authorization.py index 43884d67..2547bc7a 100644 --- a/msgraph/core/middleware/authorization.py +++ b/msgraph/core/middleware/authorization.py @@ -2,35 +2,25 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import httpx +from kiota_abstractions.authentication import AuthenticationProvider +from kiota_http.middleware.middleware import BaseMiddleware + from .._enums import FeatureUsageFlag -from .abc_token_credential import TokenCredential -from .middleware import BaseMiddleware class AuthorizationHandler(BaseMiddleware): - def __init__(self, credential: TokenCredential, **kwargs): + + def __init__(self, auth_provider: AuthenticationProvider): super().__init__() - self.credential = credential - self.scopes = kwargs.get("scopes", ['.default']) - self.retry_count = 0 + self.auth_provider = auth_provider - def send(self, request, **kwargs): + async def send( + self, request: httpx.Request, transport: httpx.AsyncBaseTransport + ) -> httpx.Response: context = request.context - request.headers.update( - {'Authorization': 'Bearer {}'.format(self._get_access_token(context))} - ) + token = await self.auth_provider.get_authorization_token(str(request.url)) + request.headers.update({'Authorization': f'Bearer {token}'}) 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) + response = await super().send(request, transport) 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) From 0ce79a29242ee2ca3d04568fdc8d2224f39886ec Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Mon, 17 Oct 2022 18:11:34 +0300 Subject: [PATCH 03/40] Update telemetry handler --- msgraph/core/middleware/telemetry.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/msgraph/core/middleware/telemetry.py b/msgraph/core/middleware/telemetry.py index 9db9b707..1548515b 100644 --- a/msgraph/core/middleware/telemetry.py +++ b/msgraph/core/middleware/telemetry.py @@ -1,25 +1,30 @@ import platform +import httpx +from kiota_http.middleware.middleware import BaseMiddleware from urllib3.util import parse_url from .._constants import SDK_VERSION from .._enums import NationalClouds -from .middleware import BaseMiddleware class TelemetryHandler(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): - if self.is_graph_url(request.url): + async def send( + self, request: httpx.Request, transport: httpx.AsyncBaseTransport + ) -> httpx.Response: + """Adds telemetry headers and sends the http request. + """ + if self.is_graph_url(str(request.url)): self._add_client_request_id_header(request) self._append_sdk_version_header(request) 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 is_graph_url(self, url): @@ -28,17 +33,12 @@ def is_graph_url(self, url): endpoints = set(item.value for item in NationalClouds) base_url = parse_url(url) - endpoint = "{}://{}".format( - base_url.scheme, - base_url.netloc, - ) + 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 From 71baca9cbf4d55d4ad14178edb510344ee0884c2 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Mon, 17 Oct 2022 18:13:19 +0300 Subject: [PATCH 04/40] Code formatting changes --- msgraph/core/middleware/abc_token_credential.py | 11 ----------- msgraph/core/middleware/request_context.py | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 msgraph/core/middleware/abc_token_credential.py 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/request_context.py b/msgraph/core/middleware/request_context.py index ff44b23a..2e825c33 100644 --- a/msgraph/core/middleware/request_context.py +++ b/msgraph/core/middleware/request_context.py @@ -13,6 +13,7 @@ class RequestContext: of middleware as well as a FeatureUsage  property to keep track of middleware used in making the request. """ + def __init__(self, middleware_control, headers): """Constructor for request context instances From 6a28617210c559c4c3c76f086add153432534b6e Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Mon, 17 Oct 2022 18:14:19 +0300 Subject: [PATCH 05/40] Update middleware pipeline to take dependency on kiota core --- msgraph/core/middleware/middleware.py | 60 ++++++--------------------- 1 file changed, 13 insertions(+), 47 deletions(-) diff --git a/msgraph/core/middleware/middleware.py b/msgraph/core/middleware/middleware.py index ac31134f..fd0bf0ae 100644 --- a/msgraph/core/middleware/middleware.py +++ b/msgraph/core/middleware/middleware.py @@ -3,64 +3,30 @@ # Licensed under the MIT License. # ------------------------------------ import json -import ssl -from requests.adapters import HTTPAdapter -from urllib3 import PoolManager +from kiota_http.middleware import MiddlewarePipeline 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/ +class GraphMiddlewarePipeline(MiddlewarePipeline): + """Entry point of graph specific middleware + The pipeline is implemented as a linked-list """ - 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): + async def send(self, request): - middleware_control_json = request.headers.pop('middleware_control', None) - if middleware_control_json: - middleware_control = json.loads(middleware_control_json) + middleware_control_string = request.headers.pop('middleware_control', None) + if middleware_control_string: + middleware_control = json.loads(middleware_control_string) 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) + return await self._first_middleware.send(request, self._transport) + # No middleware in pipeline, delete request optoions from header and + # send the request + del request.headers['request_options'] + return await self._transport.handle_async_request(request) From a8efe82875b43c62c1a60d536ff1d38179375564 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Mon, 17 Oct 2022 18:15:08 +0300 Subject: [PATCH 06/40] Refactor graph client and factory --- msgraph/core/__init__.py | 4 +- msgraph/core/_client_factory.py | 94 -------- .../{_graph_client.py => graph_client.py} | 226 +++++++++++++++--- msgraph/core/graph_client_factory.py | 104 ++++++++ 4 files changed, 295 insertions(+), 133 deletions(-) delete mode 100644 msgraph/core/_client_factory.py rename msgraph/core/{_graph_client.py => graph_client.py} (50%) create mode 100644 msgraph/core/graph_client_factory.py diff --git a/msgraph/core/__init__.py b/msgraph/core/__init__.py index e726d590..a425d0c7 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 .graph_client import GraphClient +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 similarity index 50% rename from msgraph/core/_graph_client.py rename to msgraph/core/graph_client.py index 8b70c379..82a3af20 100644 --- a/msgraph/core/_graph_client.py +++ b/msgraph/core/graph_client.py @@ -3,10 +3,14 @@ # Licensed under the MIT License. # ------------------------------------ import json +from typing import List -from requests import Request, Session +import httpx +from kiota_abstractions.authentication import AuthenticationProvider +from kiota_http.middleware.middleware import BaseMiddleware -from ._client_factory import HTTPClientFactory +from ._enums import APIVersion, NationalClouds +from .graph_client_factory import GraphClientFactory from .middleware.request_context import RequestContext # These are middleware options that can be configured per request. @@ -29,6 +33,7 @@ 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() @@ -53,7 +58,7 @@ class GraphClient: :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 + :keyword list middleware: 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. :keyword enum api_version: The Microsoft Graph API version to be used, for example @@ -67,8 +72,10 @@ class GraphClient: 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 + :keyword client: A custom client instance from the python httpx library """ + DEFAULT_CONNECTION_TIMEOUT: int = 30 + DEFAULT_REQUEST_TIMEOUT: int = 100 __instance = None def __new__(cls, *args, **kwargs): @@ -76,41 +83,102 @@ def __new__(cls, *args, **kwargs): GraphClient.__instance = object.__new__(cls) return GraphClient.__instance - def __init__(self, **kwargs): + def __init__( + self, + auth_provider: AuthenticationProvider = None, + api_version: APIVersion = APIVersion.v1, + endpoint: NationalClouds = NationalClouds.Global, + timeout: httpx.Timeout = httpx.Timeout( + DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT + ), + client: httpx.AsyncClient = None, + middleware: List[BaseMiddleware] = None + ): """ Class constructor that accepts a session object and kwargs to - be passed to the HTTPClientFactory + be passed to the GraphClientFactory """ - self.graph_session = self.get_graph_session(**kwargs) + self.client = self.get_graph_client( + auth_provider, api_version, endpoint, timeout, client, middleware + ) @collect_options - def get(self, url: str, **kwargs): + async def get( + self, + url, + *, + params=None, + headers=None, + cookies=None, + middleware_control=None, + extensions=None + ): 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) + return await self.client.get( + url, params=params, headers=headers, cookies=cookies, extensions=extensions + ) - def options(self, url, **kwargs): + @collect_options + async def options( + self, + url, + *, + params=None, + headers=None, + cookies=None, + middleware_control=None, + extensions=None + ): 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) + return await self.client.options( + url, params=params, headers=headers, cookies=cookies, extensions=extensions + ) - def head(self, url, **kwargs): + @collect_options + async def head( + self, + url, + *, + params=None, + headers=None, + cookies=None, + middleware_control=None, + extensions=None + ): 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) + return await self.client.head( + url, params=params, headers=headers, cookies=cookies, extensions=extensions + ) - def post(self, url, data=None, json=None, **kwargs): + @collect_options + async def post( + self, + url, + *, + content=None, + data=None, + files=None, + json=None, + params=None, + headers=None, + cookies=None, + middleware_control=None, + extensions=None + ): 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 @@ -119,9 +187,34 @@ def post(self, url, data=None, json=None, **kwargs): :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - return self.graph_session.post(self._graph_url(url), data=data, json=json, **kwargs) + return await self.client.post( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + middleware_control=middleware_control, + extensions=extensions + ) - def put(self, url, data=None, **kwargs): + @collect_options + async def put( + self, + url, + *, + content=None, + data=None, + files=None, + json=None, + params=None, + headers=None, + cookies=None, + middleware_control=None, + extensions=None + ): 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 @@ -130,9 +223,34 @@ def put(self, url, data=None, **kwargs): :rtype: requests.Response """ - return self.graph_session.put(self._graph_url(url), data=data, **kwargs) + return await self.client.put( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + middleware_control=middleware_control, + extensions=extensions + ) - def patch(self, url, data=None, **kwargs): + @collect_options + async def patch( + self, + url, + *, + content=None, + data=None, + files=None, + json=None, + params=None, + headers=None, + cookies=None, + middleware_control=None, + extensions=None + ): 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 @@ -140,37 +258,71 @@ def patch(self, url, data=None, **kwargs): :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - return self.graph_session.patch(self._graph_url(url), data=data, **kwargs) + return await self.client.patch( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + middleware_control=middleware_control, + extensions=extensions + ) - def delete(self, url, **kwargs): + @collect_options + async def delete( + self, + url, + *, + params=None, + headers=None, + cookies=None, + middleware_control=None, + extensions=None + ): 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 + return await self.client.delete( + url, + params=params, + headers=headers, + cookies=cookies, + middleware_control=middleware_control, + extensions=extensions + ) + + # 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): + def get_graph_client( + auth_provider: AuthenticationProvider, + api_version: APIVersion, + endpoint: NationalClouds, + timeout: httpx.Timeout, + client: httpx.AsyncClient, + middleware: BaseMiddleware, + ): """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: + if auth_provider and middleware: raise ValueError( "Invalid parameters! Both TokenCredential and middleware cannot be passed" ) - if not credential and not middleware: + if not auth_provider 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) + if auth_provider: + return GraphClientFactory(api_version, endpoint, timeout, + client).create_with_default_middleware(auth_provider) + # return GraphClientFactory( + # **kwargs).create_with_custom_middleware(middleware) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py new file mode 100644 index 00000000..50894adf --- /dev/null +++ b/msgraph/core/graph_client_factory.py @@ -0,0 +1,104 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import httpx +from kiota_abstractions.authentication import AuthenticationProvider +from kiota_http.kiota_client_factory import KiotaClientFactory +from kiota_http.middleware import AsyncKiotaTransport + +from ._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT +from ._enums import APIVersion, NationalClouds +from .middleware import AuthorizationHandler, GraphMiddlewarePipeline, TelemetryHandler + + +class GraphClientFactory(KiotaClientFactory): + """Constructs httpx AsyncClient 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, + api_version: APIVersion, + endpoint: NationalClouds, + timeout: httpx.Timeout, + client: httpx.AsyncClient, + ): + """Class constructor that accepts a user provided session object and kwargs + to configure the request handling behaviour of the client""" + self.api_version = api_version + self.endpoint = endpoint + self.timeout = timeout + self.client = client + + def create_with_default_middleware( + self, auth_provider: AuthenticationProvider + ) -> 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 + """ + graph_async_client = httpx.AsyncClient( + base_url=self._get_base_url(), timeout=self.timeout, http2=True + ) + current_transport = graph_async_client._transport + middleware = self._get_default_middleware(auth_provider, current_transport) + + graph_async_client._transport = AsyncKiotaTransport( + transport=current_transport, middleware=middleware + ) + return graph_async_client + + # 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 _get_base_url(self): + """Helper method to set the base url""" + base_url = self.endpoint + '/' + self.api_version + return base_url + + # def _get_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 _get_default_middleware( + self, auth_provider: AuthenticationProvider, transport: httpx.AsyncBaseTransport + ) -> GraphMiddlewarePipeline: + """ + Helper method that constructs a middleware_pipeline with the specified middleware + """ + middleware_pipeline = GraphMiddlewarePipeline(transport) + middleware = [AuthorizationHandler(auth_provider), TelemetryHandler()] + for ware in middleware: + middleware_pipeline.add_middleware(ware) + + return middleware_pipeline From 6b34cf065e3d2b0120ed30ee1381c777e6c0bfc0 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 19 Oct 2022 21:05:00 +0300 Subject: [PATCH 07/40] Update existing middleware --- msgraph/core/middleware/__init__.py | 6 ++-- msgraph/core/middleware/authorization.py | 22 ++++++++++++--- msgraph/core/middleware/middleware.py | 36 ++++++++++++++++-------- msgraph/core/middleware/telemetry.py | 5 ++-- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/msgraph/core/middleware/__init__.py b/msgraph/core/middleware/__init__.py index a9186b8c..42e0c0ac 100644 --- a/msgraph/core/middleware/__init__.py +++ b/msgraph/core/middleware/__init__.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from .authorization import AuthorizationHandler +from .authorization import GraphAuthorizationHandler from .middleware import GraphMiddlewarePipeline -from .telemetry import TelemetryHandler +from .redirect import GraphRedirectHandler +from .retry import GraphRetryHandler +from .telemetry import GraphTelemetryHandler diff --git a/msgraph/core/middleware/authorization.py b/msgraph/core/middleware/authorization.py index 2547bc7a..81ff4324 100644 --- a/msgraph/core/middleware/authorization.py +++ b/msgraph/core/middleware/authorization.py @@ -2,25 +2,39 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +from typing import TypeVar + import httpx from kiota_abstractions.authentication import AuthenticationProvider from kiota_http.middleware.middleware import BaseMiddleware from .._enums import FeatureUsageFlag +from .middleware import GraphRequest -class AuthorizationHandler(BaseMiddleware): +class GraphAuthorizationHandler(BaseMiddleware): + """ + Transparently authorize requests by adding authorization header to the request + """ def __init__(self, auth_provider: AuthenticationProvider): + """Constructor for authorization handler + + Args: + auth_provider (AuthenticationProvider): AuthorizationProvider instance + that will be used to fetch the token + """ super().__init__() + self.auth_provider = auth_provider async def send( - self, request: httpx.Request, transport: httpx.AsyncBaseTransport + self, request: GraphRequest, transport: httpx.AsyncBaseTransport ) -> httpx.Response: - context = request.context + + request.context.feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED + token = await self.auth_provider.get_authorization_token(str(request.url)) request.headers.update({'Authorization': f'Bearer {token}'}) - context.set_feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED response = await super().send(request, transport) return response diff --git a/msgraph/core/middleware/middleware.py b/msgraph/core/middleware/middleware.py index fd0bf0ae..9d11e606 100644 --- a/msgraph/core/middleware/middleware.py +++ b/msgraph/core/middleware/middleware.py @@ -4,29 +4,41 @@ # ------------------------------------ import json +import httpx from kiota_http.middleware import MiddlewarePipeline from .request_context import RequestContext +class GraphRequest(httpx.Request): + """Http Request object with a custom request context + """ + context: RequestContext + + class GraphMiddlewarePipeline(MiddlewarePipeline): - """Entry point of graph specific middleware - The pipeline is implemented as a linked-list + """Chain of graph specific middleware that process requests in the same order + and responses in reverse order to requests. The pipeline is implemented as a linked-list """ - async def send(self, request): + async def send(self, request: GraphRequest) -> httpx.Response: + """Passes the request to the next middleware if available or makes a network request + + Args: + request (httpx.Request): The http request + + Returns: + httpx.Response: The http response + """ - middleware_control_string = request.headers.pop('middleware_control', None) - if middleware_control_string: - middleware_control = json.loads(middleware_control_string) - else: - middleware_control = dict() + request_options = {} + options = request.headers.pop('request_options', None) + if options: + request_options = json.loads(options) - request.context = RequestContext(middleware_control, request.headers) + request.context = RequestContext(request_options, request.headers) if self._middleware_present(): return await self._first_middleware.send(request, self._transport) - # No middleware in pipeline, delete request optoions from header and - # send the request - del request.headers['request_options'] + # No middleware in pipeline, send the request. return await self._transport.handle_async_request(request) diff --git a/msgraph/core/middleware/telemetry.py b/msgraph/core/middleware/telemetry.py index 1548515b..7aaea99e 100644 --- a/msgraph/core/middleware/telemetry.py +++ b/msgraph/core/middleware/telemetry.py @@ -6,15 +6,16 @@ from .._constants import SDK_VERSION from .._enums import NationalClouds +from .middleware import GraphRequest -class TelemetryHandler(BaseMiddleware): +class GraphTelemetryHandler(BaseMiddleware): """Middleware component that attaches metadata to a Graph request in order to help the SDK team improve the developer experience. """ async def send( - self, request: httpx.Request, transport: httpx.AsyncBaseTransport + self, request: GraphRequest, transport: httpx.AsyncBaseTransport ) -> httpx.Response: """Adds telemetry headers and sends the http request. """ From b1e8dfb9073f3b67cfaaafadbae2e31bdf655f0e Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 19 Oct 2022 21:05:13 +0300 Subject: [PATCH 08/40] Add redirect handler --- msgraph/core/middleware/redirect.py | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 msgraph/core/middleware/redirect.py diff --git a/msgraph/core/middleware/redirect.py b/msgraph/core/middleware/redirect.py new file mode 100644 index 00000000..b68eba90 --- /dev/null +++ b/msgraph/core/middleware/redirect.py @@ -0,0 +1,30 @@ +import httpx +from kiota_http.middleware import RedirectHandler + +from .._enums import FeatureUsageFlag +from .middleware import GraphRequest + + +class GraphRedirectHandler(RedirectHandler): + """Middleware designed to handle 3XX responses transparently + """ + + async def send( + self, request: GraphRequest, transport: httpx.AsyncBaseTransport + ) -> httpx.Response: + """Sends the http request object to the next middleware or redirects + the request if necessary. + """ + request.context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + + retryable = True + while retryable: + response = await super().send(request, transport) + redirect_location = self.get_redirect_location(response) + if redirect_location and self.should_redirect: + retryable = self.increment(response) + request = response.next_request + continue + return response + + raise Exception(f"Too many redirects. {response.history}") From e1e5bfa7025457c07a64032e0f95ef503c6588d0 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 19 Oct 2022 21:05:26 +0300 Subject: [PATCH 09/40] Add a retry handler --- msgraph/core/middleware/retry.py | 207 +++---------------------------- 1 file changed, 17 insertions(+), 190 deletions(-) diff --git a/msgraph/core/middleware/retry.py b/msgraph/core/middleware/retry.py index 814b27a8..214e99c2 100644 --- a/msgraph/core/middleware/retry.py +++ b/msgraph/core/middleware/retry.py @@ -1,221 +1,48 @@ -import datetime -import random import time -from email.utils import parsedate_to_datetime + +import httpx +from kiota_http.middleware import RetryHandler from .._enums import FeatureUsageFlag -from .middleware import BaseMiddleware +from .middleware import GraphRequest -class RetryHandler(BaseMiddleware): +class GraphRetryHandler(RetryHandler): """ - 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. + Middleware that handles failed requests """ - 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): + async def send(self, request: GraphRequest, transport: httpx.AsyncBaseTransport): """ 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 + retry_valid = self.retries_allowed + + request.context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED while retry_valid: start_time = time.time() if retry_count > 0: - request.headers.update({'retry-attempt': '{}'.format(retry_count)}) - response = super().send(request, **kwargs) + request.headers.update({'retry-attempt': f'{retry_count}'}) + response = await super().send(request, transport) # Check if the request needs to be retried based on the response method # and status code - if self.should_retry(retry_options, response): + if self.should_retry(request, response): # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_options, retry_count) + retry_valid = self.check_retry_valid(retry_count) # Get the delay time between retries - delay = self.get_delay_time(retry_options, retry_count, response) + delay = self.get_delay_time(retry_count, response) - if retry_valid and delay < absolute_time_limit: + if retry_valid and delay < self.timeout: time.sleep(delay) end_time = time.time() - absolute_time_limit -= (end_time - start_time) + self.timeout -= (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) From 50430fa409794902d34d8df693aa681b8f9cffe0 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 19 Oct 2022 21:05:58 +0300 Subject: [PATCH 10/40] Update request context with types --- msgraph/core/middleware/request_context.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/msgraph/core/middleware/request_context.py b/msgraph/core/middleware/request_context.py index 2e825c33..f03fe0ea 100644 --- a/msgraph/core/middleware/request_context.py +++ b/msgraph/core/middleware/request_context.py @@ -4,17 +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 + """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: @@ -27,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 From dbd531b7d531a6d6e5f5b3df36e31ca826a67576 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 19 Oct 2022 21:06:13 +0300 Subject: [PATCH 11/40] Remove unnecessary files --- .vscode/settings.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .vscode/settings.json 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 From 8701a8fcd12c11d98dbb753f1ad2b3cd1de0b021 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 19 Oct 2022 21:07:51 +0300 Subject: [PATCH 12/40] Harmonize client and factory factory to --- msgraph/core/graph_client.py | 185 +++++++++++---------------- msgraph/core/graph_client_factory.py | 69 ++++++---- 2 files changed, 119 insertions(+), 135 deletions(-) diff --git a/msgraph/core/graph_client.py b/msgraph/core/graph_client.py index 82a3af20..0f651ede 100644 --- a/msgraph/core/graph_client.py +++ b/msgraph/core/graph_client.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ import json -from typing import List +from typing import List, Optional import httpx from kiota_abstractions.authentication import AuthenticationProvider @@ -11,24 +11,6 @@ from ._enums import APIVersion, NationalClouds from .graph_client_factory import GraphClientFactory -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): @@ -36,17 +18,15 @@ def collect_options(func): def wrapper(*args, **kwargs): - middleware_control = dict() + # These are middleware options that can be configured per request. + # Supports options for default middleware as well as custom middleware. - 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)} + options = kwargs.pop('request_options', None) + if options: + if 'headers' in kwargs: + kwargs['headers'].update({'request_options': json.dumps(options)}) + else: + kwargs['headers'] = {'request_options': json.dumps(options)} return func(*args, **kwargs) @@ -85,14 +65,14 @@ def __new__(cls, *args, **kwargs): def __init__( self, - auth_provider: AuthenticationProvider = None, + auth_provider: Optional[AuthenticationProvider] = None, api_version: APIVersion = APIVersion.v1, endpoint: NationalClouds = NationalClouds.Global, timeout: httpx.Timeout = httpx.Timeout( DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT ), - client: httpx.AsyncClient = None, - middleware: List[BaseMiddleware] = None + client: Optional[httpx.AsyncClient] = None, + middleware: Optional[List[BaseMiddleware]] = None ): """ Class constructor that accepts a session object and kwargs to @@ -110,7 +90,7 @@ async def get( params=None, headers=None, cookies=None, - middleware_control=None, + request_options=None, extensions=None ): r"""Sends a GET request. Returns :class:`Response` object. @@ -118,9 +98,10 @@ async def get( :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - return await self.client.get( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) + async with self.client as client: + return await client.get( + url, params=params, headers=headers, cookies=cookies, extensions=extensions + ) @collect_options async def options( @@ -130,7 +111,7 @@ async def options( params=None, headers=None, cookies=None, - middleware_control=None, + request_options=None, extensions=None ): r"""Sends a OPTIONS request. Returns :class:`Response` object. @@ -138,10 +119,10 @@ async def options( :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - - return await self.client.options( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) + async with self.client as client: + return await client.options( + url, params=params, headers=headers, cookies=cookies, extensions=extensions + ) @collect_options async def head( @@ -151,7 +132,7 @@ async def head( params=None, headers=None, cookies=None, - middleware_control=None, + request_options=None, extensions=None ): r"""Sends a HEAD request. Returns :class:`Response` object. @@ -159,10 +140,10 @@ async def head( :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - - return await self.client.head( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) + async with self.client as client: + return await client.head( + url, params=params, headers=headers, cookies=cookies, extensions=extensions + ) @collect_options async def post( @@ -176,7 +157,7 @@ async def post( params=None, headers=None, cookies=None, - middleware_control=None, + request_options=None, extensions=None ): r"""Sends a POST request. Returns :class:`Response` object. @@ -187,18 +168,18 @@ async def post( :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - return await self.client.post( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - middleware_control=middleware_control, - extensions=extensions - ) + async with self.client as client: + return await client.post( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + extensions=extensions + ) @collect_options async def put( @@ -212,7 +193,7 @@ async def put( params=None, headers=None, cookies=None, - middleware_control=None, + request_options=None, extensions=None ): r"""Sends a PUT request. Returns :class:`Response` object. @@ -222,19 +203,18 @@ async def put( :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - - return await self.client.put( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - middleware_control=middleware_control, - extensions=extensions - ) + async with self.client as client: + return await client.put( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + extensions=extensions + ) @collect_options async def patch( @@ -248,7 +228,7 @@ async def patch( params=None, headers=None, cookies=None, - middleware_control=None, + request_options=None, extensions=None ): r"""Sends a PATCH request. Returns :class:`Response` object. @@ -258,18 +238,18 @@ async def patch( :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - return await self.client.patch( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - middleware_control=middleware_control, - extensions=extensions - ) + async with self.client as client: + return await client.patch( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + extensions=extensions + ) @collect_options async def delete( @@ -279,7 +259,7 @@ async def delete( params=None, headers=None, cookies=None, - middleware_control=None, + request_options=None, extensions=None ): r"""Sends a DELETE request. Returns :class:`Response` object. @@ -287,30 +267,19 @@ async def delete( :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ - return await self.client.delete( - url, - params=params, - headers=headers, - cookies=cookies, - middleware_control=middleware_control, - extensions=extensions - ) - - # 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 + async with self.client as client: + return await client.delete( + url, params=params, headers=headers, cookies=cookies, extensions=extensions + ) @staticmethod def get_graph_client( - auth_provider: AuthenticationProvider, + auth_provider: Optional[AuthenticationProvider], api_version: APIVersion, endpoint: NationalClouds, timeout: httpx.Timeout, - client: httpx.AsyncClient, - middleware: BaseMiddleware, + client: Optional[httpx.AsyncClient], + middleware: Optional[List[BaseMiddleware]], ): """Method to always return a single instance of a HTTP Client""" @@ -321,8 +290,8 @@ def get_graph_client( if not auth_provider and not middleware: raise ValueError("Invalid parameters!. Missing TokenCredential or middleware") - if auth_provider: + if auth_provider and not middleware: return GraphClientFactory(api_version, endpoint, timeout, client).create_with_default_middleware(auth_provider) - # return GraphClientFactory( - # **kwargs).create_with_custom_middleware(middleware) + return GraphClientFactory(api_version, endpoint, timeout, + client).create_with_custom_middleware(middleware) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index 50894adf..4d4e88ce 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -2,14 +2,23 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +from typing import List, Optional + import httpx from kiota_abstractions.authentication import AuthenticationProvider from kiota_http.kiota_client_factory import KiotaClientFactory from kiota_http.middleware import AsyncKiotaTransport +from kiota_http.middleware.middleware import BaseMiddleware from ._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT from ._enums import APIVersion, NationalClouds -from .middleware import AuthorizationHandler, GraphMiddlewarePipeline, TelemetryHandler +from .middleware import ( + GraphAuthorizationHandler, + GraphMiddlewarePipeline, + GraphRedirectHandler, + GraphRetryHandler, + GraphTelemetryHandler, +) class GraphClientFactory(KiotaClientFactory): @@ -37,7 +46,7 @@ def __init__( api_version: APIVersion, endpoint: NationalClouds, timeout: httpx.Timeout, - client: httpx.AsyncClient, + client: Optional[httpx.AsyncClient], ): """Class constructor that accepts a user provided session object and kwargs to configure the request handling behaviour of the client""" @@ -54,42 +63,43 @@ def create_with_default_middleware( Returns: httpx.AsycClient: An instance of the AsyncClient object """ - graph_async_client = httpx.AsyncClient( - base_url=self._get_base_url(), timeout=self.timeout, http2=True - ) - current_transport = graph_async_client._transport + if not self.client: + self.client = httpx.AsyncClient( + base_url=self._get_base_url(), timeout=self.timeout, http2=True + ) + current_transport = self.client._transport middleware = self._get_default_middleware(auth_provider, current_transport) - graph_async_client._transport = AsyncKiotaTransport( + self.client._transport = AsyncKiotaTransport( transport=current_transport, middleware=middleware ) - return graph_async_client + return self.client + + def create_with_custom_middleware( + self, middleware: Optional[List[BaseMiddleware]] + ) -> httpx.AsyncClient: + """Applies a custom middleware chain to the HTTP Client - # 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 self.client: + self.client = httpx.AsyncClient( + base_url=self._get_base_url(), timeout=self.timeout, http2=True + ) + current_transport = self.client._transport - # :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 + self.client._transport = AsyncKiotaTransport( + transport=current_transport, middleware=middleware + ) + return self.client def _get_base_url(self): """Helper method to set the base url""" base_url = self.endpoint + '/' + self.api_version return base_url - # def _get_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 _get_default_middleware( self, auth_provider: AuthenticationProvider, transport: httpx.AsyncBaseTransport ) -> GraphMiddlewarePipeline: @@ -97,7 +107,12 @@ def _get_default_middleware( Helper method that constructs a middleware_pipeline with the specified middleware """ middleware_pipeline = GraphMiddlewarePipeline(transport) - middleware = [AuthorizationHandler(auth_provider), TelemetryHandler()] + middleware = [ + GraphAuthorizationHandler(auth_provider), + GraphRedirectHandler(), + GraphRetryHandler(), + GraphTelemetryHandler() + ] for ware in middleware: middleware_pipeline.add_middleware(ware) From 5bb4754ab25078f9b1b67bc4fa481e3074e66804 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 21 Oct 2022 18:09:43 +0300 Subject: [PATCH 13/40] Update graph client tests --- msgraph/core/graph_client.py | 6 +-- tests/unit/test_graph_client.py | 84 +++++++++------------------------ 2 files changed, 25 insertions(+), 65 deletions(-) diff --git a/msgraph/core/graph_client.py b/msgraph/core/graph_client.py index 0f651ede..b934ba0f 100644 --- a/msgraph/core/graph_client.py +++ b/msgraph/core/graph_client.py @@ -6,7 +6,7 @@ from typing import List, Optional import httpx -from kiota_abstractions.authentication import AuthenticationProvider +from kiota_abstractions.authentication import AccessTokenProvider from kiota_http.middleware.middleware import BaseMiddleware from ._enums import APIVersion, NationalClouds @@ -65,7 +65,7 @@ def __new__(cls, *args, **kwargs): def __init__( self, - auth_provider: Optional[AuthenticationProvider] = None, + auth_provider: Optional[AccessTokenProvider] = None, api_version: APIVersion = APIVersion.v1, endpoint: NationalClouds = NationalClouds.Global, timeout: httpx.Timeout = httpx.Timeout( @@ -274,7 +274,7 @@ async def delete( @staticmethod def get_graph_client( - auth_provider: Optional[AuthenticationProvider], + auth_provider: Optional[AccessTokenProvider], api_version: APIVersion, endpoint: NationalClouds, timeout: httpx.Timeout, diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py index b908fa5b..16b538c9 100644 --- a/tests/unit/test_graph_client.py +++ b/tests/unit/test_graph_client.py @@ -2,95 +2,55 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import httpx 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 +from msgraph.core.middleware.authorization import GraphAuthorizationHandler -def test_graph_client_with_default_middleware(): +def test_graph_client_with_default_middleware(mock_token_provider): """ 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 + graph_client = GraphClient(auth_provider=mock_token_provider) + assert isinstance(graph_client.client, httpx.AsyncClient) + assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' -def test_graph_client_with_custom_middleware(): + +def test_graph_client_with_custom_middleware(mock_token_provider): """ Test creating a graph client with custom middleware works as expected """ - credential = _CustomTokenCredential() middleware = [ - AuthorizationHandler(credential), + GraphAuthorizationHandler(token_provider=mock_token_provider), ] - client = GraphClient(middleware=middleware) + graph_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 + assert isinstance(graph_client.client, httpx.AsyncClient) + assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' -def test_graph_client_with_custom_configuration(): +def test_graph_client_with_custom_configuration(mock_token_provider): """ Test creating a graph client with custom middleware works as expected """ - credential = _CustomTokenCredential() - client = GraphClient( - credential=credential, api_version=APIVersion.beta, cloud=NationalClouds.China + graph_client = GraphClient( + auth_provider=mock_token_provider, + api_version=APIVersion.beta, + endpoint=NationalClouds.China ) - assert client.graph_session.base_url == NationalClouds.China + '/' + APIVersion.beta + assert str(graph_client.client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' -def test_graph_client_uses_same_session(): +def test_graph_client_uses_same_session(mock_token_provider): """ 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 - + graph_client1 = GraphClient(auth_provider=mock_token_provider) -class _CustomTokenCredential: - def get_token(self, scopes): - return ['{token:https://graph.microsoft.com/}'] + graph_client2 = GraphClient(auth_provider=mock_token_provider) + assert graph_client1 is graph_client2 From 2dc0bb222dd5d268c20387be5381b424f4e147c2 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 21 Oct 2022 18:29:08 +0300 Subject: [PATCH 14/40] Add more graph client tests --- tests/unit/test_graph_client.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py index 16b538c9..4f2d9644 100644 --- a/tests/unit/test_graph_client.py +++ b/tests/unit/test_graph_client.py @@ -9,7 +9,7 @@ from msgraph.core.middleware.authorization import GraphAuthorizationHandler -def test_graph_client_with_default_middleware(mock_token_provider): +def test_initialize_graph_client_with_default_middleware(mock_token_provider): """ Test creating a graph client with default middleware works as expected """ @@ -20,7 +20,7 @@ def test_graph_client_with_default_middleware(mock_token_provider): assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' -def test_graph_client_with_custom_middleware(mock_token_provider): +def test_initialize_graph_client_with_custom_middleware(mock_token_provider): """ Test creating a graph client with custom middleware works as expected """ @@ -31,6 +31,24 @@ def test_graph_client_with_custom_middleware(mock_token_provider): assert isinstance(graph_client.client, httpx.AsyncClient) assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' + +def test_initialize_graph_client_both_token_provider_and_custom_middleware(mock_token_provider): + """ + Test creating a graph client with both token provider and custom middleware throws an error + """ + middleware = [ + GraphAuthorizationHandler(token_provider=mock_token_provider), + ] + with pytest.raises(Exception): + graph_client = GraphClient(token_provider=mock_token_provider,middleware=middleware) + +def test_initialize_graph_client_without_token_provider_or_custom_middleware(): + """ + Test creating a graph client with default middleware works as expected + """ + + with pytest.raises(Exception): + graph_client = GraphClient() def test_graph_client_with_custom_configuration(mock_token_provider): From df0936c0e0996ef244fd7cff5e76d93ac3e9a1eb Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 21 Oct 2022 19:23:35 +0300 Subject: [PATCH 15/40] Update graph client factory tests --- msgraph/core/graph_client_factory.py | 10 ++-- tests/unit/test_client_factory.py | 90 ++++++++++++++-------------- 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index 4d4e88ce..5fc88166 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -5,7 +5,7 @@ from typing import List, Optional import httpx -from kiota_abstractions.authentication import AuthenticationProvider +from kiota_abstractions.authentication import AccessTokenProvider from kiota_http.kiota_client_factory import KiotaClientFactory from kiota_http.middleware import AsyncKiotaTransport from kiota_http.middleware.middleware import BaseMiddleware @@ -56,7 +56,7 @@ def __init__( self.client = client def create_with_default_middleware( - self, auth_provider: AuthenticationProvider + self, token_provider: AccessTokenProvider ) -> httpx.AsyncClient: """Constructs native HTTP AsyncClient(httpx.AsyncClient) instances configured with a custom transport loaded with a default pipeline of middleware. @@ -68,7 +68,7 @@ def create_with_default_middleware( base_url=self._get_base_url(), timeout=self.timeout, http2=True ) current_transport = self.client._transport - middleware = self._get_default_middleware(auth_provider, current_transport) + middleware = self._get_default_middleware(token_provider, current_transport) self.client._transport = AsyncKiotaTransport( transport=current_transport, middleware=middleware @@ -101,14 +101,14 @@ def _get_base_url(self): return base_url def _get_default_middleware( - self, auth_provider: AuthenticationProvider, transport: httpx.AsyncBaseTransport + self, token_provider: AccessTokenProvider, transport: httpx.AsyncBaseTransport ) -> GraphMiddlewarePipeline: """ Helper method that constructs a middleware_pipeline with the specified middleware """ middleware_pipeline = GraphMiddlewarePipeline(transport) middleware = [ - GraphAuthorizationHandler(auth_provider), + GraphAuthorizationHandler(token_provider), GraphRedirectHandler(), GraphRetryHandler(), GraphTelemetryHandler() diff --git a/tests/unit/test_client_factory.py b/tests/unit/test_client_factory.py index db414b45..1b337b71 100644 --- a/tests/unit/test_client_factory.py +++ b/tests/unit/test_client_factory.py @@ -2,55 +2,57 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +from msgraph.core.middleware.middleware import GraphMiddlewarePipeline import pytest -from requests import Session -from requests.adapters import HTTPAdapter +import httpx +from kiota_http.middleware import AsyncKiotaTransport -from msgraph.core import APIVersion, HTTPClientFactory, NationalClouds +from msgraph.core import APIVersion, GraphClientFactory, NationalClouds from msgraph.core._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from msgraph.core.middleware.authorization import AuthorizationHandler +from msgraph.core.middleware import GraphAuthorizationHandler -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)) -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) - 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(mock_token_provider): + """Test creation of GraphClient using default middleware""" + client = GraphClientFactory( + api_version=APIVersion.beta, + timeout=httpx.Timeout(5, connect=5,), + endpoint = NationalClouds.Global, + client=None + ).create_with_default_middleware(token_provider=mock_token_provider) -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(client, httpx.AsyncClient) + assert isinstance(client._transport, AsyncKiotaTransport) + assert str(client.base_url) == f'{NationalClouds.Global}/{APIVersion.beta}/' - assert isinstance(middleware, HTTPAdapter) - -def test_create_with_custom_middleware(): +def test_create_with_custom_middleware(mock_token_provider): """Test creation of HTTP Clients with custom middleware""" - credential = _CustomTokenCredential() middleware = [ - AuthorizationHandler(credential), + GraphAuthorizationHandler(mock_token_provider), ] - client = HTTPClientFactory().create_with_custom_middleware(middleware=middleware) - custom_middleware = client.get_adapter('https://') + client = GraphClientFactory( + api_version=APIVersion.v1, + timeout=httpx.Timeout(5, connect=5,), + endpoint = NationalClouds.Global, + client=None + ).create_with_custom_middleware(middleware=middleware) - assert isinstance(custom_middleware, HTTPAdapter) + assert isinstance(client, httpx.AsyncClient) + assert isinstance(client._transport, AsyncKiotaTransport) + assert str(client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' def test_get_base_url(): @@ -58,21 +60,17 @@ 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 - + client = GraphClientFactory(api_version=APIVersion.beta, endpoint=NationalClouds.Germany, timeout=httpx.Timeout(5, connect=5,), client=None) + assert client._get_base_url() == f'{NationalClouds.Germany}/{APIVersion.beta}' -def test_register_middleware(): - credential = _CustomTokenCredential() - middleware = [ - AuthorizationHandler(credential), - ] - client = HTTPClientFactory() - client._register(middleware) - assert isinstance(client.session.get_adapter('https://'), HTTPAdapter) +def test_get_default_middleware(mock_token_provider): + client = GraphClientFactory(api_version=APIVersion.beta, endpoint=NationalClouds.Germany, timeout=httpx.Timeout(5, connect=5,), client=None) + middleware = client._get_default_middleware(mock_token_provider, httpx.AsyncClient()._transport) + + assert isinstance(middleware, GraphMiddlewarePipeline) + + + -class _CustomTokenCredential: - def get_token(self, scopes): - return ['{token:https://graph.microsoft.com/}'] From cb989deabfc3fc37b52f88a6eb4689646e86cb6b Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 21 Oct 2022 19:25:51 +0300 Subject: [PATCH 16/40] Code formatting --- tests/unit/test_client_factory.py | 52 ++++++++++++++++++++----------- tests/unit/test_graph_client.py | 8 +++-- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/tests/unit/test_client_factory.py b/tests/unit/test_client_factory.py index 1b337b71..4917ff63 100644 --- a/tests/unit/test_client_factory.py +++ b/tests/unit/test_client_factory.py @@ -2,17 +2,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from msgraph.core.middleware.middleware import GraphMiddlewarePipeline -import pytest import httpx +import pytest from kiota_http.middleware import AsyncKiotaTransport from msgraph.core import APIVersion, GraphClientFactory, NationalClouds from msgraph.core._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT from msgraph.core.middleware import GraphAuthorizationHandler - - - +from msgraph.core.middleware.middleware import GraphMiddlewarePipeline # def test_initialize_with_custom_config(): # """Test creation of HTTP Client will use custom configuration if they are passed""" @@ -28,10 +25,13 @@ def test_create_with_default_middleware(mock_token_provider): """Test creation of GraphClient using default middleware""" client = GraphClientFactory( api_version=APIVersion.beta, - timeout=httpx.Timeout(5, connect=5,), - endpoint = NationalClouds.Global, + timeout=httpx.Timeout( + 5, + connect=5, + ), + endpoint=NationalClouds.Global, client=None - ).create_with_default_middleware(token_provider=mock_token_provider) + ).create_with_default_middleware(token_provider=mock_token_provider) assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) @@ -45,10 +45,13 @@ def test_create_with_custom_middleware(mock_token_provider): ] client = GraphClientFactory( api_version=APIVersion.v1, - timeout=httpx.Timeout(5, connect=5,), - endpoint = NationalClouds.Global, + timeout=httpx.Timeout( + 5, + connect=5, + ), + endpoint=NationalClouds.Global, client=None - ).create_with_custom_middleware(middleware=middleware) + ).create_with_custom_middleware(middleware=middleware) assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) @@ -60,17 +63,28 @@ def test_get_base_url(): Test base url is formed by combining the national cloud endpoint with Api version """ - client = GraphClientFactory(api_version=APIVersion.beta, endpoint=NationalClouds.Germany, timeout=httpx.Timeout(5, connect=5,), client=None) + client = GraphClientFactory( + api_version=APIVersion.beta, + endpoint=NationalClouds.Germany, + timeout=httpx.Timeout( + 5, + connect=5, + ), + client=None + ) assert client._get_base_url() == f'{NationalClouds.Germany}/{APIVersion.beta}' def test_get_default_middleware(mock_token_provider): - client = GraphClientFactory(api_version=APIVersion.beta, endpoint=NationalClouds.Germany, timeout=httpx.Timeout(5, connect=5,), client=None) + client = GraphClientFactory( + api_version=APIVersion.beta, + endpoint=NationalClouds.Germany, + timeout=httpx.Timeout( + 5, + connect=5, + ), + client=None + ) middleware = client._get_default_middleware(mock_token_provider, httpx.AsyncClient()._transport) - - assert isinstance(middleware, GraphMiddlewarePipeline) - - - - + assert isinstance(middleware, GraphMiddlewarePipeline) diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py index 4f2d9644..68140d42 100644 --- a/tests/unit/test_graph_client.py +++ b/tests/unit/test_graph_client.py @@ -31,7 +31,8 @@ def test_initialize_graph_client_with_custom_middleware(mock_token_provider): assert isinstance(graph_client.client, httpx.AsyncClient) assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' - + + def test_initialize_graph_client_both_token_provider_and_custom_middleware(mock_token_provider): """ Test creating a graph client with both token provider and custom middleware throws an error @@ -40,8 +41,9 @@ def test_initialize_graph_client_both_token_provider_and_custom_middleware(mock_ GraphAuthorizationHandler(token_provider=mock_token_provider), ] with pytest.raises(Exception): - graph_client = GraphClient(token_provider=mock_token_provider,middleware=middleware) - + graph_client = GraphClient(token_provider=mock_token_provider, middleware=middleware) + + def test_initialize_graph_client_without_token_provider_or_custom_middleware(): """ Test creating a graph client with default middleware works as expected From 339abe93eabeb12e0f4b18c61743cbf9b725dbca Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:45:38 +0300 Subject: [PATCH 17/40] Add trio async test runner environment --- Pipfile | 5 ++- Pipfile.lock | 115 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 89 insertions(+), 31 deletions(-) diff --git a/Pipfile b/Pipfile index eed51a53..86e20ee4 100644 --- a/Pipfile +++ b/Pipfile @@ -19,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 = "*" \ No newline at end of file +azure-identity = "*" +trio = "==0.22.0" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index 99e2c295..38fc4303 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1686db8ab300430bfaaf36688f3e5a76bc4648883b26e2c17757ca03cba5c12e" + "sha256": "e7871083e8b8cd80989c0083111a80f2344fa6a706613f8bf78abf3446029c4b" }, "pipfile-spec": 6, "requires": {}, @@ -102,11 +102,11 @@ }, "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": [ @@ -144,7 +144,7 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.1.1" }, "frozenlist": { @@ -267,7 +267,7 @@ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==3.4" }, "microsoft-kiota-abstractions": { @@ -288,11 +288,11 @@ }, "microsoft-kiota-http": { "hashes": [ - "sha256:634653aa4ea3ea3c0de2a7e300893b02265085c8ee392c609fa1248b1d7a2959", - "sha256:c27de018315995a8d54853a932f20a4775d6164f89988e5cd3822ac14f42ab44" + "sha256:0e0d54d32ba00c026a97e6270b288666d885945c56469fc681e50ac50140dd28", + "sha256:e1248d9527fc1dfe799f1c5d8457939ae9211dd1bfb88e5f8d33af67fbe6b727" ], "index": "pypi", - "version": "==0.1.0" + "version": "==0.1.1" }, "multidict": { "hashes": [ @@ -478,11 +478,19 @@ "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": [ @@ -590,6 +598,7 @@ "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" ], + "markers": "os_name == 'nt' and implementation_name != 'pypy'", "version": "==1.15.1" }, "charset-normalizer": { @@ -597,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": [ @@ -696,15 +705,16 @@ "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" ], + "markers": "python_version >= '3.6'", "version": "==38.0.1" }, "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": [ @@ -714,6 +724,14 @@ "markers": "python_version >= '3.7'", "version": "==0.19" }, + "exceptiongroup": { + "hashes": [ + "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337", + "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96" + ], + "markers": "python_version < '3.11'", + "version": "==1.0.0rc9" + }, "flit": { "hashes": [ "sha256:06a93a6737fa9380ba85fe8d7f28efb6c93c4f4ee9c7d00cc3375a81f33b91a4", @@ -735,7 +753,7 @@ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==3.4" }, "iniconfig": { @@ -863,6 +881,14 @@ ], "version": "==0.4.3" }, + "outcome": { + "hashes": [ + "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672", + "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", @@ -889,11 +915,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": [ @@ -916,11 +942,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": [ @@ -970,6 +996,13 @@ "index": "pypi", "version": "==3.10.0" }, + "pytest-trio": { + "hashes": [ + "sha256:c01b741819aec2c419555f28944e132d3c711dae1e673d63260809bf92c30c31" + ], + "index": "pypi", + "version": "==0.7.0" + }, "pywin32": { "hashes": [ "sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc", @@ -1006,6 +1039,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", @@ -1019,6 +1067,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], + "markers": "python_version >= '3.7'", "version": "==2.0.1" }, "tomli-w": { @@ -1037,6 +1086,14 @@ "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==0.11.5" }, + "trio": { + "hashes": [ + "sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf", + "sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0" + ], + "index": "pypi", + "version": "==0.22.0" + }, "types-cryptography": { "hashes": [ "sha256:b9f8aa93d9e4d7ff920961cdce3dc7cca479946890924d68356b66f15f4f35e3", @@ -1057,7 +1114,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" }, "urllib3": { From b27c07d22908520fe22ef8d8f3885d1400d785f1 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:47:12 +0300 Subject: [PATCH 18/40] Set intuitive graph client and factory kwargs --- msgraph/core/graph_client.py | 22 +++++++++++----------- msgraph/core/graph_client_factory.py | 6 +++--- msgraph/core/middleware/__init__.py | 3 ++- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/msgraph/core/graph_client.py b/msgraph/core/graph_client.py index b934ba0f..b74bb7b9 100644 --- a/msgraph/core/graph_client.py +++ b/msgraph/core/graph_client.py @@ -65,9 +65,9 @@ def __new__(cls, *args, **kwargs): def __init__( self, - auth_provider: Optional[AccessTokenProvider] = None, + token_provider: Optional[AccessTokenProvider] = None, api_version: APIVersion = APIVersion.v1, - endpoint: NationalClouds = NationalClouds.Global, + base_url: NationalClouds = NationalClouds.Global, timeout: httpx.Timeout = httpx.Timeout( DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT ), @@ -79,7 +79,7 @@ def __init__( be passed to the GraphClientFactory """ self.client = self.get_graph_client( - auth_provider, api_version, endpoint, timeout, client, middleware + token_provider, api_version, base_url, timeout, client, middleware ) @collect_options @@ -274,24 +274,24 @@ async def delete( @staticmethod def get_graph_client( - auth_provider: Optional[AccessTokenProvider], + token_provider: Optional[AccessTokenProvider], api_version: APIVersion, - endpoint: NationalClouds, + base_url: NationalClouds, timeout: httpx.Timeout, client: Optional[httpx.AsyncClient], middleware: Optional[List[BaseMiddleware]], ): """Method to always return a single instance of a HTTP Client""" - if auth_provider and middleware: + if token_provider and middleware: raise ValueError( "Invalid parameters! Both TokenCredential and middleware cannot be passed" ) - if not auth_provider and not middleware: + if not token_provider and not middleware: raise ValueError("Invalid parameters!. Missing TokenCredential or middleware") - if auth_provider and not middleware: - return GraphClientFactory(api_version, endpoint, timeout, - client).create_with_default_middleware(auth_provider) - return GraphClientFactory(api_version, endpoint, timeout, + if token_provider and not middleware: + return GraphClientFactory(api_version, base_url, timeout, + client).create_with_default_middleware(token_provider) + return GraphClientFactory(api_version, base_url, timeout, client).create_with_custom_middleware(middleware) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index 5fc88166..150f7d08 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -44,14 +44,14 @@ class GraphClientFactory(KiotaClientFactory): def __init__( self, api_version: APIVersion, - endpoint: NationalClouds, + base_url: NationalClouds, timeout: httpx.Timeout, client: Optional[httpx.AsyncClient], ): """Class constructor that accepts a user provided session object and kwargs to configure the request handling behaviour of the client""" self.api_version = api_version - self.endpoint = endpoint + self.base_url = base_url self.timeout = timeout self.client = client @@ -97,7 +97,7 @@ def create_with_custom_middleware( def _get_base_url(self): """Helper method to set the base url""" - base_url = self.endpoint + '/' + self.api_version + base_url = self.base_url + '/' + self.api_version return base_url def _get_default_middleware( diff --git a/msgraph/core/middleware/__init__.py b/msgraph/core/middleware/__init__.py index 42e0c0ac..2cdae665 100644 --- a/msgraph/core/middleware/__init__.py +++ b/msgraph/core/middleware/__init__.py @@ -3,7 +3,8 @@ # Licensed under the MIT License. # ------------------------------------ from .authorization import GraphAuthorizationHandler -from .middleware import GraphMiddlewarePipeline +from .middleware import GraphMiddlewarePipeline, GraphRequest from .redirect import GraphRedirectHandler +from .request_context import GraphRequestContext from .retry import GraphRetryHandler from .telemetry import GraphTelemetryHandler From 6548c14a226104da7f70e89a7cf88c80f5dfef33 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:48:19 +0300 Subject: [PATCH 19/40] Correct argument names --- msgraph/core/middleware/authorization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/msgraph/core/middleware/authorization.py b/msgraph/core/middleware/authorization.py index 81ff4324..e3848570 100644 --- a/msgraph/core/middleware/authorization.py +++ b/msgraph/core/middleware/authorization.py @@ -5,7 +5,7 @@ from typing import TypeVar import httpx -from kiota_abstractions.authentication import AuthenticationProvider +from kiota_abstractions.authentication import AccessTokenProvider from kiota_http.middleware.middleware import BaseMiddleware from .._enums import FeatureUsageFlag @@ -17,7 +17,7 @@ class GraphAuthorizationHandler(BaseMiddleware): Transparently authorize requests by adding authorization header to the request """ - def __init__(self, auth_provider: AuthenticationProvider): + def __init__(self, token_provider: AccessTokenProvider): """Constructor for authorization handler Args: @@ -26,7 +26,7 @@ def __init__(self, auth_provider: AuthenticationProvider): """ super().__init__() - self.auth_provider = auth_provider + self.token_provider = token_provider async def send( self, request: GraphRequest, transport: httpx.AsyncBaseTransport @@ -34,7 +34,7 @@ async def send( request.context.feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED - token = await self.auth_provider.get_authorization_token(str(request.url)) + token = await self.token_provider.get_authorization_token(str(request.url)) request.headers.update({'Authorization': f'Bearer {token}'}) response = await super().send(request, transport) return response From 41e028a0804a7ed5bf327790a6b92dd5350c9c62 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:48:38 +0300 Subject: [PATCH 20/40] Add request property to response --- msgraph/core/middleware/middleware.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/msgraph/core/middleware/middleware.py b/msgraph/core/middleware/middleware.py index 9d11e606..e6ae0043 100644 --- a/msgraph/core/middleware/middleware.py +++ b/msgraph/core/middleware/middleware.py @@ -7,13 +7,13 @@ import httpx from kiota_http.middleware import MiddlewarePipeline -from .request_context import RequestContext +from .request_context import GraphRequestContext class GraphRequest(httpx.Request): """Http Request object with a custom request context """ - context: RequestContext + context: GraphRequestContext class GraphMiddlewarePipeline(MiddlewarePipeline): @@ -36,9 +36,11 @@ async def send(self, request: GraphRequest) -> httpx.Response: if options: request_options = json.loads(options) - request.context = RequestContext(request_options, request.headers) + request.context = GraphRequestContext(request_options, request.headers) if self._middleware_present(): return await self._first_middleware.send(request, self._transport) # No middleware in pipeline, send the request. - return await self._transport.handle_async_request(request) + response = await self._transport.handle_async_request(request) + response.request = request + return response From 78f722835c894df2db3134a14c8b6d78eef94cf2 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:49:47 +0300 Subject: [PATCH 21/40] Fix redirect handler --- msgraph/core/middleware/redirect.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/msgraph/core/middleware/redirect.py b/msgraph/core/middleware/redirect.py index b68eba90..54a4d86b 100644 --- a/msgraph/core/middleware/redirect.py +++ b/msgraph/core/middleware/redirect.py @@ -1,5 +1,8 @@ +import typing +from http import client + import httpx -from kiota_http.middleware import RedirectHandler +from kiota_http.middleware import BaseMiddleware, RedirectHandler from .._enums import FeatureUsageFlag from .middleware import GraphRequest @@ -19,12 +22,14 @@ async def send( retryable = True while retryable: - response = await super().send(request, transport) + response = await super(RedirectHandler, self).send(request, transport) redirect_location = self.get_redirect_location(response) if redirect_location and self.should_redirect: retryable = self.increment(response) - request = response.next_request + request = self._build_redirect_request(request, response) continue + + response.history = self.history return response raise Exception(f"Too many redirects. {response.history}") From d2c1187a6ab3fc38a5cc1d8bba4bd7f3315f7909 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:50:24 +0300 Subject: [PATCH 22/40] Update argument names --- msgraph/core/middleware/request_context.py | 2 +- msgraph/core/middleware/retry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/msgraph/core/middleware/request_context.py b/msgraph/core/middleware/request_context.py index f03fe0ea..1f520a15 100644 --- a/msgraph/core/middleware/request_context.py +++ b/msgraph/core/middleware/request_context.py @@ -9,7 +9,7 @@ from .._enums import FeatureUsageFlag -class RequestContext: +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 diff --git a/msgraph/core/middleware/retry.py b/msgraph/core/middleware/retry.py index 214e99c2..0c85ca23 100644 --- a/msgraph/core/middleware/retry.py +++ b/msgraph/core/middleware/retry.py @@ -26,7 +26,7 @@ async def send(self, request: GraphRequest, transport: httpx.AsyncBaseTransport) start_time = time.time() if retry_count > 0: request.headers.update({'retry-attempt': f'{retry_count}'}) - response = await super().send(request, transport) + response = await super(RetryHandler, self).send(request, transport) # Check if the request needs to be retried based on the response method # and status code if self.should_retry(request, response): From 55159f6966759bffda5a9f28b1130ca62374004c Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:50:46 +0300 Subject: [PATCH 23/40] Convert url objects to string --- msgraph/core/middleware/telemetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msgraph/core/middleware/telemetry.py b/msgraph/core/middleware/telemetry.py index 7aaea99e..8c131be2 100644 --- a/msgraph/core/middleware/telemetry.py +++ b/msgraph/core/middleware/telemetry.py @@ -19,7 +19,7 @@ async def send( ) -> httpx.Response: """Adds telemetry headers and sends the http request. """ - if self.is_graph_url(str(request.url)): + if self.is_graph_url(request.url): self._add_client_request_id_header(request) self._append_sdk_version_header(request) self._add_host_os_header(request) @@ -33,7 +33,7 @@ def is_graph_url(self, url): non-graph endpoints""" endpoints = set(item.value for item in NationalClouds) - base_url = parse_url(url) + base_url = parse_url(str(url)) endpoint = f"{base_url.scheme}://{base_url.netloc}" return endpoint in endpoints From a8c220572ef1ce612483ed221fcfd26f45337f92 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 02:51:13 +0300 Subject: [PATCH 24/40] Update http test suite --- tests/conftest.py | 56 +++++ tests/integration/__init__.py | 4 - tests/integration/test_graphclient.py | 91 --------- tests/integration/test_http_client_factory.py | 86 -------- tests/integration/test_retry.py | 91 --------- tests/integration/test_telemetry.py | 72 ------- tests/unit/test_auth_handler.py | 35 ---- tests/unit/test_graph_auth_handler.py | 25 +++ tests/unit/test_graph_client.py | 11 +- ...actory.py => test_graph_client_factory.py} | 25 ++- tests/unit/test_graph_middleware_pipeline.py | 18 ++ tests/unit/test_graph_redirect_handler.py | 31 +++ tests/unit/test_graph_retry_handler.py | 90 ++++++++ tests/unit/test_graph_telemetry_handler.py | 108 ++++++++++ tests/unit/test_middleware_pipeline.py | 89 -------- tests/unit/test_retry_handler.py | 192 ------------------ tests/unit/test_telemetry_handler.py | 146 ------------- 17 files changed, 348 insertions(+), 822 deletions(-) create mode 100644 tests/conftest.py delete mode 100644 tests/integration/__init__.py delete mode 100644 tests/integration/test_graphclient.py delete mode 100644 tests/integration/test_http_client_factory.py delete mode 100644 tests/integration/test_retry.py delete mode 100644 tests/integration/test_telemetry.py delete mode 100644 tests/unit/test_auth_handler.py create mode 100644 tests/unit/test_graph_auth_handler.py rename tests/unit/{test_client_factory.py => test_graph_client_factory.py} (79%) create mode 100644 tests/unit/test_graph_middleware_pipeline.py create mode 100644 tests/unit/test_graph_redirect_handler.py create mode 100644 tests/unit/test_graph_retry_handler.py create mode 100644 tests/unit/test_graph_telemetry_handler.py delete mode 100644 tests/unit/test_middleware_pipeline.py delete mode 100644 tests/unit/test_retry_handler.py delete mode 100644 tests/unit/test_telemetry_handler.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..6bdd4c41 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +import httpx +import pytest +from azure.identity.aio import DefaultAzureCredential +from kiota_abstractions.authentication import AccessTokenProvider +from kiota_authentication_azure.azure_identity_access_token_provider import ( + AzureIdentityAccessTokenProvider, +) + +from msgraph.core import APIVersion, NationalClouds +from msgraph.core.middleware import GraphRequest, GraphRequestContext + +BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 + + +class MockAccessTokenProvider(AccessTokenProvider): + + async def get_authorization_token(self, request: GraphRequest) -> str: + """Returns a string representing a dummy token + Args: + request (GraphRequest): Graph request object + """ + return "Sample token" + + def get_allowed_hosts_validator(self) -> None: + pass + + +@pytest.fixture +def mock_token_provider(): + return MockAccessTokenProvider() + + +@pytest.fixture +def mock_transport(): + return httpx.AsyncClient()._transport + + +@pytest.fixture +def mock_request(): + req = httpx.Request('GET', "https://example.org") + req.context = GraphRequestContext({}, req.headers) + 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/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_graph_auth_handler.py b/tests/unit/test_graph_auth_handler.py new file mode 100644 index 00000000..a2467b73 --- /dev/null +++ b/tests/unit/test_graph_auth_handler.py @@ -0,0 +1,25 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import httpx +import pytest +from kiota_abstractions.authentication import AccessTokenProvider + +from msgraph.core._enums import FeatureUsageFlag +from msgraph.core.middleware import GraphAuthorizationHandler, GraphRequestContext + + +def test_auth_handler_initialization(mock_token_provider): + auth_handler = GraphAuthorizationHandler(mock_token_provider) + assert isinstance(auth_handler.token_provider, AccessTokenProvider) + + +@pytest.mark.trio +async def test_auth_handler_send(mock_token_provider, mock_request, mock_transport): + auth_handler = GraphAuthorizationHandler(mock_token_provider) + resp = await auth_handler.send(mock_request, mock_transport) + assert isinstance(resp, httpx.Response) + assert resp.status_code == 200 + assert 'Authorization' in resp.request.headers + assert resp.request.context.feature_usage == hex(FeatureUsageFlag.AUTH_HANDLER_ENABLED) diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py index 68140d42..d2e16a0a 100644 --- a/tests/unit/test_graph_client.py +++ b/tests/unit/test_graph_client.py @@ -4,6 +4,7 @@ # ------------------------------------ import httpx import pytest +from asyncmock import AsyncMock from msgraph.core import APIVersion, GraphClient, NationalClouds from msgraph.core.middleware.authorization import GraphAuthorizationHandler @@ -14,7 +15,7 @@ def test_initialize_graph_client_with_default_middleware(mock_token_provider): Test creating a graph client with default middleware works as expected """ - graph_client = GraphClient(auth_provider=mock_token_provider) + graph_client = GraphClient(token_provider=mock_token_provider) assert isinstance(graph_client.client, httpx.AsyncClient) assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' @@ -58,9 +59,9 @@ def test_graph_client_with_custom_configuration(mock_token_provider): Test creating a graph client with custom middleware works as expected """ graph_client = GraphClient( - auth_provider=mock_token_provider, + token_provider=mock_token_provider, api_version=APIVersion.beta, - endpoint=NationalClouds.China + base_url=NationalClouds.China ) assert str(graph_client.client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' @@ -70,7 +71,7 @@ def test_graph_client_uses_same_session(mock_token_provider): """ Test graph client is a singleton class and uses the same session """ - graph_client1 = GraphClient(auth_provider=mock_token_provider) + graph_client1 = GraphClient(token_provider=mock_token_provider) - graph_client2 = GraphClient(auth_provider=mock_token_provider) + graph_client2 = GraphClient(token_provider=mock_token_provider) assert graph_client1 is graph_client2 diff --git a/tests/unit/test_client_factory.py b/tests/unit/test_graph_client_factory.py similarity index 79% rename from tests/unit/test_client_factory.py rename to tests/unit/test_graph_client_factory.py index 4917ff63..21197bd4 100644 --- a/tests/unit/test_client_factory.py +++ b/tests/unit/test_graph_client_factory.py @@ -11,14 +11,17 @@ from msgraph.core.middleware import GraphAuthorizationHandler from msgraph.core.middleware.middleware import GraphMiddlewarePipeline -# 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_initialize_with_custom_config(): + """Test creation of HTTP Client will use custom configuration if they are passed""" + client = GraphClientFactory( + api_version=APIVersion.beta, base_url=NationalClouds.Global, timeout=(5, 5), client=None + ) + + assert client.api_version == APIVersion.beta + assert client.base_url == NationalClouds.Global + assert client.timeout == (5, 5) + assert not isinstance(client.client, httpx.AsyncClient) def test_create_with_default_middleware(mock_token_provider): @@ -29,7 +32,7 @@ def test_create_with_default_middleware(mock_token_provider): 5, connect=5, ), - endpoint=NationalClouds.Global, + base_url=NationalClouds.Global, client=None ).create_with_default_middleware(token_provider=mock_token_provider) @@ -49,7 +52,7 @@ def test_create_with_custom_middleware(mock_token_provider): 5, connect=5, ), - endpoint=NationalClouds.Global, + base_url=NationalClouds.Global, client=None ).create_with_custom_middleware(middleware=middleware) @@ -65,7 +68,7 @@ def test_get_base_url(): """ client = GraphClientFactory( api_version=APIVersion.beta, - endpoint=NationalClouds.Germany, + base_url=NationalClouds.Germany, timeout=httpx.Timeout( 5, connect=5, @@ -78,7 +81,7 @@ def test_get_base_url(): def test_get_default_middleware(mock_token_provider): client = GraphClientFactory( api_version=APIVersion.beta, - endpoint=NationalClouds.Germany, + base_url=NationalClouds.Germany, timeout=httpx.Timeout( 5, connect=5, diff --git a/tests/unit/test_graph_middleware_pipeline.py b/tests/unit/test_graph_middleware_pipeline.py new file mode 100644 index 00000000..379a2a83 --- /dev/null +++ b/tests/unit/test_graph_middleware_pipeline.py @@ -0,0 +1,18 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import httpx +import pytest + +from msgraph.core.middleware import GraphMiddlewarePipeline, GraphRequestContext + + +@pytest.mark.trio +async def test_middleware_pipeline_send(mock_transport, mock_request): + pipeline = GraphMiddlewarePipeline(mock_transport) + response = await pipeline.send(mock_request) + + assert isinstance(response, httpx.Response) + assert 'request_options' not in response.request.headers + assert isinstance(response.request.context, GraphRequestContext) diff --git a/tests/unit/test_graph_redirect_handler.py b/tests/unit/test_graph_redirect_handler.py new file mode 100644 index 00000000..7eaf1369 --- /dev/null +++ b/tests/unit/test_graph_redirect_handler.py @@ -0,0 +1,31 @@ +import httpx +import pytest +from kiota_abstractions.authentication import AccessTokenProvider + +from msgraph.core._enums import FeatureUsageFlag +from msgraph.core.middleware import GraphRedirectHandler, GraphRequestContext + + +@pytest.mark.trio +async def test_redirect_handler_send(mock_token_provider, mock_request, mock_transport): + redirect_handler = GraphRedirectHandler() + + req = httpx.Request('GET', "https://httpbin.org/redirect/2") + req.context = GraphRequestContext({}, req.headers) + resp = await redirect_handler.send(req, mock_transport) + + assert isinstance(resp, httpx.Response) + assert resp.status_code == 200 + assert resp.request.context.feature_usage == hex(FeatureUsageFlag.REDIRECT_HANDLER_ENABLED) + + +@pytest.mark.trio +async def test_redirect_handler_send_max_redirects( + mock_token_provider, mock_request, mock_transport +): + redirect_handler = GraphRedirectHandler() + + req = httpx.Request('GET', "https://httpbin.org/redirect/7") + req.context = GraphRequestContext({}, req.headers) + with pytest.raises(Exception) as e: + resp = await redirect_handler.send(req, mock_transport) diff --git a/tests/unit/test_graph_retry_handler.py b/tests/unit/test_graph_retry_handler.py new file mode 100644 index 00000000..97ec5988 --- /dev/null +++ b/tests/unit/test_graph_retry_handler.py @@ -0,0 +1,90 @@ +import httpx +import pytest +from kiota_abstractions.authentication import AccessTokenProvider + +from msgraph.core._enums import FeatureUsageFlag +from msgraph.core.middleware import GraphRequestContext, GraphRetryHandler + +# @pytest.mark.trio +# async def test_redirect_handler_send(mock_token_provider): +# redirect_handler = GraphRetryHandler() + +# req = httpx.Request('GET', "https://httpbin.org/redirect/2") +# req.context = GraphRequestContext({}, req.headers) +# resp = await redirect_handler.send(req, mock_transport) + +# assert isinstance(resp, httpx.Response) +# assert resp.status_code == 302 +# assert resp.request.context.feature_usage == hex(FeatureUsageFlag.REDIRECT_HANDLER_ENABLED) + + +@pytest.mark.trio +async def test_no_retry_success_response(mock_transport): + """ + Test that a request with valid http header and a success response is not retried + """ + retry_handler = GraphRetryHandler() + + req = httpx.Request('GET', "https://httpbin.org/status/200") + req.context = GraphRequestContext({}, req.headers) + resp = await retry_handler.send(req, mock_transport) + + assert isinstance(resp, httpx.Response) + assert resp.status_code == 200 + assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) + with pytest.raises(KeyError): + resp.request.headers["retry-attempt"] + + +@pytest.mark.trio +async def test_valid_retry_429(mock_transport): + """ + Test that a request with valid http header and 503 response is retried + """ + retry_handler = GraphRetryHandler() + + req = httpx.Request('GET', "https://httpbin.org/status/429") + req.context = GraphRequestContext({}, req.headers) + resp = await retry_handler.send(req, mock_transport) + + assert isinstance(resp, httpx.Response) + + assert resp.status_code == 429 + assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) + assert int(resp.request.headers["retry-attempt"]) > 0 + + +@pytest.mark.trio +async def test_valid_retry_503(mock_transport): + """ + Test that a request with valid http header and 503 response is retried + """ + retry_handler = GraphRetryHandler() + + req = httpx.Request('GET', "https://httpbin.org/status/503") + req.context = GraphRequestContext({}, req.headers) + resp = await retry_handler.send(req, mock_transport) + + assert isinstance(resp, httpx.Response) + + assert resp.status_code == 503 + assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) + assert int(resp.request.headers["retry-attempt"]) > 0 + + +@pytest.mark.trio +async def test_valid_retry_504(mock_transport): + """ + Test that a request with valid http header and 503 response is retried + """ + retry_handler = GraphRetryHandler() + + req = httpx.Request('GET', "https://httpbin.org/status/504") + req.context = GraphRequestContext({}, req.headers) + resp = await retry_handler.send(req, mock_transport) + + assert isinstance(resp, httpx.Response) + + assert resp.status_code == 504 + assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) + assert int(resp.request.headers["retry-attempt"]) > 0 diff --git a/tests/unit/test_graph_telemetry_handler.py b/tests/unit/test_graph_telemetry_handler.py new file mode 100644 index 00000000..2bee75f7 --- /dev/null +++ b/tests/unit/test_graph_telemetry_handler.py @@ -0,0 +1,108 @@ +# ------------------------------------ +# 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, GraphClient, NationalClouds +from msgraph.core.middleware import GraphRequestContext, GraphTelemetryHandler + +BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 + + +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 From c5819d40537a9d6e5dbfcccbb6535b037401ce52 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 14:38:20 +0300 Subject: [PATCH 25/40] Remove python 3.5 support --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7fb90ada21198b749372ba7a0bd5cb8e1448544b Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 14:57:37 +0300 Subject: [PATCH 26/40] Ensure request context property is set when redirecting --- msgraph/core/middleware/redirect.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/msgraph/core/middleware/redirect.py b/msgraph/core/middleware/redirect.py index 54a4d86b..82206a6c 100644 --- a/msgraph/core/middleware/redirect.py +++ b/msgraph/core/middleware/redirect.py @@ -26,7 +26,10 @@ async def send( redirect_location = self.get_redirect_location(response) if redirect_location and self.should_redirect: retryable = self.increment(response) - request = self._build_redirect_request(request, response) + new_request = self._build_redirect_request(request, response) + new_request.context = request.context + request = new_request + continue response.history = self.history From ce0f5416b69049ccfa7f8b93926fdefea70c8897 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 15:54:44 +0300 Subject: [PATCH 27/40] Update client docstrings --- msgraph/core/graph_client.py | 4 +-- msgraph/core/graph_client_factory.py | 38 +++++++++++++--------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/msgraph/core/graph_client.py b/msgraph/core/graph_client.py index b74bb7b9..1907d801 100644 --- a/msgraph/core/graph_client.py +++ b/msgraph/core/graph_client.py @@ -36,7 +36,7 @@ def wrapper(*args, **kwargs): 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 + :keyword token_provider: AccessTokenProvider 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 list that will be used to create a middleware pipeline. The middleware should be arranged in the order in which they will @@ -45,7 +45,7 @@ class GraphClient: `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. + :keyword enum base_url: 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. diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index 150f7d08..7b0c5c5a 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -22,23 +22,8 @@ class GraphClientFactory(KiotaClientFactory): - """Constructs httpx AsyncClient 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 + """Constructs httpx.AsyncClient instances configured with either custom or default + pipeline of graph specific middleware. """ def __init__( @@ -48,8 +33,20 @@ def __init__( timeout: httpx.Timeout, client: Optional[httpx.AsyncClient], ): - """Class constructor that accepts a user provided session object and kwargs - to configure the request handling behaviour of the client""" + """Class constructor accepts a user provided client object and kwargs to configure the + request handling behaviour of the client + + Args: + api_version (APIVersion): 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. + base_url (NationalClouds): a supported Microsoft Graph cloud endpoint. + timeout (httpx.Timeout):Default connection and read timeout values for all session + requests.Specify a tuple in the form of httpx.Timeout( + REQUEST_TIMEOUT, connect=CONNECTION_TIMEOUT), + client (Optional[httpx.AsyncClient]): A custom AsynClient instance from the + python httpx library + """ self.api_version = api_version self.base_url = base_url self.timeout = timeout @@ -80,7 +77,8 @@ def create_with_custom_middleware( ) -> httpx.AsyncClient: """Applies a custom middleware chain to the HTTP Client - :param list middleware: Custom middleware(HTTPAdapter) list that will be used to create + 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. """ From abccacb549d8d1fd42ce5caa2c667de5335666b2 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 27 Oct 2022 18:15:37 +0300 Subject: [PATCH 28/40] Update kiota http package --- Pipfile | 4 +-- Pipfile.lock | 82 ++++++++++++++++++++-------------------------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/Pipfile b/Pipfile index 86e20ee4..8b9e1b99 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] # Packages required to run the application microsoft-kiota-abstractions = '*' -microsoft-kiota-http = "*" +microsoft-kiota-http = "==0.1.2" microsoft-kiota-authentication-azure = '*' httpx = {version = "==0.23.0", extras = ["http2"]} @@ -23,4 +23,4 @@ pytest-trio = "==0.7.0" pytest-mock = "==3.10.0" asyncmock = "==0.4.2" azure-identity = "*" -trio = "==0.22.0" \ No newline at end of file +trio = "==0.22.0" diff --git a/Pipfile.lock b/Pipfile.lock index 38fc4303..9ba75c10 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e7871083e8b8cd80989c0083111a80f2344fa6a706613f8bf78abf3446029c4b" + "sha256": "5a951eb82b0a9e2b23557fcbaa8b04f7644601ef47a9fc5cd471209dad0216f9" }, "pipfile-spec": 6, "requires": {}, @@ -288,11 +288,11 @@ }, "microsoft-kiota-http": { "hashes": [ - "sha256:0e0d54d32ba00c026a97e6270b288666d885945c56469fc681e50ac50140dd28", - "sha256:e1248d9527fc1dfe799f1c5d8457939ae9211dd1bfb88e5f8d33af67fbe6b727" + "sha256:3f1eb3ded4e1db34785636a1633c8584e109093636459a1b45283b2790e159db", + "sha256:539955502a19e74b83a3d4681a5207ce1673eaf023a1a01a62447b6f018bfd9f" ], "index": "pypi", - "version": "==0.1.1" + "version": "==0.1.2" }, "multidict": { "hashes": [ @@ -726,11 +726,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337", - "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96" + "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41", + "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad" ], "markers": "python_version < '3.11'", - "version": "==1.0.0rc9" + "version": "==1.0.0" }, "flit": { "hashes": [ @@ -773,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": [ @@ -1080,11 +1062,11 @@ }, "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": [ From d690941044fbc46530ba9226bd8f110050c5c179 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 28 Oct 2022 23:36:53 +0300 Subject: [PATCH 29/40] Refactor graph client factory --- msgraph/core/graph_client_factory.py | 101 +++++++++++---------------- 1 file changed, 39 insertions(+), 62 deletions(-) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index 7b0c5c5a..fa6cc843 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +from __future__ import annotations + from typing import List, Optional import httpx @@ -10,8 +12,6 @@ from kiota_http.middleware import AsyncKiotaTransport from kiota_http.middleware.middleware import BaseMiddleware -from ._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from ._enums import APIVersion, NationalClouds from .middleware import ( GraphAuthorizationHandler, GraphMiddlewarePipeline, @@ -26,54 +26,33 @@ class GraphClientFactory(KiotaClientFactory): pipeline of graph specific middleware. """ - def __init__( - self, - api_version: APIVersion, - base_url: NationalClouds, - timeout: httpx.Timeout, - client: Optional[httpx.AsyncClient], - ): - """Class constructor accepts a user provided client object and kwargs to configure the - request handling behaviour of the client - - Args: - api_version (APIVersion): 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. - base_url (NationalClouds): a supported Microsoft Graph cloud endpoint. - timeout (httpx.Timeout):Default connection and read timeout values for all session - requests.Specify a tuple in the form of httpx.Timeout( - REQUEST_TIMEOUT, connect=CONNECTION_TIMEOUT), - client (Optional[httpx.AsyncClient]): A custom AsynClient instance from the - python httpx library - """ - self.api_version = api_version - self.base_url = base_url - self.timeout = timeout - self.client = client - + @staticmethod def create_with_default_middleware( - self, token_provider: AccessTokenProvider + client: httpx.AsyncClient, + token_provider: Optional[AccessTokenProvider] = None ) -> 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 """ - if not self.client: - self.client = httpx.AsyncClient( - base_url=self._get_base_url(), timeout=self.timeout, http2=True - ) - current_transport = self.client._transport - middleware = self._get_default_middleware(token_provider, current_transport) + current_transport = client._transport + middleware = GraphClientFactory._get_common_middleware() + if token_provider: + middleware.insert(0, GraphAuthorizationHandler(token_provider)) + + middleware_pipeline = GraphClientFactory._create_middleware_pipeline( + middleware, current_transport + ) - self.client._transport = AsyncKiotaTransport( - transport=current_transport, middleware=middleware + client._transport = AsyncKiotaTransport( + transport=current_transport, middleware=middleware_pipeline ) - return self.client + return client + @staticmethod def create_with_custom_middleware( - self, middleware: Optional[List[BaseMiddleware]] + client: httpx.AsyncClient, middleware: Optional[List[BaseMiddleware]] ) -> httpx.AsyncClient: """Applies a custom middleware chain to the HTTP Client @@ -82,36 +61,34 @@ def create_with_custom_middleware( a middleware pipeline. The middleware should be arranged in the order in which they will modify the request. """ - if not self.client: - self.client = httpx.AsyncClient( - base_url=self._get_base_url(), timeout=self.timeout, http2=True - ) - current_transport = self.client._transport + current_transport = client._transport + middleware_pipeline = GraphClientFactory._create_middleware_pipeline( + middleware, current_transport + ) - self.client._transport = AsyncKiotaTransport( - transport=current_transport, middleware=middleware + client._transport = AsyncKiotaTransport( + transport=current_transport, middleware=middleware_pipeline ) - return self.client + return client + + @staticmethod + def _get_common_middleware() -> List[BaseMiddleware]: + """ + Helper method that returns a list of cross cutting middleware + """ + middleware = [GraphRedirectHandler(), GraphRetryHandler(), GraphTelemetryHandler()] - def _get_base_url(self): - """Helper method to set the base url""" - base_url = self.base_url + '/' + self.api_version - return base_url + return middleware - def _get_default_middleware( - self, token_provider: AccessTokenProvider, transport: httpx.AsyncBaseTransport + @staticmethod + def _create_middleware_pipeline( + middleware: Optional[List[BaseMiddleware]], transport: httpx.AsyncBaseTransport ) -> GraphMiddlewarePipeline: """ Helper method that constructs a middleware_pipeline with the specified middleware """ middleware_pipeline = GraphMiddlewarePipeline(transport) - middleware = [ - GraphAuthorizationHandler(token_provider), - GraphRedirectHandler(), - GraphRetryHandler(), - GraphTelemetryHandler() - ] - for ware in middleware: - middleware_pipeline.add_middleware(ware) - + if middleware: + for ware in middleware: + middleware_pipeline.add_middleware(ware) return middleware_pipeline From 3c7f84a3686d75109a6bcb5e13e38316a4510dd6 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 28 Oct 2022 23:37:57 +0300 Subject: [PATCH 30/40] Update graph client --- msgraph/core/graph_client.py | 68 +++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/msgraph/core/graph_client.py b/msgraph/core/graph_client.py index 1907d801..00982f76 100644 --- a/msgraph/core/graph_client.py +++ b/msgraph/core/graph_client.py @@ -9,6 +9,8 @@ from kiota_abstractions.authentication import AccessTokenProvider from kiota_http.middleware.middleware import BaseMiddleware +from msgraph.core import middleware + from ._enums import APIVersion, NationalClouds from .graph_client_factory import GraphClientFactory @@ -36,23 +38,21 @@ def wrapper(*args, **kwargs): class GraphClient: """Constructs a custom HTTPClient to be used for requests against Microsoft Graph - :keyword token_provider: AccessTokenProvider 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 list that will be used to create + Args: + token_provider (AccessTokenProvider): Used to acquire an access token for the + Microsoft Graph API. + api_version (APIVersion): 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. + base_url (NationalClouds): a supported Microsoft Graph cloud endpoint. + timeout (httpx.Timeout):Default connection and read timeout values for all session + requests.Specify a tuple in the form of httpx.Timeout( + REQUEST_TIMEOUT, connect=CONNECTION_TIMEOUT), + client (Optional[httpx.AsyncClient]): A custom AsynClient instance from the + python httpx library + middleware (BaseMiddlware): 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. - :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 base_url: 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 client: A custom client instance from the python httpx library """ DEFAULT_CONNECTION_TIMEOUT: int = 30 DEFAULT_REQUEST_TIMEOUT: int = 100 @@ -78,7 +78,7 @@ def __init__( Class constructor that accepts a session object and kwargs to be passed to the GraphClientFactory """ - self.client = self.get_graph_client( + self.client = self._get_graph_client( token_provider, api_version, base_url, timeout, client, middleware ) @@ -272,26 +272,30 @@ async def delete( url, params=params, headers=headers, cookies=cookies, extensions=extensions ) - @staticmethod - def get_graph_client( - token_provider: Optional[AccessTokenProvider], - api_version: APIVersion, - base_url: NationalClouds, - timeout: httpx.Timeout, - client: Optional[httpx.AsyncClient], - middleware: Optional[List[BaseMiddleware]], + def _get_graph_client( + self, token_provider: Optional[AccessTokenProvider], api_version: APIVersion, + base_url: NationalClouds, timeout: httpx.Timeout, client: Optional[httpx.AsyncClient], + middleware: Optional[List[BaseMiddleware]] ): """Method to always return a single instance of a HTTP Client""" - + if not client: + client = httpx.AsyncClient( + base_url=self._get_base_url(base_url, api_version), timeout=timeout, http2=True + ) if token_provider and middleware: raise ValueError( "Invalid parameters! Both TokenCredential and middleware cannot be passed" ) - if not token_provider and not middleware: - raise ValueError("Invalid parameters!. Missing TokenCredential or middleware") - if token_provider and not middleware: - return GraphClientFactory(api_version, base_url, timeout, - client).create_with_default_middleware(token_provider) - return GraphClientFactory(api_version, base_url, timeout, - client).create_with_custom_middleware(middleware) + if middleware: + return GraphClientFactory.create_with_custom_middleware( + client=client, middleware=middleware + ) + return GraphClientFactory.create_with_default_middleware( + client=client, token_provider=token_provider + ) + + def _get_base_url(self, base_url: str, api_version: APIVersion) -> str: + """Helper method to set the complete base url""" + base_url = f'{base_url}/{api_version}' + return base_url From 13147d334cae08535ad6638f77cb2e4d9e31c64c Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 28 Oct 2022 23:38:15 +0300 Subject: [PATCH 31/40] Add graph request adapter --- msgraph/core/graph_request_adapter.py | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 msgraph/core/graph_request_adapter.py diff --git a/msgraph/core/graph_request_adapter.py b/msgraph/core/graph_request_adapter.py new file mode 100644 index 00000000..ca88e263 --- /dev/null +++ b/msgraph/core/graph_request_adapter.py @@ -0,0 +1,32 @@ +from kiota_abstractions.authentication import AuthenticationProvider +from kiota_abstractions.serialization import ( + Parsable, + ParsableFactory, + ParseNode, + ParseNodeFactory, + ParseNodeFactoryRegistry, + SerializationWriterFactory, + SerializationWriterFactoryRegistry, +) +from kiota_http.httpx_request_adapter import HttpxRequestAdapter + +from .graph_client import GraphClient +from .graph_client_factory import GraphClientFactory + + +class GraphRequestAdapter(HttpxRequestAdapter): + + def __init__( + self, + authentication_provider: AuthenticationProvider, + parse_node_factory: ParseNodeFactory = ParseNodeFactoryRegistry(), + serialization_writer_factory: + SerializationWriterFactory = SerializationWriterFactoryRegistry(), + http_client: GraphClient = GraphClient() + ) -> None: + super().__init__( + authentication_provider=authentication_provider, + parse_node_factory=parse_node_factory, + serialization_writer_factory=serialization_writer_factory, + http_client=http_client + ) From 9c1b19e49a91a147c2cad4ad3e94241adb2644ac Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Fri, 28 Oct 2022 23:38:44 +0300 Subject: [PATCH 32/40] Update tests --- tests/unit/test_graph_client.py | 21 +++--- tests/unit/test_graph_client_factory.py | 92 +++++++++--------------- tests/unit/test_graph_request_adapter.py | 26 +++++++ 3 files changed, 70 insertions(+), 69 deletions(-) create mode 100644 tests/unit/test_graph_request_adapter.py diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py index d2e16a0a..55da17fe 100644 --- a/tests/unit/test_graph_client.py +++ b/tests/unit/test_graph_client.py @@ -45,15 +45,6 @@ def test_initialize_graph_client_both_token_provider_and_custom_middleware(mock_ graph_client = GraphClient(token_provider=mock_token_provider, middleware=middleware) -def test_initialize_graph_client_without_token_provider_or_custom_middleware(): - """ - Test creating a graph client with default middleware works as expected - """ - - with pytest.raises(Exception): - graph_client = GraphClient() - - def test_graph_client_with_custom_configuration(mock_token_provider): """ Test creating a graph client with custom middleware works as expected @@ -75,3 +66,15 @@ def test_graph_client_uses_same_session(mock_token_provider): graph_client2 = GraphClient(token_provider=mock_token_provider) assert graph_client1 is graph_client2 + + +def test_get_base_url(): + """ + Test base url is formed by combining the national cloud endpoint with + Api version + """ + url = GraphClient()._get_base_url( + base_url=NationalClouds.Germany, + api_version=APIVersion.beta, + ) + assert url == f'{NationalClouds.Germany}/{APIVersion.beta}' diff --git a/tests/unit/test_graph_client_factory.py b/tests/unit/test_graph_client_factory.py index 21197bd4..9195c4c6 100644 --- a/tests/unit/test_graph_client_factory.py +++ b/tests/unit/test_graph_client_factory.py @@ -10,84 +10,56 @@ from msgraph.core._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT from msgraph.core.middleware import GraphAuthorizationHandler from msgraph.core.middleware.middleware import GraphMiddlewarePipeline +from msgraph.core.middleware.redirect import GraphRedirectHandler +from msgraph.core.middleware.retry import GraphRetryHandler +from msgraph.core.middleware.telemetry import GraphTelemetryHandler -def test_initialize_with_custom_config(): - """Test creation of HTTP Client will use custom configuration if they are passed""" - client = GraphClientFactory( - api_version=APIVersion.beta, base_url=NationalClouds.Global, timeout=(5, 5), client=None - ) +def test_create_with_default_middleware_no_auth_provider(): + """Test creation of GraphClient without a token provider does not + add the Authorization middleware""" + client = GraphClientFactory.create_with_default_middleware(client=httpx.AsyncClient()) - assert client.api_version == APIVersion.beta - assert client.base_url == NationalClouds.Global - assert client.timeout == (5, 5) - assert not isinstance(client.client, httpx.AsyncClient) + assert isinstance(client, httpx.AsyncClient) + assert isinstance(client._transport, AsyncKiotaTransport) + pipeline = client._transport.middleware + assert isinstance(pipeline, GraphMiddlewarePipeline) + assert not isinstance(pipeline._first_middleware, GraphAuthorizationHandler) def test_create_with_default_middleware(mock_token_provider): - """Test creation of GraphClient using default middleware""" - client = GraphClientFactory( - api_version=APIVersion.beta, - timeout=httpx.Timeout( - 5, - connect=5, - ), - base_url=NationalClouds.Global, - client=None - ).create_with_default_middleware(token_provider=mock_token_provider) + """Test creation of GraphClient using default middleware and passing a token + provider adds Authorization middleware""" + client = GraphClientFactory.create_with_default_middleware( + client=httpx.AsyncClient(), token_provider=mock_token_provider + ) assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) - assert str(client.base_url) == f'{NationalClouds.Global}/{APIVersion.beta}/' + pipeline = client._transport.middleware + assert isinstance(pipeline, GraphMiddlewarePipeline) + assert isinstance(pipeline._first_middleware, GraphAuthorizationHandler) def test_create_with_custom_middleware(mock_token_provider): """Test creation of HTTP Clients with custom middleware""" middleware = [ - GraphAuthorizationHandler(mock_token_provider), + GraphTelemetryHandler(), ] - client = GraphClientFactory( - api_version=APIVersion.v1, - timeout=httpx.Timeout( - 5, - connect=5, - ), - base_url=NationalClouds.Global, - client=None - ).create_with_custom_middleware(middleware=middleware) + client = GraphClientFactory.create_with_custom_middleware( + client=httpx.AsyncClient(), middleware=middleware + ) assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) - assert str(client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' + pipeline = client._transport.middleware + assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) -def test_get_base_url(): - """ - Test base url is formed by combining the national cloud endpoint with - Api version - """ - client = GraphClientFactory( - api_version=APIVersion.beta, - base_url=NationalClouds.Germany, - timeout=httpx.Timeout( - 5, - connect=5, - ), - client=None - ) - assert client._get_base_url() == f'{NationalClouds.Germany}/{APIVersion.beta}' - - -def test_get_default_middleware(mock_token_provider): - client = GraphClientFactory( - api_version=APIVersion.beta, - base_url=NationalClouds.Germany, - timeout=httpx.Timeout( - 5, - connect=5, - ), - client=None - ) - middleware = client._get_default_middleware(mock_token_provider, httpx.AsyncClient()._transport) +def test_get_common_middleware(): + middleware = GraphClientFactory._get_common_middleware() - assert isinstance(middleware, GraphMiddlewarePipeline) + assert len(middleware) == 3 + assert isinstance(middleware[0], GraphRedirectHandler) + assert isinstance(middleware[1], GraphRetryHandler) + assert isinstance(middleware[2], GraphTelemetryHandler) diff --git a/tests/unit/test_graph_request_adapter.py b/tests/unit/test_graph_request_adapter.py new file mode 100644 index 00000000..8379c1df --- /dev/null +++ b/tests/unit/test_graph_request_adapter.py @@ -0,0 +1,26 @@ +import httpx +import pytest +from asyncmock import AsyncMock +from kiota_abstractions.serialization import ( + ParseNodeFactoryRegistry, + SerializationWriterFactoryRegistry, +) + +from msgraph.core.graph_request_adapter import GraphRequestAdapter +from tests.conftest import mock_token_provider + + +def test_create_graph_request_adapter(mock_token_provider): + request_adapter = GraphRequestAdapter(mock_token_provider) + assert request_adapter._authentication_provider is mock_token_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): + GraphRequestAdapter(None) From 7cd67e4635bbd783fb75b110cc2519f3b0c6ac97 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Mon, 31 Oct 2022 14:40:51 +0300 Subject: [PATCH 33/40] Pip kiota dependencies to specific versions --- Pipfile | 4 ++-- Pipfile.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile b/Pipfile index 8b9e1b99..357132ac 100644 --- a/Pipfile +++ b/Pipfile @@ -4,9 +4,9 @@ verify_ssl = true name = "pypi" [packages] # Packages required to run the application -microsoft-kiota-abstractions = '*' +microsoft-kiota-abstractions = "==0.1.0" microsoft-kiota-http = "==0.1.2" -microsoft-kiota-authentication-azure = '*' +microsoft-kiota-authentication-azure = "==0.1.0" httpx = {version = "==0.23.0", extras = ["http2"]} [dev-packages] # Packages required to develop the application diff --git a/Pipfile.lock b/Pipfile.lock index 9ba75c10..6ed5a01f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5a951eb82b0a9e2b23557fcbaa8b04f7644601ef47a9fc5cd471209dad0216f9" + "sha256": "ce3368503cf713b3415a8582cfd0bd6018dd6f620722ff83f461a32d50af7295" }, "pipfile-spec": 6, "requires": {}, From ffb0495943926fc1b7292632f55b98ec9c02c956 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 1 Nov 2022 21:08:30 +0300 Subject: [PATCH 34/40] Update graph client factory and adapter --- msgraph/core/graph_client_factory.py | 71 ++++++++++++--------------- msgraph/core/graph_request_adapter.py | 7 +-- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index fa6cc843..b8c8a9f5 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -7,18 +7,15 @@ from typing import List, Optional import httpx -from kiota_abstractions.authentication import AccessTokenProvider from kiota_http.kiota_client_factory import KiotaClientFactory from kiota_http.middleware import AsyncKiotaTransport from kiota_http.middleware.middleware import BaseMiddleware -from .middleware import ( - GraphAuthorizationHandler, - GraphMiddlewarePipeline, - GraphRedirectHandler, - GraphRetryHandler, - GraphTelemetryHandler, -) +from ._enums import APIVersion, NationalClouds +from .middleware import GraphTelemetryHandler + +DEFAULT_CONNECTION_TIMEOUT: int = 30 +DEFAULT_REQUEST_TIMEOUT: int = 100 class GraphClientFactory(KiotaClientFactory): @@ -28,31 +25,37 @@ class GraphClientFactory(KiotaClientFactory): @staticmethod def create_with_default_middleware( - client: httpx.AsyncClient, - token_provider: Optional[AccessTokenProvider] = None + 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 """ + timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT) + client = httpx.AsyncClient( + base_url=GraphClientFactory._get_base_url(host, api_version), + timeout=timeout, + http2=True + ) current_transport = client._transport - middleware = GraphClientFactory._get_common_middleware() - if token_provider: - middleware.insert(0, GraphAuthorizationHandler(token_provider)) - - middleware_pipeline = GraphClientFactory._create_middleware_pipeline( + middleware = KiotaClientFactory._get_default_middleware() + middleware.append(GraphTelemetryHandler()) + middleware_pipeline = KiotaClientFactory._create_middleware_pipeline( middleware, current_transport ) client._transport = AsyncKiotaTransport( - transport=current_transport, middleware=middleware_pipeline + transport=current_transport, pipeline=middleware_pipeline ) return client @staticmethod def create_with_custom_middleware( - client: httpx.AsyncClient, middleware: Optional[List[BaseMiddleware]] + middleware: Optional[List[BaseMiddleware]], + api_version: APIVersion = APIVersion.v1, + host: NationalClouds = NationalClouds.Global, ) -> httpx.AsyncClient: """Applies a custom middleware chain to the HTTP Client @@ -61,34 +64,24 @@ def create_with_custom_middleware( a middleware pipeline. The middleware should be arranged in the order in which they will modify the request. """ + timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT) + client = httpx.AsyncClient( + base_url=GraphClientFactory._get_base_url(host, api_version), + timeout=timeout, + http2=True + ) current_transport = client._transport - middleware_pipeline = GraphClientFactory._create_middleware_pipeline( + middleware_pipeline = KiotaClientFactory._create_middleware_pipeline( middleware, current_transport ) client._transport = AsyncKiotaTransport( - transport=current_transport, middleware=middleware_pipeline + transport=current_transport, pipeline=middleware_pipeline ) return client @staticmethod - def _get_common_middleware() -> List[BaseMiddleware]: - """ - Helper method that returns a list of cross cutting middleware - """ - middleware = [GraphRedirectHandler(), GraphRetryHandler(), GraphTelemetryHandler()] - - return middleware - - @staticmethod - def _create_middleware_pipeline( - middleware: Optional[List[BaseMiddleware]], transport: httpx.AsyncBaseTransport - ) -> GraphMiddlewarePipeline: - """ - Helper method that constructs a middleware_pipeline with the specified middleware - """ - middleware_pipeline = GraphMiddlewarePipeline(transport) - if middleware: - for ware in middleware: - middleware_pipeline.add_middleware(ware) - return middleware_pipeline + 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/graph_request_adapter.py b/msgraph/core/graph_request_adapter.py index ca88e263..81c458fb 100644 --- a/msgraph/core/graph_request_adapter.py +++ b/msgraph/core/graph_request_adapter.py @@ -1,8 +1,6 @@ +import httpx from kiota_abstractions.authentication import AuthenticationProvider from kiota_abstractions.serialization import ( - Parsable, - ParsableFactory, - ParseNode, ParseNodeFactory, ParseNodeFactoryRegistry, SerializationWriterFactory, @@ -10,7 +8,6 @@ ) from kiota_http.httpx_request_adapter import HttpxRequestAdapter -from .graph_client import GraphClient from .graph_client_factory import GraphClientFactory @@ -22,7 +19,7 @@ def __init__( parse_node_factory: ParseNodeFactory = ParseNodeFactoryRegistry(), serialization_writer_factory: SerializationWriterFactory = SerializationWriterFactoryRegistry(), - http_client: GraphClient = GraphClient() + http_client: httpx.AsyncClient = GraphClientFactory.create_with_default_middleware() ) -> None: super().__init__( authentication_provider=authentication_provider, From 8cd221f02098d12c74428397d2289a0751c97261 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 1 Nov 2022 21:09:52 +0300 Subject: [PATCH 35/40] Remove duplicate files already in http core --- msgraph/core/__init__.py | 1 - msgraph/core/graph_client.py | 301 ------------------- msgraph/core/middleware/__init__.py | 4 - msgraph/core/middleware/authorization.py | 40 --- msgraph/core/middleware/middleware.py | 46 --- msgraph/core/middleware/redirect.py | 38 --- msgraph/core/middleware/retry.py | 48 --- tests/unit/__init__.py | 4 - tests/unit/test_graph_auth_handler.py | 25 -- tests/unit/test_graph_client.py | 80 ----- tests/unit/test_graph_middleware_pipeline.py | 18 -- tests/unit/test_graph_redirect_handler.py | 31 -- tests/unit/test_graph_retry_handler.py | 90 ------ 13 files changed, 726 deletions(-) delete mode 100644 msgraph/core/graph_client.py delete mode 100644 msgraph/core/middleware/authorization.py delete mode 100644 msgraph/core/middleware/middleware.py delete mode 100644 msgraph/core/middleware/redirect.py delete mode 100644 msgraph/core/middleware/retry.py delete mode 100644 tests/unit/__init__.py delete mode 100644 tests/unit/test_graph_auth_handler.py delete mode 100644 tests/unit/test_graph_client.py delete mode 100644 tests/unit/test_graph_middleware_pipeline.py delete mode 100644 tests/unit/test_graph_redirect_handler.py delete mode 100644 tests/unit/test_graph_retry_handler.py diff --git a/msgraph/core/__init__.py b/msgraph/core/__init__.py index a425d0c7..c3fa5860 100644 --- a/msgraph/core/__init__.py +++ b/msgraph/core/__init__.py @@ -4,7 +4,6 @@ # ------------------------------------ from ._constants import SDK_VERSION from ._enums import APIVersion, NationalClouds -from .graph_client import GraphClient from .graph_client_factory import GraphClientFactory __version__ = SDK_VERSION diff --git a/msgraph/core/graph_client.py b/msgraph/core/graph_client.py deleted file mode 100644 index 00982f76..00000000 --- a/msgraph/core/graph_client.py +++ /dev/null @@ -1,301 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json -from typing import List, Optional - -import httpx -from kiota_abstractions.authentication import AccessTokenProvider -from kiota_http.middleware.middleware import BaseMiddleware - -from msgraph.core import middleware - -from ._enums import APIVersion, NationalClouds -from .graph_client_factory import GraphClientFactory - - -def collect_options(func): - """Collect middleware options into a middleware control dict and pass it as a header""" - - def wrapper(*args, **kwargs): - - # These are middleware options that can be configured per request. - # Supports options for default middleware as well as custom middleware. - - options = kwargs.pop('request_options', None) - if options: - if 'headers' in kwargs: - kwargs['headers'].update({'request_options': json.dumps(options)}) - else: - kwargs['headers'] = {'request_options': json.dumps(options)} - - return func(*args, **kwargs) - - return wrapper - - -class GraphClient: - """Constructs a custom HTTPClient to be used for requests against Microsoft Graph - - Args: - token_provider (AccessTokenProvider): Used to acquire an access token for the - Microsoft Graph API. - api_version (APIVersion): 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. - base_url (NationalClouds): a supported Microsoft Graph cloud endpoint. - timeout (httpx.Timeout):Default connection and read timeout values for all session - requests.Specify a tuple in the form of httpx.Timeout( - REQUEST_TIMEOUT, connect=CONNECTION_TIMEOUT), - client (Optional[httpx.AsyncClient]): A custom AsynClient instance from the - python httpx library - middleware (BaseMiddlware): 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. - """ - DEFAULT_CONNECTION_TIMEOUT: int = 30 - DEFAULT_REQUEST_TIMEOUT: int = 100 - __instance = None - - def __new__(cls, *args, **kwargs): - if not GraphClient.__instance: - GraphClient.__instance = object.__new__(cls) - return GraphClient.__instance - - def __init__( - self, - token_provider: Optional[AccessTokenProvider] = None, - api_version: APIVersion = APIVersion.v1, - base_url: NationalClouds = NationalClouds.Global, - timeout: httpx.Timeout = httpx.Timeout( - DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT - ), - client: Optional[httpx.AsyncClient] = None, - middleware: Optional[List[BaseMiddleware]] = None - ): - """ - Class constructor that accepts a session object and kwargs to - be passed to the GraphClientFactory - """ - self.client = self._get_graph_client( - token_provider, api_version, base_url, timeout, client, middleware - ) - - @collect_options - async def get( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - 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 - """ - async with self.client as client: - return await client.get( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - @collect_options - async def options( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - 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 - """ - async with self.client as client: - return await client.options( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - @collect_options - async def head( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - 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 - """ - async with self.client as client: - return await client.head( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - @collect_options - async def post( - self, - url, - *, - content=None, - data=None, - files=None, - json=None, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - 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 - """ - async with self.client as client: - return await client.post( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - extensions=extensions - ) - - @collect_options - async def put( - self, - url, - *, - content=None, - data=None, - files=None, - json=None, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - 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 - """ - async with self.client as client: - return await client.put( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - extensions=extensions - ) - - @collect_options - async def patch( - self, - url, - *, - content=None, - data=None, - files=None, - json=None, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - 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 - """ - async with self.client as client: - return await client.patch( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - extensions=extensions - ) - - @collect_options - async def delete( - self, - url, - *, - params=None, - headers=None, - cookies=None, - request_options=None, - extensions=None - ): - 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 - """ - async with self.client as client: - return await client.delete( - url, params=params, headers=headers, cookies=cookies, extensions=extensions - ) - - def _get_graph_client( - self, token_provider: Optional[AccessTokenProvider], api_version: APIVersion, - base_url: NationalClouds, timeout: httpx.Timeout, client: Optional[httpx.AsyncClient], - middleware: Optional[List[BaseMiddleware]] - ): - """Method to always return a single instance of a HTTP Client""" - if not client: - client = httpx.AsyncClient( - base_url=self._get_base_url(base_url, api_version), timeout=timeout, http2=True - ) - if token_provider and middleware: - raise ValueError( - "Invalid parameters! Both TokenCredential and middleware cannot be passed" - ) - - if middleware: - return GraphClientFactory.create_with_custom_middleware( - client=client, middleware=middleware - ) - return GraphClientFactory.create_with_default_middleware( - client=client, token_provider=token_provider - ) - - def _get_base_url(self, base_url: str, api_version: APIVersion) -> str: - """Helper method to set the complete base url""" - base_url = f'{base_url}/{api_version}' - return base_url diff --git a/msgraph/core/middleware/__init__.py b/msgraph/core/middleware/__init__.py index 2cdae665..508a0819 100644 --- a/msgraph/core/middleware/__init__.py +++ b/msgraph/core/middleware/__init__.py @@ -2,9 +2,5 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from .authorization import GraphAuthorizationHandler -from .middleware import GraphMiddlewarePipeline, GraphRequest -from .redirect import GraphRedirectHandler from .request_context import GraphRequestContext -from .retry import GraphRetryHandler from .telemetry import GraphTelemetryHandler diff --git a/msgraph/core/middleware/authorization.py b/msgraph/core/middleware/authorization.py deleted file mode 100644 index e3848570..00000000 --- a/msgraph/core/middleware/authorization.py +++ /dev/null @@ -1,40 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import TypeVar - -import httpx -from kiota_abstractions.authentication import AccessTokenProvider -from kiota_http.middleware.middleware import BaseMiddleware - -from .._enums import FeatureUsageFlag -from .middleware import GraphRequest - - -class GraphAuthorizationHandler(BaseMiddleware): - """ - Transparently authorize requests by adding authorization header to the request - """ - - def __init__(self, token_provider: AccessTokenProvider): - """Constructor for authorization handler - - Args: - auth_provider (AuthenticationProvider): AuthorizationProvider instance - that will be used to fetch the token - """ - super().__init__() - - self.token_provider = token_provider - - async def send( - self, request: GraphRequest, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: - - request.context.feature_usage = FeatureUsageFlag.AUTH_HANDLER_ENABLED - - token = await self.token_provider.get_authorization_token(str(request.url)) - request.headers.update({'Authorization': f'Bearer {token}'}) - response = await super().send(request, transport) - return response diff --git a/msgraph/core/middleware/middleware.py b/msgraph/core/middleware/middleware.py deleted file mode 100644 index e6ae0043..00000000 --- a/msgraph/core/middleware/middleware.py +++ /dev/null @@ -1,46 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import json - -import httpx -from kiota_http.middleware import MiddlewarePipeline - -from .request_context import GraphRequestContext - - -class GraphRequest(httpx.Request): - """Http Request object with a custom request context - """ - context: GraphRequestContext - - -class GraphMiddlewarePipeline(MiddlewarePipeline): - """Chain of graph specific middleware that process requests in the same order - and responses in reverse order to requests. The pipeline is implemented as a linked-list - """ - - async def send(self, request: GraphRequest) -> httpx.Response: - """Passes the request to the next middleware if available or makes a network request - - Args: - request (httpx.Request): The http request - - Returns: - httpx.Response: The http response - """ - - request_options = {} - options = request.headers.pop('request_options', None) - if options: - request_options = json.loads(options) - - request.context = GraphRequestContext(request_options, request.headers) - - if self._middleware_present(): - return await self._first_middleware.send(request, self._transport) - # No middleware in pipeline, send the request. - response = await self._transport.handle_async_request(request) - response.request = request - return response diff --git a/msgraph/core/middleware/redirect.py b/msgraph/core/middleware/redirect.py deleted file mode 100644 index 82206a6c..00000000 --- a/msgraph/core/middleware/redirect.py +++ /dev/null @@ -1,38 +0,0 @@ -import typing -from http import client - -import httpx -from kiota_http.middleware import BaseMiddleware, RedirectHandler - -from .._enums import FeatureUsageFlag -from .middleware import GraphRequest - - -class GraphRedirectHandler(RedirectHandler): - """Middleware designed to handle 3XX responses transparently - """ - - async def send( - self, request: GraphRequest, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: - """Sends the http request object to the next middleware or redirects - the request if necessary. - """ - request.context.feature_usage = FeatureUsageFlag.REDIRECT_HANDLER_ENABLED - - retryable = True - while retryable: - response = await super(RedirectHandler, self).send(request, transport) - redirect_location = self.get_redirect_location(response) - if redirect_location and self.should_redirect: - retryable = self.increment(response) - new_request = self._build_redirect_request(request, response) - new_request.context = request.context - request = new_request - - continue - - response.history = self.history - return response - - raise Exception(f"Too many redirects. {response.history}") diff --git a/msgraph/core/middleware/retry.py b/msgraph/core/middleware/retry.py deleted file mode 100644 index 0c85ca23..00000000 --- a/msgraph/core/middleware/retry.py +++ /dev/null @@ -1,48 +0,0 @@ -import time - -import httpx -from kiota_http.middleware import RetryHandler - -from .._enums import FeatureUsageFlag -from .middleware import GraphRequest - - -class GraphRetryHandler(RetryHandler): - """ - Middleware that handles failed requests - """ - - async def send(self, request: GraphRequest, transport: httpx.AsyncBaseTransport): - """ - Sends the http request object to the next middleware or retries the request if necessary. - """ - response = None - retry_count = 0 - retry_valid = self.retries_allowed - - request.context.feature_usage = FeatureUsageFlag.RETRY_HANDLER_ENABLED - - while retry_valid: - start_time = time.time() - if retry_count > 0: - request.headers.update({'retry-attempt': f'{retry_count}'}) - response = await super(RetryHandler, self).send(request, transport) - # Check if the request needs to be retried based on the response method - # and status code - if self.should_retry(request, response): - # check that max retries has not been hit - retry_valid = self.check_retry_valid(retry_count) - - # Get the delay time between retries - delay = self.get_delay_time(retry_count, response) - - if retry_valid and delay < self.timeout: - time.sleep(delay) - end_time = time.time() - self.timeout -= (end_time - start_time) - # increment the count for retries - retry_count += 1 - - continue - break - return response 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_graph_auth_handler.py b/tests/unit/test_graph_auth_handler.py deleted file mode 100644 index a2467b73..00000000 --- a/tests/unit/test_graph_auth_handler.py +++ /dev/null @@ -1,25 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import httpx -import pytest -from kiota_abstractions.authentication import AccessTokenProvider - -from msgraph.core._enums import FeatureUsageFlag -from msgraph.core.middleware import GraphAuthorizationHandler, GraphRequestContext - - -def test_auth_handler_initialization(mock_token_provider): - auth_handler = GraphAuthorizationHandler(mock_token_provider) - assert isinstance(auth_handler.token_provider, AccessTokenProvider) - - -@pytest.mark.trio -async def test_auth_handler_send(mock_token_provider, mock_request, mock_transport): - auth_handler = GraphAuthorizationHandler(mock_token_provider) - resp = await auth_handler.send(mock_request, mock_transport) - assert isinstance(resp, httpx.Response) - assert resp.status_code == 200 - assert 'Authorization' in resp.request.headers - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.AUTH_HANDLER_ENABLED) diff --git a/tests/unit/test_graph_client.py b/tests/unit/test_graph_client.py deleted file mode 100644 index 55da17fe..00000000 --- a/tests/unit/test_graph_client.py +++ /dev/null @@ -1,80 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import httpx -import pytest -from asyncmock import AsyncMock - -from msgraph.core import APIVersion, GraphClient, NationalClouds -from msgraph.core.middleware.authorization import GraphAuthorizationHandler - - -def test_initialize_graph_client_with_default_middleware(mock_token_provider): - """ - Test creating a graph client with default middleware works as expected - """ - - graph_client = GraphClient(token_provider=mock_token_provider) - - assert isinstance(graph_client.client, httpx.AsyncClient) - assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' - - -def test_initialize_graph_client_with_custom_middleware(mock_token_provider): - """ - Test creating a graph client with custom middleware works as expected - """ - middleware = [ - GraphAuthorizationHandler(token_provider=mock_token_provider), - ] - graph_client = GraphClient(middleware=middleware) - - assert isinstance(graph_client.client, httpx.AsyncClient) - assert str(graph_client.client.base_url) == f'{NationalClouds.Global}/{APIVersion.v1}/' - - -def test_initialize_graph_client_both_token_provider_and_custom_middleware(mock_token_provider): - """ - Test creating a graph client with both token provider and custom middleware throws an error - """ - middleware = [ - GraphAuthorizationHandler(token_provider=mock_token_provider), - ] - with pytest.raises(Exception): - graph_client = GraphClient(token_provider=mock_token_provider, middleware=middleware) - - -def test_graph_client_with_custom_configuration(mock_token_provider): - """ - Test creating a graph client with custom middleware works as expected - """ - graph_client = GraphClient( - token_provider=mock_token_provider, - api_version=APIVersion.beta, - base_url=NationalClouds.China - ) - - assert str(graph_client.client.base_url) == f'{NationalClouds.China}/{APIVersion.beta}/' - - -def test_graph_client_uses_same_session(mock_token_provider): - """ - Test graph client is a singleton class and uses the same session - """ - graph_client1 = GraphClient(token_provider=mock_token_provider) - - graph_client2 = GraphClient(token_provider=mock_token_provider) - assert graph_client1 is graph_client2 - - -def test_get_base_url(): - """ - Test base url is formed by combining the national cloud endpoint with - Api version - """ - url = GraphClient()._get_base_url( - base_url=NationalClouds.Germany, - api_version=APIVersion.beta, - ) - assert url == f'{NationalClouds.Germany}/{APIVersion.beta}' diff --git a/tests/unit/test_graph_middleware_pipeline.py b/tests/unit/test_graph_middleware_pipeline.py deleted file mode 100644 index 379a2a83..00000000 --- a/tests/unit/test_graph_middleware_pipeline.py +++ /dev/null @@ -1,18 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import httpx -import pytest - -from msgraph.core.middleware import GraphMiddlewarePipeline, GraphRequestContext - - -@pytest.mark.trio -async def test_middleware_pipeline_send(mock_transport, mock_request): - pipeline = GraphMiddlewarePipeline(mock_transport) - response = await pipeline.send(mock_request) - - assert isinstance(response, httpx.Response) - assert 'request_options' not in response.request.headers - assert isinstance(response.request.context, GraphRequestContext) diff --git a/tests/unit/test_graph_redirect_handler.py b/tests/unit/test_graph_redirect_handler.py deleted file mode 100644 index 7eaf1369..00000000 --- a/tests/unit/test_graph_redirect_handler.py +++ /dev/null @@ -1,31 +0,0 @@ -import httpx -import pytest -from kiota_abstractions.authentication import AccessTokenProvider - -from msgraph.core._enums import FeatureUsageFlag -from msgraph.core.middleware import GraphRedirectHandler, GraphRequestContext - - -@pytest.mark.trio -async def test_redirect_handler_send(mock_token_provider, mock_request, mock_transport): - redirect_handler = GraphRedirectHandler() - - req = httpx.Request('GET', "https://httpbin.org/redirect/2") - req.context = GraphRequestContext({}, req.headers) - resp = await redirect_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - assert resp.status_code == 200 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.REDIRECT_HANDLER_ENABLED) - - -@pytest.mark.trio -async def test_redirect_handler_send_max_redirects( - mock_token_provider, mock_request, mock_transport -): - redirect_handler = GraphRedirectHandler() - - req = httpx.Request('GET', "https://httpbin.org/redirect/7") - req.context = GraphRequestContext({}, req.headers) - with pytest.raises(Exception) as e: - resp = await redirect_handler.send(req, mock_transport) diff --git a/tests/unit/test_graph_retry_handler.py b/tests/unit/test_graph_retry_handler.py deleted file mode 100644 index 97ec5988..00000000 --- a/tests/unit/test_graph_retry_handler.py +++ /dev/null @@ -1,90 +0,0 @@ -import httpx -import pytest -from kiota_abstractions.authentication import AccessTokenProvider - -from msgraph.core._enums import FeatureUsageFlag -from msgraph.core.middleware import GraphRequestContext, GraphRetryHandler - -# @pytest.mark.trio -# async def test_redirect_handler_send(mock_token_provider): -# redirect_handler = GraphRetryHandler() - -# req = httpx.Request('GET', "https://httpbin.org/redirect/2") -# req.context = GraphRequestContext({}, req.headers) -# resp = await redirect_handler.send(req, mock_transport) - -# assert isinstance(resp, httpx.Response) -# assert resp.status_code == 302 -# assert resp.request.context.feature_usage == hex(FeatureUsageFlag.REDIRECT_HANDLER_ENABLED) - - -@pytest.mark.trio -async def test_no_retry_success_response(mock_transport): - """ - Test that a request with valid http header and a success response is not retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/200") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - assert resp.status_code == 200 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - with pytest.raises(KeyError): - resp.request.headers["retry-attempt"] - - -@pytest.mark.trio -async def test_valid_retry_429(mock_transport): - """ - Test that a request with valid http header and 503 response is retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/429") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - - assert resp.status_code == 429 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - assert int(resp.request.headers["retry-attempt"]) > 0 - - -@pytest.mark.trio -async def test_valid_retry_503(mock_transport): - """ - Test that a request with valid http header and 503 response is retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/503") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - - assert resp.status_code == 503 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - assert int(resp.request.headers["retry-attempt"]) > 0 - - -@pytest.mark.trio -async def test_valid_retry_504(mock_transport): - """ - Test that a request with valid http header and 503 response is retried - """ - retry_handler = GraphRetryHandler() - - req = httpx.Request('GET', "https://httpbin.org/status/504") - req.context = GraphRequestContext({}, req.headers) - resp = await retry_handler.send(req, mock_transport) - - assert isinstance(resp, httpx.Response) - - assert resp.status_code == 504 - assert resp.request.context.feature_usage == hex(FeatureUsageFlag.RETRY_HANDLER_ENABLED) - assert int(resp.request.headers["retry-attempt"]) > 0 From 240205fafcf9ef286f458a96ca1b05d3231f9d54 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 1 Nov 2022 21:10:27 +0300 Subject: [PATCH 36/40] Update telemetry handler to set context and feature flags --- msgraph/core/middleware/telemetry.py | 39 +++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/msgraph/core/middleware/telemetry.py b/msgraph/core/middleware/telemetry.py index 8c131be2..01d66f9a 100644 --- a/msgraph/core/middleware/telemetry.py +++ b/msgraph/core/middleware/telemetry.py @@ -1,12 +1,18 @@ +import http +import json import platform import httpx -from kiota_http.middleware.middleware import BaseMiddleware +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 GraphRequest +from .._enums import FeatureUsageFlag, NationalClouds +from .request_context import GraphRequestContext + + +class GraphRequest(httpx.Request): + context: GraphRequestContext class GraphTelemetryHandler(BaseMiddleware): @@ -14,11 +20,11 @@ class GraphTelemetryHandler(BaseMiddleware): the SDK team improve the developer experience. """ - async def send( - self, request: GraphRequest, transport: httpx.AsyncBaseTransport - ) -> httpx.Response: + 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) self._append_sdk_version_header(request) @@ -28,6 +34,27 @@ async def send( 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""" From 7743c4a741ec3b728e8f0df0897795255998bc7b Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Tue, 1 Nov 2022 21:10:52 +0300 Subject: [PATCH 37/40] Update tests --- tests/conftest.py | 24 ++++--- tests/unit/test_graph_client_factory.py | 75 +++++++++++----------- tests/unit/test_graph_request_adapter.py | 7 +- tests/unit/test_graph_telemetry_handler.py | 13 +++- 4 files changed, 62 insertions(+), 57 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6bdd4c41..f77d5e91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,44 +1,42 @@ import httpx import pytest -from azure.identity.aio import DefaultAzureCredential -from kiota_abstractions.authentication import AccessTokenProvider +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.middleware import GraphRequest, GraphRequestContext +from msgraph.core.graph_client_factory import GraphClientFactory +from msgraph.core.middleware import GraphRequestContext BASE_URL = NationalClouds.Global + '/' + APIVersion.v1 -class MockAccessTokenProvider(AccessTokenProvider): +class MockAuthenticationProvider(AnonymousAuthenticationProvider): - async def get_authorization_token(self, request: GraphRequest) -> str: + async def get_authorization_token(self, request: httpx.Request) -> str: """Returns a string representing a dummy token Args: request (GraphRequest): Graph request object """ - return "Sample token" - - def get_allowed_hosts_validator(self) -> None: - pass + request.headers['Authorization'] = 'Sample token' + return @pytest.fixture -def mock_token_provider(): - return MockAccessTokenProvider() +def mock_auth_provider(): + return MockAuthenticationProvider() @pytest.fixture def mock_transport(): - return httpx.AsyncClient()._transport + client = GraphClientFactory.create_with_default_middleware() + return client._transport @pytest.fixture def mock_request(): req = httpx.Request('GET', "https://example.org") - req.context = GraphRequestContext({}, req.headers) return req diff --git a/tests/unit/test_graph_client_factory.py b/tests/unit/test_graph_client_factory.py index 9195c4c6..f9f35407 100644 --- a/tests/unit/test_graph_client_factory.py +++ b/tests/unit/test_graph_client_factory.py @@ -4,62 +4,59 @@ # ------------------------------------ import httpx import pytest -from kiota_http.middleware import AsyncKiotaTransport +from kiota_http.middleware import ( + AsyncKiotaTransport, + MiddlewarePipeline, + ParametersNameDecodingHandler, +) from msgraph.core import APIVersion, GraphClientFactory, NationalClouds -from msgraph.core._constants import DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT -from msgraph.core.middleware import GraphAuthorizationHandler -from msgraph.core.middleware.middleware import GraphMiddlewarePipeline -from msgraph.core.middleware.redirect import GraphRedirectHandler -from msgraph.core.middleware.retry import GraphRetryHandler from msgraph.core.middleware.telemetry import GraphTelemetryHandler -def test_create_with_default_middleware_no_auth_provider(): - """Test creation of GraphClient without a token provider does not - add the Authorization middleware""" - client = GraphClientFactory.create_with_default_middleware(client=httpx.AsyncClient()) +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.middleware - assert isinstance(pipeline, GraphMiddlewarePipeline) - assert not isinstance(pipeline._first_middleware, GraphAuthorizationHandler) + pipeline = client._transport.pipeline + assert isinstance(pipeline, MiddlewarePipeline) + assert isinstance(pipeline._first_middleware, ParametersNameDecodingHandler) + assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) -def test_create_with_default_middleware(mock_token_provider): - """Test creation of GraphClient using default middleware and passing a token - provider adds Authorization middleware""" - client = GraphClientFactory.create_with_default_middleware( - client=httpx.AsyncClient(), token_provider=mock_token_provider - ) - - assert isinstance(client, httpx.AsyncClient) - assert isinstance(client._transport, AsyncKiotaTransport) - pipeline = client._transport.middleware - assert isinstance(pipeline, GraphMiddlewarePipeline) - assert isinstance(pipeline._first_middleware, GraphAuthorizationHandler) - - -def test_create_with_custom_middleware(mock_token_provider): +def test_create_with_custom_middleware(): """Test creation of HTTP Clients with custom middleware""" middleware = [ GraphTelemetryHandler(), ] - client = GraphClientFactory.create_with_custom_middleware( - client=httpx.AsyncClient(), middleware=middleware - ) + client = GraphClientFactory.create_with_custom_middleware(middleware=middleware) assert isinstance(client, httpx.AsyncClient) assert isinstance(client._transport, AsyncKiotaTransport) - pipeline = client._transport.middleware + pipeline = client._transport.pipeline assert isinstance(pipeline._first_middleware, GraphTelemetryHandler) -def test_get_common_middleware(): - middleware = GraphClientFactory._get_common_middleware() - - assert len(middleware) == 3 - assert isinstance(middleware[0], GraphRedirectHandler) - assert isinstance(middleware[1], GraphRetryHandler) - assert isinstance(middleware[2], 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_request_adapter.py b/tests/unit/test_graph_request_adapter.py index 8379c1df..6a0a58e8 100644 --- a/tests/unit/test_graph_request_adapter.py +++ b/tests/unit/test_graph_request_adapter.py @@ -7,12 +7,11 @@ ) from msgraph.core.graph_request_adapter import GraphRequestAdapter -from tests.conftest import mock_token_provider -def test_create_graph_request_adapter(mock_token_provider): - request_adapter = GraphRequestAdapter(mock_token_provider) - assert request_adapter._authentication_provider is mock_token_provider +def test_create_graph_request_adapter(mock_auth_provider): + request_adapter = GraphRequestAdapter(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 diff --git a/tests/unit/test_graph_telemetry_handler.py b/tests/unit/test_graph_telemetry_handler.py index 2bee75f7..b0a24720 100644 --- a/tests/unit/test_graph_telemetry_handler.py +++ b/tests/unit/test_graph_telemetry_handler.py @@ -9,12 +9,23 @@ import httpx import pytest -from msgraph.core import SDK_VERSION, APIVersion, GraphClient, NationalClouds +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 From 159efdc1e6a664a1571a45ce161cbcc40dcd91c4 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 2 Nov 2022 18:26:41 +0300 Subject: [PATCH 38/40] Update to latest changes from kiota http --- msgraph/core/graph_client_factory.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/msgraph/core/graph_client_factory.py b/msgraph/core/graph_client_factory.py index b8c8a9f5..6cbbbcf7 100644 --- a/msgraph/core/graph_client_factory.py +++ b/msgraph/core/graph_client_factory.py @@ -14,9 +14,6 @@ from ._enums import APIVersion, NationalClouds from .middleware import GraphTelemetryHandler -DEFAULT_CONNECTION_TIMEOUT: int = 30 -DEFAULT_REQUEST_TIMEOUT: int = 100 - class GraphClientFactory(KiotaClientFactory): """Constructs httpx.AsyncClient instances configured with either custom or default @@ -33,16 +30,13 @@ def create_with_default_middleware( Returns: httpx.AsycClient: An instance of the AsyncClient object """ - timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT) - client = httpx.AsyncClient( - base_url=GraphClientFactory._get_base_url(host, api_version), - timeout=timeout, - http2=True - ) + 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 = KiotaClientFactory.get_default_middleware() middleware.append(GraphTelemetryHandler()) - middleware_pipeline = KiotaClientFactory._create_middleware_pipeline( + middleware_pipeline = KiotaClientFactory.create_middleware_pipeline( middleware, current_transport ) @@ -64,14 +58,11 @@ def create_with_custom_middleware( a middleware pipeline. The middleware should be arranged in the order in which they will modify the request. """ - timeout = httpx.Timeout(DEFAULT_REQUEST_TIMEOUT, connect=DEFAULT_CONNECTION_TIMEOUT) - client = httpx.AsyncClient( - base_url=GraphClientFactory._get_base_url(host, api_version), - timeout=timeout, - http2=True - ) + 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_pipeline = KiotaClientFactory.create_middleware_pipeline( middleware, current_transport ) From b25f4ac8c1678ee6e94068e7b388df77c2ad93c3 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Wed, 2 Nov 2022 19:10:47 +0300 Subject: [PATCH 39/40] Bump kiota http to version 0.2.0 --- Pipfile | 2 +- Pipfile.lock | 62 ++++++++++++------------- tests/unit/test_graph_client_factory.py | 8 +--- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/Pipfile b/Pipfile index 357132ac..e14cf569 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] # Packages required to run the application microsoft-kiota-abstractions = "==0.1.0" -microsoft-kiota-http = "==0.1.2" +microsoft-kiota-http = "==0.2.0" microsoft-kiota-authentication-azure = "==0.1.0" httpx = {version = "==0.23.0", extras = ["http2"]} diff --git a/Pipfile.lock b/Pipfile.lock index 6ed5a01f..8e5dcb7e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ce3368503cf713b3415a8582cfd0bd6018dd6f620722ff83f461a32d50af7295" + "sha256": "bf905ef0b9652ce4a74de976c703ca85f10d9a3ca25515f2f304aa36a36b8dc4" }, "pipfile-spec": 6, "requires": {}, @@ -288,11 +288,11 @@ }, "microsoft-kiota-http": { "hashes": [ - "sha256:3f1eb3ded4e1db34785636a1633c8584e109093636459a1b45283b2790e159db", - "sha256:539955502a19e74b83a3d4681a5207ce1673eaf023a1a01a62447b6f018bfd9f" + "sha256:12392f4d270001dfeba0464e602ef17922e9990cd6db1e26746b1c9d5263cbbb", + "sha256:34f61b0b732df4875f8f7058907af8f22fa6bc919aff9dc70dd7a806288ee7ab" ], "index": "pypi", - "version": "==0.1.2" + "version": "==0.2.0" }, "multidict": { "hashes": [ @@ -678,35 +678,35 @@ }, "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" + "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.1" + "version": "==38.0.3" }, "dill": { "hashes": [ diff --git a/tests/unit/test_graph_client_factory.py b/tests/unit/test_graph_client_factory.py index f9f35407..f6746fbd 100644 --- a/tests/unit/test_graph_client_factory.py +++ b/tests/unit/test_graph_client_factory.py @@ -4,11 +4,7 @@ # ------------------------------------ import httpx import pytest -from kiota_http.middleware import ( - AsyncKiotaTransport, - MiddlewarePipeline, - ParametersNameDecodingHandler, -) +from kiota_http.middleware import AsyncKiotaTransport, MiddlewarePipeline, RedirectHandler from msgraph.core import APIVersion, GraphClientFactory, NationalClouds from msgraph.core.middleware.telemetry import GraphTelemetryHandler @@ -22,7 +18,7 @@ def test_create_with_default_middleware(): assert isinstance(client._transport, AsyncKiotaTransport) pipeline = client._transport.pipeline assert isinstance(pipeline, MiddlewarePipeline) - assert isinstance(pipeline._first_middleware, ParametersNameDecodingHandler) + assert isinstance(pipeline._first_middleware, RedirectHandler) assert isinstance(pipeline._current_middleware, GraphTelemetryHandler) From b576910d3ad21d4f9d0097b8623500a238997d50 Mon Sep 17 00:00:00 2001 From: samwelkanda Date: Thu, 3 Nov 2022 15:16:26 +0300 Subject: [PATCH 40/40] Rename adapter to BaseGraphRequestAdapter --- msgraph/core/__init__.py | 1 + ...aph_request_adapter.py => base_graph_request_adapter.py} | 2 +- ...equest_adapter.py => test_base_graph_request_adapter.py} | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) rename msgraph/core/{graph_request_adapter.py => base_graph_request_adapter.py} (95%) rename tests/unit/{test_graph_request_adapter.py => test_base_graph_request_adapter.py} (80%) diff --git a/msgraph/core/__init__.py b/msgraph/core/__init__.py index c3fa5860..f0b08984 100644 --- a/msgraph/core/__init__.py +++ b/msgraph/core/__init__.py @@ -4,6 +4,7 @@ # ------------------------------------ from ._constants import SDK_VERSION from ._enums import APIVersion, NationalClouds +from .base_graph_request_adapter import BaseGraphRequestAdapter from .graph_client_factory import GraphClientFactory __version__ = SDK_VERSION diff --git a/msgraph/core/graph_request_adapter.py b/msgraph/core/base_graph_request_adapter.py similarity index 95% rename from msgraph/core/graph_request_adapter.py rename to msgraph/core/base_graph_request_adapter.py index 81c458fb..2acfdc10 100644 --- a/msgraph/core/graph_request_adapter.py +++ b/msgraph/core/base_graph_request_adapter.py @@ -11,7 +11,7 @@ from .graph_client_factory import GraphClientFactory -class GraphRequestAdapter(HttpxRequestAdapter): +class BaseGraphRequestAdapter(HttpxRequestAdapter): def __init__( self, diff --git a/tests/unit/test_graph_request_adapter.py b/tests/unit/test_base_graph_request_adapter.py similarity index 80% rename from tests/unit/test_graph_request_adapter.py rename to tests/unit/test_base_graph_request_adapter.py index 6a0a58e8..3b9b1487 100644 --- a/tests/unit/test_graph_request_adapter.py +++ b/tests/unit/test_base_graph_request_adapter.py @@ -6,11 +6,11 @@ SerializationWriterFactoryRegistry, ) -from msgraph.core.graph_request_adapter import GraphRequestAdapter +from msgraph.core.base_graph_request_adapter import BaseGraphRequestAdapter def test_create_graph_request_adapter(mock_auth_provider): - request_adapter = GraphRequestAdapter(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( @@ -22,4 +22,4 @@ def test_create_graph_request_adapter(mock_auth_provider): def test_create_request_adapter_no_auth_provider(): with pytest.raises(TypeError): - GraphRequestAdapter(None) + BaseGraphRequestAdapter(None)