diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91a23eb..2a9d5aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,10 +32,10 @@ jobs: pip install -r requirements.txt - name: Run linter - run: flake8 + run: flake8 tiny_api_client - name: Run type checker - run: mypy tiny_api_client.py + run: mypy tiny_api_client - name: Run test suite run: pytest -vvvv diff --git a/Pipfile b/Pipfile index 44559c9..5d72f83 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,7 @@ name = "pypi" requests = "*" [dev-packages] -mypy = "*" +mypy = "1.8.0" pytest = "*" sphinx = "*" build = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 0b11c8f..9a78122 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a79655949fa83a6ddeb9c4eca268c32910f225d6081384736bc32a46fe9fa914" + "sha256": "e09be3e9122482f7a9a2e0eabcb25e49c4fc78cc39296d3383fb6910788251cd" }, "pipfile-spec": 6, "requires": { @@ -122,11 +122,11 @@ }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.4" + "version": "==3.6" }, "requests": { "hashes": [ @@ -134,7 +134,6 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "urllib3": { @@ -157,11 +156,11 @@ }, "babel": { "hashes": [ - "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900", - "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed" + "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", + "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" ], "markers": "python_version >= '3.7'", - "version": "==2.13.1" + "version": "==2.14.0" }, "build": { "hashes": [ @@ -169,7 +168,6 @@ "sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==1.0.3" }, "certifi": { @@ -180,64 +178,6 @@ "markers": "python_version >= '3.6'", "version": "==2023.11.17" }, - "cffi": { - "hashes": [ - "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", - "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", - "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", - "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", - "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", - "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", - "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", - "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", - "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", - "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", - "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", - "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", - "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", - "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", - "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", - "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", - "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", - "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", - "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", - "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", - "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", - "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", - "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", - "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", - "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", - "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", - "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", - "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", - "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", - "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", - "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", - "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", - "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", - "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", - "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", - "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", - "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", - "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", - "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", - "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", - "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", - "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", - "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", - "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", - "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", - "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", - "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", - "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", - "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", - "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", - "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", - "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" - ], - "markers": "python_version >= '3.8'", - "version": "==1.16.0" - }, "cfgv": { "hashes": [ "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", @@ -342,49 +282,20 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, - "cryptography": { - "hashes": [ - "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf", - "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84", - "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e", - "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8", - "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7", - "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1", - "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88", - "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86", - "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179", - "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81", - "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20", - "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548", - "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d", - "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d", - "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5", - "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1", - "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147", - "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936", - "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797", - "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696", - "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72", - "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da", - "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723" - ], - "markers": "python_version >= '3.7'", - "version": "==41.0.5" - }, "distlib": { "hashes": [ - "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", - "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" + "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", + "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" ], - "version": "==0.3.7" + "version": "==0.3.8" }, "docutils": { "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" + "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", + "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.18.1" + "markers": "python_version >= '3.7'", + "version": "==0.20.1" }, "exceptiongroup": { "hashes": [ @@ -392,7 +303,6 @@ "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==1.2.0" }, "filelock": { @@ -409,7 +319,6 @@ "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" ], "index": "pypi", - "markers": "python_full_version >= '3.8.1'", "version": "==6.1.0" }, "flake8-pyproject": { @@ -417,24 +326,23 @@ "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a" ], "index": "pypi", - "markers": "python_version >= '3.6'", "version": "==1.2.3" }, "identify": { "hashes": [ - "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545", - "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407" + "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d", + "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34" ], "markers": "python_version >= '3.8'", - "version": "==2.5.32" + "version": "==2.5.33" }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.4" + "version": "==3.6" }, "imagesize": { "hashes": [ @@ -446,11 +354,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb", - "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743" + "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", + "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" ], "markers": "python_version >= '3.8'", - "version": "==6.8.0" + "version": "==7.0.1" }, "iniconfig": { "hashes": [ @@ -462,12 +370,11 @@ }, "isort": { "hashes": [ - "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", - "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" ], "index": "pypi", - "markers": "python_full_version >= '3.8.0'", - "version": "==5.12.0" + "version": "==5.13.2" }, "jaraco.classes": { "hashes": [ @@ -477,14 +384,6 @@ "markers": "python_version >= '3.8'", "version": "==3.3.0" }, - "jeepney": { - "hashes": [ - "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", - "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.8.0" - }, "jinja2": { "hashes": [ "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", @@ -601,37 +500,36 @@ }, "mypy": { "hashes": [ - "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46", - "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41", - "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc", - "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9", - "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5", - "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901", - "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665", - "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357", - "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d", - "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418", - "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010", - "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe", - "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96", - "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f", - "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d", - "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac", - "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8", - "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05", - "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3", - "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210", - "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9", - "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1", - "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc", - "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401", - "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee", - "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1", - "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391" + "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", + "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", + "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", + "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", + "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", + "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.7.0" + "version": "==1.8.0" }, "mypy-extensions": { "hashes": [ @@ -643,24 +541,24 @@ }, "nh3": { "hashes": [ - "sha256:116c9515937f94f0057ef50ebcbcc10600860065953ba56f14473ff706371873", - "sha256:18415df36db9b001f71a42a3a5395db79cf23d556996090d293764436e98e8ad", - "sha256:203cac86e313cf6486704d0ec620a992c8bc164c86d3a4fd3d761dd552d839b5", - "sha256:2b0be5c792bd43d0abef8ca39dd8acb3c0611052ce466d0401d51ea0d9aa7525", - "sha256:377aaf6a9e7c63962f367158d808c6a1344e2b4f83d071c43fbd631b75c4f0b2", - "sha256:525846c56c2bcd376f5eaee76063ebf33cf1e620c1498b2a40107f60cfc6054e", - "sha256:5529a3bf99402c34056576d80ae5547123f1078da76aa99e8ed79e44fa67282d", - "sha256:7771d43222b639a4cd9e341f870cee336b9d886de1ad9bec8dddab22fe1de450", - "sha256:88c753efbcdfc2644a5012938c6b9753f1c64a5723a67f0301ca43e7b85dcf0e", - "sha256:93a943cfd3e33bd03f77b97baa11990148687877b74193bf777956b67054dcc6", - "sha256:9be2f68fb9a40d8440cbf34cbf40758aa7f6093160bfc7fb018cce8e424f0c3a", - "sha256:a0c509894fd4dccdff557068e5074999ae3b75f4c5a2d6fb5415e782e25679c4", - "sha256:ac8056e937f264995a82bf0053ca898a1cb1c9efc7cd68fa07fe0060734df7e4", - "sha256:aed56a86daa43966dd790ba86d4b810b219f75b4bb737461b6886ce2bde38fd6", - "sha256:e8986f1dd3221d1e741fda0a12eaa4a273f1d80a35e31a1ffe579e7c621d069e", - "sha256:f99212a81c62b5f22f9e7c3e347aa00491114a5647e1f13bbebd79c3e5f08d75" - ], - "version": "==0.2.14" + "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", + "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf", + "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305", + "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601", + "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28", + "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7", + "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3", + "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911", + "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf", + "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0", + "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5", + "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97", + "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d", + "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e", + "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3", + "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6" + ], + "version": "==0.2.15" }, "nodeenv": { "hashes": [ @@ -688,11 +586,11 @@ }, "platformdirs": { "hashes": [ - "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", - "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" ], - "markers": "python_version >= '3.7'", - "version": "==4.0.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, "pluggy": { "hashes": [ @@ -704,12 +602,11 @@ }, "pre-commit": { "hashes": [ - "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", - "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660" + "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376", + "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.5.0" + "version": "==3.6.0" }, "pycodestyle": { "hashes": [ @@ -719,13 +616,6 @@ "markers": "python_version >= '3.8'", "version": "==2.11.1" }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, "pyflakes": { "hashes": [ "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", @@ -752,12 +642,11 @@ }, "pytest": { "hashes": [ - "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", - "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==7.4.3" + "version": "==7.4.4" }, "pytest-mock": { "hashes": [ @@ -765,7 +654,6 @@ "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==3.12.0" }, "pyyaml": { @@ -838,7 +726,6 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "requests-toolbelt": { @@ -865,21 +752,13 @@ "markers": "python_full_version >= '3.7.0'", "version": "==13.7.0" }, - "secretstorage": { - "hashes": [ - "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", - "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.3" - }, "setuptools": { "hashes": [ - "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2", - "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6" + "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", + "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" ], "markers": "python_version >= '3.8'", - "version": "==69.0.2" + "version": "==69.0.3" }, "snowballstemmer": { "hashes": [ @@ -894,17 +773,15 @@ "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5" ], "index": "pypi", - "markers": "python_version >= '3.9'", "version": "==7.2.6" }, "sphinx-rtd-theme": { "hashes": [ - "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0", - "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931" + "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b", + "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.3.0" + "version": "==2.0.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -976,25 +853,23 @@ "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==4.0.2" }, "types-requests": { "hashes": [ - "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc", - "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92" + "sha256:0f8c0c9764773384122813548d9eea92a5c4e1f33ed54556b508968ec5065cee", + "sha256:2e2230c7bc8dd63fa3153c1c0ae335f8a368447f0582fc332f17d54f88e69027" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.31.0.10" + "version": "==2.31.0.20231231" }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "version": "==4.9.0" }, "urllib3": { "hashes": [ @@ -1006,11 +881,11 @@ }, "virtualenv": { "hashes": [ - "sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353", - "sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd" + "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3", + "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b" ], "markers": "python_version >= '3.7'", - "version": "==20.24.7" + "version": "==20.25.0" }, "zipp": { "hashes": [ diff --git a/pyproject.toml b/pyproject.toml index 26b6fc1..c775ba5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,4 @@ docs = ["sphinx", "sphinx-rtd-theme"] "Bug Tracker" = "https://github.com/sanjacob/tiny-api-client/issues" [tool.isort] -length_sort = true - -[tool.flake8] -max-line-length = 88 +no_sections = true diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 35d8e98..c667c2c 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -374,7 +374,7 @@ def fetch_my_endpoint(self, response): def test_session_member(mock_requests, example_url, example_note): @api_client(example_url) class MyClient: - def __init__(self, session: str): + def __init__(self, session: dict[str, str]): self._session = session @get('/my-endpoint') diff --git a/tiny_api_client.py b/tiny_api_client.py deleted file mode 100644 index 60ff4e9..0000000 --- a/tiny_api_client.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -Tiny API Client - -The short and sweet way to create an API client - -Basic usage: - >>> from tiny_api_client import api_client, get - >>> @api_client("https://example.org/api") - ... class MyClient: - ... @get("/profile/{user_id}") - ... def fetch_profile(response): - ... return response - >>> client = MyClient() - >>> client.fetch_profile(user_id=...) - -""" - -# Copyright (C) 2023, Jacob Sánchez Pérez - -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import string -import logging -from typing import Any, TypeVar, Concatenate -from functools import wraps -from xml.etree import ElementTree -from collections.abc import Callable - -import requests -from typing_extensions import Protocol, ParamSpec - -__all__ = ['api_client', 'get', 'post', 'put', 'patch', 'delete', 'api_client_method'] - -_logger = logging.getLogger(__name__) -_logger.addHandler(logging.NullHandler()) - - -class APIClientError(Exception): - """Base error class for the API Client""" - pass - - -class APIEmptyResponseError(APIClientError): - """The API response is empty""" - pass - - -class APIStatusError(APIClientError): - """The API returned an error status""" - pass - - -class APINoURLError(APIClientError): - """The API has no URL declared""" - pass - - -APIEndpointP = ParamSpec('APIEndpointP') -APIEndpointT = TypeVar('APIEndpointT') - -APIEndpointHandler = Callable[APIEndpointP, APIEndpointT] -APIEndpoint = Callable[Concatenate[Any, APIEndpointP], APIEndpointT] - - -class APIDecoratorFactory(Protocol): - def __call__(self, endpoint: str, *, version: int = 1, use_api: bool = True, - json: bool = True, xml: bool = False, **g_kwargs) -> Callable[ - ..., - Callable[ - [Callable[APIEndpointP, APIEndpointT]], - Callable[Concatenate[Any, APIEndpointP], APIEndpointT] - ]]: ... - - -def api_client_method(method: str) -> APIDecoratorFactory: - """Construct an API client decorator function for an specific HTTP method - - Basic usage: - >>> get = api_client_decorator('GET') - >>> @get("/profile/{user_id}") - ... def fetch_profile(response): - ... return response - >>> client.fetch_profile(user_id=...) - - :param string method: The HTTP verb for the decorator - """ - - class dict_safe(dict): - def __missing__(self, key: Any) -> str: - return '' - - def request(endpoint: str, *, version: int = 1, use_api: bool = True, - json: bool = True, xml: bool = False, **g_kwargs) -> Callable[ - [Callable[APIEndpointP, APIEndpointT]], - Callable[Concatenate[Any, APIEndpointP], APIEndpointT] - ]: - """Declare an endpoint with a given HTTP method - - Basic usage: - >>> from tiny_api_client import get, post - >>> @get("/posts") - ... def get_posts(self, response): - ... return response - >>> @post("/posts") - ... def create_post(self, response): - ... return response - - :param string endpoint: Endpoint to make call to, including placeholders - :param int version: API version to which the endpoint belongs - :param bool json: Toggles JSON parsing of response before returning - :param dict g_kwargs: Any extra keyword argument will be passed to requests - """ - - def request_decorator(func: Callable[APIEndpointP, APIEndpointT]) -> Callable[ - Concatenate[Any, APIEndpointP], APIEndpointT]: - """Return wrapped function. - - :param function func: Function to decorate - """ - - @wraps(func) - def request_wrapper(self, /, *args: APIEndpointP.args, - **kwargs: APIEndpointP.kwargs) -> APIEndpointT: - """Wrap function in REST API call. - - :param list args: Passed to the function being wrapped - :param dict kwargs: Any kwargs will be passed to requests - """ - if self._url is None: - raise APINoURLError() - - if not hasattr(self, '__client_session'): - _logger.info("Creating new requests session") - self.__client_session = requests.Session() - - param_endpoint = endpoint.format_map(dict_safe(kwargs)) - - # Remove parameters meant for endpoint formatting - formatter = string.Formatter() - for x in formatter.parse(endpoint): - kwargs.pop(x[1], None) # type: ignore - - url = self._url.format(version=version) - endpoint_format = f"{url}{param_endpoint}" - - if not use_api: - endpoint_format = param_endpoint - if endpoint_format[-1] == "/": - endpoint_format = endpoint_format[:-1] - - _logger.debug(f"Making request to {endpoint_format}") - - cookies = None - if hasattr(self, '_cookies'): - cookies = self._cookies - elif hasattr(self, '_session'): - _logger.warning("_session is deprecated. Use _cookies instead.") - cookies = self._session - - # This line generates some errors due to kwargs being passed to - # the non-kwarg-ed requests.request method - response = self.__client_session.request( - method, endpoint_format, timeout=self.__api_timeout, - cookies=cookies, **kwargs, **g_kwargs) # type: ignore - endpoint_response: Any = response - - if json: - endpoint_response = response.json() - - if not endpoint_response: - raise APIEmptyResponseError() - elif self.__api_status_key in endpoint_response: - status_code = endpoint_response[self.__api_status_key] - _logger.warning(f"Code {status_code} in {endpoint_format}") - - if self.__api_status_handler is not None: - self.__api_status_handler(status_code) - else: - raise APIStatusError('Server responded with an error code') - - if self.__api_results_key in endpoint_response: - endpoint_response = endpoint_response[self.__api_results_key] - elif xml: - endpoint_response = ElementTree.fromstring(response.text) - - return func(self, endpoint_response, *args) - return request_wrapper - return request_decorator - return request - - -APIClient = TypeVar('APIClient', bound=type[Any]) -APIStatusHandler = Callable[[Any], None] | None - - -def api_client(url: str | None = None, /, *, timeout: int | None = None, - status_handler: APIStatusHandler = None, status_key: str = 'status', - results_key: str = 'results') -> Callable[[APIClient], APIClient]: - """Annotate a class to use the api client method decorators - - Basic usage: - >>> @api_client("https://example.org/api") - >>> class MyClient: - ... ... - - :param string url: The URL of the API root which can include placeholders (see docs) - :param int timeout: The timeout to use in seconds - :param Callable status_handler: A function that handles error codes from the API - :param string status_key: The key of the API response that contains the status code - :param string results_key: The key of the API response that contains the results - """ - - def wrap(cls: APIClient) -> APIClient: - cls._url = url - cls.__api_timeout = timeout - cls.__api_status_handler = status_handler - cls.__api_status_key = status_key - cls.__api_results_key = results_key - return cls - - return wrap - - -get = api_client_method('GET') -post = api_client_method('POST') -put = api_client_method('PUT') -patch = api_client_method('PATCH') -delete = api_client_method('DELETE') diff --git a/tiny_api_client/__init__.py b/tiny_api_client/__init__.py new file mode 100644 index 0000000..869be90 --- /dev/null +++ b/tiny_api_client/__init__.py @@ -0,0 +1,313 @@ +""" +Tiny API Client + +The short and sweet way to create an API client + +Basic usage: + >>> from tiny_api_client import api_client, get + >>> @api_client("https://example.org/api") + ... class MyClient: + ... @get("/profile/{user_id}") + ... def fetch_profile(response): + ... return response + >>> client = MyClient() + >>> client.fetch_profile(user_id=...) + +""" + +# Copyright (C) 2024, Jacob Sánchez Pérez + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +import logging +import requests +import string +from collections.abc import Callable +from dataclasses import dataclass +from functools import wraps +from typing import Any, Concatenate, ParamSpec, Protocol, TypeVar +from xml.etree import ElementTree + +__all__ = ['api_client', 'get', 'post', 'put', 'patch', 'delete'] + +_logger = logging.getLogger(__name__) +_logger.addHandler(logging.NullHandler()) + +# Typing + +P = ParamSpec('P') +T = TypeVar('T') + +APIStatusHandler = Callable[[Any], None] | None +APIClient = TypeVar('APIClient', bound=type[Any]) + + +class RequestDecorator(Protocol): + def __call__( + self, func: Callable[Concatenate[Any, Any, P], T] + ) -> Callable[Concatenate[Any, P], T]: ... + + +class DecoratorFactory(Protocol): + def __call__( + self, route: str, *, version: int = 1, use_api: bool = True, + json: bool = True, xml: bool = False, **g_kwargs: Any + ) -> RequestDecorator: ... + + +# Exceptions + +class APIClientError(Exception): + """Base error class for the API Client""" + pass + + +class APIEmptyResponseError(APIClientError): + """The API response is empty""" + pass + + +class APIStatusError(APIClientError): + """The API returned an error status""" + pass + + +class APINoURLError(APIClientError): + """The API has no URL declared""" + pass + + +# Tiny API Client + +@dataclass +class Endpoint: + route: str + version: int + use_api: bool + json: bool + xml: bool + kwargs: dict[str, Any] + + +def _format_endpoint(url: str, endpoint: str, use_api: bool, + positional_args: dict[str, Any]) -> str: + """Build final endpoint URL for an API call.""" + + class dict_safe(dict[str, Any]): + """Dict subclass to replace positional endpoint parameters""" + def __missing__(self, key: str) -> str: + return '' + + param_endpoint = endpoint.format_map(dict_safe(positional_args)) + endpoint_url = f"{url}{param_endpoint}" if use_api else param_endpoint + return endpoint_url.rstrip('/') + + +def _pop_api_kwargs(endpoint: str, kwargs: dict[str, Any]) -> dict[str, Any]: + """Remove positional endpoint arguments from kwargs before passing + additional arguments to `requests`. + """ + formatter = string.Formatter() + for x in formatter.parse(endpoint): + if x[1] is not None: + kwargs.pop(x[1], None) + return kwargs + + +def _make_request(client: Any, method: str, endpoint: str, + **kwargs: Any) -> Any: + """Use `requests` to send out a request to the API endpoint.""" + if not hasattr(client, '__client_session'): + # Create a session to reuse connections + _logger.info("Creating new requests session") + client.__client_session = requests.Session() + + # The following assertion causes issues in testing + # since MagicMock is not an instance of Session + # Thus, the return type has to be Any for now + # assert isinstance(client.__client_session, requests.Session) + + _logger.debug(f"Making request to {endpoint}") + + cookies = None + if hasattr(client, '_cookies'): + cookies = client._cookies + elif hasattr(client, '_session'): + _logger.warning("_session is deprecated.") + cookies = client._session + + return client.__client_session.request( + method, endpoint, + timeout=client.__api_timeout, + cookies=cookies, **kwargs + ) + + +def _handle_response(response: Any, + json: bool, xml: bool, + status_key: str, results_key: str, + status_handler: APIStatusHandler) -> Any: + """Parse json or XML response after request is complete""" + endpoint_response: Any = response + + if json: + endpoint_response = response.json() + + if not endpoint_response: + raise APIEmptyResponseError() + + if status_key in endpoint_response: + status_code = endpoint_response[status_key] + _logger.warning(f"Code {status_code} from {response.url}") + + if status_handler is not None: + status_handler(status_code) + else: + raise APIStatusError('Server responded with an error code') + + if results_key in endpoint_response: + endpoint_response = endpoint_response[results_key] + elif xml: + endpoint_response = ElementTree.fromstring(response.text) + + return endpoint_response + + +def make_api_call(method: str, client: Any, + endpoint: Endpoint, **kwargs: Any) -> Any: + """Calls the API endpoint and handles result.""" + if client._url is None: + raise APINoURLError() + + # Build final API endpoint URL + url = client._url.format(version=endpoint.version) + route = _format_endpoint(url, endpoint.route, endpoint.use_api, kwargs) + + # Remove parameters meant for endpoint formatting + kwargs = _pop_api_kwargs(endpoint.route, kwargs) + + response = _make_request(client, method, route, + **kwargs, **endpoint.kwargs) + endpoint_response = _handle_response( + response, + endpoint.json, + endpoint.xml, + client.__api_status_key, + client.__api_results_key, + client.__api_status_handler) + + return endpoint_response + + +def api_client_method(method: str) -> DecoratorFactory: + """Create a decorator factory for one of the http methods + + This superfactory can create factories for arbitrary http verbs + (GET, POST, etc.). Unless specifying an http verb not already + covered by this library, this function should not be called + directly. + + Basic usage: + >>> get = api_client_decorator('GET') + >>> @get("/profile/{user_id}") + ... def fetch_profile(response): + ... return response + >>> client.fetch_profile(user_id=...) + + :param str method: The HTTP verb for the decorator + """ + + def request(route: str, *, version: int = 1, use_api: bool = True, + json: bool = True, xml: bool = False, + **request_kwargs: Any) -> RequestDecorator: + """Declare an endpoint with the given HTTP method and parameters + + Basic usage: + >>> from tiny_api_client import get, post + >>> @get("/posts") + ... def get_posts(self, response): + ... return response + >>> @post("/posts") + ... def create_post(self, response): + ... return response + + :param str endpoint: Endpoint including positional placeholders + :param int version: Replaces version placeholder in API URL + :param bool json: Toggle JSON parsing of response + :param bool xml: Toggle XML parsing of response + :param dict g_kwargs: Any keyword arguments passed to requests + """ + endpoint = Endpoint(route, version, use_api, json, xml, request_kwargs) + + def request_decorator(func: Callable[Concatenate[Any, Any, P], T] + ) -> Callable[Concatenate[Any, P], T]: + """Decorator created when calling @get(...) and others. + + :param function func: Function to decorate + """ + + @wraps(func) + def request_wrapper(self: Any, /, + *args: P.args, **kwargs: P.kwargs) -> T: + """Replace endpoint parameters and call API endpoint, + then execute user-defined API endpoint handler. + + :param list args: Passed to the function being wrapped + :param dict kwargs: Any kwargs are passed to requests + """ + print(kwargs) + response = make_api_call(method, self, endpoint, **kwargs) + return func(self, response, *args) + return request_wrapper + return request_decorator + return request + + +def api_client(url: str | None = None, /, *, + timeout: int | None = None, + status_handler: APIStatusHandler = None, + status_key: str = 'status', results_key: str = 'results' + ) -> Callable[[APIClient], APIClient]: + """Annotate a class to use the api client method decorators + + Basic usage: + >>> @api_client("https://example.org/api") + >>> class MyClient: + ... ... + + :param str url: The root URL of the API server + :param int timeout: Timeout for requests in seconds + :param Callable status_handler: Error handler for status codes + :param str status_key: Key of response that contains status codes + :param str results_key: Key of response that contains results + """ + + def wrap(cls: APIClient) -> APIClient: + cls._url = url + cls.__api_timeout = timeout + cls.__api_status_handler = status_handler + cls.__api_status_key = status_key + cls.__api_results_key = results_key + return cls + + return wrap + + +get = api_client_method('GET') +post = api_client_method('POST') +put = api_client_method('PUT') +patch = api_client_method('PATCH') +delete = api_client_method('DELETE') diff --git a/py.typed b/tiny_api_client/py.typed similarity index 100% rename from py.typed rename to tiny_api_client/py.typed