diff --git a/.github/workflows/test-library.yml b/.github/workflows/test-library.yml index eba95662..996dcf9c 100644 --- a/.github/workflows/test-library.yml +++ b/.github/workflows/test-library.yml @@ -8,10 +8,24 @@ on: - 'main' - 'release' paths-ignore: - - '.github/workflows/**' + - '.github/**' + - 'examples/**' + - 'meraki/aio/**' + - 'notebooks/**' + - 'README.md' + - 'LICENSE' + - 'setup.py' pull_request: branches: - 'main' + paths-ignore: + - '.github/**' + - 'examples/**' + - 'meraki/aio/**' + - 'notebooks/**' + - 'README.md' + - 'LICENSE' + - 'setup.py' workflow_dispatch: @@ -22,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -44,3 +58,4 @@ jobs: - name: Test with pytest run: | pytest --apikey ${{ secrets.TEST_ORG_API_KEY }} --o ${{ secrets.TEST_ORG_ID }} + diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..41044e82 --- /dev/null +++ b/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +aiohttp = "~=3.9.0" +requests = "~=2.31.0" +meraki = "~=1.38.0" +jinja2 = "==3.1.2" +pytest = "~=7.2.1" + +[dev-packages] + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..22b0bb19 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,617 @@ +{ + "_meta": { + "hash": { + "sha256": "006defe90174b428fed374855c6662123aecc46ae8994807d560ab365dfd2354" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f", + "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c", + "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af", + "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4", + "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a", + "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489", + "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213", + "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01", + "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5", + "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361", + "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26", + "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0", + "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4", + "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8", + "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1", + "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7", + "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6", + "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a", + "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd", + "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4", + "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499", + "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183", + "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544", + "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821", + "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501", + "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f", + "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe", + "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f", + "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672", + "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5", + "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2", + "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57", + "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87", + "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0", + "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f", + "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7", + "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed", + "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70", + "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0", + "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f", + "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d", + "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f", + "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d", + "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431", + "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff", + "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf", + "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83", + "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690", + "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587", + "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e", + "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb", + "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3", + "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66", + "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014", + "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35", + "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f", + "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0", + "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449", + "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23", + "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5", + "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd", + "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4", + "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b", + "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558", + "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd", + "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766", + "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a", + "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636", + "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d", + "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590", + "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e", + "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d", + "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c", + "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28", + "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065", + "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.9.1" + }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "attrs": { + "hashes": [ + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.6" + }, + "frozenlist": { + "hashes": [ + "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6", + "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01", + "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251", + "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9", + "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b", + "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87", + "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf", + "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f", + "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0", + "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2", + "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b", + "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc", + "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c", + "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467", + "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9", + "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1", + "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a", + "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79", + "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167", + "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300", + "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf", + "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea", + "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2", + "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab", + "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3", + "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb", + "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087", + "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc", + "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8", + "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62", + "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f", + "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326", + "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c", + "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431", + "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963", + "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7", + "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef", + "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3", + "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956", + "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781", + "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472", + "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc", + "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839", + "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672", + "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3", + "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503", + "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d", + "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8", + "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b", + "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc", + "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f", + "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559", + "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b", + "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95", + "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb", + "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963", + "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919", + "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f", + "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3", + "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1", + "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jinja2": { + "hashes": [ + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==3.1.2" + }, + "markupsafe": { + "hashes": [ + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", + "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", + "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.3" + }, + "meraki": { + "hashes": [ + "sha256:2d8c23726e2a65fceb0d2ba343d79ac633cdf73d2a58349f4fe214d5fdf6718d", + "sha256:cd9a7cc92aa2d8243eace96906486e00b8dccac964b1e6695e0495695bae442e" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.38.0" + }, + "multidict": { + "hashes": [ + "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", + "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", + "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", + "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", + "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", + "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", + "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", + "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", + "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", + "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", + "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", + "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", + "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", + "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", + "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", + "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", + "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", + "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", + "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", + "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", + "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", + "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", + "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", + "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", + "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", + "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", + "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", + "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", + "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", + "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", + "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", + "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", + "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", + "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", + "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", + "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", + "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", + "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", + "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", + "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", + "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", + "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", + "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", + "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", + "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", + "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", + "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", + "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", + "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", + "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", + "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", + "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", + "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", + "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", + "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", + "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", + "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", + "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", + "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", + "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", + "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", + "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", + "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", + "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", + "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", + "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", + "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", + "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", + "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", + "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", + "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", + "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", + "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", + "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.4" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pytest": { + "hashes": [ + "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", + "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.2.2" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + }, + "yarl": { + "hashes": [ + "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", + "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", + "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", + "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", + "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", + "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", + "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", + "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", + "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", + "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", + "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", + "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", + "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", + "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", + "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", + "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", + "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", + "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", + "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", + "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", + "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", + "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", + "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", + "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", + "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", + "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", + "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", + "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", + "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", + "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", + "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", + "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", + "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", + "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", + "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", + "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", + "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", + "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", + "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", + "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", + "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", + "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", + "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", + "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", + "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", + "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", + "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", + "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", + "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", + "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", + "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", + "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", + "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", + "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", + "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", + "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", + "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", + "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", + "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", + "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", + "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", + "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", + "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", + "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", + "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", + "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", + "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", + "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", + "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", + "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", + "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", + "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", + "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", + "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", + "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", + "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", + "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", + "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", + "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", + "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", + "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", + "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", + "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", + "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", + "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", + "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", + "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", + "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", + "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", + "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.4" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 34d50e7a..c015976d 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,76 @@ # Meraki Dashboard API Python Library -The Meraki Dashboard API Python library provides all current Meraki [dashboard API](https://developer.cisco.com/meraki/api-v1/) calls to interface with the Cisco Meraki cloud-managed platform. Meraki generates the library based on dashboard API's OpenAPI spec to keep it up to date with the latest API releases, and provides the full source code for the library including the tools used to generate the library, if you are participating in the Early Access program or would like to contribute to the development of the library. Meraki welcomes constructive pull requests that maintain backwards compatibility with prior versions. The library requires Python 3.7+, receives support from the community, and you can install it via [PyPI](https://pypi.org/project/meraki/): +The Meraki Dashboard API Python library provides all current +Meraki [dashboard API](https://developer.cisco.com/meraki/api-v1/) calls to interface with the Cisco Meraki +cloud-managed platform. Meraki generates the library based on dashboard API's OpenAPI spec to keep it up to date with +the latest API releases, and provides the full source code for the library including the tools used to generate the +library, if you are participating in the Early Access program or would like to contribute to the development of the +library. Meraki welcomes constructive pull requests that maintain backwards compatibility with prior versions. The +library requires Python 3.8+, receives support from the community, and you can install it +via [PyPI](https://pypi.org/project/meraki/): pip install --upgrade meraki -If you participate in [our Early Access program](https://community.meraki.com/t5/Developers-APIs/UPDATED-Beta-testing-with-the-Meraki-Developer-Early-Access/m-p/145344#M5808) and would like to use early access features via the library, you'll [find instructions in the generator readme](https://github.com/meraki/dashboard-api-python/tree/main/generator#readme). +If you participate +in [our Early Access program](https://community.meraki.com/t5/Developers-APIs/UPDATED-Beta-testing-with-the-Meraki-Developer-Early-Access/m-p/145344#M5808) +and would like to use early access features via the library, +you'll [find instructions in the generator readme](https://github.com/meraki/dashboard-api-python/tree/main/generator#readme). ## Features -While you can make direct HTTP requests to dashboard API in any programming language or REST API client, using a client library can make it easier for you to focus on your specific use case, without the overhead of having to write functions to handle the dashboard API calls. The Python library can also take care of error handling, logging, retries, and other convenient processes and options for you automatically. +While you can make direct HTTP requests to dashboard API in any programming language or REST API client, using a client +library can make it easier for you to focus on your specific use case, without the overhead of having to write functions +to handle the dashboard API calls. The Python library can also take care of error handling, logging, retries, and other +convenient processes and options for you automatically. -* Support for all API endpoints, as it uses the [OpenAPI specification](https://api.meraki.com/api/v1/openapiSpec) to generate source code +* Support for all API endpoints, as it uses the [OpenAPI specification](https://api.meraki.com/api/v1/openapiSpec) to + generate source code * Log all API requests made to a local file as well as on-screen console -* Automatic retries upon 429 rate limit errors, using the [`Retry-After` field](https://developer.cisco.com/meraki/api-v1/#!rate-limit) within response headers +* Automatic retries upon 429 rate limit errors, using + the [`Retry-After` field](https://developer.cisco.com/meraki/api-v1/#!rate-limit) within response headers * Get all (or a specified number of) pages of data with built-in pagination control * Tweak settings such as maximum retries, certificate path, suppress logging, and other options * Simulate POST/PUT/DELETE calls to preview first, so that network configuration does not get changed ## Setup -1. Enable API access in your Meraki dashboard organization and obtain an API key ([instructions](https://documentation.meraki.com/zGeneral_Administration/Other_Topics/The_Cisco_Meraki_Dashboard_API)) +1. Enable API access in your Meraki dashboard organization and obtain an API + key ([instructions](https://documentation.meraki.com/zGeneral_Administration/Other_Topics/The_Cisco_Meraki_Dashboard_API)) -2. Keep your API key safe and secure, as it is similar to a password for your dashboard. If publishing your Python code to a wider audience, please research secure handling of API keys. +2. Keep your API key safe and secure, as it is similar to a password for your dashboard. If publishing your Python code + to a wider audience, please research secure handling of API keys. 3. Install the latest version of [Python 3](ttps://wiki.python.org/moin/BeginnersGuide/NonProgrammers) -4. Use _pip_ (or an alternative such as _easy_install_) to install the library from the Python [Package Index](https://pypi.org/project/meraki/): +4. Use _pip_ (or an alternative such as _easy_install_) to install the library from the + Python [Package Index](https://pypi.org/project/meraki/): * `pip install meraki` - * If you have both Python3 and Python2 installed, you may need to use `pip3` (so `pip3 install meraki`) along with `python3` on your system - * If _meraki_ was previously installed, you can upgrade to the latest non-beta release with `pip install --upgrade meraki` - -5. The library supports Meraki dashboard API v1. You can also specify the version of the library when installing with _pip_: - * See the full [release history](https://pypi.org/project/meraki/#history) to pick the version you want, or use `pip install meraki==` without including a version number to display the list of available versions + * If you have both Python3 and Python2 installed, you may need to use `pip3` (so `pip3 install meraki`) along + with `python3` on your system + * If _meraki_ was previously installed, you can upgrade to the latest non-beta release + with `pip install --upgrade meraki` + +5. The library supports Meraki dashboard API v1. You can also specify the version of the library when installing with + _pip_: + * See the full [release history](https://pypi.org/project/meraki/#history) to pick the version you want, or + use `pip install meraki==` without including a version number to display the list of available versions * Versions begin with _1_ (1.0.0b**z** for beta) * Specify the version you want with the install command; for example: `pip install meraki==1.34.0` * You can also see the version currently installed with `pip show meraki` - * End-of-life v0 versions of the Python library begin with _0_ (0.**x**.**y**) and are not supported nor recommended. + * End-of-life v0 versions of the Python library begin with _0_ (0.**x**.**y**) and are not supported nor + recommended. ## Usage -1. Export your API key as an [environment variable](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html), for example: + +1. Export your API key as + an [environment variable](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html), for example: ```shell - export MERAKI_DASHBOARD_API_KEY=093b24e85df15a3e66f1fc359f4c48493eaa1b73 + export MERAKI_DASHBOARD_API_KEY=YOUR_KEY_HERE ``` -2. Alternatively, define your API key as a variable in your source code; this method is not recommended due to its inherent insecurity. +2. Alternatively, define your API key as a variable in your source code; this method is not recommended due to its + inherent insecurity. 3. Single line of code to import and use the library goes at the top of your script: @@ -58,31 +84,48 @@ While you can make direct HTTP requests to dashboard API in any programming lang dashboard = meraki.DashboardAPI() ``` -5. Make dashboard API calls in your source code, using the format _client.scope.operation_, where _client_ is the name you defined in the previous step (**dashboard** above), _scope_ is the corresponding scope that represents the first tag from the OpenAPI spec, and _operation_ is the operation of the API endpoint. For example, to make a call to get the list of organizations accessible by the API key defined in step 1, use this function call: +5. Make dashboard API calls in your source code, using the format _client.scope.operation_, where _client_ is the name + you defined in the previous step (**dashboard** above), _scope_ is the corresponding scope that represents the first + tag from the OpenAPI spec, and _operation_ is the operation of the API endpoint. For example, to make a call to get + the list of organizations accessible by the API key defined in step 1, use this function call: ```python my_orgs = dashboard.organizations.getOrganizations() ``` ### Examples + You can find fully working example scripts in the **examples** folder. -| Script | Purpose | -|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Script | Purpose | +|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **org_wide_clients.py** | That code collects the clients of all networks, in all orgs to which the key has access. No changes are made, since only GET endpoints are called, and the data is written to local CSV output files. | ## AsyncIO -**asyncio** is a library to write concurrent code using the **async/await** syntax. Special thanks to Heimo Stieg ([@coreGreenberet](https://github.com/coreGreenberet)) who has ported the API to asyncio. + +**asyncio** is a library to write concurrent code using the **async/await** syntax. Special thanks to Heimo +Stieg ([@coreGreenberet](https://github.com/coreGreenberet)) who has ported the API to asyncio. + +### Installation on macOS + +If you use a Mac, then you may need to take +[additional Python installation steps](https://bugs.python.org/issue43404) that aren't required on other platforms. This +is [a limitation of macOS and not the library](https://github.com/meraki/dashboard-api-python/issues/226). This step is +not required on Windows. + +### Usage The usage is similiar to the sequential version above. However it has has some differences. -1. Export your API key as an [environment variable](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html), for example: +1. Export your API key as + an [environment variable](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html), for example: ```shell - export MERAKI_DASHBOARD_API_KEY=093b24e85df15a3e66f1fc359f4c48493eaa1b73 + export MERAKI_DASHBOARD_API_KEY=YOUR_KEY_HERE ``` -2. Alternatively, define your API key as a variable in your source code; this method is not recommended due to its inherent insecurity. +2. Alternatively, define your API key as a variable in your source code; this method is not recommended due to its + inherent insecurity. 3. Single line of code to import and use the library goes at the top of your script: @@ -95,44 +138,52 @@ The usage is similiar to the sequential version above. However it has has some d ```python async with meraki.aio.AsyncDashboardAPI() as aiomeraki: ``` - The **async with** statement is important here to make sure, that the client sessions will be closed after using the api. + The **async with** statement is important here to make sure, that the client sessions will be closed after using the + api. -5. Make dashboard API calls in your source code, using the format await _client.section.operation_, where _client_ is the name you defined in the previous step (**aiomeraki** above), _section_ is the corresponding group (or tag from the OpenAPI spec) from the [API docs](https://developer.cisco.com/meraki/api/#/rest), and _operation_ is the name (or operation ID from OpenAPI) of the API endpoint. For example, to make a call to get the list of organizations accessible by the API key defined in step 1, use this function call: +5. Make dashboard API calls in your source code, using the format await _client.section.operation_, where _client_ is + the name you defined in the previous step (**aiomeraki** above), _section_ is the corresponding group (or tag from + the OpenAPI spec) from the [API docs](https://developer.cisco.com/meraki/api/#/rest), and _operation_ is the name (or + operation ID from OpenAPI) of the API endpoint. For example, to make a call to get the list of organizations + accessible by the API key defined in step 1, use this function call: ```python my_orgs = await aiomeraki.organizations.getOrganizations() ``` 6. Run everything inside an event loop. + ```python import asyncio if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(my_async_entry_point()) - - # if you are using Python 3.7+ you can also simply - # use the following line instead of the two lines above + # replace my_async_entry_point with the name of your entry point method asyncio.run(my_async_entry_point()) ``` - ### Examples + You can find fully working example scripts in the **examples** folder. -| Script | Purpose | +| Script | Purpose | |-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **aio_org_wide_clients.py** | That code is a asyncio port from org_wide_clients.py and collects the clients of all networks, in all orgs to which the key has access. No changes are made, since only GET endpoints are called, and the data is written to local CSV output files. | -| **aio_ips2firewall.py** | That code will collect the source IP of security events and creates L7 firewall rules to block them. `usage: aio_ips2firewall.py [-h] -o ORGANIZATIONS [ORGANIZATIONS ...] [-f FILTER] [-s] [-d DAYS]` | - +| **aio_org_wide_clients.py** | That code is a asyncio port from org_wide_clients.py and collects the clients of all +networks, in all orgs to which the key has access. No changes are made, since only GET endpoints are called, and the +data is written to local CSV output files. | +| **aio_ips2firewall.py** | That code will collect the source IP of security events and creates L7 firewall rules to +block them. `usage: aio_ips2firewall.py [-h] -o ORGANIZATIONS [ORGANIZATIONS ...] [-f FILTER] [-s] [-d DAYS]` | ## Note for application developers and ecosystem partners -We're so glad that you're leveraging our Python library. It's best practice to identify your application with every API request that you make. You can easily do this automatically just by following the format defined in [config.py](https://github.com/meraki/dashboard-api-python/blob/master/meraki/config.py) and passing the session kwarg: +We're so glad that you're leveraging our Python library. It's best practice to identify your application with every API +request that you make. You can easily do this automatically just by following the format defined +in [config.py](https://github.com/meraki/dashboard-api-python/blob/master/meraki/config.py) and passing the session +kwarg: ``` Python MERAKI_PYTHON_SDK_CALLER ``` -Unless you are an ecosystem partner, this identifier is optional. +Unless you are an ecosystem partner, this identifier is optional. -1. If you are an ecosystem partner and you have questions about this requirement, please reach out to your ecosystem rep. +1. If you are an ecosystem partner and you have questions about this requirement, please reach out to your ecosystem + rep. 2. If you have any questions about the formatting, please ask your question by opening an issue in this repo. diff --git a/examples/local_subnet_dumper.py b/examples/local_subnet_dumper.py new file mode 100644 index 00000000..5a4c2874 --- /dev/null +++ b/examples/local_subnet_dumper.py @@ -0,0 +1,136 @@ +import json + +import meraki + +""" +2023-11-13 +Author: John M. Kuchta ( TKIPisalegacycipher // https://github.com/TKIPisalegacycipher ) +Requrires: Meraki library v1.39.0 or later. + +This script gathers all your appliances' local subnets from your organizations' networks and dumps them to a JSON file. +You might find this handy for certain IPAM exercises. Subnets from appliances in "single LAN" mode will have VLAN 0. + +EXTRA CREDIT +If you are feeling adventurous, or just want an opportunity to flex your Python skills, consider re-writing this script +to work asynchronously, which can substantially improve the speed for large environments. You might start by gathering +all the relevant information asynchronously, then doing the list comprehensions after the API calls are complete. +""" + +# You can exclude specific organization or networks here. This is optional but recommended if you have a large +# deployment including lots of irrelevant networks. +excluded_org_ids = [] +excluded_network_ids = [] + +# init session +d = meraki.DashboardAPI() + +# gather orgs +my_orgs = d.organizations.getOrganizations() +my_orgs = [org for org in my_orgs if org["id"] not in excluded_org_ids] + +print(f"done gathering organizations") + +# gather networks +my_networks = [ + d.organizations.getOrganizationNetworks( + organization["id"], total_pages=all + ) + for organization in my_orgs +] + +print(f"done gathering networks") + +my_appliance_networks = [ + network + for netlist in my_networks + for network in netlist + if network["id"] not in excluded_network_ids + and "appliance" in network["productTypes"] +] + +print(f"done gathering appliance networks") + +# gather routed networks -- appliances in passthrough mode don't have local subnets +my_appliance_routed_networks = [ + network + for network in my_appliance_networks + if d.appliance.getNetworkApplianceSettings(network["id"])["deploymentMode"] + == "routed" +] + +print(f"done gathering routed appliance networks") + +my_appliance_networks_with_vlans = [ + network + for network in my_appliance_routed_networks + if d.appliance.getNetworkApplianceVlansSettings(network["id"])["vlansEnabled"] +] + +print(f"done gathering appliance network vlan settings") + +my_appliance_networks_without_vlans = [ + network + for network in my_appliance_routed_networks + if network not in my_appliance_networks_with_vlans +] + +my_vlan_lists = [ + { + "organizationId": network["organizationId"], + "networkId": network["id"], + "vlans": d.appliance.getNetworkApplianceVlans(network["id"]), + } + for network in my_appliance_networks_with_vlans +] + +print(f"done gathering appliance network vlans") + +my_lans = [ + { + "organizationId": network["organizationId"], + "networkId": network["id"], + "lan": d.appliance.getNetworkApplianceSingleLan(network["id"]), + } + for network in my_appliance_networks_without_vlans +] + +print(f"done gathering appliance network lans") + +# unpack the subnets +vlan_subnets = list() + +for item in my_vlan_lists: + for vlan in item["vlans"]: + this_subnet = dict() + this_subnet["organizationId"] = item["organizationId"] + this_subnet["networkId"] = vlan["networkId"] + this_subnet["subnet"] = vlan["subnet"] + this_subnet["vlanId"] = vlan["id"] + this_subnet["applianceIp"] = vlan["applianceIp"] + vlan_subnets.append(this_subnet) + +lan_subnets = list() + +for item in my_lans: + this_subnet = dict() + this_subnet["organizationId"] = item["organizationId"] + this_subnet["networkId"] = item["networkId"] + this_subnet["subnet"] = item["lan"]["subnet"] + this_subnet["vlanId"] = 0 + this_subnet["applianceIp"] = item["lan"]["applianceIp"] + lan_subnets.append(this_subnet) + +all_subnets = vlan_subnets + lan_subnets + +print("done assembling subnets") + +# dump the subnets to a JSON file +json_object = json.dumps(all_subnets, indent=4) + +with open( + "subnets.json", + "w", +) as outfile: + outfile.write(json_object) + +print("subnets dumped to subnets.json") diff --git a/generator/generate_library.py b/generator/generate_library.py index e740bdf3..7c035d30 100644 --- a/generator/generate_library.py +++ b/generator/generate_library.py @@ -1,7 +1,7 @@ import getopt import os -import sys import platform +import sys import jinja2 import requests @@ -46,16 +46,17 @@ def generate_pagination_parameters(operation: str): def check_python_version(): # Check minimum Python version - version_warning_string = f'The generator requires Python 3.10 at minimum. ' \ - f'Your interpreter version details are: \n' \ - f'platform.python_version_tuple()[0] = {platform.python_version_tuple()[0]}\n' \ - f'platform.python_version_tuple()[1] = {platform.python_version_tuple()[1]}\n' \ - f'platform.python_version is {platform.python_version()}\n' \ - f'Please consult the generator readme at your convenience: ' \ - f'https://github.com/meraki/dashboard-api-python/blob/main/generator/readme.md' - if int(platform.python_version_tuple()[0]) != 3: - sys.exit(version_warning_string) - elif int(platform.python_version_tuple()[1]) < 7: + version_warning_string = ( + f"This library generator requires Python 3.10 at minimum. " + f"Your interpreter version is: {platform.python_version()}. " + f"Please consult the readme at your convenience: " + f"https://github.com/meraki/dashboard-api-python/blob/main/generator/readme.md " + f"Additional details: " + f"python_version_tuple()[0] = {platform.python_version_tuple()[0]}; " + f"python_version_tuple()[1] = {platform.python_version_tuple()[1]} " + ) + + if not (int(platform.python_version_tuple()[0]) == 3 and int(platform.python_version_tuple()[1]) >= 10): sys.exit(version_warning_string) @@ -223,7 +224,7 @@ def generate_library(spec: dict, version_number: str, is_github_action: bool): os.mkdir(directory) # Files that are not generated - non_generated = ['__init__.py', 'config.py', 'exceptions.py', 'rest_session.py', 'api/__init__.py', + non_generated = ['__init__.py', 'config.py', 'common.py', 'exceptions.py', 'rest_session.py', 'api/__init__.py', 'aio/__init__.py', 'aio/rest_session.py', 'aio/api/__init__.py', 'api/batch/__init__.py'] base_url = 'https://raw.githubusercontent.com/meraki/dashboard-api-python/master/meraki/' for file in non_generated: diff --git a/generator/generate_snippets.py b/generator/generate_snippets.py index 0f8b6aca..4b0b2298 100644 --- a/generator/generate_snippets.py +++ b/generator/generate_snippets.py @@ -4,49 +4,53 @@ import requests from jinja2 import Template -CALL_TEMPLATE = Template('''import meraki +CALL_TEMPLATE = Template( + """import meraki -# Defining your API key as a variable in source code is not recommended -API_KEY = '6bec40cf957de430a6f1f2baa056b99a4fac9ea0' -# Instead, use an environment variable as shown under the Usage section +# Defining your API key as a variable in source code is discouraged. +# This API key is for a read-only docs-specific environment. +# In your own code, use an environment variable as shown under the Usage section # @ https://github.com/meraki/dashboard-api-python/ +API_KEY = '75dd5334bef4d2bc96f26138c163c0a3fa0b5ca6' + dashboard = meraki.DashboardAPI(API_KEY) {{ parameter_assignments }} response = dashboard.{{ section }}.{{ operation }}({{ parameters }}) print(response) -''') +""" +) -REVERSE_PAGINATION = ['getNetworkEvents', 'getOrganizationConfigurationChanges'] +REVERSE_PAGINATION = ["getNetworkEvents", "getOrganizationConfigurationChanges"] # Helper function to convert camel case parameter name to snake case def snakify(param): - ret = '' + ret = "" for s in param: if s.islower(): ret += s - elif s == '_': - ret += '_' + elif s == "_": + ret += "_" else: - ret += '_' + s.lower() + ret += "_" + s.lower() return ret # Helper function to return pagination parameters depending on endpoint def generate_pagination_parameters(operation): ret = { - 'total_pages': { - 'type': 'integer or string', - 'description': 'total number of pages to retrieve, -1 or "all" for all pages', + "total_pages": { + "type": "integer or string", + "description": 'total number of pages to retrieve, -1 or "all" for all pages', }, - 'direction': { - 'type': 'string', - 'description': 'direction to paginate, either "next" or "prev" (default) page' + "direction": { + "type": "string", + "description": 'direction to paginate, either "next" or "prev" (default) page' if operation in REVERSE_PAGINATION else 'direction to paginate, either "next" (default) or "prev" page', - } + }, } return ret @@ -59,41 +63,41 @@ def parse_params(operation, parameters, param_filters=[]): # Create dict with information on endpoint's parameters params = {} for p in parameters: - name = p['name'] - if 'schema' in p: - keys = p['schema']['properties'] + name = p["name"] + if "schema" in p: + keys = p["schema"]["properties"] for k in keys: - if 'required' in p['schema'] and k in p['schema']['required']: - params[k] = {'required': True} + if "required" in p["schema"] and k in p["schema"]["required"]: + params[k] = {"required": True} else: - params[k] = {'required': False} - params[k]['in'] = p['in'] - params[k]['type'] = keys[k]['type'] - params[k]['description'] = keys[k]['description'] - if 'enum' in keys[k]: - params[k]['enum'] = keys[k]['enum'] - if 'example' in p['schema'] and k in p['schema']['example']: - params[k]['example'] = p['schema']['example'][k] - elif 'required' in p and p['required']: - params[name] = {'required': True} - params[name]['in'] = p['in'] - params[name]['type'] = p['type'] - if 'description' in p: - params[name]['description'] = p['description'] + params[k] = {"required": False} + params[k]["in"] = p["in"] + params[k]["type"] = keys[k]["type"] + params[k]["description"] = keys[k]["description"] + if "enum" in keys[k]: + params[k]["enum"] = keys[k]["enum"] + if "example" in p["schema"] and k in p["schema"]["example"]: + params[k]["example"] = p["schema"]["example"][k] + elif "required" in p and p["required"]: + params[name] = {"required": True} + params[name]["in"] = p["in"] + params[name]["type"] = p["type"] + if "description" in p: + params[name]["description"] = p["description"] else: - params[name]['description'] = '(required)' - if 'enum' in p: - params[name]['enum'] = p['enum'] + params[name]["description"] = "(required)" + if "enum" in p: + params[name]["enum"] = p["enum"] else: - params[name] = {'required': False} - params[name]['in'] = p['in'] - params[name]['type'] = p['type'] - params[name]['description'] = p['description'] - if 'enum' in p: - params[name]['enum'] = p['enum'] + params[name] = {"required": False} + params[name]["in"] = p["in"] + params[name]["type"] = p["type"] + params[name]["description"] = p["description"] + if "enum" in p: + params[name]["enum"] = p["enum"] # Add custom library parameters to handle pagination - if 'perPage' in params: + if "perPage" in params: params.update(generate_pagination_parameters(operation)) # Return parameters based on matching input filters @@ -101,77 +105,109 @@ def parse_params(operation, parameters, param_filters=[]): return params else: ret = {} - if 'required' in param_filters: - ret.update({k: v for k, v in params.items() if 'required' in v and v['required']}) - if 'pagination' in param_filters: - ret.update(generate_pagination_parameters(operation) if 'perPage' in params else {}) - if 'optional' in param_filters: - ret.update({k: v for k, v in params.items() if 'required' in v and not v['required']}) - if 'path' in param_filters: - ret.update({k: v for k, v in params.items() if 'in' in v and v['in'] == 'path'}) - if 'query' in param_filters: - ret.update({k: v for k, v in params.items() if 'in' in v and v['in'] == 'query'}) - if 'body' in param_filters: - ret.update({k: v for k, v in params.items() if 'in' in v and v['in'] == 'body'}) - if 'array' in param_filters: - ret.update({k: v for k, v in params.items() if 'in' in v and v['type'] == 'array'}) - if 'enum' in param_filters: - ret.update({k: v for k, v in params.items() if 'enum' in v}) + if "required" in param_filters: + ret.update( + {k: v for k, v in params.items() if "required" in v and v["required"]} + ) + if "pagination" in param_filters: + ret.update( + generate_pagination_parameters(operation) if "perPage" in params else {} + ) + if "optional" in param_filters: + ret.update( + { + k: v + for k, v in params.items() + if "required" in v and not v["required"] + } + ) + if "path" in param_filters: + ret.update( + {k: v for k, v in params.items() if "in" in v and v["in"] == "path"} + ) + if "query" in param_filters: + ret.update( + {k: v for k, v in params.items() if "in" in v and v["in"] == "query"} + ) + if "body" in param_filters: + ret.update( + {k: v for k, v in params.items() if "in" in v and v["in"] == "body"} + ) + if "array" in param_filters: + ret.update( + {k: v for k, v in params.items() if "in" in v and v["type"] == "array"} + ) + if "enum" in param_filters: + ret.update({k: v for k, v in params.items() if "enum" in v}) return ret # Generate text for parameter assignments def process_assignments(parameters): - text = '\n' + text = "\n" for k, v in parameters.items(): param_name = snakify(k) - if param_name == 'id': - param_name = 'id_' - - if v == 'list': - text += f'{param_name} = []\n' - elif v == 'float': - text += f'{param_name} = 0.0\n' - elif v == 'int': - text += f'{param_name} = 0\n' - elif v == 'bool': - text += f'{param_name} = False\n' - elif v == 'dict': - text += f'{param_name} = {{}}\n' - elif v == 'str': - text += f'{param_name} = \'\'\n' + if param_name == "id": + param_name = "id_" + + if v == "list": + text += f"{param_name} = []\n" + elif v == "float": + text += f"{param_name} = 0.0\n" + elif v == "int": + text += f"{param_name} = 0\n" + elif v == "bool": + text += f"{param_name} = False\n" + elif v == "dict": + text += f"{param_name} = {{}}\n" + elif v == "str": + text += f"{param_name} = ''\n" else: if type(v) == str: - value = f'\'{v}\'' + value = f"'{v}'" else: value = v - text += f'{param_name} = {value}\n' + text += f"{param_name} = {value}\n" return text def main(): # Get latest OpenAPI specification - spec = requests.get('https://api.meraki.com/api/v1/openapiSpec').json() + spec = requests.get("https://api.meraki.com/api/v1/openapiSpec").json() # Supported scopes list will include organizations, networks, devices, and all product types. - supported_scopes = ['administered', 'organizations', 'networks', 'devices', 'appliance', 'camera', 'cellularGateway', 'insight', - 'sm', 'switch', 'wireless', 'sensor', 'licensing', 'secureConnect'] + supported_scopes = [ + "administered", + "organizations", + "networks", + "devices", + "appliance", + "camera", + "cellularGateway", + "insight", + "sm", + "switch", + "wireless", + "sensor", + "licensing", + "secureConnect", + ] # legacy scopes = ['organizations', 'networks', 'devices', # 'appliance', 'camera', 'cellularGateway', 'insight', 'sm', 'switch', 'wireless'] - tags = spec['tags'] - paths = spec['paths'] + tags = spec["tags"] + paths = spec["paths"] # Scopes used when generating the library will depend on the provided version of the API spec. - scopes = {tag['name']: {} for tag in tags if tag['name'] in supported_scopes} + scopes = {tag["name"]: {} for tag in tags if tag["name"] in supported_scopes} # Organize data operations = [] for path, methods in paths.items(): for method in methods: endpoint = paths[path][method] - tags = endpoint['tags'] - operation = endpoint['operationId'] + tags = endpoint["tags"] + operation = endpoint["operationId"] operations.append(operation) scope = tags[0] if path not in scopes[scope]: @@ -181,91 +217,99 @@ def main(): # Generate API libraries for scope in scopes: - print(f'...generating {scope}') + print(f"...generating {scope}") section = scopes[scope] for path, methods in section.items(): for method, endpoint in methods.items(): # Get metadata - tags = endpoint['tags'] - operation = endpoint['operationId'] - description = endpoint['summary'] - parameters = endpoint['parameters'] if 'parameters' in endpoint else None - responses = endpoint['responses'] # not actually used here for library generation + tags = endpoint["tags"] + operation = endpoint["operationId"] + description = endpoint["summary"] + parameters = ( + endpoint["parameters"] if "parameters" in endpoint else None + ) + responses = endpoint[ + "responses" + ] # not actually used here for library generation required = {} optional = {} if parameters: - if 'perPage' in parse_params(operation, parameters): + if "perPage" in parse_params(operation, parameters): pagination = True else: pagination = False - for p, values in parse_params(operation, parameters, 'required').items(): - if 'example' in values: - required[p] = values['example'] - elif p == 'organizationId': - required[p] = '549236' - elif p == 'networkId': + for p, values in parse_params( + operation, parameters, "required" + ).items(): + if "example" in values: + required[p] = values["example"] + elif p == "organizationId": + required[p] = "549236" + elif p == "networkId": # DevNet Sandbox ALWAYS ON network @ https://n149.meraki.com/o/-t35Mb/manage/organization/overview - required[p] = 'L_646829496481105433' - elif p == 'serial': - required[p] = 'Q2QN-9J8L-SLPD' - elif values['type'] == 'array': - required[p] = 'list' - elif values['type'] == 'number': - required[p] = 'float' - elif values['type'] == 'integer': - required[p] = 'int' - elif values['type'] == 'boolean': - required[p] = 'bool' - elif values['type'] == 'object': - required[p] = 'dict' - elif values['type'] == 'string': - required[p] = 'str' + required[p] = "L_646829496481105433" + elif p == "serial": + required[p] = "Q2QN-9J8L-SLPD" + elif values["type"] == "array": + required[p] = "list" + elif values["type"] == "number": + required[p] = "float" + elif values["type"] == "integer": + required[p] = "int" + elif values["type"] == "boolean": + required[p] = "bool" + elif values["type"] == "object": + required[p] = "dict" + elif values["type"] == "string": + required[p] = "str" else: sys.exit(p, values) if pagination: if operation not in REVERSE_PAGINATION: - optional['total_pages'] = 'all' + optional["total_pages"] = "all" else: - optional['total_pages'] = 3 + optional["total_pages"] = 3 - for p, values in parse_params(operation, parameters, 'optional').items(): - if 'example' in values: - optional[p] = values['example'] + for p, values in parse_params( + operation, parameters, "optional" + ).items(): + if "example" in values: + optional[p] = values["example"] - if operation == 'createNetworkGroupPolicy': + if operation == "createNetworkGroupPolicy": print(required) print(optional) - if 'code_snippets' not in os.listdir(): - os.mkdir('code_snippets') + if "code_snippets" not in os.listdir(): + os.mkdir("code_snippets") - with open(f'code_snippets/{operation}.py', 'w', encoding='utf-8') as fp: + with open(f"code_snippets/{operation}.py", "w", encoding="utf-8") as fp: if required.items(): - parameters_text = '\n ' + parameters_text = "\n " for k, v in required.items(): param_name = snakify(k) - if param_name == 'id': - param_name = 'id_' - parameters_text += f'{param_name}, ' + if param_name == "id": + param_name = "id_" + parameters_text += f"{param_name}, " for k, v in optional.items(): - if k == 'total_pages' and v == 'all': - parameters_text += f'total_pages=\'all\'' - elif k == 'total_pages' and v == 1: - parameters_text += f'total_pages=1' + if k == "total_pages" and v == "all": + parameters_text += f"total_pages='all'" + elif k == "total_pages" and v == 1: + parameters_text += f"total_pages=1" elif type(v) == str: - parameters_text += f'\n {k}=\'{v}\', ' + parameters_text += f"\n {k}='{v}', " else: - parameters_text += f'\n {k}={v}, ' - if parameters_text[-2:] == ', ': + parameters_text += f"\n {k}={v}, " + if parameters_text[-2:] == ", ": parameters_text = parameters_text[:-2] - parameters_text += '\n' + parameters_text += "\n" else: - parameters_text = '' + parameters_text = "" fp.write( CALL_TEMPLATE.render( @@ -277,5 +321,5 @@ def main(): ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/generator/readme.md b/generator/readme.md index 84a07ca9..eb2c24ef 100644 --- a/generator/readme.md +++ b/generator/readme.md @@ -1,17 +1,28 @@ # Generating the Meraki Dashboard API Python Library -Generally speaking, you will not need to generate this yourself. Simply use the official PyPI package via `pip install --update meraki`. +Generally speaking, you will not need to generate this yourself. Simply use the official PyPI package +via `pip install --update meraki`. -However, if you participate in Early Access features, you may want to generate a library to match your org's spec. In which case, follow along. +However, if you participate in Early Access features, you may want to generate a library to match your org's spec. In +which case, follow along. > **NB:** The generator requires Python 3.10 or later. 1. Clone this repo locally. 2. Open a terminal in this `generator` folder. -3. *Optional:* If you want to work with beta endpoints, then first [review the warnings, and then opt one of your orgs into the Early API Access program](https://community.meraki.com/t5/Developers-APIs/UPDATED-Beta-testing-with-the-Meraki-Developer-Early-Access/m-p/145344#M5808). +3. *Optional:* If you want to work with beta endpoints, then + first [review the warnings, and then opt one of your orgs into the Early API Access program](https://community.meraki.com/t5/Developers-APIs/UPDATED-Beta-testing-with-the-Meraki-Developer-Early-Access/m-p/145344#M5808). 4. Run `python generate_library.py -v locally_generated -o YOUR_ORG_ID -k YOUR_API_KEY` making these replacements: -* Replace `YOUR_ORG_ID` with the org ID you want to use as reference. Use the one opted into Early API access if you want the beta endpoints. + +* Replace `YOUR_ORG_ID` with the org ID you want to use as reference. Use the one opted into Early API access if you + want the beta endpoints. * Replace `YOUR_API_KEY` with an API key that has org admin privileges on that org. -* NB: Your local system may require minor syntax tweaks (e.g. Windows may require you prepend `generate_library.py` with `.\`) -5. You will now have a `meraki` module folder inside `generator`, which you can locally reference in your scripts. Simply copy the `meraki` folder to those projects which require it. -6. In some cases, if you've already installed the official library, your scripts may prefer that one over the local folder. If that happens, then calls to early access endpoints will fail. So, if necessary, uninstall any instances of the meraki package that may have been installed in your venv or system, or replace the version installed in your venv with that which you generated here. +* NB: Your local system may require minor syntax tweaks (e.g. Windows may require you prepend `generate_library.py` + with `.\`) + +5. You will now have a `meraki` module folder inside `generator`, which you can locally reference in your scripts. + Simply copy the `meraki` folder to those projects which require it. +6. In some cases, if you've already installed the official library, your scripts may prefer that one over the local + folder. If that happens, then calls to early access endpoints will fail. So, if necessary, uninstall any instances of + the meraki package that may have been installed in your venv or system, or replace the version installed in your venv + with that which you generated here. diff --git a/meraki/__init__.py b/meraki/__init__.py index a3599e70..738518ae 100644 --- a/meraki/__init__.py +++ b/meraki/__init__.py @@ -1,25 +1,21 @@ -from datetime import datetime import logging import os -from meraki.rest_session import * from meraki.api.administered import Administered -from meraki.api.organizations import Organizations -from meraki.api.networks import Networks -from meraki.api.devices import Devices from meraki.api.appliance import Appliance +# Batch class imports +from meraki.api.batch import Batch from meraki.api.camera import Camera from meraki.api.cellularGateway import CellularGateway +from meraki.api.devices import Devices from meraki.api.insight import Insight from meraki.api.licensing import Licensing +from meraki.api.networks import Networks +from meraki.api.organizations import Organizations from meraki.api.sensor import Sensor from meraki.api.sm import Sm from meraki.api.switch import Switch from meraki.api.wireless import Wireless - -# Batch class imports -from meraki.api.batch import Batch - # Config import from meraki.config import ( API_KEY_ENVIRONMENT_VARIABLE, @@ -30,6 +26,7 @@ WAIT_ON_RATE_LIMIT, NGINX_429_RETRY_WAIT_TIME, ACTION_BATCH_RETRY_WAIT_TIME, + NETWORK_DELETE_RETRY_WAIT_TIME, RETRY_4XX_ERROR, RETRY_4XX_ERROR_WAIT_TIME, MAXIMUM_RETRIES, @@ -44,8 +41,9 @@ MERAKI_PYTHON_SDK_CALLER, USE_ITERATOR_FOR_GET_PAGES, ) +from meraki.rest_session import * -__version__ = '1.38.0' +__version__ = '1.40.1' class DashboardAPI(object): diff --git a/meraki/aio/__init__.py b/meraki/aio/__init__.py index 4ee66147..c0c5cbac 100644 --- a/meraki/aio/__init__.py +++ b/meraki/aio/__init__.py @@ -1,7 +1,6 @@ import logging import os -from meraki.aio.rest_session import * from meraki.aio.api.administered import AsyncAdministered from meraki.aio.api.appliance import AsyncAppliance from meraki.aio.api.camera import AsyncCamera @@ -15,10 +14,9 @@ from meraki.aio.api.sm import AsyncSm from meraki.aio.api.switch import AsyncSwitch from meraki.aio.api.wireless import AsyncWireless - +from meraki.aio.rest_session import * # Batch class imports from meraki.api.batch import Batch - # Config import from meraki.config import ( API_KEY_ENVIRONMENT_VARIABLE, diff --git a/meraki/aio/api/devices.py b/meraki/aio/api/devices.py index 6e33ce6b..25092e51 100644 --- a/meraki/aio/api/devices.py +++ b/meraki/aio/api/devices.py @@ -166,6 +166,7 @@ def createDeviceLiveToolsPing(self, serial: str, target: str, **kwargs): - serial (string): Serial - target (string): FQDN, IPv4 or IPv6 address - count (integer): Count parameter to pass to ping. [1..5], default 5 + - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -177,7 +178,7 @@ def createDeviceLiveToolsPing(self, serial: str, target: str, **kwargs): serial = urllib.parse.quote(str(serial), safe='') resource = f'/devices/{serial}/liveTools/ping' - body_params = ['target', 'count', ] + body_params = ['target', 'count', 'callback', ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} return self._session.post(metadata, resource, payload) @@ -212,6 +213,7 @@ def createDeviceLiveToolsPingDevice(self, serial: str, **kwargs): - serial (string): Serial - count (integer): Count parameter to pass to ping. [1..5], default 5 + - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -223,7 +225,7 @@ def createDeviceLiveToolsPingDevice(self, serial: str, **kwargs): serial = urllib.parse.quote(str(serial), safe='') resource = f'/devices/{serial}/liveTools/pingDevice' - body_params = ['count', ] + body_params = ['count', 'callback', ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} return self._session.post(metadata, resource, payload) diff --git a/meraki/aio/api/networks.py b/meraki/aio/api/networks.py index d598e821..c366bf68 100644 --- a/meraki/aio/api/networks.py +++ b/meraki/aio/api/networks.py @@ -246,7 +246,7 @@ def getNetworkClients(self, networkId: str, total_pages=1, direction='next', **k - direction (string): direction to paginate, either "next" (default) or "prev" page - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 31 days from today. - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameter t0. The value must be in seconds and be less than or equal to 31 days. The default is 1 day. - - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 10. + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 5000. Default is 10. - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. - statuses (array): Filters clients based on status. Can be one of 'Online' or 'Offline'. diff --git a/meraki/aio/api/organizations.py b/meraki/aio/api/organizations.py index c0db1e4c..78bdde55 100644 --- a/meraki/aio/api/organizations.py +++ b/meraki/aio/api/organizations.py @@ -123,6 +123,7 @@ def createOrganizationActionBatch(self, organizationId: str, actions: list, **kw - actions (array): A set of changes to make as part of this action (more details) - confirmed (boolean): Set to true for immediate execution. Set to false if the action should be previewed before executing. This property cannot be unset once it is true. Defaults to false. - synchronous (boolean): Set to true to force the batch to run synchronous. There can be at most 20 actions in synchronous batch. Defaults to false. + - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -134,7 +135,7 @@ def createOrganizationActionBatch(self, organizationId: str, actions: list, **kw organizationId = urllib.parse.quote(str(organizationId), safe='') resource = f'/organizations/{organizationId}/actionBatches' - body_params = ['confirmed', 'synchronous', 'actions', ] + body_params = ['confirmed', 'synchronous', 'actions', 'callback', ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} return self._session.post(metadata, resource, payload) @@ -2116,7 +2117,7 @@ def getOrganizationInventoryDevices(self, organizationId: str, total_pages=1, di - usedState (string): Filter results by used or unused inventory. Accepted values are 'used' or 'unused'. - search (string): Search for devices in inventory based on serial number, mac address, or model. - macs (array): Search for devices in inventory based on mac addresses. - - networkIds (array): Search for devices in inventory based on network ids. + - networkIds (array): Search for devices in inventory based on network ids. Use explicit 'null' value to get available devices only. - serials (array): Search for devices in inventory based on serials. - models (array): Search for devices in inventory based on model. - orderNumbers (array): Search for devices in inventory based on order numbers. @@ -2268,6 +2269,7 @@ def getOrganizationInventoryOnboardingCloudMonitoringNetworks(self, organization - deviceType (string): Device Type switch or wireless controller - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages - direction (string): direction to paginate, either "next" (default) or "prev" page + - search (string): Optional parameter to search on network name - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 100000. Default is 1000. - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. @@ -2286,7 +2288,7 @@ def getOrganizationInventoryOnboardingCloudMonitoringNetworks(self, organization organizationId = urllib.parse.quote(str(organizationId), safe='') resource = f'/organizations/{organizationId}/inventory/onboarding/cloudMonitoring/networks' - query_params = ['deviceType', 'perPage', 'startingAfter', 'endingBefore', ] + query_params = ['deviceType', 'search', 'perPage', 'startingAfter', 'endingBefore', ] params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} return self._session.get_pages(metadata, resource, params, total_pages, direction) @@ -3597,6 +3599,27 @@ def getOrganizationWebhooksAlertTypes(self, organizationId: str, **kwargs): + def getOrganizationWebhooksCallbacksStatus(self, organizationId: str, callbackId: str): + """ + **Return the status of an API callback** + https://developer.cisco.com/meraki/api-v1/#!get-organization-webhooks-callbacks-status + + - organizationId (string): Organization ID + - callbackId (string): Callback ID + """ + + metadata = { + 'tags': ['organizations', 'configure', 'webhooks', 'callbacks', 'statuses'], + 'operation': 'getOrganizationWebhooksCallbacksStatus' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + callbackId = urllib.parse.quote(str(callbackId), safe='') + resource = f'/organizations/{organizationId}/webhooks/callbacks/statuses/{callbackId}' + + return self._session.get(metadata, resource) + + + def getOrganizationWebhooksLogs(self, organizationId: str, total_pages=1, direction='next', **kwargs): """ **Return the log of webhook POSTs sent** diff --git a/meraki/aio/api/sm.py b/meraki/aio/api/sm.py index f2e9cee4..c4ca80b1 100644 --- a/meraki/aio/api/sm.py +++ b/meraki/aio/api/sm.py @@ -68,10 +68,11 @@ def getNetworkSmDevices(self, networkId: str, total_pages=1, direction='next', * ownerEmail, ownerUsername, osBuild, publicIp, phoneNumber, diskInfoJson, deviceCapacity, isManaged, hadMdm, isSupervised, meid, imei, iccid, simCarrierNetwork, cellularDataUsed, isHotspotEnabled, createdAt, batteryEstCharge, quarantined, avName, avRunning, asName, fwName, isRooted, loginRequired, screenLockEnabled, screenLockDelay, autoLoginDisabled, autoTags, hasMdm, hasDesktopAgent, diskEncryptionEnabled, - hardwareEncryptionCaps, passCodeLock, usesHardwareKeystore, androidSecurityPatchVersion, and url. + hardwareEncryptionCaps, passCodeLock, usesHardwareKeystore, androidSecurityPatchVersion, cellular, and url. - wifiMacs (array): Filter devices by wifi mac(s). - serials (array): Filter devices by serial(s). - ids (array): Filter devices by id(s). + - uuids (array): Filter devices by uuid(s). - scope (array): Specify a scope (one of all, none, withAny, withAll, withoutAny, or withoutAll) and a set of tags. - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. @@ -87,10 +88,10 @@ def getNetworkSmDevices(self, networkId: str, total_pages=1, direction='next', * networkId = urllib.parse.quote(str(networkId), safe='') resource = f'/networks/{networkId}/sm/devices' - query_params = ['fields', 'wifiMacs', 'serials', 'ids', 'scope', 'perPage', 'startingAfter', 'endingBefore', ] + query_params = ['fields', 'wifiMacs', 'serials', 'ids', 'uuids', 'scope', 'perPage', 'startingAfter', 'endingBefore', ] params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} - array_params = ['fields', 'wifiMacs', 'serials', 'ids', 'scope', ] + array_params = ['fields', 'wifiMacs', 'serials', 'ids', 'uuids', 'scope', ] for k, v in kwargs.items(): if k.strip() in array_params: params[f'{k.strip()}[]'] = kwargs[f'{k}'] @@ -663,7 +664,7 @@ def getNetworkSmDeviceWlanLists(self, networkId: str, deviceId: str): def getNetworkSmProfiles(self, networkId: str): """ - **List all the profiles in the network** + **List all profiles in a network** https://developer.cisco.com/meraki/api-v1/#!get-network-sm-profiles - networkId (string): Network ID diff --git a/meraki/aio/api/switch.py b/meraki/aio/api/switch.py index 209ee468..ad49aab1 100644 --- a/meraki/aio/api/switch.py +++ b/meraki/aio/api/switch.py @@ -2472,6 +2472,33 @@ def updateOrganizationConfigTemplateSwitchProfilePort(self, organizationId: str, + def getOrganizationSummarySwitchPowerHistory(self, organizationId: str, **kwargs): + """ + **Returns the total PoE power draw for all switch ports in the organization over the requested timespan (by default the last 24 hours)** + https://developer.cisco.com/meraki/api-v1/#!get-organization-summary-switch-power-history + + - organizationId (string): Organization ID + - t0 (string): The beginning of the timespan for the data. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 31 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be less than or equal to 31 days. The default is 1 day. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['switch', 'monitor', 'summary', 'power', 'history'], + 'operation': 'getOrganizationSummarySwitchPowerHistory' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/summary/switch/power/history' + + query_params = ['t0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + return self._session.get(metadata, resource, params) + + + def cloneOrganizationSwitchDevices(self, organizationId: str, sourceSerial: str, targetSerials: list): """ **Clone port-level and some switch-level configuration settings from a source switch to one or more target switches** diff --git a/meraki/aio/api/wireless.py b/meraki/aio/api/wireless.py index 762686c5..df523ec8 100644 --- a/meraki/aio/api/wireless.py +++ b/meraki/aio/api/wireless.py @@ -2429,3 +2429,129 @@ def getOrganizationWirelessDevicesEthernetStatuses(self, organizationId: str, to return self._session.get_pages(metadata, resource, params, total_pages, direction) + + + def getOrganizationWirelessDevicesPacketLossByClient(self, organizationId: str, total_pages=1, direction='next', **kwargs): + """ + **Get average packet loss for the given timespan for all clients in the organization.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-wireless-devices-packet-loss-by-client + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - networkIds (array): Filter results by network. + - ssids (array): Filter results by SSID number. + - bands (array): Filter results by band. Valid bands are: 2.4, 5, and 6. + - macs (array): Filter results by client mac address(es). + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 90 days from today. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 90 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be greater than or equal to 5 minutes and be less than or equal to 90 days. The default is 7 days. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['wireless', 'monitor', 'devices', 'packetLoss', 'byClient'], + 'operation': 'getOrganizationWirelessDevicesPacketLossByClient' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/wireless/devices/packetLoss/byClient' + + query_params = ['networkIds', 'ssids', 'bands', 'macs', 'perPage', 'startingAfter', 'endingBefore', 't0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = ['networkIds', 'ssids', 'bands', 'macs', ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f'{k.strip()}[]'] = kwargs[f'{k}'] + params.pop(k.strip()) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + + + + def getOrganizationWirelessDevicesPacketLossByDevice(self, organizationId: str, total_pages=1, direction='next', **kwargs): + """ + **Get average packet loss for the given timespan for all devices in the organization** + https://developer.cisco.com/meraki/api-v1/#!get-organization-wireless-devices-packet-loss-by-device + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - networkIds (array): Filter results by network. + - serials (array): Filter results by device. + - ssids (array): Filter results by SSID number. + - bands (array): Filter results by band. Valid bands are: 2.4, 5, and 6. + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 90 days from today. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 90 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be greater than or equal to 5 minutes and be less than or equal to 90 days. The default is 7 days. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['wireless', 'monitor', 'devices', 'packetLoss', 'byDevice'], + 'operation': 'getOrganizationWirelessDevicesPacketLossByDevice' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/wireless/devices/packetLoss/byDevice' + + query_params = ['networkIds', 'serials', 'ssids', 'bands', 'perPage', 'startingAfter', 'endingBefore', 't0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = ['networkIds', 'serials', 'ssids', 'bands', ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f'{k.strip()}[]'] = kwargs[f'{k}'] + params.pop(k.strip()) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + + + + def getOrganizationWirelessDevicesPacketLossByNetwork(self, organizationId: str, total_pages=1, direction='next', **kwargs): + """ + **Get average packet loss for the given timespan for all networks in the organization.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-wireless-devices-packet-loss-by-network + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - networkIds (array): Filter results by network. + - serials (array): Filter results by device. + - ssids (array): Filter results by SSID number. + - bands (array): Filter results by band. Valid bands are: 2.4, 5, and 6. + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 90 days from today. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 90 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be greater than or equal to 5 minutes and be less than or equal to 90 days. The default is 7 days. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['wireless', 'monitor', 'devices', 'packetLoss', 'byNetwork'], + 'operation': 'getOrganizationWirelessDevicesPacketLossByNetwork' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/wireless/devices/packetLoss/byNetwork' + + query_params = ['networkIds', 'serials', 'ssids', 'bands', 'perPage', 'startingAfter', 'endingBefore', 't0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = ['networkIds', 'serials', 'ssids', 'bands', ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f'{k.strip()}[]'] = kwargs[f'{k}'] + params.pop(k.strip()) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + diff --git a/meraki/aio/rest_session.py b/meraki/aio/rest_session.py index 74465a99..25eb2bb3 100644 --- a/meraki/aio/rest_session.py +++ b/meraki/aio/rest_session.py @@ -1,20 +1,17 @@ -import logging -import sys -from datetime import datetime +import asyncio import json -import platform import random -import urllib.parse -import asyncio import ssl +import sys import time - +import urllib.parse +from datetime import datetime import aiohttp from meraki.__init__ import __version__ +from meraki.common import * from meraki.config import * -from meraki.exceptions import * from meraki.rest_session import user_agent_extended @@ -66,18 +63,7 @@ def __init__( self.use_iterator_for_get_pages = use_iterator_for_get_pages # Check minimum Python version - version_warning_string = f'This library requires Python 3.7 at minimum. Python versions 3.6 and below ' \ - f'are end of life and end of support per the Python maintainers, and your ' \ - f'interpreter version details are: \n' \ - f'platform.python_version_tuple()[0] = {platform.python_version_tuple()[0]}\n' \ - f'platform.python_version_tuple()[1] = {platform.python_version_tuple()[1]}\n' \ - f'platform.python_version is {platform.python_version()}\n' \ - f'Please consult the readme at your convenience: ' \ - f'https://github.com/meraki/dashboard-api-python' - if int(platform.python_version_tuple()[0]) != 3: - sys.exit(version_warning_string) - elif int(platform.python_version_tuple()[1]) < 7: - sys.exit(version_warning_string) + check_python_version() # Check base URL if "v0" in self._base_url: @@ -254,7 +240,10 @@ async def _request(self, metadata, method, url, **kwargs): else: try: message = await response.json(content_type = None) - message_is_dict = True + if isinstance(message, dict): + message_is_dict = True + else: + message_is_dict = False except aiohttp.client_exceptions.ContentTypeError: message_is_dict = False try: diff --git a/meraki/api/devices.py b/meraki/api/devices.py index 2e3976d0..874a031b 100644 --- a/meraki/api/devices.py +++ b/meraki/api/devices.py @@ -166,6 +166,7 @@ def createDeviceLiveToolsPing(self, serial: str, target: str, **kwargs): - serial (string): Serial - target (string): FQDN, IPv4 or IPv6 address - count (integer): Count parameter to pass to ping. [1..5], default 5 + - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -177,7 +178,7 @@ def createDeviceLiveToolsPing(self, serial: str, target: str, **kwargs): serial = urllib.parse.quote(str(serial), safe='') resource = f'/devices/{serial}/liveTools/ping' - body_params = ['target', 'count', ] + body_params = ['target', 'count', 'callback', ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} return self._session.post(metadata, resource, payload) @@ -212,6 +213,7 @@ def createDeviceLiveToolsPingDevice(self, serial: str, **kwargs): - serial (string): Serial - count (integer): Count parameter to pass to ping. [1..5], default 5 + - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -223,7 +225,7 @@ def createDeviceLiveToolsPingDevice(self, serial: str, **kwargs): serial = urllib.parse.quote(str(serial), safe='') resource = f'/devices/{serial}/liveTools/pingDevice' - body_params = ['count', ] + body_params = ['count', 'callback', ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} return self._session.post(metadata, resource, payload) diff --git a/meraki/api/networks.py b/meraki/api/networks.py index 6326f52a..898f6682 100644 --- a/meraki/api/networks.py +++ b/meraki/api/networks.py @@ -246,7 +246,7 @@ def getNetworkClients(self, networkId: str, total_pages=1, direction='next', **k - direction (string): direction to paginate, either "next" (default) or "prev" page - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 31 days from today. - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameter t0. The value must be in seconds and be less than or equal to 31 days. The default is 1 day. - - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 10. + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 5000. Default is 10. - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. - statuses (array): Filters clients based on status. Can be one of 'Online' or 'Offline'. diff --git a/meraki/api/organizations.py b/meraki/api/organizations.py index a2055231..013d1da3 100644 --- a/meraki/api/organizations.py +++ b/meraki/api/organizations.py @@ -123,6 +123,7 @@ def createOrganizationActionBatch(self, organizationId: str, actions: list, **kw - actions (array): A set of changes to make as part of this action (more details) - confirmed (boolean): Set to true for immediate execution. Set to false if the action should be previewed before executing. This property cannot be unset once it is true. Defaults to false. - synchronous (boolean): Set to true to force the batch to run synchronous. There can be at most 20 actions in synchronous batch. Defaults to false. + - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -134,7 +135,7 @@ def createOrganizationActionBatch(self, organizationId: str, actions: list, **kw organizationId = urllib.parse.quote(str(organizationId), safe='') resource = f'/organizations/{organizationId}/actionBatches' - body_params = ['confirmed', 'synchronous', 'actions', ] + body_params = ['confirmed', 'synchronous', 'actions', 'callback', ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} return self._session.post(metadata, resource, payload) @@ -2116,7 +2117,7 @@ def getOrganizationInventoryDevices(self, organizationId: str, total_pages=1, di - usedState (string): Filter results by used or unused inventory. Accepted values are 'used' or 'unused'. - search (string): Search for devices in inventory based on serial number, mac address, or model. - macs (array): Search for devices in inventory based on mac addresses. - - networkIds (array): Search for devices in inventory based on network ids. + - networkIds (array): Search for devices in inventory based on network ids. Use explicit 'null' value to get available devices only. - serials (array): Search for devices in inventory based on serials. - models (array): Search for devices in inventory based on model. - orderNumbers (array): Search for devices in inventory based on order numbers. @@ -2268,6 +2269,7 @@ def getOrganizationInventoryOnboardingCloudMonitoringNetworks(self, organization - deviceType (string): Device Type switch or wireless controller - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages - direction (string): direction to paginate, either "next" (default) or "prev" page + - search (string): Optional parameter to search on network name - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 100000. Default is 1000. - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. @@ -2286,7 +2288,7 @@ def getOrganizationInventoryOnboardingCloudMonitoringNetworks(self, organization organizationId = urllib.parse.quote(str(organizationId), safe='') resource = f'/organizations/{organizationId}/inventory/onboarding/cloudMonitoring/networks' - query_params = ['deviceType', 'perPage', 'startingAfter', 'endingBefore', ] + query_params = ['deviceType', 'search', 'perPage', 'startingAfter', 'endingBefore', ] params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} return self._session.get_pages(metadata, resource, params, total_pages, direction) @@ -3597,6 +3599,27 @@ def getOrganizationWebhooksAlertTypes(self, organizationId: str, **kwargs): + def getOrganizationWebhooksCallbacksStatus(self, organizationId: str, callbackId: str): + """ + **Return the status of an API callback** + https://developer.cisco.com/meraki/api-v1/#!get-organization-webhooks-callbacks-status + + - organizationId (string): Organization ID + - callbackId (string): Callback ID + """ + + metadata = { + 'tags': ['organizations', 'configure', 'webhooks', 'callbacks', 'statuses'], + 'operation': 'getOrganizationWebhooksCallbacksStatus' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + callbackId = urllib.parse.quote(str(callbackId), safe='') + resource = f'/organizations/{organizationId}/webhooks/callbacks/statuses/{callbackId}' + + return self._session.get(metadata, resource) + + + def getOrganizationWebhooksLogs(self, organizationId: str, total_pages=1, direction='next', **kwargs): """ **Return the log of webhook POSTs sent** diff --git a/meraki/api/sm.py b/meraki/api/sm.py index bea94079..32fcac91 100644 --- a/meraki/api/sm.py +++ b/meraki/api/sm.py @@ -68,10 +68,11 @@ def getNetworkSmDevices(self, networkId: str, total_pages=1, direction='next', * ownerEmail, ownerUsername, osBuild, publicIp, phoneNumber, diskInfoJson, deviceCapacity, isManaged, hadMdm, isSupervised, meid, imei, iccid, simCarrierNetwork, cellularDataUsed, isHotspotEnabled, createdAt, batteryEstCharge, quarantined, avName, avRunning, asName, fwName, isRooted, loginRequired, screenLockEnabled, screenLockDelay, autoLoginDisabled, autoTags, hasMdm, hasDesktopAgent, diskEncryptionEnabled, - hardwareEncryptionCaps, passCodeLock, usesHardwareKeystore, androidSecurityPatchVersion, and url. + hardwareEncryptionCaps, passCodeLock, usesHardwareKeystore, androidSecurityPatchVersion, cellular, and url. - wifiMacs (array): Filter devices by wifi mac(s). - serials (array): Filter devices by serial(s). - ids (array): Filter devices by id(s). + - uuids (array): Filter devices by uuid(s). - scope (array): Specify a scope (one of all, none, withAny, withAll, withoutAny, or withoutAll) and a set of tags. - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. @@ -87,10 +88,10 @@ def getNetworkSmDevices(self, networkId: str, total_pages=1, direction='next', * networkId = urllib.parse.quote(str(networkId), safe='') resource = f'/networks/{networkId}/sm/devices' - query_params = ['fields', 'wifiMacs', 'serials', 'ids', 'scope', 'perPage', 'startingAfter', 'endingBefore', ] + query_params = ['fields', 'wifiMacs', 'serials', 'ids', 'uuids', 'scope', 'perPage', 'startingAfter', 'endingBefore', ] params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} - array_params = ['fields', 'wifiMacs', 'serials', 'ids', 'scope', ] + array_params = ['fields', 'wifiMacs', 'serials', 'ids', 'uuids', 'scope', ] for k, v in kwargs.items(): if k.strip() in array_params: params[f'{k.strip()}[]'] = kwargs[f'{k}'] @@ -663,7 +664,7 @@ def getNetworkSmDeviceWlanLists(self, networkId: str, deviceId: str): def getNetworkSmProfiles(self, networkId: str): """ - **List all the profiles in the network** + **List all profiles in a network** https://developer.cisco.com/meraki/api-v1/#!get-network-sm-profiles - networkId (string): Network ID diff --git a/meraki/api/switch.py b/meraki/api/switch.py index ba6e036f..8902a3e3 100644 --- a/meraki/api/switch.py +++ b/meraki/api/switch.py @@ -2472,6 +2472,33 @@ def updateOrganizationConfigTemplateSwitchProfilePort(self, organizationId: str, + def getOrganizationSummarySwitchPowerHistory(self, organizationId: str, **kwargs): + """ + **Returns the total PoE power draw for all switch ports in the organization over the requested timespan (by default the last 24 hours)** + https://developer.cisco.com/meraki/api-v1/#!get-organization-summary-switch-power-history + + - organizationId (string): Organization ID + - t0 (string): The beginning of the timespan for the data. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 31 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be less than or equal to 31 days. The default is 1 day. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['switch', 'monitor', 'summary', 'power', 'history'], + 'operation': 'getOrganizationSummarySwitchPowerHistory' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/summary/switch/power/history' + + query_params = ['t0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + return self._session.get(metadata, resource, params) + + + def cloneOrganizationSwitchDevices(self, organizationId: str, sourceSerial: str, targetSerials: list): """ **Clone port-level and some switch-level configuration settings from a source switch to one or more target switches** diff --git a/meraki/api/wireless.py b/meraki/api/wireless.py index 9db3b63e..06041097 100644 --- a/meraki/api/wireless.py +++ b/meraki/api/wireless.py @@ -2429,3 +2429,129 @@ def getOrganizationWirelessDevicesEthernetStatuses(self, organizationId: str, to return self._session.get_pages(metadata, resource, params, total_pages, direction) + + + def getOrganizationWirelessDevicesPacketLossByClient(self, organizationId: str, total_pages=1, direction='next', **kwargs): + """ + **Get average packet loss for the given timespan for all clients in the organization.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-wireless-devices-packet-loss-by-client + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - networkIds (array): Filter results by network. + - ssids (array): Filter results by SSID number. + - bands (array): Filter results by band. Valid bands are: 2.4, 5, and 6. + - macs (array): Filter results by client mac address(es). + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 90 days from today. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 90 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be greater than or equal to 5 minutes and be less than or equal to 90 days. The default is 7 days. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['wireless', 'monitor', 'devices', 'packetLoss', 'byClient'], + 'operation': 'getOrganizationWirelessDevicesPacketLossByClient' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/wireless/devices/packetLoss/byClient' + + query_params = ['networkIds', 'ssids', 'bands', 'macs', 'perPage', 'startingAfter', 'endingBefore', 't0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = ['networkIds', 'ssids', 'bands', 'macs', ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f'{k.strip()}[]'] = kwargs[f'{k}'] + params.pop(k.strip()) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + + + + def getOrganizationWirelessDevicesPacketLossByDevice(self, organizationId: str, total_pages=1, direction='next', **kwargs): + """ + **Get average packet loss for the given timespan for all devices in the organization** + https://developer.cisco.com/meraki/api-v1/#!get-organization-wireless-devices-packet-loss-by-device + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - networkIds (array): Filter results by network. + - serials (array): Filter results by device. + - ssids (array): Filter results by SSID number. + - bands (array): Filter results by band. Valid bands are: 2.4, 5, and 6. + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 90 days from today. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 90 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be greater than or equal to 5 minutes and be less than or equal to 90 days. The default is 7 days. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['wireless', 'monitor', 'devices', 'packetLoss', 'byDevice'], + 'operation': 'getOrganizationWirelessDevicesPacketLossByDevice' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/wireless/devices/packetLoss/byDevice' + + query_params = ['networkIds', 'serials', 'ssids', 'bands', 'perPage', 'startingAfter', 'endingBefore', 't0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = ['networkIds', 'serials', 'ssids', 'bands', ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f'{k.strip()}[]'] = kwargs[f'{k}'] + params.pop(k.strip()) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + + + + def getOrganizationWirelessDevicesPacketLossByNetwork(self, organizationId: str, total_pages=1, direction='next', **kwargs): + """ + **Get average packet loss for the given timespan for all networks in the organization.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-wireless-devices-packet-loss-by-network + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - networkIds (array): Filter results by network. + - serials (array): Filter results by device. + - ssids (array): Filter results by SSID number. + - bands (array): Filter results by band. Valid bands are: 2.4, 5, and 6. + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 90 days from today. + - t1 (string): The end of the timespan for the data. t1 can be a maximum of 90 days after t0. + - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be greater than or equal to 5 minutes and be less than or equal to 90 days. The default is 7 days. + """ + + kwargs.update(locals()) + + metadata = { + 'tags': ['wireless', 'monitor', 'devices', 'packetLoss', 'byNetwork'], + 'operation': 'getOrganizationWirelessDevicesPacketLossByNetwork' + } + organizationId = urllib.parse.quote(str(organizationId), safe='') + resource = f'/organizations/{organizationId}/wireless/devices/packetLoss/byNetwork' + + query_params = ['networkIds', 'serials', 'ssids', 'bands', 'perPage', 'startingAfter', 'endingBefore', 't0', 't1', 'timespan', ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = ['networkIds', 'serials', 'ssids', 'bands', ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f'{k.strip()}[]'] = kwargs[f'{k}'] + params.pop(k.strip()) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + diff --git a/meraki/common.py b/meraki/common.py new file mode 100644 index 00000000..801d1f47 --- /dev/null +++ b/meraki/common.py @@ -0,0 +1,22 @@ +import platform +from meraki.exceptions import * + + +def check_python_version(): + # Check minimum Python version + + if not ( + int(platform.python_version_tuple()[0]) == 3 + and int(platform.python_version_tuple()[1]) >= 8 + ): + message = ( + f"This library requires Python 3.8 at minimum. Python versions 3.7 and below " + f"no longer receive security updates since reaching end of life and of support " + f"per the Python maintainers. Your interpreter version is: {platform.python_version()}. " + f"Please consult the readme at your convenience: https://github.com/meraki/dashboard-api-python " + f"Additional details: " + f"python_version_tuple()[0] = {platform.python_version_tuple()[0]}; " + f"python_version_tuple()[1] = {platform.python_version_tuple()[1]} " + ) + + raise PythonVersionError(message) diff --git a/meraki/config.py b/meraki/config.py index ce8c2c15..43d33434 100644 --- a/meraki/config.py +++ b/meraki/config.py @@ -1,19 +1,19 @@ # Package Constants # Meraki dashboard API key, set either at instantiation or as an environment variable -API_KEY_ENVIRONMENT_VARIABLE = 'MERAKI_DASHBOARD_API_KEY' +API_KEY_ENVIRONMENT_VARIABLE = "MERAKI_DASHBOARD_API_KEY" # Base URL preceding all endpoint resources -DEFAULT_BASE_URL = 'https://api.meraki.com/api/v1' +DEFAULT_BASE_URL = "https://api.meraki.com/api/v1" # Maximum number of seconds for each API call SINGLE_REQUEST_TIMEOUT = 60 # Path for TLS/SSL certificate verification if behind local proxy -CERTIFICATE_PATH = '' +CERTIFICATE_PATH = "" # Proxy server and port, if needed, for HTTPS -REQUESTS_PROXY = '' +REQUESTS_PROXY = "" # Retry if 429 rate limit error encountered? WAIT_ON_RATE_LIMIT = True @@ -40,10 +40,10 @@ OUTPUT_LOG = True # Path to output log; by default, working directory of script if not specified -LOG_PATH = '' +LOG_PATH = "" # Log file name appended with date and timestamp -LOG_FILE_PREFIX = 'meraki_api_' +LOG_FILE_PREFIX = "meraki_api_" # Print output logging to console? PRINT_TO_CONSOLE = True @@ -65,7 +65,7 @@ AIO_MAXIMUM_CONCURRENT_REQUESTS = 8 # Legacy partner identifier for API usage tracking; can also be set as an environment variable BE_GEO_ID -BE_GEO_ID = '' +BE_GEO_ID = "" # Optional identifier for API usage tracking; can also be set as an environment variable MERAKI_PYTHON_SDK_CALLER # It's good practice to use this to identify your application using the format: @@ -83,4 +83,4 @@ # don't want to report version number, then you would use, at minimum: 'SunshineRainbows hunter2ForLife' # The choice is yours as long as you follow the format. You should **not** include other information in this string. # If you are an official ecosystem partner, this is required. -MERAKI_PYTHON_SDK_CALLER = '' +MERAKI_PYTHON_SDK_CALLER = "" diff --git a/meraki/exceptions.py b/meraki/exceptions.py index 7fa830b5..0130f3cd 100644 --- a/meraki/exceptions.py +++ b/meraki/exceptions.py @@ -13,13 +13,17 @@ class APIResponseError(Exception): Exception class raised from HTTP class methods. Used as a single catch-all error for any possible requests exception error that might happen during communication with Meraki API to simplify caller coding. """ + def __init__(self, obj_name, status_code, error_msg): self.obj_name = obj_name self.status_code = status_code self.reason = error_msg def exc_message(self): - return f'HTTP call within object "{self.obj_name}" failed. Status code is "{self.status_code}". Error message is: "{self.reason}".' + return ( + f'HTTP call within object "{self.obj_name}" failed. Status code is "{self.status_code}". ' + f'Error message is: "{self.reason}".' + ) def json(self): return dict(error=self.reason, status_code=self.status_code) @@ -94,3 +98,12 @@ def __init__(self, metadata, response, message): def __repr__(self): return f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}" + + +class PythonVersionError(Exception): + """Exception raised for unsupported Python versions.""" + + def __init__(self, message): + self.message = message + + super().__init__(self.message) diff --git a/meraki/rest_session.py b/meraki/rest_session.py index cce98284..dd788088 100644 --- a/meraki/rest_session.py +++ b/meraki/rest_session.py @@ -1,16 +1,15 @@ -import sys -from datetime import datetime import json -import platform import random -import urllib.parse +import sys import time +import urllib.parse +from datetime import datetime import requests -from meraki.config import * -from meraki.exceptions import * from meraki.__init__ import __version__ +from meraki.common import * +from meraki.config import * def user_agent_extended(be_geo_id, caller): @@ -55,22 +54,6 @@ def user_agent_extended(be_geo_id, caller): # Main module interface -def check_python_version(): - # Check minimum Python version - version_warning_string = f'This library requires Python 3.7 at minimum. Python versions 3.6 and below ' \ - f'are end of life and end of support per the Python maintainers, and your ' \ - f'interpreter version details are: \n' \ - f'platform.python_version_tuple()[0] = {platform.python_version_tuple()[0]}\n' \ - f'platform.python_version_tuple()[1] = {platform.python_version_tuple()[1]}\n' \ - f'platform.python_version is {platform.python_version()}\n' \ - f'Please consult the readme at your convenience: ' \ - f'https://github.com/meraki/dashboard-api-python' - if int(platform.python_version_tuple()[0]) != 3: - sys.exit(version_warning_string) - elif int(platform.python_version_tuple()[1]) < 7: - sys.exit(version_warning_string) - - class RestSession(object): def __init__( self, @@ -207,7 +190,8 @@ def request(self, metadata, method, url, **kwargs): retries -= 1 if retries == 0: if e.response and e.response.status_code: - raise APIError(metadata, APIResponseError(e.__class__.__name__, e.response.status_code, str(e))) + raise APIError(metadata, APIResponseError(e.__class__.__name__, + e.response.status_code, str(e))) else: raise APIError(metadata, APIResponseError(e.__class__.__name__, 503, str(e))) else: diff --git a/requirements.txt b/requirements.txt index 465c005e..9617cf81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -aiohttp~=3.8.3 -requests~=2.28.1 -meraki~=1.27.0 +aiohttp~=3.9.0 +requests~=2.31.0 +meraki~=1.38.0 setuptools~=67.2.0 Jinja2==3.1.2 pytest~=7.2.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 9d4cf00c..d25e289a 100644 --- a/setup.py +++ b/setup.py @@ -49,5 +49,5 @@ def find_version(fname): 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', ], - python_requires='>=3.7' + python_requires='>=3.8' ) diff --git a/tests/test_dashboard_api_python_library.py b/tests/test_dashboard_api_python_library.py index 63de223d..e0b44148 100644 --- a/tests/test_dashboard_api_python_library.py +++ b/tests/test_dashboard_api_python_library.py @@ -1,8 +1,9 @@ +import platform +import random + import pytest + import meraki -import random -import platform -import time @pytest.fixture(scope='session') @@ -13,7 +14,7 @@ def api_key(pytestconfig): @pytest.fixture(scope='session') def dashboard(api_key): - return meraki.DashboardAPI(api_key, suppress_logging=True) + return meraki.DashboardAPI(api_key, suppress_logging=True, network_delete_retry_wait_time=600) @pytest.fixture(scope='session')