pax_global_header00006660000000000000000000000064146173661540014527gustar00rootroot0000000000000052 comment=655a3792e436bba5e5e08ceb4a7457a31ffb0345 python-cloudflare-2.20.0/000077500000000000000000000000001461736615400152475ustar00rootroot00000000000000python-cloudflare-2.20.0/.coveragerc000066400000000000000000000001321461736615400173640ustar00rootroot00000000000000[run] omit = CloudFlare/tests/* [report] exclude_also = def __repr__ self.logger python-cloudflare-2.20.0/.gitignore000066400000000000000000000013031461736615400172340ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/*.rst docs/_build/* # PyBuilder target/ # Signing tarball/ python-cloudflare-2.20.0/.mypy.ini000066400000000000000000000006041461736615400170240ustar00rootroot00000000000000[mypy] # no_incremental = True # disallow_untyped_defs = True extra_checks = True strict_equality = True warn_incomplete_stub = True warn_no_return = True warn_redundant_casts= True warn_return_any = True warn_unreachable = True warn_unused_configs = True warn_unused_ignores = True [mypy-ConfigParser.*] ignore_missing_imports = True [mypy-setuptools.*] ignore_missing_imports = True python-cloudflare-2.20.0/.readthedocs.yaml000066400000000000000000000012271461736615400205000ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Optionally declare the Python requirements required to build your docs python: install: - path: . - requirements: docs/requirements.txt - requirements: requirements.txt # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py builder: "html" # Optionally build your docs in additional formats such as PDF and ePub formats: - pdf - epub python-cloudflare-2.20.0/.travis.yml000066400000000000000000000003601461736615400173570ustar00rootroot00000000000000 language: python python: - "2.7" - "3.4" - "3.5" - "3.6" - "3.7" - "3.8" virtualenv: system_site_packages: true install: - pip install -r requirements.txt - python setup.py -q install script: - pytest - make cli4test python-cloudflare-2.20.0/CHANGELOG.md000066400000000000000000003476301461736615400170750ustar00rootroot00000000000000# Change Log - 2024-05-10 11:43:43 +0200 [4281e37](../../commit/4281e371190738232100dd31f79630b31b8caf3c) 2.20.0 - 2024-05-10 11:43:20 +0200 [a28996b](../../commit/a28996b6382a7e9fcd16dcd2e475a6cd4b891bf7) Added 2.20.* notes - 2024-05-10 11:35:00 +0200 [7c1b27c](../../commit/7c1b27c8b6d5a7f47a98020716dd767d8b3e1834) extra AI api endpoints - 2024-05-10 11:27:06 +0200 [ba62a0c](../../commit/ba62a0c6625eb38eef51e7dc8390120409cb9831) more api endpoints - 2024-05-09 11:56:55 +0200 [71afe0e](../../commit/71afe0e7a9fcf7b8e17a4a7d293eeb3e7a780f30) moved 2.20.* warnings into a proper Python PendingDeprecationWarning message - 2024-05-08 11:41:37 +0200 [89d76d9](../../commit/89d76d9e23ccce3779aa6080fc7613729bc7fbf1) Merge branch 'spew-warning-for-2.20' - 2024-05-08 11:34:59 +0200 [2720d21](../../commit/2720d21e4227e0a1a7728d50cfe283c346bc1ff9) Added --warnings flag to cli4 and associated warnings= flag to cloudflare() call. Provides an ability to control warning messages on or after 2.20.* release - 2024-05-07 11:54:19 +0200 [ddbe166](../../commit/ddbe16605f96a445a8dd730036ebe85e8416e417) reduce processing of OpenAPI logic when content-type is application/json - 2024-05-07 11:46:45 +0200 [676d258](../../commit/676d258abf89e2c07693756fc63a003049578334) 2.20.* warning now in its own file and warning sent to stderr or logging only if release number 2.20 or above - 2024-05-02 12:49:53 +1000 [3181096](../../commit/31810968cfaefa9bbf3e253f702159a6504efe8d) output major version release imminent warning - 2024-04-29 19:15:50 +0100 [34bcb1b](../../commit/34bcb1b3f504b4ddfd8040e6972ddd2c71bc673c) HTTPError can show up when you plan with the URL value - 2024-04-29 18:38:57 +0100 [7535d9a](../../commit/7535d9a7e00bb3e2623ee11ff466f4bcb75f3161) CHANGELOG.md pushed to github - 2024-04-29 18:38:21 +0100 [1bda322](../../commit/1bda322ccb2093c262cb65777327b1d1133d2a5a) 2.19.4 - 2024-04-29 18:36:37 +0100 [a3cda63](../../commit/a3cda63d40e9bf25cbd90bef76b28ad16285c2ba) #186 - explain how to maps arguments - 2024-04-29 18:28:56 +0100 [389029a](../../commit/389029a6c8c20cd125333c81f758ffb718169ab9) #188 - url now uses strings - as it should! - 2024-04-29 18:07:15 +0100 [4bd76db](../../commit/4bd76dbd42a84a851719b7a5a3f47176db3a189f) #190 - remove Python 3.5 support because of f-string - 2024-04-29 18:04:12 +0100 [02e513f](../../commit/02e513f1607be04f031ee9387872d6666cabeb1a) 2.19.3 - 2024-04-29 18:02:44 +0100 [8f2e7ad](../../commit/8f2e7ad974c71a4b507eb90ddc8d0210f318ab4e) Add version 3.0 code and pinning info to README - 2024-04-29 18:01:58 +0100 [b4dbc6e](../../commit/b4dbc6e8ee954ae264f72e2ac38d4f9c47201e19) more api endpoints - 2024-04-29 18:01:25 +0100 [63f1839](../../commit/63f18392d3346924f81e59e681a877a940cbd656) now that AI methods are in library - no need for CLOUDFLARE_API_EXTRAS code - 2024-04-18 16:35:32 -0700 [0c1fb92](../../commit/0c1fb926b0bf0595b025272b9e80bc77ed7c5ad3) more api endpoints - 2024-03-19 15:02:14 -0700 [839ac32](../../commit/839ac3206320206178fd67438a52e0f4357f3903) remove f-string for Python <3.6 compatibility - 2024-03-11 22:25:20 -0300 [ab8a1aa](../../commit/ab8a1aaa189ac5afdab20f966ef44fd675e1437b) added documentation urls, etc for pypi - 2024-03-11 22:21:01 -0300 [fc0e111](../../commit/fc0e1111e1824ea8c78cda43530b51343fb0a348) finally removed the VOID calls, added /ai/run endpoints - 2024-03-04 19:50:56 -0500 [56ffcfc](../../commit/56ffcfc1557610ae4d18eb305fbeb418966f038e) still testing docs - 2024-03-04 19:46:21 -0500 [82d2dcb](../../commit/82d2dcb2f8dc51bc72e3bca455bba38c55e66f8b) added more download stats - 2024-03-04 19:32:03 -0500 [1ae9c2a](../../commit/1ae9c2aa7a67f8f15bce6cdabc3a2b71a8744c11) still testing docs - 2024-03-04 19:29:14 -0500 [f9b565a](../../commit/f9b565a772c1ff06b2ed8a432734fa67ae2b24bc) added download stats - 2024-03-04 00:02:19 -0500 [984284f](../../commit/984284f563e90c91e296a0c5d5d2f65dad9f8a4e) still testing docs - 2024-03-04 00:01:25 -0500 [cf2b3d7](../../commit/cf2b3d7b6630f677e9224ed91ea4b8d462fbdd92) still testing docs - 2024-03-01 12:05:09 -0800 [26f6af8](../../commit/26f6af8bc79ab6c3c8c0c8526a73012d46350c0d) still testing docs - 2024-03-01 11:39:45 -0800 [35c0e06](../../commit/35c0e068df3459f6cfcb4c39cadd15f526386e4c) start of longstanding need for documentation, update of copyright string - 2024-03-01 10:15:38 -0800 [4fe03ee](../../commit/4fe03eed3c7bcc2130846b1be6ea4c9427a1bb99) readthedocs initial setup - long overdue - 2024-02-26 23:33:35 -0800 [77fee2e](../../commit/77fee2e5fcf9e3246f6f2d9f194dbb65e54cdcf4) pylint fixes - 2024-02-26 23:30:57 -0800 [e7cc964](../../commit/e7cc96452fb84338525ed1537e3523157ba634de) moved all requests code/exceptions into network where it belongs, import cleanup, exception handling cleanup - 2024-02-26 23:20:06 -0800 [dbaf7d6](../../commit/dbaf7d67c53e0baaf7a1ab6f33ee860c50672686) handle python < 3.10 to get utc time correctly - 2024-02-23 16:47:01 -0800 [036c5ed](../../commit/036c5edeac4d40e8bd9584541ce4a84f2e686d20) document cli4 having --header flag - 2024-02-23 16:39:32 -0800 [6eed220](../../commit/6eed2201254769b9bbcea69252fdad51bae62aa2) cli4 has --header flag, plus http_headers needed some more syntax checking - 2024-02-22 19:03:24 -0800 [dec9835](../../commit/dec98352d027e205bed762bebdbc8ea0eecbd7c5) added Python 3.5 note - 2024-02-22 15:10:24 -0800 [b3cf637](../../commit/b3cf637c2318e6aa09150a3f5f55e5a8a90221f3) remove excessive import statements - 2024-02-22 15:09:41 -0800 [c5a5587](../../commit/c5a5587abff4213ffc9fdeac515390a0982f73ee) improved date/time rfc/iso code, lint fixes - 2024-02-22 13:49:22 +0900 [9dca32b](../../commit/9dca32b2eb2511ce7b5ae3585ffec17b0eab8278) CHANGELOG.md pushed to github - 2024-02-22 13:49:15 +0900 [28b768a](../../commit/28b768a0d02309cef9f150bfc41cb9c3d9c2eecd) 2.19.2 - 2024-02-22 13:48:52 +0900 [11d1270](../../commit/11d1270b65ce089b87b029483f517c56d288bff5) typo - 2024-02-22 13:37:13 +0900 [287c8a7](../../commit/287c8a790b3adf80352972d9ca63c14eab2f8c3a) CHANGELOG.md pushed to github - 2024-02-22 13:36:59 +0900 [6f9e5cf](../../commit/6f9e5cf637fb743e6e9a231ac2b3e37211d492d4) 2.19.1 - 2024-02-22 13:34:05 +0900 [08123c1](../../commit/08123c1cb1fd49c5727c89d5bfdac95f36f01624) http_headers documentation - 2024-02-22 13:14:37 +0900 [44751a0](../../commit/44751a003c33d75d73d51889728e75be4bafb7f5) fix tag name creation - 2024-02-22 11:25:20 +0900 [10d8af3](../../commit/10d8af3dce16af13127cb3e7fe2688acbdca9ad0) CHANGELOG.md pushed to github - 2024-02-22 11:23:43 +0900 [32d1461](../../commit/32d1461896e7a11242ac51c619b1f38ada0867c7) 2.18.1.rc1 - 2024-02-22 11:23:09 +0900 [a71dd35](../../commit/a71dd354fe44b8802e205767f16b92f8b0b533ab) added http_headers - 2024-02-22 09:32:55 +0900 [c2c4adf](../../commit/c2c4adf2d4f75ee76bb25d82ac8628945644bd98) solidfied the Python3 only mindset - 2024-02-19 16:45:40 +0900 [64f3a01](../../commit/64f3a011bb68812493c926b87a7c7b777199e5c6) brought man page up to date - 2024-02-18 18:48:47 +0900 [618d02e](../../commit/618d02e546a82f1c26f483cd46bc14c808d44508) create dummy loa document on-the-fly with very-basic pdf content - 2024-02-17 08:13:17 +0900 [4b2ec55](../../commit/4b2ec5520fe6cf53974f0b6ab65dad9f3c68e139) CHANGELOG.md pushed to github - 2024-02-17 08:12:56 +0900 [c7d9ae3](../../commit/c7d9ae3b7055b30fc2abfbd267d489ca2a1027f4) 2.19.0 - 2024-02-17 08:10:02 +0900 [9135f17](../../commit/9135f1774da0e8bba0981d35d7920e238676ddc2) typo introduced before 2.18.x - now fixed - 2024-02-17 08:09:18 +0900 [3901158](../../commit/3901158a1de0993b6870c03600d67d503e335195) purge_cache test - was chasing a different bug - 2024-02-14 13:42:44 -0800 [ba42673](../../commit/ba42673e3957b15c024b91e56e2963c4ef4aebe8) typo - 2024-02-12 14:24:48 -0800 [c5b035a](../../commit/c5b035a637a55025dabae13e0db7b6aa30e32439) CHANGELOG.md pushed to github - 2024-02-12 14:24:13 -0800 [b2ec8f7](../../commit/b2ec8f7e7e92d3319555adc1384ea2326c5b9618) 2.18.2 - 2024-02-12 13:18:19 -0800 [6245faf](../../commit/6245fafc1b42b514f79ad79239fb483d5711b3b7) coverage should only be CloudFlare/*.py files - 2024-02-12 11:10:29 -0800 [6c309ec](../../commit/6c309ec74aeb8398e84ceaccd57efd2486a504af) when more than one Content-Type is recommended try to make a good choice - 2024-02-12 10:03:18 -0800 [bf93a19](../../commit/bf93a1942ed82835d30113d3d0647dc08d175275) one more %r needed - 2024-02-12 10:02:49 -0800 [664c992](../../commit/664c9920f6184956048cd2801e4e22b1bdfe2ae6) add error print for error chain - to test that code - 2024-02-11 12:03:50 -0800 [212c080](../../commit/212c080f2d241f375e01777ef24088ab9be24dd6) more api calls - 2024-02-10 22:43:29 -0800 [eeb1355](../../commit/eeb1355a00a1773f007820c51865effd129277af) Issue182 triggered some rethink of the exception code plus some longstanding cleanup - 2024-02-10 22:26:12 -0800 [9f8fe7a](../../commit/9f8fe7aae4929623aea112706830ac72d3f45ce9) cleanup error message - 2024-02-10 18:33:24 -0800 [60c67b6](../../commit/60c67b6b05ecc48958bc77a54a87aa9540b0096b) print error before assert - 2024-02-10 15:21:38 -0800 [1acba51](../../commit/1acba516a8e6ca50ec708ff966eae1c6f3f57ae2) typo - 2024-02-10 15:21:04 -0800 [5a314b1](../../commit/5a314b17b45a05a73ca31c9a16eabc7608d64b2b) CloudFlare/tests/test_find.py - 2024-02-10 15:12:38 -0800 [4a8c255](../../commit/4a8c25559116ac7a87dad26f5933613fbf0017fd) cleaner AI examples using find() call - 2024-02-10 15:11:05 -0800 [6f38180](../../commit/6f381806cf534820b4cc3d1b38c4a4054c7d9b8c) cleaner AI exampled using the find() call and/or sanitized calls without at-symbols - 2024-02-10 15:00:34 -0800 [e588f6b](../../commit/e588f6b8fc43ff84f507bed5429edf6fbbc301b7) cleaner sanitize code for keywords, dashes, at-symbols. Cleaner API Exception returns. Added unified find() call to reduce code - 2024-02-10 12:37:38 -0800 [abcdcee](../../commit/abcdcee61d506932c7a84b8a79a653e838141146) improved error message - minor - 2024-02-09 16:32:14 -0800 [0e7bbec](../../commit/0e7bbecce85dd0ebe0ff0e25a7ea70ccc795ef7e) cleanup of _content_type value - not needed if None, which is the default - 2024-02-09 13:25:10 -0800 [07348fe](../../commit/07348fe1fae35da8e052048b5a76a7814fca381a) CHANGELOG.md pushed to github - 2024-02-09 13:24:46 -0800 [742c156](../../commit/742c15613ac0cf7762ccad53a54422a81afcfc7d) 2.18.1 - 2024-02-09 13:20:06 -0800 [2a3e059](../../commit/2a3e059df6a7f4e46c575c86030a90184c4b8917) add data and files to be present - which is not normall legal, convert code from dict to set for files data to improve requests() handling - 2024-02-09 13:19:08 -0800 [10455f3](../../commit/10455f38d8d55665e5c3b007673cff73ce040d86) handle --form with set() - was missing - 2024-02-09 12:14:25 -0800 [719c1f9](../../commit/719c1f96f0004184d55e30638be6a77cdbc257f2) cleanup of version checking code for readability - 2024-02-09 11:32:08 -0800 [299ef93](../../commit/299ef93ec76b009e570beca417a851a30312dc70) CHANGELOG.md pushed to github - 2024-02-09 11:31:47 -0800 [5de022e](../../commit/5de022e4c79817e99bf0119f72c2df657d2f41a6) start using mypy for a checking source code - 2024-02-09 11:30:55 -0800 [61e8108](../../commit/61e810847fb47fe7d10b4db14cd746b6d3b25954) 2.18.0 - 2024-02-08 17:17:07 -0800 [99d4228](../../commit/99d4228005df9ee479587e58e6296932c8cf6e9b) now prints correct --form data in all cases - 2024-02-08 17:07:29 -0800 [a7e1b77](../../commit/a7e1b777c68f24f78201c8ec5717690cd360b06a) handle versions 2.14.2 or below or 2.17.0 or any version above that - 2024-02-08 16:44:05 -0800 [4bd5e3f](../../commit/4bd5e3fe6b6768a631fa9ac555bd4d15d687009b) example for /accounts/:id/images/v2/direct_upload - 2024-02-08 14:19:04 -0800 [0a9cc41](../../commit/0a9cc412388298c84b2c4695b2cd75ed503e8a2e) passing params as files for multipart/form-data type APIs needed some work - 2024-02-08 14:11:49 -0800 [cd078fb](../../commit/cd078fb4264664ddee723c088c28abc97075f08d) improve debug prints. handle all the files debug formats - 2024-02-08 14:08:37 -0800 [cec7f5a](../../commit/cec7f5a074826327eee954057f2c73d6a7c18c47) added comment to remind how data/json/files behaves - 2024-02-03 11:08:43 -0800 [397ee01](../../commit/397ee018c281ec34c520ef32019705e7a4a7185a) make more generic vs mp3 only, add many clips to default clips (including French clip) - 2024-01-25 22:15:19 +0000 [bb5e1ac](../../commit/bb5e1ac4c7a12793fa5feda3a80bff902179a5e3) minor edit to handle mypy processing - 2024-01-25 20:14:58 +0000 [5907454](../../commit/5907454784d24e963d127a9a6fd8e0bcbf9cd956) damn typos! - 2024-01-23 13:42:33 +0000 [b721505](../../commit/b7215055df2abad96523ad56cf3f383b4bf83f7c) PEP8 edits via pycodestyle - 2024-01-22 22:42:30 +0000 [e90d38e](../../commit/e90d38eaa5ab40bca52d93d81bf1b7549ab70bda) handle case where zone and account disallow load balancing - 2024-01-22 22:29:06 +0000 [cf595ab](../../commit/cf595ab09a0f1da8146217229655c75709f77071) CHANGELOG.md pushed to github - 2024-01-22 22:28:12 +0000 [fadbace](../../commit/fadbace4bbbb47e30089984b0fc88481249904c5) 2.17.0 - 2024-01-22 22:27:13 +0000 [ee72509](../../commit/ee725093486668f380a59cf68f208da110950cf6) make 4xx error handling more generic - 2024-01-22 22:26:13 +0000 [ea9e609](../../commit/ea9e6094480612747c557a27148f6e7549537b52) testing load balancers becuase they return 412 HTTP errors - 2024-01-21 17:38:08 +0000 [e09b075](../../commit/e09b075e740322382e15c67c01e519b3d9609370) debug messages line lenght needs to be longer so that error return messages are shown clearly - 2024-01-20 18:21:28 +0000 [3b8e699](../../commit/3b8e69997a538e030fb63da1c8a81c4e65ca345b) issue-181 HTTP response code 412 not handled - 2024-01-12 12:34:07 -0800 [2da9a7b](../../commit/2da9a7b56b3140c2d47d70f52ea8265d5b9d6dd4) handle 404 errors with invalid API error structure - 2024-01-12 12:33:24 -0800 [f39b3c1](../../commit/f39b3c1824d52b09fa0b7aeeeb1f03a3750d5c9f) test get/put/patch/delete and post with no data - all expected return error - 2024-01-12 11:42:56 -0800 [6acca0e](../../commit/6acca0ef657d703298a735011087619630897ab0) now testing api calls with four id values - 2024-01-12 11:06:44 -0800 [604d2bd](../../commit/604d2bd96dd90e5c6638f8b01169dafbf1a23a35) now testing api calls with three id values - 2024-01-12 11:05:06 -0800 [1a5a859](../../commit/1a5a8599b07e128f8834aa765537937e1d2974b7) improve error message responses for 400-405 http reponses code - they now decode Cloudflare error values - 2024-01-11 15:32:09 -0800 [e3b2631](../../commit/e3b26318cb334f913b4c1c4c4d29be4cde80b8ab) tuna tweeks, coverage html added - 2024-01-11 13:44:54 -0800 [dc18d57](../../commit/dc18d571b3533a69c2dfdc39fc25a97b7ff58977) lint found minor stuff - 2024-01-11 13:44:19 -0800 [c7658a3](../../commit/c7658a38ee5143f06a5a99ea3201fdebc4487a6f) lint found minor stuff, fixed longstanding _parts list size - 2024-01-11 11:48:33 -0800 [1ba1ca5](../../commit/1ba1ca5d6a209115a4c58b4673cc9d20fe887c30) lint found minor stuff - 2024-01-11 11:47:37 -0800 [dbec555](../../commit/dbec555e4151cd8db0ef78b8ea79c04af4f2bca6) added lots of %s %r tests - 2024-01-11 11:39:06 -0800 [e44bf75](../../commit/e44bf75ce6b4138c7b308bc91ae1d481a9bf8f16) lint found minor stuff - 2024-01-11 11:38:12 -0800 [0ac418b](../../commit/0ac418bdec9b39f4a2990a54a3cc314c8a259386) updated lint rules - 2024-01-05 15:44:19 -0800 [3d9f960](../../commit/3d9f960b7eef90f533e418007f062fc52b0cfcbf) cleanup of application/octet-stream duplicate code - 2024-01-05 15:24:29 -0800 [cbf250f](../../commit/cbf250f71c1695ff8c66973b374e6cbcc3221ec2) /accounts/:id/urlscanner/scan/:id/screenshot returns a binary object - a png file - 2024-01-05 12:39:27 -0800 [231b7a7](../../commit/231b7a7197fcc8c47aa44c901a409e4532e683d4) handle zero length content responses (normally 204 returns to delete) - 2024-01-05 12:38:47 -0800 [ed08910](../../commit/ed08910ec5a4f5ec9dc9484cfdd19c5bf1770acf) testing rulesets where response is None to delete - 2024-01-04 15:43:43 -0800 [26893b2](../../commit/26893b205f120f762d8ee3c462ce5c03237c9547) CHANGELOG.md pushed to github - 2024-01-04 15:42:32 -0800 [ce080f3](../../commit/ce080f3da8061dabbc0fc1604e92eaba4f0a4cf1) 2.16.0 - 2024-01-04 15:42:09 -0800 [f2441b1](../../commit/f2441b1f2320840fed526d81370cbde5f00d8b4e) added CloudFlare/tests as a package - a better place to keep tests - 2024-01-04 15:39:18 -0800 [401149c](../../commit/401149c20e074b824fc8f4467c92b08fa327f568) return http code 200 and more 2xx now accepted, check for decode() errors - 2024-01-04 15:37:25 -0800 [68ab29c](../../commit/68ab29c6a6d8963025bfb29ba1908881edbb93a7) confine account to first one - 2024-01-04 15:36:51 -0800 [7e88785](../../commit/7e88785a2439f76f7e9e6264d34f200550e6c4c6) testing pdf uploading and downloading - 2024-01-02 09:00:43 -0800 [6ba9ced](../../commit/6ba9ced49892023377cff9298d652a6dbc857d0f) cleanup to show correct sequence of firewall and filter creation and deletion - 2024-01-01 17:30:17 -0800 [8a04e95](../../commit/8a04e956687e42dafd244d65cb4eb32fa05f10ca) now builds TABLE-OF-COMMANDS.md - 2024-01-01 17:29:31 -0800 [0c85423](../../commit/0c85423812fec5bd8720292259b189175da99590) more table - based on examples/example_list_api_from_web.py - 2024-01-01 17:21:23 -0800 [38ea7a2](../../commit/38ea7a2cf476d098b4d391ae3fbc3eaec25ade67) moved to api_from_openapi() - 2024-01-01 17:11:09 -0800 [f42454b](../../commit/f42454ba2b3e269d64e44126f364338865d88b1b) update email examples to reflect newer api - 2024-01-01 13:09:11 -0800 [e6d9692](../../commit/e6d9692dfa29e12bcb4656b4c8542ae023a6ae9d) added workers create and delete - 2023-12-31 16:55:20 -0800 [2ff132d](../../commit/2ff132db5e276238e387baaca11d96bdd62a477d) test invalid dns records - 2023-12-31 16:19:21 -0800 [26a90c3](../../commit/26a90c3703d0960c7a6d8b78170a520f2408d157) more tests - invalid types and string returns - 2023-12-31 16:15:12 -0800 [8dbf8cd](../../commit/8dbf8cd2cbcbccdbb28eb84b24345e6254089c26) force check for None on email/key/token/certtoken - 2023-12-31 12:42:47 -0800 [e7224a8](../../commit/e7224a844c48abc7668ef5c8672654a14e3d81f7) /graphql tests - 2023-12-31 12:04:15 -0800 [28a3fd7](../../commit/28a3fd7a686e6050f0aee3cf23d604c4c89aa0a9) incorrect coding of /zones/:id/logs/received/fields - 2023-12-31 11:34:41 -0800 [0487170](../../commit/048717072aa7bef0b4826cd3fbb761f2fdd7fa09) typo - 2023-12-31 11:11:43 -0800 [3124aa6](../../commit/3124aa6b0d999d8e7d2c3248cc241d58c878fc1a) easier date code - 2023-12-31 10:47:45 -0800 [08d9d4c](../../commit/08d9d4cb5ea721f8e69ceb15799e82143bb77ef6) test raw=True calls via listing zones - 2023-12-31 09:44:48 -0800 [0b6271a](../../commit/0b6271aebfbe77e099f42db5b7f46355dcfe295e) moved /tests to /CloudFlare/tests - 2023-12-31 09:37:34 -0800 [74c7ce0](../../commit/74c7ce0920a67f34dde5a3cd7c6edd5a5fb42c8f) removed api_decode_from_web.py as the old api.cloudflare.com website has gone away - 2023-12-31 09:36:06 -0800 [5795d40](../../commit/5795d4041b7107d16858804346ca96d4cf50850b) moved /tests to /CloudFlare/tests. Removed find_package - as it was installing /tests and conflicting with other packages - 2023-12-31 09:33:59 -0800 [5622686](../../commit/562268622c679e261b405838708e93f3d78e3b0c) tests more return values - 2023-12-31 09:33:05 -0800 [fb245ee](../../commit/fb245ee77f180cfb0401a6a1a19ff567aa446d18) OpenAPI code now returns clean errors and version number. cli4 prints fetch errors cleanly - 2023-12-30 18:13:09 -0800 [d6cd999](../../commit/d6cd99988ff17af1c070c79b8d141e51e01fb51c) moved tests to under CloudFlare - 2023-12-30 18:06:31 -0800 [5390d3d](../../commit/5390d3d5c81d6395de043c5c2084a49b8a6c8408) typo - 2023-12-30 17:48:41 -0800 [609044f](../../commit/609044ffe134b098596ae33b0bfd61c049f3d34f) markdown cleanup - 2023-12-30 17:21:07 -0800 [8c53a8a](../../commit/8c53a8a59db60e197716c7895c80b2941c07aa65) openapi url now in code where it belongs, args checked for type str, logging messgaes for various errors, http error code 400 & 429 handled - 2023-12-30 17:16:24 -0800 [53af1a2](../../commit/53af1a2b0717d36bf3ec598d6030bf229f732b55) --openapi call now with optional argument as url is built into code - 2023-12-30 17:15:35 -0800 [5558e19](../../commit/5558e19c1c65b468034fe3ab2c78ac92e0a6958b) all tests now similar structure, callable from command line, debug on by default for command line calls - 2023-12-30 17:12:16 -0800 [b2c3eaa](../../commit/b2c3eaa581a25ea81f0e07d3898e2e7041a95495) move OPENAPI_URL into code where it belongs - 2023-12-30 16:42:11 -0800 [e8ba3e3](../../commit/e8ba3e3c5b1193178efcf69ddc3baeac468b9338) only create one logger - longstanding issue solved - 2023-12-30 16:09:50 -0800 [e460d78](../../commit/e460d7808fd83c8fb93499a7fead7b893c968557) added openapi_url config - rarely used outside of development - 2023-12-29 14:12:05 -0800 [4383548](../../commit/4383548771b214862150b0c2bcd59fd516a56125) deal with /certificates case - which is unique - 2023-12-28 14:02:39 -0800 [98c0526](../../commit/98c0526d92e517804dad136e6b8dbe638e654b5b) do not run tests if nothing has changed - 2023-12-28 14:02:09 -0800 [4a8626f](../../commit/4a8626f9a20e2bd4781dd5f9b386010d1431ee02) make zone choice random, add delay for import as it is rate limited - 2023-12-28 14:00:53 -0800 [3b17a03](../../commit/3b17a03d67d4f7d6b82b5cda2da3b56e7bf3055b) remove excess code - 2023-12-28 12:38:27 -0800 [678b70a](../../commit/678b70a5d539dc6e87e59b75550ff3fcef625631) moved to use tempfile.TemporaryFile so more portable over many systems - 2023-12-28 12:32:05 -0800 [01ebc72](../../commit/01ebc7286eddb67387b49f06939ebd8075d45fe2) moved to use tempfile.TemporaryFile so more portable over many systems - 2023-12-28 11:59:04 -0800 [6b1cbe4](../../commit/6b1cbe4e446f877fad61f5b2d7cc60e2faeddad7) CHANGELOG.md pushed to github - 2023-12-28 11:58:34 -0800 [aac4e6f](../../commit/aac4e6f1ee520f42c7ffa7fbf92195f955f27ea1) 2.15.1 - 2023-12-28 11:58:00 -0800 [fddc424](../../commit/fddc424adb7d9bb7d205bb8aedcf6bbd1d9536eb) fixed a bad edit from 241af4d which stopped data being send on PUT/POST/PATCH/DELETE - 2023-12-28 11:49:45 -0800 [62eb860](../../commit/62eb8602e979ff7437728727444f3332c2f21b4b) example of purge_cache for free or enterprise accounts - 2023-12-27 11:53:31 -0800 [64ee01b](../../commit/64ee01b4df74b4847e378485659d98a49c0d72a7) CHANGELOG.md pushed to github - 2023-12-27 11:50:33 -0800 [6153da0](../../commit/6153da03fde5ab5c4869d6e021d9f8f0cf22826f) 2.15.0 - 2023-12-27 11:49:53 -0800 [d6ae4ea](../../commit/d6ae4eaed86bb8e3df4a68a91ccfdb2cd3ea032d) added Content-Type processing on add, removed old api web checking (hence remove bs4), refactoring of all add methods, network now handles data/json cleanly, results returned based on Content-Type via new code, setup/requirements removed BeautifulSoup/bs4 - 2023-12-26 15:18:00 -0800 [91bd97d](../../commit/91bd97d1778e533832ea06dd798deb4cd4c69eaf) corrected missing tests; added debug test - useful for coverage - 2023-12-26 15:17:07 -0800 [25c981a](../../commit/25c981ad37bedb3156b414a8dca23988864e790f) test cf.add() - 2023-12-26 15:16:44 -0800 [edf9e51](../../commit/edf9e511857ede29420c687ee142b8c17b7cecaa) complete rewrite - maybe better; maybe not; however, test runs are nicer to look at - 2023-12-26 11:09:14 -0800 [61ca2d6](../../commit/61ca2d6ee7846403f892ea5a763ca67d9ae37458) automate the list of aliases - far better for future - 2023-12-26 09:50:44 -0800 [75234ea](../../commit/75234ea40aff1d29860c386ec64db43b1a521854) Allow PUT and POST to both send files, move params into files when both types used - 2023-12-25 16:17:21 -0800 [fc74b5a](../../commit/fc74b5ac5e5085763dae72e4a45641912e02b1b4) more test cases - 2023-12-25 16:16:37 -0800 [a42d836](../../commit/a42d83616557376865990cd3af44c39ddc785765) more test cases - 2023-12-25 12:56:15 -0800 [945afcc](../../commit/945afcc8ee9192b274e5800f35dfe70b89215176) more test cases and moved pytest to pytest with coverage - 2023-12-25 12:54:04 -0800 [08da722](../../commit/08da7224adf0574729a242e9e8bb6090a193eccf) __init__.py needed for coverage to work - 2023-12-23 16:23:36 -0800 [42d8a62](../../commit/42d8a62e48d78670d666e9076418e22994989467) added more AI examples - 2023-12-23 10:26:50 -0800 [241af4d](../../commit/241af4dd12d5285f789d9c4a626c45dc66e4d905) cleanup of params vs content - 2023-12-21 14:20:44 -0800 [57e9c0f](../../commit/57e9c0f10671b72be84db6c4ff302b4792ba8a43) updated to handle new keyword handling method - 2023-12-21 14:15:59 -0800 [6769d9f](../../commit/6769d9f2678cca64737d108df9093b8f818e9ea2) updated to handle new keyword handling method - 2023-12-14 17:40:29 +0000 [d7ca6d1](../../commit/d7ca6d1ba1a7e84ea843095d33f5eb55add7afdb) Added first pass at Content-Type processing from openapi data - 2023-12-13 14:01:10 +0000 [d141d87](../../commit/d141d872876c4d3e88449c54fcdf246caee7343d) CHANGELOG.md pushed to github - 2023-12-13 13:59:55 +0000 [9b0787e](../../commit/9b0787efa929d374392e9db4f6fcec05700de0fe) 2.14.3 - 2023-12-13 13:52:53 +0000 [ab6f4db](../../commit/ab6f4db695c8666489491abb503ada59f3c30f49) handle {account-identifier} with a dash vs underscore. Arggg! - 2023-12-13 13:52:03 +0000 [f445051](../../commit/f445051a99a9b933e4b7ddf50ba82d477b3f74b1) /live added - but does not response yet - 2023-11-25 20:05:23 -0800 [5b7b386](../../commit/5b7b386b91700d21cd24f5cefbcb90c6f08ccd45) CHANGELOG.md pushed to github - 2023-11-25 20:04:41 -0800 [b80db60](../../commit/b80db60510874da0a268af10bd1f89cd109707f4) added AI info - 2023-11-25 19:43:28 -0800 [c88bbfa](../../commit/c88bbfaec951db80130cdc3404801c29680c6c05) account name via -a flag now. should not be needed - 2023-11-25 19:26:44 -0800 [3621a4d](../../commit/3621a4d18919f65a8f5b8979e76d3a0bd453fd12) clean up and more - 2023-11-25 19:25:59 -0800 [282aa54](../../commit/282aa543947fe87aa16687a94a88a780a3883b1f) stable diffusion example - 2023-11-25 19:25:07 -0800 [3e40a69](../../commit/3e40a69089db1c3f567117768e34e2a7247e2a01) clean up and more - 2023-11-25 15:14:00 -0800 [1eeca98](../../commit/1eeca983753691186490dde261ffa69e6c731d2f) move config info into code - 2023-11-25 15:00:14 -0800 [d79b0ec](../../commit/d79b0ece436b66daf210a25d90fc25333d52803b) first pass a new AI API calls - 2023-11-25 13:06:07 -0800 [115fac8](../../commit/115fac8f32284e96df36ea98b0cdf5a416cdff56) CHANGELOG.md pushed to github - 2023-11-25 13:05:33 -0800 [bf47169](../../commit/bf471698839dd7910480133620cfaecfe1e7c2ba) 2.14.1 - 2023-11-25 13:05:09 -0800 [d5f707d](../../commit/d5f707d2a9916ae8e860219a3f4dbaf8d3a2d1f8) cleanup of usage, added more flag descriptions - 2023-11-25 13:04:33 -0800 [d23ed90](../../commit/d23ed909706259e8297fc5497b59cb41f32ffbb5) cleanup of usage and getops values - now consistent - 2023-11-25 12:42:24 -0800 [142a01b](../../commit/142a01ba26f4600b375f0678c92226870d01368b) timeout values now work from config or api call, added support for image binary results, fixed logging if binary - 2023-11-25 12:39:05 -0800 [453fed3](../../commit/453fed3e3c4d60946b5519bd236a983d9dcce272) handle raw byte output either via --image flag or if return from api is bytes - 2023-11-25 10:22:15 -0800 [e08610b](../../commit/e08610bfaa5958c25f6dadb7c9a3842b7fe42793) CHANGELOG.md pushed to github - 2023-11-25 10:21:56 -0800 [807f99b](../../commit/807f99bdd5e524da14b376f382622d14e77216c9) 2.13.1 - 2023-11-25 10:17:37 -0800 [7dd1888](../../commit/7dd1888758a88cccd8daed9fcac9867608d1d26a) Merge pull request #175 from Daic115/master - 2023-11-25 10:10:15 -0800 [838ef13](../../commit/838ef13b811e1aed3ed498ee34e959fc42d2b102) remove Unicode from README - this caused issues on Windows install - 2023-11-25 10:06:52 -0800 [cd6f7d1](../../commit/cd6f7d1cad868d105e5d27f0a2b4653d9ab9efdd) CHANGELOG.md pushed to github - 2023-11-25 10:06:36 -0800 [541c8f6](../../commit/541c8f6983463bca0de98b57af370b2150e64133) 2.12.5 - 2023-11-25 10:06:19 -0800 [2610c98](../../commit/2610c98795703942cf7bf22b6e5cd3c278515414) more api endpoints - 2023-11-15 15:58:23 +0800 [4524c50](../../commit/4524c5053c20c830093a35641f4f9a74210e997e) Update setup.py - 2023-09-21 18:45:16 -0700 [419c90b](../../commit/419c90b24692c9a426c0d47c01dea4358f1fe909) 2.12.4 - 2023-09-21 12:18:54 -0700 [1982e4e](../../commit/1982e4ed03eac6614da51ee55de47f372551ca73) 2.12.3 - 2023-09-21 12:17:12 -0700 [4180462](../../commit/41804623aa6725cf677f29ccadebc2651f4aa798) added ips and issue114 tests - 2023-09-21 10:33:13 -0700 [0cbfbd3](../../commit/0cbfbd3aca7ae409042d41ada8412208d87bc1d2) add importlib_resources info for older Python versions - 2023-09-21 10:11:06 -0700 [b5aae7e](../../commit/b5aae7eb3ee51160e0529bd375d1c842e28a1c96) Merge pull request #156 from UsamaSadiq/remove-future-dependency - 2023-09-21 10:10:57 -0700 [5b3e687](../../commit/5b3e687d80a7283dee1e8782a32c83d266ced061) Merge branch 'master' into remove-future-dependency - 2023-09-21 10:03:51 -0700 [4e9eb55](../../commit/4e9eb55f7dfbc382ab07e504b4df4aa4965bc609) Merge branch 'UsamaSadiq-remove-future-dependency' - 2023-09-21 10:03:38 -0700 [e715dd7](../../commit/e715dd707c086194fac85299568b934275b5df40) UsamaSadiq-remove-future-dependency - remove future - 2023-09-21 09:52:22 -0700 [57fac2f](../../commit/57fac2fa1e234898aff130b55737bea38eb42d51) Merge branch 'BjoernPetersen-fix-invalid-escape-sequence' - 2023-09-21 09:51:55 -0700 [2bccbb9](../../commit/2bccbb9988e9b193724bc74005e72ebdd623f726) Merge branch 'fix-invalid-escape-sequence' of https://github.com/BjoernPetersen/python-cloudflare into BjoernPetersen-fix-invalid-escape-sequence - 2023-09-19 12:24:33 -0700 [65a7e3d](../../commit/65a7e3d565be62154c6990f2ae76ef2ba0e4e61b) CHANGELOG.md pushed to github - 2023-09-19 12:24:18 -0700 [4b2254b](../../commit/4b2254bc24a2e87d937543efbfe06d54e3f4e4f2) 2.12.2 - 2023-09-19 12:23:45 -0700 [483a20b](../../commit/483a20bf6448a52dd5182ed0de9cead5acbc947f) more api endpoints - 2023-09-19 12:13:49 -0700 [e3555cf](../../commit/e3555cf70abaa37d39c086b8d081e6859e0ed35e) 2.12.1 - 2023-09-19 12:12:22 -0700 [0f0a8a0](../../commit/0f0a8a04f0ce02ab8fdf42583256133e327e9e4c) Merge branch 'dkoston-requests_timeouts_and_retries' Added dkoston's network code to manage timeouts and retry's - 2023-09-19 12:10:19 -0700 [ca72137](../../commit/ca72137d0a42c27f65929eac2df23b9632979881) Merge branch 'requests_timeouts_and_retries' of https://github.com/dkoston/python-cloudflare into dkoston-requests_timeouts_and_retries Pull Request 173 from dkoston - requests_timeouts_and_retries - 2023-09-19 09:54:49 -0700 [8076d56](../../commit/8076d560f680731a63c94d7fd90c107ffe25b4b7) CHANGELOG.md pushed to github - 2023-09-19 09:54:25 -0700 [14d734a](../../commit/14d734a1dabea2eb2689f8f14779c2238b791fab) 2.11.8 - 2023-09-19 09:53:33 -0700 [77c275c](../../commit/77c275cf31be71c83894a0bd87c26810dac5ed05) more twine tweaks - 2023-09-19 09:51:14 -0700 [aa31bb3](../../commit/aa31bb386c9658cbe15dc25c4fd9ef4860899851) Added cli4 -e option to display example file path names - 2023-09-19 10:51:06 -0500 [28e4244](../../commit/28e4244bc93787bff0ea37b0a5c28cecbf1962cc) Add `global_request_timeout` and `max_request_retries` configuration options. Set default request timeout to 5s. Add basic tests instantiating Cloudflare.Cloudflare - 2023-08-19 09:23:43 -0700 [0ea52bc](../../commit/0ea52bca64e6c09ff4c6823321dec501e5d34e09) CHANGELOG.md pushed to github - 2023-08-19 09:23:25 -0700 [3f1404b](../../commit/3f1404b4af674554657912de91804663b91d2ed6) 2.11.7 - 2023-08-19 09:22:52 -0700 [94f0b0b](../../commit/94f0b0b081736a36ee27dbb0b454219fe8038121) more api endpoints - 2023-07-25 16:30:40 +0200 [77400ea](../../commit/77400ea62f181dfc07f7c32d157bb5232fefefe6) Make RegEx string a raw string literal - 2023-06-24 12:58:51 -0700 [d08af99](../../commit/d08af997389e426e7323e8daffd8d9049ac8cd74) more api endpoints - 2023-05-29 20:19:27 -0700 [26a05cf](../../commit/26a05cfe6b71076393f568906356ea8602082e62) update examples and README to use == for numberic values - 2023-05-21 10:54:24 -0700 [a930e2c](../../commit/a930e2c7f139e3aafbac8f13323d4e62bbf39496) 2.11.6 release - 2023-05-21 10:53:52 -0700 [6c12eee](../../commit/6c12eee7f7e818e39303a52701a7828f587a8c6e) more api endpoints - 2023-05-21 10:27:14 -0700 [891f120](../../commit/891f12032e7ca149835b881b51038996171307a0) 2.11.5 release - 2023-05-21 10:25:41 -0700 [74f1999](../../commit/74f19994c75e08a869eca9ac7aac4e109549a5b0) remove --api option and leave --openapi in place - 2023-05-20 11:49:22 -0700 [6d6dd33](../../commit/6d6dd33fcc547d0ea8083e19cb3636b7c0d5d84c) 2.11.4 release - 2023-05-20 11:48:30 -0700 [eab68ba](../../commit/eab68ba03525bfc1ae7ee46769a0368f5d393d5d) handle quoted strings - 2023-05-19 17:42:57 -0700 [9ae8cba](../../commit/9ae8cba9c7ccbe814cb944048eeb6dda908f7021) CHANGELOG.md pushed to github - 2023-05-19 17:42:40 -0700 [e644136](../../commit/e644136ab20f81743d94d3097ad7312262029ee0) 2.11.3 release - 2023-05-19 17:42:07 -0700 [36c40ce](../../commit/36c40cefefe69e2c8cb5037ba59c231a8431096d) handle multipart/form-data correctly for more than one file and with params/data - 2023-05-19 10:58:29 -0700 [13739c5](../../commit/13739c53ec96449924a872ee589e01b7171bcf8b) CHANGELOG.md pushed to github - 2023-05-19 10:58:08 -0700 [31f7d0b](../../commit/31f7d0b7d6b27bcc925d0d74f2899239ddcc4389) 2.11.2 release - 2023-05-19 10:56:25 -0700 [0a0b492](../../commit/0a0b49298fea6a2bc5fb430a3385ee80ac9bf703) python keywords not handled correctly at command level - 2023-02-22 20:30:01 +0500 [144cc1e](../../commit/144cc1e5313f308f391ace5c18e15d3e19998cb0) fix: remove future dependency and imports - 2022-11-26 23:37:57 +0100 [08a02ef](../../commit/08a02eff29dccdf2c6bdd1d903e8c26a05955ca8) added after openapi review - 2022-11-26 23:36:42 +0100 [a67ad74](../../commit/a67ad74f3f6707b9cba1eefa8be70f7cfa82dab8) added deprecated processing, now shows version - 2022-11-26 23:34:20 +0100 [47c7a3b](../../commit/47c7a3bb4bf4af554995c0b7b6a9e0f720c80aad) cleanup - 2022-11-24 19:04:43 +0000 [a887285](../../commit/a8872852966b4ff4674cf3c9682adf209417d48f) CHANGELOG.md pushed to github - 2022-11-24 19:04:23 +0000 [4267ff9](../../commit/4267ff98cf90b6e5d7a3d802cf90f7c5bdc8deba) 2.11.1 release - 2022-11-24 19:03:24 +0000 [7315385](../../commit/731538589ba736ffcc47be69784139850136694f) more api endpoints - 2022-11-24 18:51:51 +0000 [b70509b](../../commit/b70509b5f3c788bd4159982881932a83381ec265) firewall rules example - 2022-11-24 18:43:40 +0000 [afa8cc0](../../commit/afa8cc02d7bc3e490da0fe89ac1fe6c055a9c09c) add openapi support, added tuna for testing - 2022-11-24 18:43:02 +0000 [3af9aa8](../../commit/3af9aa8fbd1c1f8d1734a8466e61fffa21796012) add openapi support, delay yaml and jsonlines load to speed up everything, other cleanup - 2022-11-24 18:41:20 +0000 [dc2a6f8](../../commit/dc2a6f8c633a995bf79dde9638baacbe4d1a2254) add openapi support, delay bs4 load to speed up everything, improve requests imports - 2022-11-23 21:11:47 +0000 [f7b4a8c](../../commit/f7b4a8cbc5b2d89d544c96f08fe4773fceeb9433) missing import json, speed up to import requests - 2022-11-17 20:53:40 +0000 [e62e39c](../../commit/e62e39cd9673c06a3f61cbeda295a628d355f43f) CHANGELOG.md pushed to github - 2022-11-17 20:53:06 +0000 [e68245e](../../commit/e68245ef7b4b31c5f58971b965984cc88d969f51) 2.10.5 release - 2022-11-17 20:52:00 +0000 [72a7f55](../../commit/72a7f55857b70e7504daa13fd27f4b8c48f0fe3a) reverse usage of utf-8 in README introduced in commit da73108 - 2022-11-09 18:57:19 -0800 [28c0f26](../../commit/28c0f26c4dc82c67b05c07fb6c64b6b54a65598a) deprecated apis - 2022-11-09 18:56:40 -0800 [85d2108](../../commit/85d21082b6f3ca61d8ce233b2d894b2dc695ddf7) spelling typo - 2022-11-09 08:52:50 -0800 [58280fa](../../commit/58280fab5f936df1511232aa99f46e0487157e70) add more api endpoints - 2022-11-08 12:05:35 -0800 [95878c5](../../commit/95878c5115e54123a834a6107f1f55fb099cfd09) 2.10.4 release - 2022-11-08 12:01:47 -0800 [0d1575d](../../commit/0d1575d141d81cc965a64a91adaaed39591f1996) Merge branch 'issue151-missing-calls' - 2022-11-08 12:00:58 -0800 [cb85a20](../../commit/cb85a20f7677110af15d6457f5bc4e58fa4a3c88) issue151 added the missing calls found on developers site - 2022-11-08 11:44:37 -0800 [1302a55](../../commit/1302a5587aa80f046687c41b0e32f5954c31a90c) simplify not found message - it was overly complicated code - 2022-11-08 11:42:44 -0800 [45270f8](../../commit/45270f8b4ffe55b166a1b5d26c3cd2a3c747dd4d) The error message from unused/void parts of the tree should be simpler - 2022-11-04 17:06:35 -0700 [1f588a3](../../commit/1f588a331a67bb84509aa1e2876ad96c8112fd89) changed /zones/:id/logs/control to not callable - 2022-11-03 15:31:45 -0700 [713e306](../../commit/713e30602d4e137b6832d1c0664807ba28b6b4ca) CHANGELOG.md pushed to github - 2022-11-03 15:31:25 -0700 [accb642](../../commit/accb642794ae1928be66376f0605b7fedb43a6b4) 2.10.3 release - 2022-11-03 15:30:21 -0700 [da73108](../../commit/da7310880de4f1e41f31004e36a67eaeba98f064) add /logpush/edge/jobs info - 2022-11-03 15:24:20 -0700 [f511729](../../commit/f51172947bad68529120980c22924003009733ae) handle tabs and spaces in passed JSON values - longstanding issue not noted till today - 2022-11-03 15:23:13 -0700 [d4ac4a4](../../commit/d4ac4a48865782069d4629e966d7dae4bbe064a8) Handle /:id/:id plus camelcase id's - 2022-11-03 15:22:20 -0700 [d8ec3ad](../../commit/d8ec3ad51e043fa9f593d1a64744ac386c26142b) /zones/logpush/edge plus other API endpoints - 2022-10-02 12:49:28 -0700 [6dc4d7d](../../commit/6dc4d7deea58d9bef639ef308279468686b47111) more twine moves and cleanup of setup - 2022-10-02 12:48:55 -0700 [145d51e](../../commit/145d51e57116cdd209d2ef857b35b2658c603e6b) tabs vs spaces - 2022-10-02 09:36:34 -0700 [aa2072f](../../commit/aa2072fdcee6617021087036be13529908678cbb) CHANGELOG.md pushed to github - 2022-10-02 09:36:04 -0700 [5169f67](../../commit/5169f675a5030f223f0fd4d9ae75bb36944313b1) 2.10.1 release (now signed by mahtin@mahtin.com) - 2022-10-02 08:33:06 -0700 [0d831d8](../../commit/0d831d814dc0c51a96ed57f38a237c71f050f096) Merge pull request #149 from huangsen365/patch-1 - 2022-10-01 22:06:39 +0800 [515dff2](../../commit/515dff27959331a7cc41be572006e78db83ca61b) Update example_dns_export.py - 2022-09-30 10:05:42 -0700 [e5ebd91](../../commit/e5ebd91d85ac9e9dad3fa078447d64217c247941) /radar API added - 2022-09-14 17:55:28 -0700 [96747e2](../../commit/96747e20f286f23c9c8fc03b893c8c9bf85b1881) better messages - 2022-09-14 17:47:32 -0700 [4f63908](../../commit/4f63908cdbe722efd84ec75f682fb120656809af) do something useful with api listing - 2022-09-14 12:30:03 -0700 [4d7962d](../../commit/4d7962d599142747d817b81f388d6829fd81a4e9) issue 148 example - 2022-09-09 09:20:34 -0700 [75649ed](../../commit/75649edeff796c01154fba7dfa065cec7317b9f5) more api calls - web3 stuff - 2022-09-09 09:14:00 -0700 [d4a3cfe](../../commit/d4a3cfe565deed150f0043ddca2e6883aa088ce4) cleaned up error exit processing - because it was not correct - 2022-09-07 16:47:59 -0700 [b15d20c](../../commit/b15d20c9a590d93351415bb8bc9e0bdbfeec5dff) CHANGELOG.md pushed to github - 2022-09-07 16:47:42 -0700 [475022a](../../commit/475022ab2470d29719dad0112f7b92a88ea710cf) 2.10.1 release - 2022-09-07 16:40:59 -0700 [1f433ac](../../commit/1f433acee99a9128fe53e5099018e8f9baf8c3db) handle exceptions - 2022-09-07 16:40:34 -0700 [a28a13e](../../commit/a28a13e8312c98c3824dc2c50c551ebacb563d25) handle email/key or token as per issue-114, plus fix some small issues with exception handling - 2022-09-07 16:39:07 -0700 [0523914](../../commit/0523914fccd552d1e007d9a126fdc8b223020991) updated docs to handle key vs token becuase of issue-114 - 2022-09-07 16:38:33 -0700 [3ba3fe3](../../commit/3ba3fe3011702b2d8e69f915a01851d07c497a2a) exit with error if error in api call - 2022-09-01 17:32:24 -0400 [7bcec22](../../commit/7bcec22b8088cf95a0afdd278c4c66dce485a53c) more api calls - 2022-08-25 10:30:25 -0700 [56207f7](../../commit/56207f70401b39c8278f4e6ff7931251911a1653) email/routing/enabled -> email/routing/enable - 2022-08-23 16:13:55 -0700 [aad464c](../../commit/aad464c87017ca7c6fa81d32c81e333ea1c3740b) more api calls - 2022-08-23 16:13:18 -0700 [977aa24](../../commit/977aa2429ae63126f70a8486710a0a36a9c181aa) pesky python keyword handling - 2022-08-23 16:01:07 -0700 [13c1681](../../commit/13c1681502ac334dae8bed4faf43a31d39a6aa36) more pylint fixes - 2022-08-23 15:25:07 -0700 [68f3f2f](../../commit/68f3f2f0db4b1348c3838d3e82fde60c67570af9) python3 class object fix (finally), function name cleanup, identifiers/parts major cleanup, logger pylint corrrections, other pylint cleanup - 2022-08-23 11:39:48 -0700 [5af934e](../../commit/5af934ebd29f4bff0632ab6518b2aa0a1963d746) silenced some longstanding (and won't get fixed) messages - 2022-08-23 11:38:58 -0700 [06b1185](../../commit/06b1185b30bbdcfbc9a3052de755d8b9d80838b6) semicolon? - 2022-08-23 08:10:20 -0700 [7c25e8b](../../commit/7c25e8b15089dcc065469cdfa3c7eeee846a6c6b) made getattr() logic more explicit - could lead the way to on-the-fly tree building later - 2022-08-16 10:30:15 -0700 [f96d558](../../commit/f96d558b2b06cf064ceba8cd14335d23e703ea39) script_monitor -> page_shield - 2022-08-14 08:46:44 -0700 [b3b875a](../../commit/b3b875ae2166fa7e1cc5aa557d5f96e8edc33b6b) http_custom_errors & virtual_networks - 2022-08-13 09:04:24 -0700 [3a957f0](../../commit/3a957f04141eec8528cfd8750a3255d2ce9e52ce) CHANGELOG.md pushed to github - 2022-08-13 09:02:34 -0700 [bdaa1c5](../../commit/bdaa1c5d51742399a332eca8a15dfc24fa93ec4e) 2.9.12 release - 2022-08-13 09:01:39 -0700 [3f0801d](../../commit/3f0801d2889aa6ca4fda171b478b9d1e576c79b9) CHANGELOG.md pushed to github - 2022-08-11 18:10:41 -0700 [184ecd5](../../commit/184ecd55e3af6cdb22b78bf10fcc4fec97a0c23f) make api cleanup - not used in production - 2022-08-11 18:10:10 -0700 [49a34bd](../../commit/49a34bd2ef5cf20b4fb49fe6eb1cfda6d7dcb513) stream/clip & api_gateway added - 2022-08-10 15:12:59 -0700 [9334b13](../../commit/9334b13e2046108626b87756e286ffc67ab1c0dc) examples/example_time_calls.py - just a simple timer - 2022-08-10 15:09:53 -0700 [bd220aa](../../commit/bd220aacaf5aeb2e85ca22069a96f89b2cd8a9db) __del__() added - not very fancy; but cleaner network close now - 2022-08-10 14:29:13 -0700 [4a5da8b](../../commit/4a5da8b8b1d47b59f636dc78799e91de587097b8) finally fixed the quotes to make consistent - 2022-08-10 14:20:19 -0700 [c22db3e](../../commit/c22db3e34977451c87e92b9de581888e191d42c8) cleanup of error handling for adding api calls - 2022-08-10 13:03:59 -0700 [0db4961](../../commit/0db49616ffca6a3920780a7639567cd75672106c) typo in r2 calls - 2022-08-09 16:42:55 -0700 [a44aaa3](../../commit/a44aaa348952910d95f33fd1b7eb51915ecf553e) updated email api call end points - 2022-08-07 14:33:10 -0700 [2ba207a](../../commit/2ba207a10f9635ea587cf1bd24d7d14f67673ecc) allow binary files for file upload via --binary flag - 2022-08-07 14:11:44 -0700 [10b30c9](../../commit/10b30c9ca57fd0e937fd7b9e224d352b4864924b) some R2 calls added - there is still some calls that are not yet documented - 2022-08-02 16:39:17 -0500 [85d1404](../../commit/85d1404d04c77a9187646a530b588212118bac9b) .pypirc needed for twine - 2022-08-02 16:37:42 -0500 [78e9c3b](../../commit/78e9c3b361c2065778371ab2c7dc73b578abebe9) wheel typo,setup to twine for pypi,api cleanup - 2022-07-06 12:43:49 -0700 [aeaec3d](../../commit/aeaec3d943a2f544280909f1ea7749a784b939cb) added .../intel/miscategorization - 2022-07-03 09:28:04 -0700 [a860114](../../commit/a86011497471b4fa1698295b44eb1a9db3bc79e1) edit of "Get deployment stage logs" call - 2022-06-21 18:15:15 -0700 [b0d2335](../../commit/b0d2335e20f257a5f6280fa18562912c6e6123a8) CHANGELOG.md pushed to github - 2022-06-21 18:15:01 -0700 [edc7b0b](../../commit/edc7b0b23043ae8955c94000c0b037b89232d786) 2.9.11 release - 2022-06-21 18:14:13 -0700 [ec9970d](../../commit/ec9970dd381e083f95c38075a93a6ecd8aa45704) user token checking example added - 2022-06-21 10:10:14 -0700 [fafa4e4](../../commit/fafa4e442d3614b8f56db04217172257e0c10d97) more api calls - 2022-05-19 11:04:21 -0700 [c1347aa](../../commit/c1347aa583b659290a56398eccda4e2558b4ce77) more api calls - 2022-05-14 12:44:20 -0700 [ac3e2cc](../../commit/ac3e2cc4153393af3e739a77d510c8adbddb5e3e) more api call - 2022-05-14 12:43:54 -0700 [97eb933](../../commit/97eb9331a8c9b1282268b7ff438d32ff1b97f8c6) started uncallable endpoint reporting - but not finished yet - 2022-04-19 15:30:14 -0700 [b9b91b6](../../commit/b9b91b612d3ac8818565cc2d3aee1c00c4cea0e9) more api calls - 2022-04-07 12:16:45 -0700 [9168ee7](../../commit/9168ee7b40be8b6ddbf9a4a8394ce0d5ad6bb796) 2.9.10 release - 2022-04-07 12:16:01 -0700 [c9a170c](../../commit/c9a170cc5f172fd3a0d05d6ce8274391cdced4e7) fixed getless calls failing - 2022-04-01 21:27:13 -0700 [966fec9](../../commit/966fec9cd1201310254bb572126414bd81f7e3c3) CHANGELOG.md pushed to github - 2022-04-01 21:26:53 -0700 [efa53f2](../../commit/efa53f27bd34a8ec7c84afb11363850b17e5a2d4) 2.9.9 release - 2022-04-01 21:25:23 -0700 [0bc445a](../../commit/0bc445a3b09ed84bbca8998eaad73219150a9bbf) read_config() now compatible with older and newer ConfigParser usage - 2022-04-01 10:44:55 -0700 [38ec20c](../../commit/38ec20c1be595549c03cee1f28460fc0e058d47b) CHANGELOG.md pushed to github - 2022-04-01 10:34:32 -0700 [756fb08](../../commit/756fb08b211c248f60d5d83bc20e0c6fa7897afe) documented the "import" keywork issue for dns_records.import calls - 2022-04-01 10:33:52 -0700 [3dcf29e](../../commit/3dcf29e53c9088969cee9e1139120c3bd82bb8a1) moved to newer authentication on PyPI - 2022-03-30 20:59:46 -0700 [4365b80](../../commit/4365b804ec92a8625bd4ddcdd0fb436fcab5387a) CHANGELOG.md pushed to github - 2022-03-30 20:59:28 -0700 [e748c42](../../commit/e748c422da389ef831435a168cc85bad7603ec9e) CHANGELOG.md pushed to github - 2022-03-30 20:59:25 -0700 [fe751a9](../../commit/fe751a9e80684d2095c3baac624d52f9bd606430) 2.9.8 release - 2022-03-30 16:17:41 -0700 [7b763f1](../../commit/7b763f1b57b076fcafce90664f919149ad4280f0) custom_csrs added - 2022-03-30 16:04:28 -0700 [cbeee2c](../../commit/cbeee2c8c4bf0996ee28b4bb0c80707775e6a84b) #136 pointed out that the config file profile name has a capital F in it. Now it takes lower case "f" value - 2022-03-30 15:41:14 -0700 [da1645c](../../commit/da1645c8aca27544b579029b3767369a94611d36) CHANGELOG.md pushed to github - 2022-03-30 15:40:52 -0700 [38886f9](../../commit/38886f94b9e30abed1c09e66514b25011c100d4d) 2.9.7 release - 2022-03-30 15:40:34 -0700 [f9486e6](../../commit/f9486e61e7e9bae4133e2b46436969969fbcedf7) Described the change to the enviornment variables - 2022-03-30 15:29:45 -0700 [2c4ee60](../../commit/2c4ee603f53aab555aba764ad95b65bf2da012f1) config file processing was messed up - 2022-03-30 15:28:39 -0700 [456ab7f](../../commit/456ab7ff528d1d3af9e5ecb09e14c136d51d634e) more api calls - 2022-03-30 13:48:59 -0700 [c01370b](../../commit/c01370bd95772806a6445f28c4fa3af88c4e99ef) Merge branch 'Changaco-patch-1' Changaco patch-1 - 2022-03-30 13:48:19 -0700 [9c73821](../../commit/9c73821e68dddaf78585f5c8963ee0869bd0619d) Merge branch 'patch-1' of https://github.com/Changaco/python-cloudflare into Changaco-patch-1 Changaco patch-1 - 2022-03-30 13:45:11 -0700 [b5e7356](../../commit/b5e7356b9168958e9b3017210ef42a999b887506) print() was outdate - from pull #120 - 2022-03-30 13:35:48 -0700 [955430c](../../commit/955430c0c9b980a806fe9492ddef24c362a03524) example for email commands - shows the use of underscore vs dash - 2022-03-30 13:23:03 -0700 [6f8ef61](../../commit/6f8ef6133d85c572d58eccf0dc134289e3a1b66c) Merge pull request #132 from phntom/master - 2022-03-30 13:16:48 -0700 [f3dd7ae](../../commit/f3dd7ae3d1a1dbde554cdcde5c48bfa416eefc98) Merge pull request #133 from JaredPage/add-bot-management-handle-null-errors - 2022-03-30 12:30:05 -0700 [5f444bc](../../commit/5f444bcde8014249d84aafe56a484dc8d917259b) CHANGELOG.md pushed to github - 2022-03-30 12:29:39 -0700 [9c1198c](../../commit/9c1198c40b0d6b37117fbefcfb1e0a0de377f447) 2.9.6 release - 2022-03-30 12:28:05 -0700 [d486194](../../commit/d4861943f9f669e9a3f6d62d57d19a15c5042448) two missing else statements - 2022-03-30 12:20:12 -0700 [2d89fc7](../../commit/2d89fc7c249377aeafc956176625cc872649bc16) Move the somewhat out of date table of commands into its own file - which then needs to be created automatically in the Makefile - 2022-03-30 12:06:36 -0700 [388ba06](../../commit/388ba06159415847a9976bd3404ac21a1210aab6) CHANGELOG.md pushed to github - 2022-03-30 12:06:05 -0700 [0de3fa2](../../commit/0de3fa20a5023c2703eeb73cf3ce3e31f47cd02f) 2.9.5 release - 2022-03-30 12:02:46 -0700 [88822c4](../../commit/88822c49c45fe645bc51177be6ddde46618bc8b2) Upgrade pandoc setup and remove tabs from README - 2022-03-30 09:50:40 -0700 [0535bb4](../../commit/0535bb41c0a1c47c44af95d3d15b6b879f169de1) 2.9.4 release - 2022-03-30 09:12:13 -0700 [d2622ed](../../commit/d2622ed0e7723f5a3e39f92d750cd41a9c44f2ca) CHANGELOG.md pushed to github - 2022-03-30 09:11:40 -0700 [b01d16e](../../commit/b01d16ea503c5de00f168d2d6fe2b0a08576fd5a) 2.9.3 release - 2022-03-30 09:07:21 -0700 [657772b](../../commit/657772be05cf8c6036fc66ee78686fa258962884) trailing slashes are not needed in API - 2022-03-30 09:06:57 -0700 [2e4d26f](../../commit/2e4d26f3d03638ff4a6c1ebe0acbf12f763d04e2) API added yet another param - 2022-03-10 06:02:12 +1100 [44d7d65](../../commit/44d7d65ea96a1dbe4577a164bb223abd5160c2d7) update environment variables to be prefixed with `CLOUDFLARE_` - 2022-03-04 00:24:31 +1000 [a2640a0](../../commit/a2640a0403429bf5f8ed61a7c11888359a2d0d39) Added python example for pagerules - 2022-03-04 00:07:22 +1000 [8b0d26c](../../commit/8b0d26c2ede79205048429397a4d9c3495e7b7fb) Added Bot Management ability and fixed an edge case for the API return - 2022-03-02 14:52:13 +0200 [578a26b](../../commit/578a26bf9614fe82de8d936acbe174fb4fa7019c) Merge pull request #1 from phntom/phntom-patch-dash-typo-cloudflare-py - 2022-03-02 14:51:03 +0200 [dd6d802](../../commit/dd6d8029a52cccf56923b370687be63aff4c0786) typo in cloudflare.py - 2022-02-28 16:44:57 -0800 [6c0e470](../../commit/6c0e470dac2241e5a24e51a38b5da46d982a7751) CHANGELOG.md pushed to github - 2022-02-28 16:44:34 -0800 [10f82a8](../../commit/10f82a8c4cc06fdca7588c51cc8bdb7fe2665896) 2.9.2 release - 2022-02-28 16:43:45 -0800 [24d0991](../../commit/24d0991a34241ea8803b02c6ccfe0ffebf244919) show version number from api url - 2022-02-28 16:34:41 -0800 [9be2aef](../../commit/9be2aef54016213abea93889a471f56f757000d1) handle ?params in API - which should not be there - 2022-02-28 16:32:29 -0800 [1378ad8](../../commit/1378ad8311912d5244fbd6c40c307bbc2b172b85) api moved from accounts/cfd_tunnel/tunnels to accounts/cfd_tunnel - 2022-02-23 12:09:30 -0800 [3d8740e](../../commit/3d8740ed4b7d49bbfd36c0f7c398b54097abf50d) more api calls - 2022-02-23 12:07:41 -0800 [f1e3531](../../commit/f1e3531229d694ee0401b35163b2756dc22f9de8) keyword handling still needed fixes - 2022-02-17 09:33:51 -0800 [1ca59e8](../../commit/1ca59e870751822c354f96b57bb8194e336f846b) finally handle KeyboardInterrupt cleanly - it was easy - 2022-02-16 16:18:54 -0800 [4bb7894](../../commit/4bb78946aa0fc7a0d733a0cfebbaeaa0744b6825) CHANGELOG.md pushed to github - 2022-02-16 16:18:06 -0800 [78a7616](../../commit/78a7616cb8e903954cd7043eb0af02c17f268bdd) 2.9.1 release - 2022-02-16 16:15:15 -0800 [a25605c](../../commit/a25605cdf5bdfbf318b76900f0f5c43ac1f87e2c) instance4 added - its a hack, but will do for now - 2022-02-16 15:59:15 -0800 [36692f3](../../commit/36692f3d8ef6697aad50f96607e5e14073807eb5) first batch of email api calls - 2022-02-13 17:44:16 -0800 [4ae2013](../../commit/4ae20130dab0a55dca8765e72974e0f7ea7e39cd) API added yet another param - 2022-02-13 17:18:26 -0800 [6125b75](../../commit/6125b754c29876a64c0080f087f03dcabdcc54d9) API added yet another param - 2022-02-13 16:21:26 -0800 [9128600](../../commit/912860070e45955f73249304aa70558dd7f459a1) typo - 2022-02-13 16:16:26 -0800 [d868c94](../../commit/d868c94ea624a199ae0c0f3fb23e5e680b4ad835) typo - 2021-07-28 11:19:50 -0700 [7a0d715](../../commit/7a0d715203b886f80b27f475ea8f35b25017613b) sanitize passed variables, handle errors from network better, first pass at keywork conflict issues - 2021-07-21 12:31:50 +0200 [c6b7423](../../commit/c6b74232b329e83785c93f8abe177cde860d6bba) bdist_wheel added as universal - 2021-07-21 12:24:02 +0200 [eedf7c7](../../commit/eedf7c745d2450e28893272e34e90ec27ce12b7e) account rules list example - 2021-07-21 11:55:47 +0200 [ee4e774](../../commit/ee4e77446410e80d9455c39d229ff72c72f4d8a1) dns import example - showing how to get around reserved word - 2021-07-21 11:52:50 +0200 [e02c380](../../commit/e02c38067f2911106fff12d1e364eea484bf9ae6) handle
 within description - which should be ignored
 - 2021-07-21 11:52:02 +0200 [cde763a](../../commit/cde763aaf0f0e9769689664b77378737af01b5a7) document :: option
 - 2021-07-21 11:51:28 +0200 [23a9366](../../commit/23a936630ef052c92b9816acb91092f17154e7de) indents - what a pain!
 - 2021-07-21 11:48:02 +0200 [215f5b2](../../commit/215f5b2f492ace78164547850712c4a696b9672f) bdist_wheel added as universal
 - 2021-07-21 11:47:15 +0200 [ff66435](../../commit/ff664357f9fe2b24767e909a8777864f6d8efa11) handle /pages using {} for account_id
 - 2021-04-01 12:02:16 +0200 [eb42047](../../commit/eb42047e2576ca7ec7b9b8b0edaa4e78f9138374) fix the installation path of the cli4 man page
 - 2020-12-31 16:19:30 -0800 [4c80399](../../commit/4c80399ad27e67b88138058e76b12002db8e7829) CHANGELOG.md pushed to github
 - 2020-12-31 16:18:47 -0800 [6ea3d2f](../../commit/6ea3d2f03dc15b3c59ce29529ce4a420a51758a4) 2.8.15 release
 - 2020-12-31 16:17:37 -0800 [ebea9ea](../../commit/ebea9ea5ca1490e1388bfe0b8ac119ba405efb31) added cursor example
 - 2020-12-31 16:15:51 -0800 [534fbde](../../commit/534fbde1a6a7bf1e8ced9053f1625f46aac845b5) zones/rulesets added
 - 2020-12-31 13:52:03 -0800 [cd535c1](../../commit/cd535c1e26b6a1a6703d8ca03ee98f90fa13d364) Merge pull request #107 from Martin407/patch-1
 - 2020-12-31 13:51:00 -0800 [c2cb66a](../../commit/c2cb66a8569d8e18d8f13be254e4e3c885778086) Merge pull request #106 from Martin407/patch-2
 - 2020-12-03 14:24:07 -0800 [dc14687](../../commit/dc14687e4957c4ed93f65fe697b2821a0c6f4a93) CHANGELOG.md pushed to github
 - 2020-12-03 14:22:47 -0800 [9e170eb](../../commit/9e170ebe2f9139d2b0620d7f31be2330dd532aaa) 2.8.14 release
 - 2020-11-14 12:58:07 -0500 [acc3b99](../../commit/acc3b9963581ad5b875f6f80b517822261775d62) Removing excess trailing parenthesis
 - 2020-11-14 12:51:53 -0500 [bf583e2](../../commit/bf583e2ccce999cdeaa27ac3a6860ef425eb9343) Removing excess trailing parenthesis
 - 2020-09-20 15:58:25 -0700 [b70b520](../../commit/b70b5209664dc64ae4f0e5773806aec05b799cbd) first pass at adding travis CI
 - 2020-09-17 14:17:45 -0700 [9975223](../../commit/997522321c61f08cc91bcc61f3ee707f38179f9a) zones/waiting_rooms, accounts/diagnostics, and more
 - 2020-08-13 10:41:22 -0700 [8ed99e4](../../commit/8ed99e4899637a7d7bc11c4bee76982537fcc45f) CHANGELOG.md pushed to github
 - 2020-08-13 10:41:11 -0700 [f926130](../../commit/f926130d61bd71203b5f91f4dae947538cce669b) 2.8.13 release
 - 2020-08-13 10:40:42 -0700 [16b7e88](../../commit/16b7e88dd302959ae8d2b8ba3f16dc21f07e0bae) configparser added it haste, but not needed - oops!
 - 2020-08-12 18:11:09 -0700 [76475a0](../../commit/76475a09e3ae941b740f80b1c6c59b9d23e30200) CHANGELOG.md pushed to github
 - 2020-08-12 18:10:43 -0700 [e817165](../../commit/e817165797af62a5a4a241f403bbb2a74ac011fd) 2.8.12 release
 - 2020-08-12 10:46:07 -0700 [a76518f](../../commit/a76518f3d5caac7772000f6e3edb942882289c4a) CHANGELOG.md pushed to github
 - 2020-08-12 10:45:32 -0700 [9121e05](../../commit/9121e05bc2187ae1541df725bed7e780713c3a3f) 2.8.11 release
 - 2020-08-12 10:45:10 -0700 [f020a0b](../../commit/f020a0b7a54f5b30fcbbc674ac18034e2d2c116f) /zones/:id/access/...
 - 2020-08-12 10:30:56 -0700 [ff2a701](../../commit/ff2a701d4117c4291480b65a1b54a9537f297111) 2.8.10 release
 - 2020-08-12 10:30:31 -0700 [97645f7](../../commit/97645f7167b35adba0cf10685e5a7807b1ad3e37) configparser missing - oops!
 - 2020-08-04 14:40:24 -0700 [316a22c](../../commit/316a22c679a225bb5b967bfbd7b06ff31835db05) CHANGELOG.md pushed to github
 - 2020-08-04 14:40:02 -0700 [827242e](../../commit/827242e926c88b02c95f5fd9a9fdeb39ac9ca2b5) 2.8.9 release
 - 2020-08-04 14:39:17 -0700 [ef9a5f3](../../commit/ef9a5f3aee7b5230047cd20ab63b7c83d78bd8e7) cleaner and easier way to find missing api calls - this changes --dump/--api for the better
 - 2020-08-04 14:30:11 -0700 [21649a9](../../commit/21649a9e9bf6bcaac7a9160d73e7f93f496f0e09) /zones/:id/access/apps/policies - fixed along with a bunch more similar typos
 - 2020-07-30 20:21:48 -0700 [f4c5e0f](../../commit/f4c5e0f2e888b5c6e8588e965ae366d66b5cdb13) added more profile info
 - 2020-07-30 20:16:18 -0700 [2f0a39d](../../commit/2f0a39d00c567bbd05b5404cd6d3d6f0b6002f3f) revoke-tokens -> revoke_tokens
 - 2020-07-27 12:51:12 -0700 [f1df7b6](../../commit/f1df7b6177d6fd6dffc692900c2b61bf4073b36d) updated and included AMP RealURL/Signed Exchange API
 - 2020-07-24 11:15:01 -0700 [5631b35](../../commit/5631b35295fa844e4d1c43cb638ee5fbc44879b7) CHANGELOG.md pushed to github
 - 2020-07-24 11:14:39 -0700 [c138287](../../commit/c138287ebab6d231e47214f6336c554ff659d53c) 2.8.8 release
 - 2020-07-24 11:11:00 -0700 [80918ea](../../commit/80918ea248c5c2cf3f9013ca762eb844d898de1e) now with curl style debug - i.e. matches api information page
 - 2020-07-24 11:10:39 -0700 [586ba80](../../commit/586ba80e664da8974c963b9a16874ce3a63904af) now with curl style debug - i.e. matches api information page
 - 2020-07-24 11:09:13 -0700 [3cd4064](../../commit/3cd4064bb3d01006c095669fbd9b4255584cd6b3) now with access_requests as underscore
 - 2020-07-24 11:08:38 -0700 [fbd0582](../../commit/fbd0582e359bc27b9012184c333203f3b3773d40) make sure verbose, etc is always used
 - 2020-07-20 12:18:57 -0700 [fc69383](../../commit/fc69383990659ce47f98bd098006049feec9d18b) CHANGELOG.md pushed to github
 - 2020-07-20 12:18:36 -0700 [2952f9f](../../commit/2952f9fb1d29a2f89417cc3de4328d4830f32dda) 2.8.7 release
 - 2020-07-20 12:17:32 -0700 [f3652ad](../../commit/f3652ad8f7ca67464d0949b28de111ed7359b53a) dashes vs underscores - finally tamed!
 - 2020-07-20 12:16:42 -0700 [25ac677](../../commit/25ac67776d471cdf7378a1f66e3c7169ef74be18) /zones/:id/access/apps/:id/revoke-tokens - added
 - 2020-07-20 12:15:50 -0700 [5aba7f0](../../commit/5aba7f04e6e10b0a20bc452f6d79cd42969391e8) improve deprecated code - add dates, check expire, improve parse of api webpage
 - 2020-07-18 10:59:14 -0700 [5ca2b6f](../../commit/5ca2b6fd4944014cf56fc9a24acd9b268e02bea1) added support for dashes/underscores in commands and python calls - kinda overdue
 - 2020-07-18 10:58:33 -0700 [88db48c](../../commit/88db48c181c0387d29b0616917e3e586bf7bf8be) cleanup of logic around uuid match. added support for dashes/underscores in commands
 - 2020-07-18 10:47:21 -0700 [ca858f6](../../commit/ca858f6286d120a953534e2eae16fc5ea5f0cc58) /zones/:id/access/apps/revoke-tokens - removed as depricated
 - 2020-07-18 09:21:37 -0700 [94e4236](../../commit/94e4236c551e1f8f88a5709db1b072ccd0415236) removed deprecated /organizations and /user/virtual_dns api
 - 2020-07-17 16:34:25 -0700 [7edcc96](../../commit/7edcc96d8b836624c69ec7ebf8abc6dfeb7b2542) Added base_url to config and env variables
 - 2020-07-17 16:31:55 -0700 [f23a419](../../commit/f23a419d88fc9a84a6a8bc2e1f6be954c86be28e) accounts/:id/rules/lists/bulk_operations/:operation_id - syntax fixed
 - 2020-07-14 19:00:41 -0700 [16fec80](../../commit/16fec8047ca97d05abf73fd9ae730681c2a0e10f) CHANGELOG.md pushed to github
 - 2020-07-14 19:00:30 -0700 [3ce15c1](../../commit/3ce15c1bd3e79f8deeef4eff1c184ac7f407fa75) 2.8.6 release
 - 2020-07-14 18:59:59 -0700 [4aa1148](../../commit/4aa1148b3e9389b9654df64e255ed353947bd47f) Added working GraphQL examples
 - 2020-07-14 15:35:50 -0700 [5c25aa8](../../commit/5c25aa80b15a78aee574724b115ce172d6ca2401) CHANGELOG.md pushed to github
 - 2020-07-14 15:35:33 -0700 [ec0a1cc](../../commit/ec0a1cc492b14e2cd0d901c6034dbcfcf6c0828f) 2.8.5 release
 - 2020-07-14 14:58:41 -0700 [a3bd103](../../commit/a3bd1032e9c4f4a56578af2011e0fc369841cc0f) improved debug for jSON based data/params
 - 2020-07-13 13:00:32 -0700 [fdb3dc1](../../commit/fdb3dc13604885541ec8dbac20e0b6b87c4a1e0d) CHANGELOG.md pushed to github
 - 2020-07-13 13:00:16 -0700 [a1ec29f](../../commit/a1ec29f9566e8e0c9bba7f2a9defbea987c6b214) 2.8.4 release
 - 2020-07-13 12:57:06 -0700 [ae96ed4](../../commit/ae96ed4c2edc087ae06c50202765026d7bb8139a) rules,access/logs,access/apps,etc added
 - 2020-07-13 12:56:06 -0700 [ccc96f1](../../commit/ccc96f1040da92db6b04a36de7883b6d61830b0e) api decode and Makefile now consistent - no leading slash
 - 2020-07-13 12:46:35 -0700 [b5c1f60](../../commit/b5c1f60b42a6b48fc11faa8a69cfa8ce1a5a2609) moved network functions - but forgot one call - now fixed
 - 2020-06-23 17:37:09 -0700 [5f3a974](../../commit/5f3a97494f9d3582a2be0b3baaf59f3d24c2b549) moved network functions into their own file - part of splitting up a large file
 - 2020-06-22 18:45:20 -0700 [dd5e51f](../../commit/dd5e51fb4a6cab05f5bd8a678da1dce02d1846f2) CHANGELOG.md pushed to github
 - 2020-06-22 18:45:09 -0700 [f617736](../../commit/f617736837ca83056cbfb6e4aedb201c4a14a2de) 2.8.3 release
 - 2020-06-22 18:44:22 -0700 [1a3eb2a](../../commit/1a3eb2ad44ed66e575d50da80d4ab04e96be840f) added example code for custom_hostnames
 - 2020-06-22 18:38:19 -0700 [806b626](../../commit/806b62626c373e9f269d63c355d22e4977dfa59e) added uuid support and converter for custom_hostnames
 - 2020-06-22 18:36:49 -0700 [9dcb55f](../../commit/9dcb55f083ca2dc95facbd72f43a7ed5fb50d207) one more place where a missing error response could do harm
 - 2020-06-22 18:36:14 -0700 [4262a2b](../../commit/4262a2b07e25c07980c164966d48eb3ce870bd22) added zone type and more dns info in dump - mainly for documentation reasons
 - 2020-06-22 12:42:28 -0700 [b74a781](../../commit/b74a78115db17a31b65b1e4a7c5b8bb6d31033ad) CHANGELOG.md pushed to github
 - 2020-06-22 12:42:03 -0700 [d82318f](../../commit/d82318fcb14d458b771805ef758abe9e5951dc78) 2.8.2 release
 - 2020-06-22 12:41:17 -0700 [40ab439](../../commit/40ab4396b6cbf25df52f6ad44498b94222e3af01) removed excess imports including both __future__ and others
 - 2020-06-22 12:29:07 -0700 [ff91110](../../commit/ff9111072169bc4849964e294c2d4fcf572f5ec9) Merge branch 'master' of github.com:cloudflare/python-cloudflare
 - 2020-06-22 12:27:52 -0700 [61e73e3](../../commit/61e73e36d10d438942e46cd62e247d1662ad069a) Merge pull request #96 from FelixSchwarz/master
 - 2020-06-22 09:22:46 +0200 [8680ef5](../../commit/8680ef56c538661d468fe271a610d87400a249f3) setup.py: remove unnecessary dependency on future
 - 2020-06-19 22:54:28 -0700 [4ecb111](../../commit/4ecb11172749346bfe020e334ca956e6cf7bf7b2) added reference to CHANGELOG
 - 2020-06-19 22:40:51 -0700 [fc501c0](../../commit/fc501c0f0fdef203fa3065519e0b33ce5699b0fe) made CHANGELOG smaller
 - 2020-06-19 15:31:42 -0700 [b0caf37](../../commit/b0caf3746125681b76b8e1ea7e99ab6915655eb1) CHANGELOG.md pushed to github
 - 2020-06-19 15:31:01 -0700 [23784a4](../../commit/23784a40db8b92c76447f7bf212938eb4605e2ae) 2.8.1 release
 - 2020-06-19 15:28:48 -0700 [f8cc4a7](../../commit/f8cc4a73afef1329821884added6a792874dcdb4) now able to check and confirm API is up to date
 - 2020-06-19 15:08:48 -0700 [f266fb5](../../commit/f266fb50e30d93ae1c32553703c9c514f2888903) added code to query api documentation (via beautifulsoup4) to build a current api listing
 - 2020-06-18 17:03:24 -0700 [f25bdfa](../../commit/f25bdfaf59ad4d25948482b3fe98099912168898) pylint stuff
 - 2020-06-18 16:56:59 -0700 [9c2424e](../../commit/9c2424e57e719c5e5a0e561d5b187e57dbf39610) moved connection to _connection() to accomodate additional call types
 - 2020-06-18 16:42:43 -0700 [84f57b4](../../commit/84f57b4240acecc20eeb1e5aef2bd047e3eb77ca) handled  accounts/:account_identifier/storage/kv/namespaces/:namespace_identifier/values/:key_name return as binary data
 - 2020-06-17 13:05:01 -0700 [355ae5d](../../commit/355ae5defff5c02880f553c5a16c84662c1571ca) handled any key_name for accounts/:account_identifier/storage/kv/namespaces/:namespace_identifier/values/:key_name
 - 2020-06-17 12:28:09 -0700 [dc07708](../../commit/dc0770861e8fd7d45f431ee0bf38d87c6aeb4223) addressing/prefixes/delegations added
 - 2020-05-12 18:44:20 -0700 [1906a68](../../commit/1906a680fca36f525610422ff7914afe0491f6e6) CHANGELOG.md pushed to github
 - 2020-05-12 18:43:44 -0700 [8280ec9](../../commit/8280ec96a9387f685fa3bf4e7ec1779406fcd90c) cleaned up upload commands
 - 2020-05-12 18:40:51 -0700 [916aa5b](../../commit/916aa5beb77481e6b891be75f91da404da07df99) 2.7.1 release
 - 2020-05-12 18:34:25 -0700 [ddf442e](../../commit/ddf442eae8a8d29fe71d052669f5867ae0d62d4f) added zones/ssl/certificate_packs and zones/origin_tls_client_auth
 - 2020-05-12 18:26:20 -0700 [0a5ec5b](../../commit/0a5ec5b39d8cdc1f20b9a57951d08304a839cb38) CHANGELOG.md pushed to github
 - 2020-05-12 18:25:52 -0700 [1c05b7b](../../commit/1c05b7bb1f5a05e9fcfebdfaa2d12bec0e8547f8) 2.7.0 release
 - 2020-05-12 18:25:10 -0700 [c1d9295](../../commit/c1d92955051e180dc4132d520830b5ba795097d2) added man page to dist
 - 2020-05-12 18:24:20 -0700 [5a8844e](../../commit/5a8844e651925bea01f12f38cdfb1ba52bdd4907) added github signing commands to Makefile
 - 2020-04-28 14:12:16 -0700 [82b5cb7](../../commit/82b5cb7565c0fdb6e4de81dbbea12b74801b753f) added graphql handling with no results in response, corrected handling when error code is missing
 - 2020-04-28 14:09:49 -0700 [4c90fa8](../../commit/4c90fa8a0f8a88ff0a3962611a18b2eb8eaed47b) added graphql API endpoint
 - 2020-04-28 14:09:04 -0700 [1e949db](../../commit/1e949db4ad705d1fd0ec6387d77f99cd08ca0198) fixed usage string - missing spaces, allowed PUT & POST for file uploading to handle graphql API call
 - 2020-04-28 14:08:04 -0700 [65f3cba](../../commit/65f3cba24823e858f019ea4c3d0b528a93251f1c) updated man page - back in sync with code
 - 2020-04-08 18:14:15 -0700 [13901c1](../../commit/13901c17dc42b00516275955c3d1e009c3fe0ac2) CHANGELOG.md pushed to github
 - 2020-04-08 18:13:30 -0700 [1da709c](../../commit/1da709c2138491dc8316fad04d0221236f97439e) 2.6.5 release
 - 2020-04-08 18:12:48 -0700 [450dd7d](../../commit/450dd7d721b5be7b257571018a6d49af2d90db13) updated api command list
 - 2020-04-09 08:57:24 +0800 [c5d4890](../../commit/c5d489032c29e2d84d7611518a68a637ccf89b0d) Merge pull request #90 from pygrigori/master
 - 2020-04-08 17:55:33 -0700 [513b92c](../../commit/513b92c0f7340d5e49ee134d7ca025d5b1d534d6) typo
 - 2020-04-08 17:51:20 -0700 [d0ed19e](../../commit/d0ed19e619e95bcefc4cc09a28acb48886cd89f5) CHANGELOG.md pushed to github
 - 2020-04-08 17:51:05 -0700 [d478bb3](../../commit/d478bb3cf8b36749ff2aeeb1d197550b2ccecf08) 2.6.4 release
 - 2020-04-08 17:49:59 -0700 [fceb488](../../commit/fceb48839e4eab2da0a76f04a003ecfab2b74945) cli4 now accepts more than one call on the command line
 - 2020-04-08 17:49:39 -0700 [4e67548](../../commit/4e6754846eb48edfd5d297c04b685fcfffd65683) cli4 now accepts more than one call on the command line
 - 2020-04-08 17:42:21 -0700 [1c70a8e](../../commit/1c70a8e9b4b2e531d0e61a26f4cd729b86e2c51e) still not got tests done - but housekeeping moving along
 - 2020-04-08 17:41:12 -0700 [5b28101](../../commit/5b28101c6d01ec7f4fec6cf2be64ba23181fbdea) an example of a more complex zone name search
 - 2020-03-13 17:06:50 +0300 [d7cdb0c](../../commit/d7cdb0cd5b0408b30c403d4d83915466a1e3d558) added support for account audit logs
 - 2020-02-09 12:40:13 -0800 [102286e](../../commit/102286ed5bb6f920d4f6270fd28f7b2d19e9b7bc) CHANGELOG.md pushed to github
 - 2020-02-09 12:38:06 -0800 [017c240](../../commit/017c240d9d9c410352d94e1422249dce7234032b) 2.6.3 release
 - 2020-02-09 12:27:51 -0800 [adfc471](../../commit/adfc471ca872faf1610444e0c52f3d689c28d549) Merge pull request #65 from xens/keep_proxied_state
 - 2020-02-09 12:24:53 -0800 [b3809fe](../../commit/b3809fe7941a6461e2651cbf44f949f252ee5222) CHANGELOG.md pushed to github
 - 2020-02-09 12:24:41 -0800 [c30609b](../../commit/c30609bfa4869718ecdbfdc45ba626bb527b6fba) 2.6.2 release
 - 2020-02-09 12:22:46 -0800 [1d7ca2f](../../commit/1d7ca2fde4db9d4d5cf90ae598ae8336fcb85630) added /accounts/addressing/... & /zones/secondary_dns/force_axfr & /zones/logs/control/retention/...
 - 2020-02-05 18:19:32 -0800 [365eed7](../../commit/365eed731d80cc48f07f4efc6b1b65f70b01303f) CHANGELOG.md pushed to github
 - 2020-02-05 18:17:40 -0800 [ebabedc](../../commit/ebabedcbd3baaf7f9a9c34bb5a06ff5a71baee8d) 2.6.1 release
 - 2020-02-05 18:17:00 -0800 [e0eb323](../../commit/e0eb323a713698be0e10f1d5066e982eace511f0) restored extras= config file functions
 - 2020-01-17 22:01:20 -0800 [2ec542b](../../commit/2ec542b8274e2b6ae1a9a888922aafae8d5f5e0e) CHANGELOG.md pushed to github
 - 2020-01-17 22:01:02 -0800 [d3d96c9](../../commit/d3d96c9d9f7838bc50d6293bfe668344baad32ed) 2.6.0 release
 - 2020-01-17 21:59:50 -0800 [5f06e8a](../../commit/5f06e8a111258f2a8b08f267ed5193e61a5fbe2e) config file allows a per-method values
 - 2020-01-17 21:50:54 -0800 [1a684ae](../../commit/1a684ae5e66cb2463288ff482ea99a3e2981c56a) rewrite of config file to bring up to spec and to allow for per-method auth values
 - 2020-01-17 21:49:45 -0800 [8981db6](../../commit/8981db6cbb6eb80a045bcde13c28d2177c839307) started to handle pipe errors - not quite working yet
 - 2020-01-16 19:53:50 -0800 [4905fcf](../../commit/4905fcf5d175ac4403d893b4e32e1243148f4de9) CHANGELOG.md pushed to github
 - 2020-01-16 19:53:11 -0800 [99e8eeb](../../commit/99e8eeb992012fce2debec4a5d5397615143aa25) pylint made me do this! - yet there is still more to do
 - 2020-01-16 10:12:44 -0800 [25cd7b9](../../commit/25cd7b902ffb737dc3688e183683f0273c590de3) Merge pull request #80 from acdha/patch-1
 - 2020-01-15 21:43:24 -0800 [a3b4538](../../commit/a3b4538de8ac18c3c3011804f6b51cc7aef631cb) Merge pull request #80 from acdha/patch-1
 - 2020-01-15 21:41:21 -0800 [f3ad952](../../commit/f3ad952fd2f06c90ada22d7e86bd17048161359e) Merge pull request #59 from mnordhoff/patch-1
 - 2020-01-15 15:42:25 -0800 [77abe67](../../commit/77abe673a3001c1c07b7b2a6f305e1e52cafd518) CHANGELOG.md pushed to github
 - 2020-01-15 15:41:34 -0800 [4acc9a3](../../commit/4acc9a33d9f9d9a676c08ec713b3a56b723e6800) 2.5.1 release
 - 2020-01-15 15:17:51 -0800 [1efa5e9](../../commit/1efa5e92d9e243750d1fe36cb5414558d1ee4207) made sure error chain would correctly be passed with an exception
 - 2020-01-15 15:16:55 -0800 [734a814](../../commit/734a814553a1769c4c12474e1cf8005c663a47ff) changed converters to have more meaningful error handlers
 - 2020-01-15 15:15:30 -0800 [5cc639a](../../commit/5cc639a9dc8282dbd497ef35793f744fa3cd0aea) pylint cleanup
 - 2020-01-15 11:53:20 -0800 [dbd874f](../../commit/dbd874f3ef2b0df53792cd5370b750a2fb0d5e9f) CHANGELOG.md pushed to github
 - 2020-01-15 11:52:53 -0800 [9cc8427](../../commit/9cc8427c3076760d90af536062508beb8822c9d8) 2.5.0 release
 - 2020-01-15 11:51:49 -0800 [d3ffa85](../../commit/d3ffa853bc2d00fad854b2f334a7c0eb6854d0ca) Added support for profiles - see README
 - 2020-01-14 14:39:03 -0800 [12836e3](../../commit/12836e3d442886cc4c2423757eab874eb2114e9c) CHANGELOG.md pushed to github
 - 2020-01-14 14:38:09 -0800 [7e9c358](../../commit/7e9c358e116674252965a9ca3a58eedef94d3491) 2.4.5 release
 - 2020-01-14 14:37:45 -0800 [ddbd10d](../../commit/ddbd10dac23deefaf42056eb0420ad00e7db536c) added /accounts/ and other api calls
 - 2020-01-14 13:42:51 -0800 [d79c539](../../commit/d79c53986dfe85ceaf833f6b004d4662562ae89b) CHANGELOG.ms pushed to github
 - 2020-01-14 13:42:22 -0800 [5ec421b](../../commit/5ec421b2327013115c888192ce2a99066694b8de) 2.4.4 release
 - 2020-01-14 13:41:58 -0800 [378f3a7](../../commit/378f3a7ce63a937baf5f09c5d152aa55f2261f5e) added logpush
 - 2020-01-14 10:37:04 -0800 [e80cdf8](../../commit/e80cdf880d0ce5880b43bdc44b89528e2f53d7fc) CHANGELOG.ms pushed to github
 - 2020-01-13 22:21:25 -0800 [9be691c](../../commit/9be691c7a9cf2bd1181bf85dd220c6f3d2ca914a) 2.4.3 release
 - 2020-01-13 22:20:50 -0800 [defd8c1](../../commit/defd8c1f4f3ec550b2182ef06edba18d8e067490) added more API calls
 - 2020-01-13 21:34:33 -0800 [eb6b260](../../commit/eb6b26043f19e32ba76dfe53883905067b1f5442) CHANGELOG.ms pushed to github
 - 2020-01-13 21:34:00 -0800 [67fdfcb](../../commit/67fdfcb5e3480a1c99624c71e3a798d5698685cf) 2.4.2
 - 2020-01-13 21:32:35 -0800 [b7311a3](../../commit/b7311a368f4822134c6a7576bb9e9e9433b157a7) --quite options yields TypeError - fixed!
 - 2020-01-13 21:31:33 -0800 [9b31b51](../../commit/9b31b514a566c1b102d5960bf9be3d2a387426e2) --quite options yields TypeError - fixed!
 - 2020-01-13 20:22:30 -0800 [1e356dc](../../commit/1e356dc30a2a4b7218b41e33f458711bae961791) typo
 - 2020-01-13 20:19:46 -0800 [6347e64](../../commit/6347e64ea023143698da3793d9bc1f56092bf3bc) CHANGELOG.ms pushed to github
 - 2020-01-13 20:17:20 -0800 [b5cd481](../../commit/b5cd4811925aa1aa14241b3cc34e35c19bad2c0e) Python 3.8 needed SyntaxWarning fixes - as annoying as that was!
 - 2020-01-08 14:52:33 -0800 [d76dd70](../../commit/d76dd70ba7c8e07f8ef2e465e35e40fa74bd6949) CHANGELOG.md pushed to github
 - 2020-01-08 14:51:45 -0800 [0eb6d73](../../commit/0eb6d735393a8b371bae5916fb14faf9262b41cb) 2.4.0 release
 - 2020-01-08 14:50:06 -0800 [41c5348](../../commit/41c5348cc7b1a462f337ea553a464a3c827afd4d) removed python2 from setup.py so that pypi is only Python3
 - 2020-01-08 14:40:56 -0800 [ca5cb96](../../commit/ca5cb961d772bc6b7b962bf33e25929960a18566) auth methods cleaned up to allow empty/null strings in values plus confirmed all authentication code is consistent. Updated README.
 - 2019-11-20 10:37:17 -0800 [f71c3e2](../../commit/f71c3e221735ad50230918f3c9bee6d34471ae58) CHANGELOG.md pushed to github
 - 2019-11-20 10:36:15 -0800 [b80a4dc](../../commit/b80a4dc6d516d1737f16be3c7c4f13a83be905bb) 2.3.1 release
 - 2019-11-12 09:39:39 -0500 [2a5f24c](../../commit/2a5f24c5515d76bc7bedcd3e90a6e4c578303046) Explain zone lookup behaviour in cli4
 - 2019-10-08 14:32:36 +0100 [ce30a10](../../commit/ce30a10d9dabe8dff3ff3941aff9568ffaaa0704) Merge pull request #78 from Tugzrida/patch-1
 - 2019-10-08 14:10:33 +0100 [6e63048](../../commit/6e63048956493726414cc478d9234769d037c67e) Merge pull request #76 from nijel/patch-1
 - 2019-09-20 11:03:08 +0200 [09d0605](../../commit/09d06054111cbc8f6454a3ae5568e240432dd22a) Clarify token use with env variables and config files (#2)
 - 2019-09-16 14:05:30 +0200 [c4f9107](../../commit/c4f9107d47867926f60b4188aacabc5e0682593d) Enhance API Token documentation
 - 2019-09-13 16:52:56 +1000 [de33ceb](../../commit/de33ceb5d4b87f60e01eff58bf53679eb93ee748) Spelling fix
 - 2019-08-23 20:14:17 +0200 [9f28c65](../../commit/9f28c659d784fe06c705ea7977acee2bf3a2cc2e) Add support for API Tokens
 - 2019-05-20 08:24:29 -0700 [cf084ee](../../commit/cf084ee22b454cbb50804c748ee969cab9c451a2) 2.2.0 release
 - 2019-05-20 08:23:52 -0700 [b024609](../../commit/b0246091fc0d367e7e36bb422ebe7e55a93bf6c0) 2.2.0 release
 - 2019-05-14 09:25:39 -0700 [630612a](../../commit/630612a52adad05fc9fc1ec05c52a9c08eacaa80) Add Python 3.7
 - 2019-04-17 23:23:22 +0200 [a47bfcb](../../commit/a47bfcbe78f1d06412354540199832fbf19cca2d) Keep proxied state during IP-address update
 - 2019-04-12 13:53:41 +0100 [286cb0f](../../commit/286cb0f25e5bf13adacdf9ce7956bd514e508214) Merge pull request #64 from aaranmcguire/support-secondary-dns
 - 2019-04-12 13:52:50 +0100 [11cc361](../../commit/11cc361b17e39ebf4ff3c4ab21a9d9e1bacaa578) Merge pull request #57 from sulf1ron/master
 - 2019-04-12 13:21:28 +0100 [2255073](../../commit/22550738c9680779dcdc5152eda8c355e0924f8c) Merge pull request #53 from weisi/master
 - 2019-04-12 13:20:13 +0100 [f48ab53](../../commit/f48ab53cb7c1e5b7d69ce1cc86b84d5cafaed4a7) Merge pull request #58 from dargor/fix_typos
 - 2019-04-12 10:06:58 +0100 [019393a](../../commit/019393a88f2e36730bb1efed45580cc9b756a8d3) DNS-3431: Support Secondary DNS Endpoints
 - 2018-10-13 18:31:10 +0000 [6726add](../../commit/6726add5d35ee8bb5644e78506c2fe97743f8b6b) Remove logger from requirements.txt.
 - 2018-10-09 17:02:00 +0200 [63a4159](../../commit/63a4159eb331b11e2a8c29641d101a08da9f4290) Fix some typos.
 - 2018-10-02 22:48:39 +0800 [a8e47cd](../../commit/a8e47cd4b6759f255dc49377ab221d521ecc2904) Fix a minor bug in examples\example_update_dynamic_dns.py
 - 2018-08-05 18:33:00 -0400 [f4eb124](../../commit/f4eb124be4c9817e826deb81bd7a0040c347ef30) README: Fix typo of kwarg "params" in sample code.
 - 2018-03-05 01:56:27 -0800 [fd6464e](../../commit/fd6464e15b91263f1ce395e4336b1c1fac542880) requests needs to be newer than 2.4.2 it order to use json keyword
 - 2018-02-25 03:42:55 -0800 [c07a6c5](../../commit/c07a6c5418ff1be83f8a828d211d5e56785254f4) CHANGELOG.md pushed to github
 - 2018-02-25 03:42:04 -0800 [33feada](../../commit/33feada897f25b00f8728474e4050e68935c96b5) 2.1.0 release
 - 2018-02-25 03:41:16 -0800 [b6d79dc](../../commit/b6d79dcb495a070946c7765a63d97623d1c64b19) minor lint thingy
 - 2018-02-25 03:39:57 -0800 [ed9bb8d](../../commit/ed9bb8dc467745fc2d1b4d41760f37d7048ba2cb) added support for NDJSON, cheaned up command preload
 - 2018-02-25 02:09:58 -0800 [c2224e4](../../commit/c2224e4fa466ba8372e2f5898fc299b8923c6c73) NDJSON output supported - used by Enterprise Logs
 - 2018-02-25 01:05:45 -0800 [ead7bbd](../../commit/ead7bbdca170f1e126e9d09703fe43eb7a80b10f) added jsonlines package in order to handle NDJSON formatted responses
 - 2018-02-23 21:50:51 -0800 [c73a674](../../commit/c73a674ac1835e4c9b3738797383fbc9d846f635) finally clean up the one large cli4 routine. it needed to be split a long time ago
 - 2018-02-23 21:37:55 -0800 [1a6593c](../../commit/1a6593c728ee6b99d2a7bf37980945c932ceffd3) forgot files on some post calls
 - 2018-02-22 23:20:56 -0800 [2d3ba3a](../../commit/2d3ba3a559bab6ee7ee3ef6cb31fa2a2366b0672) tweak of workers file upload syntax
 - 2018-02-22 23:19:16 -0800 [a9995a7](../../commit/a9995a77679ce6c11852b77ba926e76a60639342) added support for cleaner command line filename passing - used for workers javascript uploads
 - 2018-02-22 23:11:12 -0800 [71f7ddd](../../commit/71f7dddd44463c50ac74a49576f3668865d54c0c) major cleanup using add() method which is cleaner and makes pylint happy
 - 2018-02-22 23:10:20 -0800 [d4f1ebd](../../commit/d4f1ebd78c069fda4823ff5fbf9e537884ea81e4) fixed logger lint issue, added add() for cleaner command loading, added html response support for madia
 - 2018-02-14 22:22:16 -0800 [2988312](../../commit/2988312d61f14b2752747367188508b089366a03) CHANGELOG.md pushed to github
 - 2018-02-14 22:21:55 -0800 [60e7c61](../../commit/60e7c61647b0b7726e7d3919d4a3059e3f8a15d9) 2.0.4 release
 - 2018-02-14 22:21:35 -0800 [43bf227](../../commit/43bf2277ac99030381984704542eead0e65e66f8) Cloudflare Workers examples added
 - 2018-02-14 22:20:59 -0800 [3129f8e](../../commit/3129f8e61071aef85a14251f4090fbb01c87f03e) workers typo
 - 2018-02-14 20:26:35 -0800 [d9ea7e8](../../commit/d9ea7e8bbd6fa1c17ef3535e339d79643b817df5) CHANGELOG.md pushed to github
 - 2018-02-14 20:25:04 -0800 [25ddfb3](../../commit/25ddfb3338115d65e7a7ae3640b91963aa0b0167) 2.0.3 release
 - 2018-02-14 20:23:28 -0800 [0e3fc8a](../../commit/0e3fc8a4b81daed210f5809050b53f93838513ae) python2/python3 edits
 - 2018-02-14 19:21:36 -0800 [9b60c39](../../commit/9b60c39ac2d6e5d99d92d0fd018cf55b1fba134d) CHANGELOG.md pushed to github
 - 2018-02-14 19:20:24 -0800 [1b7012e](../../commit/1b7012e81e13d124fc5f02fda2a90e8344db082b) 2.0.1 release
 - 2018-02-14 17:29:44 -0800 [db943d3](../../commit/db943d38ee5147bcd54cc7ac48ac7dee90a5eda3) python2/python3 updates - print functions part
 - 2018-02-14 17:16:25 -0800 [04ef03f](../../commit/04ef03f0508515b82e3104e186e8d2eff7c93240) no changes, but documented how to lower level http debugging
 - 2018-02-14 17:09:02 -0800 [d2d2f63](../../commit/d2d2f6323a41964f2280ce4d94eb4d7bf30a6eca) added javascript content-type for Cloudflare workers, added support for python2/python3 unicode differences, added params for DELETE method
 - 2018-02-14 17:04:31 -0800 [c7f6759](../../commit/c7f675966ab7bb73cf7ef593bb42036c97ec15ab) added Cloudflare workers
 - 2018-02-14 14:07:29 -0800 [1477dda](../../commit/1477dda07a92bdf0d22802808c1c7fb3ce897aa2) python2/python3 json output fixed for unicode, added cloudflare workers script/file uploading, added script name support
 - 2018-02-07 03:34:47 -0800 [7d5cf4d](../../commit/7d5cf4df461526665eb340326219c6052e6ca542) CHANGELOG.md pushed to github
 - 2018-02-07 03:34:27 -0800 [fdb6a7f](../../commit/fdb6a7f645ee47f3a1f2a93e6ce8b154031ff5e3) 2.0.1 release
 - 2018-02-07 03:33:28 -0800 [25e3193](../../commit/25e31935af224f6adfe6f7d14c99fe26ff2cd47b) doing that python3 thingy
 - 2018-02-07 03:33:06 -0800 [73d6386](../../commit/73d63864f4c0b91142bd4ad51009b5196425efed) doing that python3 thingy
 - 2018-02-07 03:32:00 -0800 [7c0411c](../../commit/7c0411c86a5ccb8df1a2262a0864cc56c2e1ad1c) yet another fix for unicode/utf8 returned JSON data
 - 2018-02-07 03:28:07 -0800 [99005c1](../../commit/99005c187795d7a46a52eb138404d7632829b031) settings/brotli & settings/privacy_pass added
 - 2018-02-04 22:07:44 -0800 [cfa6b2a](../../commit/cfa6b2a89420870db632ab250d498bd376fa6769) tabs vs spaces - arggg!
 - 2018-02-04 21:58:44 -0800 [930ce84](../../commit/930ce84c81cda5dcebe4be582b6a20a0bfb7b8b0) Merge pull request #46 from cloudflare/example_always_use_https
 - 2018-02-04 21:58:25 -0800 [ecb32aa](../../commit/ecb32aa51483848d868ba1f45dfa733f626bcefe) added example of always_use_https which can be the basis of any settings value change
 - 2018-02-04 21:54:06 -0800 [a39ef72](../../commit/a39ef72534bc7453e7583f3f02ab1afc96181ce7) added example of always_use_https which can be the basis of any settings value change
 - 2018-01-21 19:35:49 -0800 [cadc0eb](../../commit/cadc0ebc21b631ae46e2a7262a4e52988ccc231c) CHANGELOG.md pushed to github
 - 2018-01-21 19:35:15 -0800 [631f2b2](../../commit/631f2b2c60aff223f0ed3dd33d80084277c07f16) 2.0.0 release
 - 2018-01-21 19:32:55 -0800 [1b4a7a8](../../commit/1b4a7a8bf57fabcb1e8086e492dded0477c5f525) moved to use requests.Session(), split network out, added upwrapped code for enterprise log share support
 - 2018-01-21 19:29:22 -0800 [91c0cc2](../../commit/91c0cc258c4a8b0072c4f7d52d0be32b1c076b3e) added to api: load_balancing_analytics, audit_logs, logs/received, ssl/universal/settings
 - 2017-10-31 04:57:47 -0700 [3831bb0](../../commit/3831bb066f2c7be8c37b6348421d743a1d19ba8e) typo
 - 2017-10-31 04:46:11 -0700 [31da853](../../commit/31da8531efd1956171a88d958363d945e7fffd8b) CHANGELOG.md pushed to github
 - 2017-10-31 04:45:40 -0700 [0c71205](../../commit/0c71205320597ef766f10ea4111dff482ba17d62) 1.8.1 release
 - 2017-10-31 04:44:38 -0700 [378dede](../../commit/378dede277ad755b16fbfa5bd31cdc50f2a6ed9c) cleaned up string response for /zones/:id/dns_records/export API call
 - 2017-10-09 08:21:04 -0700 [a25a4a9](../../commit/a25a4a99ffe3c86d143cf0c05b70fd303e94ac3f) unwonnd json write
 - 2017-10-09 08:17:32 -0700 [0d61289](../../commit/0d6128920088961c62ff0b6c276cdc34c79053da) Merge branch 'master' of github.com:cloudflare/python-cloudflare
 - 2017-10-09 08:15:59 -0700 [97cd013](../../commit/97cd013a3bab0c718f5e487565a524c9d8ae2e07) added with statement example
 - 2017-10-09 08:13:14 -0700 [5c4a872](../../commit/5c4a87299ea6be802d9d7ad128b7f31daba13788) final json write mofified to help unicode mindset
 - 2017-10-09 08:11:58 -0700 [0820e21](../../commit/0820e21dcab2d81d3a6cf2f5eb897ecea0f1473e) added code to handle python with construct, cleaned-up parts variable, allowed simple get() call without get method
 - 2017-10-09 06:29:21 -0700 [409da8b](../../commit/409da8b15cf3d20a93a1fd8b766d9561a7314f89) Merge pull request #39 from drbachler/README
 - 2017-10-09 15:10:16 +0200 [c69914a](../../commit/c69914a9d57ed3a2200e69ff885351bbc905cc37) Fixed typo in README.md
 - 2017-09-27 15:12:51 -0700 [84ece92](../../commit/84ece92e5bb4309ae8d31a1e8ef8c8238669238b) CHANGELOG.md pushed to github
 - 2017-09-27 15:12:36 -0700 [522595b](../../commit/522595bc8cadf85b74b354a54951d0342311b45f) 1.8.0 release
 - 2017-09-27 15:02:40 -0700 [3358773](../../commit/3358773568ba1454bd0b423960ec2e290e7a270f) Cloudflare CA CLI examples added
 - 2017-09-27 15:01:03 -0700 [3731c06](../../commit/3731c0600fd3b09131cde9d4d565a1d537fd0187) needed to handle files= paramater for all POST calls
 - 2017-09-12 00:16:44 -0700 [593b06b](../../commit/593b06b4be3f4463b0bb1a93696189216b074d22) CHANGELOG.md pushed to github
 - 2017-09-12 00:16:18 -0700 [49f428e](../../commit/49f428e52ed0f1ea8845b7fa241d8044bea7d776) 1.7.5 release
 - 2017-09-11 08:30:00 -0700 [dfe0afb](../../commit/dfe0afbec237c3f217f63ea8a92990eecb43a437) added more API commands. Made some efficency edits to the python code
 - 2017-08-30 10:37:45 -0700 [06b4c6b](../../commit/06b4c6b9c1adf0ce7520762ecf46b013c4d840da) added example python code for dns_records/export
 - 2017-08-27 18:44:04 -0700 [b596a90](../../commit/b596a903600e2d20dee4435e90b18933615ecbe2) typo
 - 2017-08-27 11:53:56 -0700 [61a13eb](../../commit/61a13eb6888d1ef4eab57f9d4025e35606865e91) CHANGELOG.md pushed to github
 - 2017-08-27 11:53:43 -0700 [671758e](../../commit/671758e07830be2f9f1f70af393b92ba5e4ed46e) 1.7.4 release
 - 2017-08-27 11:52:19 -0700 [510a4b6](../../commit/510a4b65fee4d22513474d28698e8f4359c6c335) added /zones/amp & /zones/dns_analytics API calls
 - 2017-08-27 11:03:58 -0700 [a56ab78](../../commit/a56ab782de87d2fe9c94218b239c2bda0ac477be) CHANGELOG.md pushed to github
 - 2017-08-27 11:03:46 -0700 [6d5952c](../../commit/6d5952cbda642328c06b3c71b87214aa9ecc5304) 1.7.3 release
 - 2017-08-27 11:03:18 -0700 [434efb8](../../commit/434efb8f7d716e3c968b62db09fa8a7147787ac9) added dns_records/export documentation
 - 2017-08-27 10:48:34 -0700 [9a35d4f](../../commit/9a35d4f5180d40d6754e9dcf203feb188d0fc24b) CHANGELOG.md pushed to github
 - 2017-08-27 10:48:19 -0700 [c1bb920](../../commit/c1bb920a10b4ea017ef0c9b40896d30e49f5ed19) 1.7.2 release
 - 2017-08-27 10:47:41 -0700 [680d5b9](../../commit/680d5b9f6a805625be1ce8b50c8ab9ebb4e25d22) added dns_records/export API endpoint hence added code to handle non-JSON responses. added some initial http error code processing
 - 2017-08-27 09:28:57 -0700 [77a69b7](../../commit/77a69b72c76edd0cc76dde717ca85b1924ae17a9) CHANGELOG.md pushed to github
 - 2017-08-27 09:28:31 -0700 [d443410](../../commit/d4434106da68ef1828caaf49e8a25c442bb2dba2) 1.7.1 release
 - 2017-08-26 23:54:06 -0700 [a86700a](../../commit/a86700af42da5efe49ae9d6178aeec315a373150) typo
 - 2017-08-26 23:49:40 -0700 [684b001](../../commit/684b001baa6fb772bc55ca59be3d24f0766eabc5) CHANGELOG.md pushed to github
 - 2017-08-26 23:49:08 -0700 [de7bbee](../../commit/de7bbee0a048f300eec39cdf00f129b17e5df7d3) 1.7.0 release
 - 2017-08-26 23:47:04 -0700 [ee52cb3](../../commit/ee52cb33f95502e4b2df861cc5201f3a537137ba) tags added and some general cleanup
 - 2017-08-26 23:46:13 -0700 [d400c56](../../commit/d400c565cb1a29440a45319e7feca2a93b081c90) support for dns_records/import and file upload via library and cli4 command
 - 2017-08-23 02:59:08 -0700 [412ced6](../../commit/412ced60ccefb3c38302d7e431c78512a27f4290) CHANGELOG.md pushed to github
 - 2017-08-23 02:58:51 -0700 [b1481fa](../../commit/b1481fa3cbf08d7a5b054c4488cda1e8357611c1) 1.6.2 release
 - 2017-08-23 02:57:48 -0700 [16129a3](../../commit/16129a394cdc43e07e67cea9b6c14358311f66f4) removed requirement for logger package as its not used. changed Logger class  to CFlogger to remove confusion - issues/30
 - 2017-08-23 02:25:51 -0700 [90e3ea5](../../commit/90e3ea57e473b2cf332b7cc5ed22d5bf66ffe36a) added a delete dns record example - issues/33
 - 2017-08-22 05:10:15 -0700 [3fc396c](../../commit/3fc396cb685bf57ce094299a7505216d78624ac2) missing chmod +x on examples/example_paging_thru_zones.py
 - 2017-08-22 05:09:21 -0700 [1e13e8d](../../commit/1e13e8d5cbbe0c37f0a9d563869d51d91858cd50) CHANGELOG.md pushed to github
 - 2017-08-22 04:51:51 -0700 [3a03516](../../commit/3a035163d3613756e8bb3e4bc26fb3642091861c) CHANGELOG.md pushed to github
 - 2017-08-22 04:51:16 -0700 [cae98bb](../../commit/cae98bba8564d95a64ac6aa293805b1e587db2c6) 1.6.0 release
 - 2017-08-22 04:39:52 -0700 [4745e20](../../commit/4745e20cc337d7d22a4c87ccc58a21961415a603) Merge pull request #35 from Bellardia/implement-argo
 - 2017-08-22 04:37:06 -0700 [b07a3c9](../../commit/b07a3c99650b8a566116ff7ab68ba6860f4361fb) Merge pull request #34 from Bellardia/Bellardia-patch-1
 - 2017-08-22 03:58:09 -0700 [fb12f30](../../commit/fb12f3000824051d34008a2c63e9eabce0822edc) fixed paging examples, as per pull request #28 & #29
 - 2017-08-22 03:48:11 -0700 [152c9a1](../../commit/152c9a1fcdd447eca9aa6a5ef358aad1ac5307e1) Merge pull request #29 from crlorentzen/patch-1
 - 2017-08-22 03:47:04 -0700 [9926050](../../commit/9926050443a81c821d93454c1745f420e1c2247c) Merge pull request #28 from crlorentzen/patch-2
 - 2017-08-16 00:04:24 -0400 [d8d15c7](../../commit/d8d15c7163b19cec0859f4247de9c212d1bfecef) Add support for Argo
 - 2017-08-15 11:03:51 -0400 [182a770](../../commit/182a7705d66ee8e9275066a703c734238eba59d3) Add support for organizational monitors and pools
 - 2017-01-25 13:09:31 -0500 [6c86fef](../../commit/6c86fef75e01a2b8c1a92925cec3ac0dbac19bd6) Fixed pagination of raw example, same as c842b04
 - 2017-01-25 13:05:46 -0500 [c842b04](../../commit/c842b04473627a22788d9e8bfac8729f72f504c9) Fix Pagination in example raw code
 - 2016-12-30 10:34:40 -0800 [5358208](../../commit/5358208360c891cdabb954ba85c8ea269891a350) CHANGELOG.md pushed to github
 - 2016-12-30 10:34:09 -0800 [f0a4fea](../../commit/f0a4fea03901b6547b861613769c758c439580dd) added reference to blog for historic reasons
 - 2016-12-30 10:27:17 -0800 [c1c5096](../../commit/c1c5096e4c8e20609638dfde9b662be9c70c9c7b) 1.5.1 release
 - 2016-12-30 10:27:06 -0800 [abf116c](../../commit/abf116c5f58a5e32ce7878bd7fc6ba6ccd2d4bba) added opportunistic_encryption, opportunistic_encryption, subscriptions API calls
 - 2016-12-30 08:32:30 -0800 [37ddc7e](../../commit/37ddc7eea035b5629d5ec7e09ea6020c4c5520ff) removed import CloudFlare.exceptions from examples - as per @yesbox edits for python3
 - 2016-12-30 08:18:51 -0800 [08aafa4](../../commit/08aafa4dc46fc3768ab046092bad6771691aa621) CHANGELOG.md pushed to github
 - 2016-12-30 08:18:19 -0800 [dba1257](../../commit/dba1257bd75196d4811ab3051c4c3a1519f42271) 1.5.0 release
 - 2016-12-30 08:13:26 -0800 [f82c055](../../commit/f82c055147250b15cf6378ffa17ba9c803e7ad92) Merge branch 'yesbox-refactor_imports'
 - 2016-12-30 16:18:13 +0100 [c013305](../../commit/c0133052de575c2e277d51643b8c8d39bbf96fe3) Refactor imports
 - 2016-12-29 20:25:27 -0800 [b4465b9](../../commit/b4465b9930441ebc9cc4ac454f38a28047e2c19e) 1.4.11 release
 - 2016-12-29 20:24:05 -0800 [9c54a10](../../commit/9c54a10f9477232ea733ff99be07fd26ad7f21a0) moved converts into seperate file. corrected zone name converter to be simpler
 - 2016-12-29 12:30:23 -0800 [9381a70](../../commit/9381a70f9c562eb606c808427006f86d23392dd0) CHANGELOG.md pushed to github
 - 2016-12-29 12:29:51 -0800 [4413e7b](../../commit/4413e7b6b3187fbb538febf83da5174203bbf0d4) 1.4.10 release
 - 2016-12-29 12:28:50 -0800 [0d17f04](../../commit/0d17f0438eb30484edcb5b229105a75ed6d1a21b) 1.4.9 release
 - 2016-12-29 12:24:11 -0800 [12bb621](../../commit/12bb621300f68354cabc2d7eceaa75cd9d35cad4) Merge branch 'Sarga-master'
 - 2016-12-29 13:00:10 +0200 [1d0ac45](../../commit/1d0ac45fbd08a8d7b5d57dd977ff81b11f19b59b) added http2,pseudo_ipv4 settings
 - 2016-12-29 11:56:33 +0200 [5a2094f](../../commit/5a2094f39aab1f9060a7253a1876b9dc68f1b5b4) added http2,pseudo_ipv4 settings
 - 2016-12-29 11:51:16 +0200 [cf90637](../../commit/cf90637ba83a3225fb63a6a2280911ab9c0b4c68) added http2,pseudo_ipv4 settings
 - 2016-12-28 20:55:45 -0800 [180ea9c](../../commit/180ea9c27042db38317695d9c62989c54f8b55e2) CHANGELOG.md pushed to github
 - 2016-12-28 20:55:07 -0800 [ed8a55d](../../commit/ed8a55db66352307a917433c93885491aabd5582) 1.4.8 release
 - 2016-12-28 20:54:37 -0800 [61b316c](../../commit/61b316c7ef7dc095c3bb0b0da565facff70dd9c6) added rules support for /zones/:id/firewall/waf/packages/:id/rules/:id
 - 2016-12-28 20:40:02 -0800 [520abbb](../../commit/520abbbf9127a9537aa976eda6f177003932a1c3) CHANGELOG.md pushed to github
 - 2016-12-28 20:38:48 -0800 [a9ca66f](../../commit/a9ca66ff17d56009a81ecdc82060e846fbbccdf1) 1.4.7 release
 - 2016-12-28 20:38:06 -0800 [8122b6d](../../commit/8122b6d911c1fee698241e82996201a1365587e1) Merge branch 'rita3ko-update/load-balancing'
 - 2016-12-28 20:35:47 -0800 [bd1886d](../../commit/bd1886dfb414f64ce5d901dd6a5f8363dcb8fa21) Merge branch 'update/load-balancing' of https://github.com/rita3ko/python-cloudflare into rita3ko-update/load-balancing
 - 2016-12-28 20:32:32 -0800 [ad841f5](../../commit/ad841f527bd5f3852e3a434a0f74557f59fe3c84) CHANGELOG.md pushed to github
 - 2016-12-28 20:31:37 -0800 [e15a4ba](../../commit/e15a4ba6abc7b576d7401d8bdcd3215faccb4402) 1.4.6 release
 - 2016-12-28 20:30:45 -0800 [d5529e4](../../commit/d5529e4f0f54908c2c15fb16b16c3e6e6c0e3f78) added API support for third param - used by /zones/:id/firewall/waf/packages/:id/groups/:id & /zones/:id/firewall/waf/packages/:id/rules/:id
 - 2016-12-28 11:19:14 -0800 [b1407ac](../../commit/b1407ac432879f0c740f128e808fa6be281c5c1f) Update Load Balancing endpoints to reflect latest API changes
 - 2016-12-27 10:27:23 -0800 [9c59aa3](../../commit/9c59aa3ea0847711b75e39b6d9d722c468c0ea05) CHANGELOG.md pushed to github
 - 2016-12-27 10:26:52 -0800 [c27d098](../../commit/c27d098b90f3de1a40d1e56aab7ddd5c72dbe2f7) 1.4.5 release
 - 2016-12-27 10:26:03 -0800 [9d722f1](../../commit/9d722f163a71d849868c77091efa695331c75b30) added User-Agent support to help debug calls - should have been done on day zero - oh well
 - 2016-12-24 10:17:01 -0800 [cc467af](../../commit/cc467af835957cb316d563c39bb06a94985bf02c) added support for params being Null/None, floats, negative numbers plus added more error checking
 - 2016-12-22 16:22:26 -0800 [35f8ca1](../../commit/35f8ca1b46d0164048a7f843e14a464f655c0184) missing newline on json output
 - 2016-12-22 14:29:56 -0800 [f1f99bd](../../commit/f1f99bd26f621a5b0984d5cbb4c875108db672ff) CHANGELOG.md pushed to github
 - 2016-12-22 14:29:29 -0800 [07fd70f](../../commit/07fd70fdb2d8d44814c7877ab4bff0073bf8631d) 1.4.4 release
 - 2016-12-22 13:55:38 -0800 [0f670b5](../../commit/0f670b5ae88194a9ea314b8cc1f5b071dce56143) Added /zones/rate_limits API command
 - 2016-12-22 13:46:42 -0800 [e144c66](../../commit/e144c663b5a4af1baf1791a422e00bcf3a239e66) CHANGELOG.md pushed to github
 - 2016-12-22 13:46:09 -0800 [8231b76](../../commit/8231b76dd4b87996c73e539b36e36eedcf94637c) 1.4.3 release
 - 2016-12-22 13:45:10 -0800 [1420966](../../commit/142096680954e7aa03c2620a0f545383c04686e1) pylint work
 - 2016-12-22 13:44:41 -0800 [b8fe4da](../../commit/b8fe4da34ac997edae6be87f6cd58069e2ff0f59) pylint work because of a typo
 - 2016-12-22 13:44:06 -0800 [a52baa4](../../commit/a52baa43a276630a7325503a6e7fc5a5788135d0) rewrite and restructure to handle modules of commands - was prompted by pylint output
 - 2016-12-22 13:33:12 -0800 [5d7ddb0](../../commit/5d7ddb0db74e01c0add91a89542edf927414b422) was missing a newline at the end of the --dump command output
 - 2016-12-22 13:31:53 -0800 [0193fcf](../../commit/0193fcf21864f43502f6bbd96137d6bbee26c0ff) more pylint work
 - 2016-12-12 16:55:41 -0800 [6cdd04b](../../commit/6cdd04b0ced721d9f7116cd40b99cf8c27afa952) plenty of pylint edits - not that it changes anyway useful
 - 2016-12-11 11:23:29 -0800 [824e1eb](../../commit/824e1ebc9968571c60f1fe48c80e3908c6da4698) CHANGELOG.md pushed to github
 - 2016-12-11 11:23:05 -0800 [37fab98](../../commit/37fab98e08c814d7db066af60266c4d613960b97) 1.4.2 release
 - 2016-12-11 11:22:02 -0800 [52dda2e](../../commit/52dda2ededa1aa8fd21e7bc2dff2c1f2d2c58a9a) sanatize the returned results - just in case API is messed up
 - 2016-12-10 15:39:29 -0800 [080733b](../../commit/080733b58e144670116d7f3ccadf369d599bfc61) CHANGELOG.md pushed to github
 - 2016-12-10 15:39:01 -0800 [f3d6377](../../commit/f3d637727d74acecd8faf4c9b6d652ee2cff183f) 1.4.1 release
 - 2016-12-10 15:38:10 -0800 [8c66d32](../../commit/8c66d3253b6f257b61b0116600fb2b3d77e8f0fb) cleanup of yaml print output if yaml package isnt installed
 - 2016-12-09 16:22:51 -0800 [894ae11](../../commit/894ae11788a2b1997e4f58895d205d1f74ab16f7) Moved walk into CloudFlare class - much cleaner
 - 2016-12-06 08:24:29 -0800 [752ccb4](../../commit/752ccb45b40d2db245b6cd974b39e9c0f4880bf7) CHANGELOG.md pushed to github
 - 2016-12-06 07:52:56 -0800 [b717f6c](../../commit/b717f6c3f5087bade8cb729ca798af1575eb0f8a) Merge branch 'corywright-fix-usage-docstring'
 - 2016-12-06 07:52:35 -0800 [e87ab0a](../../commit/e87ab0ae9c88a44dc1e1274baddb8eb701fb0330) Merge branch 'fix-usage-docstring' of https://github.com/corywright/python-cloudflare into corywright-fix-usage-docstring
 - 2016-12-05 11:03:04 -0800 [99a85fb](../../commit/99a85fb2c51f626f1f10ce3e0db164277d4137a9) CHANGELOG.md pushed to github
 - 2016-12-05 11:02:16 -0800 [2d4a5ee](../../commit/2d4a5ee85ade3702013e92f3ee72b2491b9bddb6) 1.4.0 release
 - 2016-12-05 10:58:59 -0800 [734204e](../../commit/734204e901dfcd486b24ed60dec2fcea75855412) cli4 can now do maps and regions in Cloudflare Load Balancer
 - 2016-12-05 10:57:57 -0800 [957f9b0](../../commit/957f9b0af2d434e41249533c2180164215fb73d7) Removed CloudFlare Load Balancer region example - moved to cli4
 - 2016-12-05 10:56:57 -0800 [dcb7456](../../commit/dcb74565cc0f1bc8aee6bd747043192accd6ff51) Cloudflare Load Balancer example updated
 - 2016-12-04 16:17:25 -0800 [cc46b4d](../../commit/cc46b4d28aac69cf0811c8ac5973a15a0abad1dc) added unnamed data and param passing - used by CLB API
 - 2016-12-04 13:43:50 -0800 [e57f421](../../commit/e57f421854a50e0c1954a80191f7efb70036ab81) changed most id values to all zeros
 - 2016-12-04 13:34:31 -0800 [420d8da](../../commit/420d8dae3938d13f020d19f36683bfec31d9ea6b) added param examples
 - 2016-11-28 12:53:44 -0800 [0e159f5](../../commit/0e159f5b938a319cf362061497f10565df3c2a34) typo
 - 2016-11-28 12:51:42 -0800 [43afa2f](../../commit/43afa2f76f716cfa2b25ac0b06e99e5720e5fb41) CHANGELOG.md pushed to github
 - 2016-11-28 12:51:07 -0800 [b41d1bb](../../commit/b41d1bb553a807fa245dd95dc2ba2df461ff0f70) Added documentation for error response
 - 2016-11-23 15:26:02 -0500 [1f6e11c](../../commit/1f6e11c3cfeaeaa366af87257f34d32a13881cff) Fix usage typo: -put should be --put
 - 2016-10-30 16:21:52 -0700 [bed3f77](../../commit/bed3f77009be4160e3f7aef78f079c0ce8d6c9f8) 1.3.2 more Python3 work
 - 2016-10-30 16:18:23 -0700 [2008a53](../../commit/2008a534891191ca37154f78aed1fd7de18aa5e6) flushed out tabs, converted print to print() - all python3 stuff
 - 2016-10-29 12:50:28 -0700 [e179521](../../commit/e1795216aea9935b19ab2340cd63f68a8ed4065e) Added more examples of using raw mode and updated README
 - 2016-10-27 09:15:22 -0700 [8f7b510](../../commit/8f7b51064c54884c095f598f9f93107a24cf2b7f) CHANGELOG.md creation added to Makefile - and pushed to github
 - 2016-10-27 09:09:47 -0700 [7711452](../../commit/7711452b94aa1cc556fef7e7430bcb005f7ca761) CHANGELOG.md creation added to Makefile
 - 2016-10-25 05:14:20 -0700 [206a699](../../commit/206a6992005fdc891e60bdf298e330d6de701810) working CTM release
 - 2016-10-25 05:10:25 -0700 [f107f40](../../commit/f107f40ad4388ad6f71c2e171d3b1a2eb9ba878a) support for CTM errors, cleaner exceptions for invalid methods, clearer cert exception message
 - 2016-10-25 05:06:08 -0700 [e0336e1](../../commit/e0336e1172029e0f25ffc5924edf8fbe86bb324f) typo
 - 2016-10-24 06:52:00 -0700 [1555a85](../../commit/1555a854655a3f4ca68dbcc7b4b46c72cb7451e9) typo
 - 2016-10-22 11:03:26 -0700 [b5919ad](../../commit/b5919ad53dcd9a992879004eb82758bee1bd3134) typo
 - 2016-10-22 10:58:46 -0700 [b0e6bb8](../../commit/b0e6bb8fdc0b7af5af726d2227acbdeaec3d7d41) add CTM (Cloudflare Traffic Manager) API calls
 - 2016-10-21 17:29:47 -0700 [406fa56](../../commit/406fa56b8666ddee1082f1bac0b7ade527c39037) create zone and populate example needed FQDN for CNAME dns record
 - 2016-10-21 14:08:43 -0700 [bc99cb5](../../commit/bc99cb513913ccc421543111d4631e1fa96fcebe) needed for package reasons
 - 2016-10-21 14:06:03 -0700 [460d77b](../../commit/460d77bad7b255718e3986090b75458c6eef49a0) 1.3.0 - python3 finally works via pip install
 - 2016-10-21 12:54:22 -0700 [110870b](../../commit/110870b4524cad34f7ddd780d9532f5b3e09d374) 1.2.6 - python3 does not need the import statement
 - 2016-10-21 12:49:37 -0700 [968b93c](../../commit/968b93c301b51c6d44a1976d06164d518af49e11) 1.2.5 - error_chain can now be read when an error happens
 - 2016-10-21 12:48:11 -0700 [3650b3e](../../commit/3650b3e841acb2e38b55722eb76e703a789a8d22) error_chain can now be read when an error happens
 - 2016-10-20 18:12:24 -0700 [5f78fcd](../../commit/5f78fcd803c4e314de41816caba9144e1f0c8d81) added examples and man page to distributed files
 - 2016-10-19 14:29:27 -0700 [c6c3175](../../commit/c6c3175543cfb11fa49a481565f7d8bfe3a22c65) fixed CloudFlare.exceptions in raise/except
 - 2016-10-18 18:16:34 -0700 [5c59702](../../commit/5c5970256744cc711ce408e5e8d7b12cd48a1ee3) added cli4 --dump documentation
 - 2016-10-18 17:42:50 -0700 [f89c64b](../../commit/f89c64b485e98366a0a942300cd26035f5931892) added more api calls - available_rate_plans  websockets ssl/analyze ssl/verification
 - 2016-10-18 17:38:28 -0700 [6655fee](../../commit/6655fee7ecd5dadfaf6abe4831ff39d5d72a253e) walk the tree correctly for --dump option
 - 2016-10-18 14:38:42 -0700 [3e177cf](../../commit/3e177cf0a4d29741822284980e46d93a143ff3ff) typo
 - 2016-10-17 17:19:26 -0700 [88c4250](../../commit/88c4250c40bd0ae43a6dcb36fe635b03080a0e31) typo
 - 2016-10-17 17:16:49 -0700 [d9f5930](../../commit/d9f593080a94178e9e703df1b78ab25af2e61c4d) added -d/--dump command to display list of API calls
 - 2016-10-17 17:16:15 -0700 [22b85b1](../../commit/22b85b1ba22e686792b03e053caeaaa8258d7008) made add internal call start with _ and added a few missing API calls
 - 2016-10-17 11:18:42 -0700 [a11c251](../../commit/a11c251c49253910cceac50286b4482934fc1807) first public raw mode release
 - 2016-10-17 11:16:45 -0700 [2dfe728](../../commit/2dfe728f42947a6ed0f126329bcb52461d35c54d) typo - theres just "result" and "result_info" returned in raw mode
 - 2016-10-17 11:07:17 -0700 [797b90f](../../commit/797b90f11889bca2be70d923ef31e139b0dded91) Added --raw mode examples
 - 2016-10-17 10:38:39 -0700 [c1ead41](../../commit/c1ead419110b5e50bda69061ca2b8635eae085eb) Added more cleanup code and sdist/bdist options - not that bdist is useful
 - 2016-10-16 19:05:36 -0700 [06e5fcc](../../commit/06e5fccff0ed2071b851f66dab10ed2e6822554e) with raw code added we bump the version number
 - 2016-10-16 19:04:59 -0700 [052e1c0](../../commit/052e1c046bd36fe57dffaaefab7a430a207f094f) example of how to page thru data with raw option
 - 2016-10-16 18:13:28 -0700 [3ae4626](../../commit/3ae4626c5703470fd50b00066af669f777d40646) added raw flag to class so that paging values can be returned
 - 2016-10-16 18:11:59 -0700 [00ef9f8](../../commit/00ef9f8396c067d30c861b68336686d5680887a1) typo
 - 2016-10-16 18:09:13 -0700 [1d41949](../../commit/1d4194916a580cc089b1d5daf80447a64aa76f12) added --raw flag to cli4 so that paging values can be returned
 - 2016-10-16 18:07:35 -0700 [02df169](../../commit/02df169aff1113f47873cf70cf2c36ed592e9eb4) fixed typo
 - 2016-10-16 11:07:11 -0700 [961c239](../../commit/961c239adf0cf763b9add03237daf664bcc3ad9a) Changed company name to Cloudflare - dropping the capital F
 - 2016-10-15 08:25:18 -0700 [5e8bc5a](../../commit/5e8bc5a60934d8000f468f164116e01de2925d2e) Added python lint to make process - still with non-critical errors at this point
 - 2016-10-15 08:14:30 -0700 [889d072](../../commit/889d072039c798fe7ce60319e77fbb7ab5a84564) cli adds version and json options. cli man page matches cli command
 - 2016-07-04 17:17:33 -0700 [bb64ccd](../../commit/bb64ccd13754a181763c250ee525983a1bd1e4a9) bumped version number after various read_config fixes and logger requirements added
 - 2016-07-04 17:10:21 -0700 [45f02de](../../commit/45f02dede419494bdd62b3816d7b6d5bc621aa6e) Added logger to requirements.txt and setup.py
 - 2016-07-04 16:47:37 -0700 [4747180](../../commit/4747180fac0f31866834ea44b6de8d76a07be282) removed redudant code and confirmed extra code would work from an env variable
 - 2016-07-04 16:38:32 -0700 [7c84046](../../commit/7c84046ee4d60a11f735e7c6eebc38cf38031974) Merge branch 'ad-m-patch-1'
 - 2016-07-04 16:36:45 -0700 [1641115](../../commit/164111554a10e4d62d60a9aa194213788f5fc660) removed duplicate code because of multipul PRs
 - 2016-07-04 16:29:40 -0700 [0e6a6fe](../../commit/0e6a6fe5d6a8ad6e8e2e2c966ef34f04ff55fbc2) Merge branch 'fix_no_config-20160628-0927' of https://github.com/nicholaskuechler/python-cloudflare
 - 2016-07-04 16:19:25 -0700 [53da54b](../../commit/53da54b061fe8732fe3741272772d838c5d1d110) Merge branch 'niekrosink-FixEmptyExtraField'
 - 2016-07-04 12:20:41 +0200 [6272de8](../../commit/6272de825eb8d85acb0bf9459b49cc8dadd9e8b8) Fix fail parse config if no section
 - 2016-06-28 09:41:29 -0500 [f7b824f](../../commit/f7b824fb8a81b49f27e2d789c6fdaa71bcfae1bf) Fix handling of missing config when passing in email and token.
 - 2016-06-27 10:47:02 +0200 [020601b](../../commit/020601b992e957fbf2387b3738f525a302e3219c) Fixed empty extras field
 - 2016-06-21 19:08:49 -0700 [aeff51b](../../commit/aeff51b35c3841965d19ddd2561f232cb2422e24) Fixed documentation - thanks hlx98007 for pointing this out
 - 2016-06-21 18:48:53 -0700 [f060792](../../commit/f0607922b1b492b12d5c930aa26ca11f269624e2) Fixed docs and bumped version to 1.1.3
 - 2016-06-21 18:46:51 -0700 [eec307f](../../commit/eec307f1a8b677bdef55da35bbe7ab5ad643ae5a) Merge branch 'hlx98007-master'
 - 2016-06-21 18:44:24 -0700 [4d20c09](../../commit/4d20c09eefa4a7fa3079f10945a6a869f9a176fc) Merge branch 'master' of https://github.com/hlx98007/python-cloudflare into hlx98007-master
 - 2016-06-21 18:41:59 -0700 [01fe999](../../commit/01fe999ca8433e87f3b3f7334d82fa63e1092280) Fixed the issue with a missing extras= in the config file. You now dont need the extras= command
 - 2016-06-21 18:37:33 -0700 [d4261a0](../../commit/d4261a0fb8372a9933373507bc0723467f8d2001) Nulled out setup.cfg entries as they are not used
 - 2016-06-17 11:33:37 -0700 [e23e8fe](../../commit/e23e8fe1672a9321e4009cbac286360a0bac3cbd) Version 1.1.2
 - 2016-06-17 11:33:05 -0700 [2c357a8](../../commit/2c357a89af1106d150a12066288d20d628fcd29c) Fixed exceptions. Added -V for version.
 - 2016-06-17 11:32:05 -0700 [0004a42](../../commit/0004a42260d2fe7a312ab2aa64f8ad3e554df804) Typos and cleanup
 - 2016-06-17 10:08:02 -0700 [cedda2b](../../commit/cedda2b0a68e0579398d16a0a01738f38bbc80d7) Version 1.1.1
 - 2016-06-17 10:07:32 -0700 [cf2c59e](../../commit/cf2c59eb65ad2aeb254e4bb646bd269d237d764d) Fixed some exceptions. Cleaned up more code. More pylint work.
 - 2016-06-17 10:03:26 -0700 [c5f8287](../../commit/c5f82875065a60042d57c1fc514b11eded50f918) Stopped build and dist dirs being owned by root
 - 2016-06-14 20:01:21 -0300 [11320f0](../../commit/11320f0042a7f039837f8bc57c1a67ab78387f29) doc fix.
 - 2016-06-14 11:17:06 -0700 [8168029](../../commit/8168029ab3d024b768824429d91dfdb553618bec) small pylint changes
 - 2016-06-14 10:57:58 -0700 [02a13a5](../../commit/02a13a5601c1aa312dd3d0bdf4111375dc618459) small pylint changes
 - 2016-06-14 10:57:03 -0700 [2ded713](../../commit/2ded7132c95656d18ef9c56baec5385bb89a8642) small pylint changes
 - 2016-06-14 10:56:22 -0700 [f229482](../../commit/f2294821e85adf6d55a12cb87aab5f190d2521d2) small pylint changes
 - 2016-06-14 10:55:55 -0700 [1ed0955](../../commit/1ed0955a8c3f6df9c8fff30e8c7dc3c45b7e47d3) small pylint changes
 - 2016-06-13 12:50:51 -0700 [3d2d1de](../../commit/3d2d1dec078c92d97e1b9c309bb5493efde4ac85) Moved to version 1.1.0
 - 2016-06-13 12:48:26 -0700 [589c0de](../../commit/589c0dedf074a206400aee0bdd6e3b6ca44c79db) Moved core logic into its own file - CloudFlare/cloudflare.py
 - 2016-06-10 22:51:33 -0700 [78fff44](../../commit/78fff447cc8a682ed61a88a21cf89c8f39242f83) Initial pass a unix man page for cli4
 - 2016-06-10 22:46:51 -0700 [bf45916](../../commit/bf459162254562a83fdd6b600e3dfa4a297f1112) Python 3.x porting
 - 2016-06-10 22:38:42 -0700 [423120c](../../commit/423120ceedb7881984b6ba944410620571d86874) Python 3.x porting
 - 2016-06-10 22:36:46 -0700 [adf1e5b](../../commit/adf1e5b4cfb0f77074ba3191b8e01ab74103e7ba) Added yaml output support. Python 3.x porting
 - 2016-06-10 22:32:15 -0700 [9e850db](../../commit/9e850dbc9e635d52e24612e7ee47ce16dfb517e9) Added Python3.x documentation. Renamed files with dashes to underscores.
 - 2016-05-18 05:48:02 -0700 [2e2f856](../../commit/2e2f8568d9e30d1f6e52be5b0f41b3e025a1b1f9) Moved to 1.0.7
 - 2016-05-18 05:46:39 -0700 [3225f64](../../commit/3225f64e94e188362c5ed5c71c09d1a9207871e3) Cleanup of code and confirmed functionality with A/AAAA records present
 - 2016-05-18 05:44:11 -0700 [ca88ab7](../../commit/ca88ab79ba558040ffdd1da45160e504d41bc01b) Fixed issues with lack of second instance arg
 - 2016-05-18 04:20:15 -0700 [5b56376](../../commit/5b56376757baa83cb8f32b33c40b0f21f19c0e77) Now matches setup.py
 - 2016-05-18 03:41:58 -0700 [27ec99f](../../commit/27ec99fbb982b66452c9698aeba57dbfcb9a41ff) typo
 - 2016-05-18 03:28:30 -0700 [636658d](../../commit/636658dd2e4ca2fdddf9470c123e392926421a3a) Added documentation for DNS CLI commands and upped version to 1.0.6
 - 2016-05-18 03:12:45 -0700 [ec9fef2](../../commit/ec9fef22ca873cab3cd63af28b97febb57acef84) Provided support for dns names to return more than one item. Allowed more complex json/yaml results
 - 2016-05-18 02:01:20 -0700 [e9cf6aa](../../commit/e9cf6aa9125ff259b19df02bb72f48338702b2dc) Removed fqdn from list of DNS entries - not needed
 - 2016-05-17 14:29:36 -0700 [27ad0c1](../../commit/27ad0c143927e2ab24d2480ebe7732f95542c7df) Added dns_records examples
 - 2016-05-17 14:16:10 -0700 [834509f](../../commit/834509f5333149af5ac39356f31a800db062575e) Added support for DNS names after /dns_records/
 - 2016-05-17 13:18:48 -0700 [637400e](../../commit/637400e3a30a22e942f4e7e8f8144d57e594d1a5) Added a example to update a dynamic DNS entry via CloudFlare v4 API
 - 2016-05-17 08:25:29 -0700 [81113d1](../../commit/81113d1f3b3bb36a6a24fc80403da96861edf8fa) Added initial code to support Python3. A long was still to go
 - 2016-05-17 04:45:46 -0700 [4560bb8](../../commit/4560bb81e4ea3a0b6c23da5eaf422551e51d5c33) Cleaned up tabs and space - tabs only now - Python3 ready
 - 2016-05-17 04:37:39 -0700 [2a5fd9b](../../commit/2a5fd9bab88d1e68d64adec29a1af7ad111fe67f) Cleaned up tabs and space - tabs only now - Python3 ready
 - 2016-05-17 04:29:13 -0700 [a23a777](../../commit/a23a777005fb5d9623b1f9ef0b0b682b247e9255) Make pagerule example more readable
 - 2016-05-17 01:20:43 -0700 [ee50578](../../commit/ee505783f5d4a94354f61162f822505b107f9ee8) minor fix to make install work cleanly
 - 2016-05-17 01:07:03 -0700 [d55f9d1](../../commit/d55f9d10ce96499c5b7a5862828680993077321d) Merge pull request #2 from carlkibler/master
 - 2016-05-16 18:50:38 -0700 [f458c23](../../commit/f458c234abfcd1906556271d7958c44557c3fe9b) Added page_rule example
 - 2016-05-16 18:50:12 -0700 [40b5b2b](../../commit/40b5b2bfcbb6b88e93b6bb02f58366004c664b60) Bumped version number
 - 2016-05-16 18:49:48 -0700 [821b144](../../commit/821b144c26dd397d2c7e536c04dbd0e46ab694c4) Added YAML mode; added support for JSON POST data (needed for pagerules)
 - 2016-05-16 15:34:35 -0600 [5c8afef](../../commit/5c8afef70593d464faa6ce6b275e2af7b30b7e12) Process 'extras' only if they are set
 - 2016-05-16 14:28:49 -0700 [24bcf9d](../../commit/24bcf9d8929a9e899e159a1310ea256671f5577f) Added Makefile to help build and packaging process
 - 2016-05-12 18:27:38 -0700 [680962c](../../commit/680962c8c2741e25cb54492b09ee554dc65713de) Project now available at https://pypi.python.org/pypi/cloudflare
 - 2016-05-10 16:14:51 -0700 [6d8479d](../../commit/6d8479d273cc76c0fdb088460fbd6441dfe31068) Added files needed to get https://pypi.python.org/pypi working
 - 2016-05-10 16:11:31 -0700 [d3c2d97](../../commit/d3c2d9748cef82e819280501b4da6ee493a9ac32) Added lots of structure to get https://pypi.python.org/pypi working
 - 2016-05-09 21:37:43 -0700 [a6b6593](../../commit/a6b6593605019b111c794bf510650f1b331a98ab) Cleaned up API call list
 - 2016-05-09 21:21:07 -0700 [1a7d102](../../commit/1a7d10279d431cfd5021d8f0aa2e40ef0d8c9784) Cleaned up API call list
 - 2016-05-09 14:23:34 -0700 [e85e277](../../commit/e85e277297ecf369c2f563aaea2d0bd409e6e17a) Added support for two identifiers per command plus a third argument
 - 2016-05-09 12:29:22 -0700 [77f3218](../../commit/77f32183d32f2ff0d6e6c35efade4dff21cebc30) Added /zones/:zone_id/firewall/waf/packages. Fixed /zones/ssl/certificate_packs. Removed /zones/:zone_id/dnssec/status
 - 2016-05-05 13:36:08 -0700 [8ba8625](../../commit/8ba8625f7c937ace1289d771425069327b2d1a95) update highlighting
 - 2016-05-04 16:54:41 -0700 [2283130](../../commit/22831301f43743c9981fc6089314017f2495dc5a) Added copyright. Corrected github address after repo move
 - 2016-05-04 15:54:41 -0700 [113b27b](../../commit/113b27b7bd41bb82890d3ab1b6951291851ac8aa) typo
 - 2016-05-04 15:39:46 -0700 [ac56c68](../../commit/ac56c689a172a122fdcf7e5543a84d96b20f8fc1) Added ability to pass interger in JSON data via item==value in CLI
 - 2016-05-04 14:22:56 -0700 [e222ad7](../../commit/e222ad7c1e6c3d422dc4ff17c236bc009fcc9363) Made digits an integer in json data passed in PUT/POST/PATCH params
 - 2016-05-04 13:35:18 -0700 [297ade8](../../commit/297ade8d82c1b9eb01297ff126e639671e13cff0) didn't handle /a/:b/c correctly when /a existed
 - 2016-05-04 11:29:18 -0700 [9f61cbc](../../commit/9f61cbc00cb04025b8bcc7dddac68cf188a32f79) Added support for extra API calls via the configuration file. See README
 - 2016-05-04 08:46:41 -0700 [7a42be0](../../commit/7a42be043e9c3dfd36d8dcbd91cb3b81f390f009) Added requirements.txt and appropriate stuff in setup.py to handle it
 - 2016-05-04 08:39:43 -0700 [5a5a1ab](../../commit/5a5a1abbb811bec0f3e0f3e69dd2fcd81eaacc54) Added example of how to adjust proxy flag on the fly
 - 2016-05-04 08:39:12 -0700 [94d5c2d](../../commit/94d5c2d25fdb7977c30235803fe5a6447d1063ae) Added proxed flag to show how to adjust DNS entries after creation
 - 2016-05-04 08:38:30 -0700 [bc25c34](../../commit/bc25c3435aacff1feabf0d5f5e2e0547d7094c90) Added support for 40 or 48 length x509 certificate lengths. Fixed PUT paramaters
 - 2016-05-04 08:35:37 -0700 [6edde36](../../commit/6edde369464317662b849df39ff74199aea16b80) /certificates only needs X-Auth-User-Service-Key header, no email or token
 - 2016-05-04 08:34:22 -0700 [9893b45](../../commit/9893b451c5c8c9d1295bc394f89a1544d3075eb7) Added info about X-Auth-User-Service-Key which is used by /certificates API call
 - 2016-05-03 10:00:20 -0700 [21025d1](../../commit/21025d1670cd30ba60855bb0ec345e8a0a157811) Moved API v4 into it's own file plus minor cleanup
 - 2016-05-02 22:53:48 -0700 [8675a35](../../commit/8675a35af74a754092d4e83f0177f56b160196f1) README typos fixed
 - 2016-05-02 22:40:41 -0700 [b0b585c](../../commit/b0b585c4338e93a9f18aa77e5a313d85d698261a) Added lots of examples to README
 - 2016-05-02 22:28:22 -0700 [3e22b5e](../../commit/3e22b5efbd0504b2c1262096291a388600b45f61) Updated README and usage string in CLI
 - 2016-05-02 22:23:19 -0700 [ddbc44f](../../commit/ddbc44f2576e4ba69badb3e1d5bad57a3dc21542) Updated README
 - 2016-05-02 22:20:15 -0700 [1eed90b](../../commit/1eed90bb0ade23f73e663047650fee313449eb19) CloudFlare API v4 complete rewrite. Added all API calls. Added examples. Added CLU tool. Added initial tests. Updated README
 - 2016-04-23 16:27:18 -0700 [edefd99](../../commit/edefd99cc85dca0c4349d0e28d92b029794c31a9) add license
 - 2016-02-28 16:07:33 -0800 [d1de6c9](../../commit/d1de6c91900a1b216416bd0e61575385d6c8bdd8) Merge pull request #9 from servee/master
 - 2016-02-28 13:40:16 -0800 [2502ef9](../../commit/2502ef957b527f0b68f033efc6428a9a6ebe86fa) Add PATCH method
 - 2016-02-25 07:19:36 +0000 [1a88944](../../commit/1a889445561df8eac7a292a06728a115f912a3dc) fix terrible params bug in get
 - 2015-12-05 17:43:26 -0800 [c765114](../../commit/c7651141d5d9d2ea5f0d881dc6f07e671dfc4235) Merge pull request #4 from tmrtn/http-put
 - 2015-12-05 07:32:36 +0100 [7c0fc05](../../commit/7c0fc0546350baeb1031700996646ee56fd7884d) add missing HTTP method put, fix unknown exception class
 - 2015-10-15 09:46:28 +0000 [6fff910](../../commit/6fff91091979660c32ca83bdb17ae9438c88fe87) fix delete method
 - 2015-09-28 08:55:42 +0000 [3c48798](../../commit/3c487980e2ce0c6e6e8cbfb02d83a2f5fb0d6770) add exceptions + escaping urls
 - 2015-09-17 09:39:12 +0000 [28daae8](../../commit/28daae894ed4409cebb7a72c7eb4c4884b933fba) fix delete endpoint
 - 2015-08-30 02:23:06 -0700 [985ef56](../../commit/985ef5680f8bc9f7e1455ad28ba6a20a78b2c74d) fix get as well
 - 2015-08-30 01:57:13 -0700 [efefa62](../../commit/efefa62c5c94f7e0aa654f974f9d760190f1868b) fix post and delete + add purge_cache
 - 2015-08-30 01:23:14 -0700 [c37584d](../../commit/c37584d439b5e6996c49e8fc0e65e5840b5d2261) support second level urls
 - 2015-08-02 02:56:35 -0700 [9b895b1](../../commit/9b895b133398423855357e2774d8ff3336320297) actually make methods slightly more dynamic
 - 2015-04-03 23:47:41 -0700 [4a6acab](../../commit/4a6acab9ddc22413e4d46ef0687d55668a99fe8b) add purge all files in cache
 - 2015-03-17 23:04:46 -0700 [f3cba51](../../commit/f3cba510e91cc9c7605a5912965994833df1180f) uppercase method once
 - 2015-03-07 19:49:53 -0800 [710dbf9](../../commit/710dbf9f6a4a1c35da5f3a52b9955321192aa940) Update README.md
 - 2015-03-07 18:02:10 -0800 [4b9ef71](../../commit/4b9ef716ecb04cb867593007c3ac956ce816e5eb) more hacking
 - 2015-02-14 11:28:24 +0000 [8e79a53](../../commit/8e79a53d3cbc1f486fe027083712b9a9a0546552) posting dns_records to zone
 - 2015-02-10 10:09:38 +0000 [0dee0ba](../../commit/0dee0badcc43ae2a019f4d206925df5fd7477224) add getting dns records
 - 2015-02-10 09:50:51 +0000 [dcbc9a8](../../commit/dcbc9a8d2d18013112a9e0a280e73ec00a8b3a36) add logging and bug fixes
 - 2015-02-10 06:36:48 +0000 [35e9bf5](../../commit/35e9bf5746a9660d3522a934849e878934d88bf5) more debugging
 - 2015-01-26 08:42:51 +0000 [82c5020](../../commit/82c50206d3e6960d24bdefc26ef32ec95bb17c81) cleanup import
 - 2015-01-26 08:42:08 +0000 [c933789](../../commit/c933789bac0a14765399f99292b5b7ced7dfca4e) initial code commit
 - 2014-12-13 18:05:38 -0800 [948eed8](../../commit/948eed8afa9dd49213d8ac6f913d7a70e265d77a) Initial commit
python-cloudflare-2.20.0/CloudFlare/000077500000000000000000000000001461736615400172675ustar00rootroot00000000000000python-cloudflare-2.20.0/CloudFlare/__init__.py000066400000000000000000000001571461736615400214030ustar00rootroot00000000000000""" Cloudflare v4 API"""

__version__ = '2.20.0'

from .cloudflare import CloudFlare

__all__ = ['CloudFlare']
python-cloudflare-2.20.0/CloudFlare/api_decode_from_openapi.py000066400000000000000000000071201461736615400244530ustar00rootroot00000000000000""" API from OpenAPI for Cloudflare API"""

import sys
import re
import datetime
import json

API_TYPES = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE']

match_identifier = re.compile(r'\{[A-Za-z0-9_\-]*\}')

def do_path(cmd, values):
    """ do_path() """

    cmds = []

    if cmd[0] != '/':
        cmd = '/' + cmd  # make sure there's a leading /

    cmd = match_identifier.sub(':id', cmd)
    if cmd[-4:] == '/:id':
        cmd = cmd[:-4]
    if cmd[-4:] == '/:id':
        cmd = cmd[:-4]

    for action in values:
        if action == '' or action.upper() not in API_TYPES:
            continue
        if 'deprecated' in values[action] and values[action]['deprecated']:
            deprecated = True
            deprecated_date = datetime.datetime.now().strftime('%Y-%m-%d')
            deprecated_already = True
        else:
            deprecated = False
            deprecated_date = ''
            deprecated_already = False

        # The requestBody/content could be one of the following:
        # "requestBody": {
        #   "content": {
        #     "application/javascript" {
        #     "application/json" {
        #     "application/octet-stream" {
        #     "application/x-ndjson" {
        #     "multipart/form-data" {

        content_type = None
        if 'requestBody' in values[action] and values[action]['requestBody']:
            request_body = values[action]['requestBody']
            if 'content' in request_body and request_body['content']:
                content_type = ','.join(list(request_body['content'].keys()))
                if content_type == 'application/json':
                    # this is the default; so we simply ignore it
                    content_type = None

        if content_type:
            v = {
                    'action': action.upper(),
                    'cmd': cmd,
                    'deprecated': deprecated,
                    'deprecated_date': deprecated_date,
                    'deprecated_already': deprecated_already,
                    'content_type': content_type
                }
        else:
            v = {
                    'action': action.upper(),
                    'cmd': cmd,
                    'deprecated': deprecated,
                    'deprecated_date': deprecated_date,
                    'deprecated_already': deprecated_already
                }
        cmds.append(v)
    return cmds

def api_decode_from_openapi(content):
    """ API decode from OpenAPI for Cloudflare API"""

    try:
        j = json.loads(content)
    except json.decoder.JSONDecodeError as e:
        raise SyntaxError('OpenAPI json decode failed: %s' % (e)) from None

    try:
        components = j['components']
        info = j['info']
        cloudflare_version = info['version']
        openapi_version = j['openapi']
        paths = j['paths']
        servers = j['servers']
    except KeyError as e:
        raise SyntaxError('OpenAPI json missing standard OpenAPI values: %s' % (e)) from None

    if len(components) == 0:
        raise SyntaxError('OpenAPI json components missing values')

    cloudflare_url = None
    for server in servers:
        try:
            cloudflare_url = server['url']
        except KeyError as e:
            pass
    if not cloudflare_url:
        raise SyntaxError('OpenAPI json servers/server missing url value')

    all_cmds = []
    for path in paths:
        if path[0] != '/':
            sys.stderr.write("OpenAPI invalid path: %s\n" % (path))
            continue
        all_cmds += do_path(path, paths[path])

    return sorted(all_cmds, key=lambda v: v['cmd']), openapi_version, cloudflare_version, cloudflare_url
python-cloudflare-2.20.0/CloudFlare/api_extras.py000066400000000000000000000033541461736615400220050ustar00rootroot00000000000000""" API extras for Cloudflare API"""

import re

from .exceptions import CloudFlareAPIError

def api_extras(self, extras=None):
    """ API extras for Cloudflare API"""

    count = 0
    for extra in extras:
        extra = re.sub(r"^.*/client/v4/", '/', extra)
        extra = re.sub(r"^.*/v4/", '/', extra)
        extra = re.sub(r"^/", '', extra)
        if extra == '':
            continue

        # build parts of the extra command
        parts = []
        part = None
        for element in extra.split('/'):
            if element[0] == ':':
                parts.append(part)
                part = None
                continue
            if part:
                part += '/' + element
            else:
                part = element
        if part:
            parts.append(part)

        if len(parts) > 1:
            p = parts[1].split('/')
            for nn in range(0, len(p)):
                try:
                    self.add('VOID', parts[0], '/'.join(p[0:nn]))
                except CloudFlareAPIError:
                    # already exists - this is ok
                    pass

        if len(parts) > 2:
            p = parts[2].split('/')
            for nn in range(0, len(p)):
                try:
                    self.add('VOID', parts[0], parts[1], '/'.join(p[0:nn]))
                except CloudFlareAPIError:
                    # already exists - this is ok
                    pass

        while len(parts) < 3:
            parts.append(None)

        # we can only add AUTH elements presently
        try:
            self.add('AUTH', parts[0], parts[1], parts[2])
            count += 1
        except CloudFlareAPIError:
            # this is silently dropped - however, that could change
            pass

    return count
python-cloudflare-2.20.0/CloudFlare/api_v4.py000066400000000000000000001677471461736615400210510ustar00rootroot00000000000000""" API core commands for Cloudflare API"""

def api_v4(self):
    """ :meta private: """


    # The API commands for /user/
    user(self)
    user_audit_logs(self)
    user_load_balancers(self)
    user_load_balancing_analytics(self)
    user_tokens_verify(self)

    # The API commands for /radar/
    radar(self)
    radar_as112(self)
    radar_attacks(self)
    radar_bgp(self)
    radar_email(self)
    radar_http(self)

    # The API commands for /zones/
    zones(self)
    zones_access(self)
    zones_amp(self)
    zones_analytics(self)
    zones_argo(self)
    zones_dns_analytics(self)
    zones_dnssec(self)
    zones_firewall(self)
    zones_load_balancers(self)
    zones_logpush(self)
    zones_logs(self)
    zones_media(self)
    zones_origin_tls_client_auth(self)
    zones_rate_limits(self)
    zones_secondary_dns(self)
    zones_settings(self)
    zones_spectrum(self)
    zones_ssl(self)
    zones_waiting_rooms(self)
    zones_workers(self)
    zones_extras(self)
    zones_web3(self)
    zones_email(self)
    zones_api_gateway(self)

    # The API commands for /railguns/
    railguns(self)

    # The API commands for /certificates/
    certificates(self)

    # The API commands for /ips/
    ips(self)

    # The API commands for /live/
    live(self)

    # The API commands for /accounts/
    accounts(self)
    accounts_access(self)
    accounts_addressing(self)
    accounts_audit_logs(self)
    accounts_diagnostics(self)
    accounts_firewall(self)
    accounts_load_balancers(self)
    accounts_secondary_dns(self)
    accounts_stream(self)
    accounts_ai(self)
    accounts_extras(self)
    accounts_cloudforce_one(self)
    accounts_email(self)
    accounts_r2(self)

    # The API commands for /memberships/
    memberships(self)

    # The API commands for /graphql
    graphql(self)

    # Issue 151
    from_developers(self)

def user(self):
    """ :meta private: """

    self.add('AUTH', 'user')
    self.add('AUTH', 'user/billing/history')
    self.add('AUTH', 'user/billing/profile')
#   self.add('AUTH', 'user/billing/subscriptions/apps')
#   self.add('AUTH', 'user/billing/subscriptions/zones')
    self.add('AUTH', 'user/firewall/access_rules/rules')
    self.add('AUTH', 'user/invites')
    self.add('AUTH', 'user/organizations')
    self.add('AUTH', 'user/subscriptions')

def zones(self):
    """ :meta private: """

    self.add('AUTH', 'zones')
    self.add('AUTH', 'zones', 'activation_check')
    self.add('AUTH', 'zones', 'available_plans')
    self.add('AUTH', 'zones', 'available_rate_plans')
    self.add('AUTH', 'zones', 'bot_management')
    self.add('AUTH', 'zones', 'bot_management/feedback')
    self.add('AUTH', 'zones', 'client_certificates')
    self.add('AUTH', 'zones', 'custom_certificates')
    self.add('AUTH', 'zones', 'custom_certificates/prioritize')
    self.add('AUTH', 'zones', 'custom_csrs')
    self.add('AUTH', 'zones', 'custom_hostnames')
    self.add('AUTH', 'zones', 'custom_hostnames/fallback_origin')
    self.add('AUTH', 'zones', 'custom_ns')
    self.add('AUTH', 'zones', 'custom_pages')
    self.add('AUTH', 'zones', 'dns_records')
    self.add('AUTH', 'zones', 'dns_records/export')
    self.add('AUTH', 'zones', 'dns_records/import', content_type={'POST':'multipart/form-data'})
    self.add('AUTH', 'zones', 'dns_records/scan')
    self.add('AUTH', 'zones', 'dns_settings')
    self.add('AUTH', 'zones', 'dns_settings/use_apex_ns')
    self.add('AUTH', 'zones', 'filters')
    self.add('AUTH', 'zones', 'filters/validate-expr')
    self.add('AUTH', 'zones', 'healthchecks')
    self.add('AUTH', 'zones', 'healthchecks/preview')
    self.add('AUTH', 'zones', 'keyless_certificates')
    self.add('AUTH', 'zones', 'origin_max_http_version')
    self.add('AUTH', 'zones', 'pagerules')
    self.add('AUTH', 'zones', 'pagerules/settings')
    self.add('AUTH', 'zones', 'purge_cache')
    self.add('AUTH', 'zones', 'railguns')
    self.add('AUTH', 'zones', 'railguns', 'diagnose')
    self.add('AUTH', 'zones', 'security/events')
    self.add('AUTH', 'zones', 'subscription')

def zones_settings(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'settings')
    self.add('AUTH', 'zones', 'settings/0rtt')
    self.add('AUTH', 'zones', 'settings/advanced_ddos')
    self.add('AUTH', 'zones', 'settings/always_online')
    self.add('AUTH', 'zones', 'settings/always_use_https')
    self.add('AUTH', 'zones', 'settings/automatic_https_rewrites')
    self.add('AUTH', 'zones', 'settings/automatic_platform_optimization')
    self.add('AUTH', 'zones', 'settings/brotli')
    self.add('AUTH', 'zones', 'settings/browser_cache_ttl')
    self.add('AUTH', 'zones', 'settings/browser_check')
    self.add('AUTH', 'zones', 'settings/cache_level')
    self.add('AUTH', 'zones', 'settings/challenge_ttl')
    self.add('AUTH', 'zones', 'settings/ciphers')
    self.add('AUTH', 'zones', 'settings/development_mode')
    self.add('AUTH', 'zones', 'settings/early_hints')
    self.add('AUTH', 'zones', 'settings/email_obfuscation')
    self.add('AUTH', 'zones', 'settings/fonts')
    self.add('AUTH', 'zones', 'settings/h2_prioritization')
    self.add('AUTH', 'zones', 'settings/hotlink_protection')
    self.add('AUTH', 'zones', 'settings/http2')
    self.add('AUTH', 'zones', 'settings/http3')
    self.add('AUTH', 'zones', 'settings/image_resizing')
    self.add('AUTH', 'zones', 'settings/ip_geolocation')
    self.add('AUTH', 'zones', 'settings/ipv6')
    self.add('AUTH', 'zones', 'settings/min_tls_version')
    self.add('AUTH', 'zones', 'settings/minify')
    self.add('AUTH', 'zones', 'settings/mirage')
    self.add('AUTH', 'zones', 'settings/mobile_redirect')
    self.add('AUTH', 'zones', 'settings/nel')
    self.add('AUTH', 'zones', 'settings/opportunistic_encryption')
    self.add('AUTH', 'zones', 'settings/opportunistic_onion')
    self.add('AUTH', 'zones', 'settings/orange_to_orange')
    self.add('AUTH', 'zones', 'settings/origin_error_page_pass_thru')
    self.add('AUTH', 'zones', 'settings/origin_max_http_version')
    self.add('AUTH', 'zones', 'settings/polish')
    self.add('AUTH', 'zones', 'settings/prefetch_preload')
    self.add('AUTH', 'zones', 'settings/privacy_pass')
    self.add('AUTH', 'zones', 'settings/proxy_read_timeout')
    self.add('AUTH', 'zones', 'settings/pseudo_ipv4')
    self.add('AUTH', 'zones', 'settings/response_buffering')
    self.add('AUTH', 'zones', 'settings/rocket_loader')
    self.add('AUTH', 'zones', 'settings/security_header')
    self.add('AUTH', 'zones', 'settings/security_level')
    self.add('AUTH', 'zones', 'settings/server_side_exclude')
    self.add('AUTH', 'zones', 'settings/sort_query_string_for_cache')
    self.add('AUTH', 'zones', 'settings/ssl')
    self.add('AUTH', 'zones', 'settings/ssl_recommender')
    self.add('AUTH', 'zones', 'settings/tls_1_3')
    self.add('AUTH', 'zones', 'settings/tls_client_auth')
    self.add('AUTH', 'zones', 'settings/true_client_ip_header')
    self.add('AUTH', 'zones', 'settings/waf')
    self.add('AUTH', 'zones', 'settings/webp')
    self.add('AUTH', 'zones', 'settings/websockets')

    self.add('AUTH', 'zones', 'settings/zaraz/config')
    self.add('AUTH', 'zones', 'settings/zaraz/default')
    self.add('AUTH', 'zones', 'settings/zaraz/export')
    self.add('AUTH', 'zones', 'settings/zaraz/history')
    self.add('AUTH', 'zones', 'settings/zaraz/history/configs')
    self.add('AUTH', 'zones', 'settings/zaraz/publish')
    self.add('AUTH', 'zones', 'settings/zaraz/workflow')

    self.add('AUTH', 'zones', 'settings/zaraz/v2/config')
    self.add('AUTH', 'zones', 'settings/zaraz/v2/default')
    self.add('AUTH', 'zones', 'settings/zaraz/v2/export')
    self.add('AUTH', 'zones', 'settings/zaraz/v2/history')
    self.add('AUTH', 'zones', 'settings/zaraz/v2/history/configs')
    self.add('AUTH', 'zones', 'settings/zaraz/v2/publish')
    self.add('AUTH', 'zones', 'settings/zaraz/v2/workflow')

def zones_analytics(self):
    """ :meta private: """

#   self.add('AUTH', 'zones', 'analytics/colos') # deprecated 2021-03-01 - expired!
#   self.add('AUTH', 'zones', 'analytics/dashboard') # deprecated 2021-03-01 - expired!
    self.add('AUTH', 'zones', 'analytics/latency')
    self.add('AUTH', 'zones', 'analytics/latency/colos')

def zones_firewall(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'firewall/access_rules/rules')
    self.add('AUTH', 'zones', 'firewall/lockdowns')
    self.add('AUTH', 'zones', 'firewall/rules')
    self.add('AUTH', 'zones', 'firewall/ua_rules')
    self.add('AUTH', 'zones', 'firewall/waf/overrides')
    self.add('AUTH', 'zones', 'firewall/waf/packages')
    self.add('AUTH', 'zones', 'firewall/waf/packages', 'groups')
    self.add('AUTH', 'zones', 'firewall/waf/packages', 'rules')

def zones_rate_limits(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'rate_limits')

def zones_dns_analytics(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'dns_analytics/report')
    self.add('AUTH', 'zones', 'dns_analytics/report/bytime')

def zones_amp(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'amp/sxg')

def zones_logpush(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'logpush/datasets', 'fields')
    self.add('AUTH', 'zones', 'logpush/datasets', 'jobs')
    self.add('AUTH', 'zones', 'logpush/edge')
    self.add('AUTH', 'zones', 'logpush/edge/jobs')
    self.add('AUTH', 'zones', 'logpush/jobs')
    self.add('AUTH', 'zones', 'logpush/ownership')
    self.add('AUTH', 'zones', 'logpush/ownership/validate')
    self.add('AUTH', 'zones', 'logpush/validate/destination/exists')
    self.add('AUTH', 'zones', 'logpush/validate/origin')

def zones_logs(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'logs/control/retention/flag')
    self.add('AUTH_UNWRAPPED', 'zones', 'logs/received')
    self.add('AUTH', 'zones', 'logs/received/fields')
    self.add('AUTH_UNWRAPPED', 'zones', 'logs/rayids')

def railguns(self):
    """ :meta private: """

    self.add('AUTH', 'railguns')
    self.add('AUTH', 'railguns', 'zones')

def certificates(self):
    """ :meta private: """

    self.add('CERT', 'certificates')

def ips(self):
    """ :meta private: """

    self.add('OPEN', 'ips')

def live(self):
    """ :meta private: """

    self.add('AUTH', 'live')

def zones_argo(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'argo/tiered_caching')
    self.add('AUTH', 'zones', 'argo/smart_routing')

def zones_dnssec(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'dnssec')

def zones_spectrum(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'spectrum/analytics/aggregate/current')
    self.add('AUTH', 'zones', 'spectrum/analytics/events/bytime')
    self.add('AUTH', 'zones', 'spectrum/analytics/events/summary')
    self.add('AUTH', 'zones', 'spectrum/apps')

def zones_ssl(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'ssl/analyze')
    self.add('AUTH', 'zones', 'ssl/certificate_packs')
    self.add('AUTH', 'zones', 'ssl/certificate_packs/order')
    self.add('AUTH', 'zones', 'ssl/certificate_packs/quota')
    self.add('AUTH', 'zones', 'ssl/recommendation')
    self.add('AUTH', 'zones', 'ssl/verification')
    self.add('AUTH', 'zones', 'ssl/universal/settings')

def zones_origin_tls_client_auth(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'origin_tls_client_auth')
    self.add('AUTH', 'zones', 'origin_tls_client_auth/hostnames')
    self.add('AUTH', 'zones', 'origin_tls_client_auth/hostnames/certificates')
    self.add('AUTH', 'zones', 'origin_tls_client_auth/settings')

def zones_workers(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'workers/filters')
    self.add('AUTH', 'zones', 'workers/routes')
    self.add('AUTH', 'zones', 'workers/script')
    self.add('AUTH', 'zones', 'workers/script/bindings')

def zones_load_balancers(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'load_balancers')

def zones_secondary_dns(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'secondary_dns')
    self.add('AUTH', 'zones', 'secondary_dns/force_axfr')
    self.add('AUTH', 'zones', 'secondary_dns/incoming')
    self.add('AUTH', 'zones', 'secondary_dns/outgoing')
    self.add('AUTH', 'zones', 'secondary_dns/outgoing/disable')
    self.add('AUTH', 'zones', 'secondary_dns/outgoing/enable')
    self.add('AUTH', 'zones', 'secondary_dns/outgoing/force_notify')
    self.add('AUTH', 'zones', 'secondary_dns/outgoing/status')

def user_load_balancers(self):
    """ :meta private: """

    self.add('AUTH', 'user/load_balancers/monitors')
    self.add('AUTH', 'user/load_balancers/monitors', 'preview')
    self.add('AUTH', 'user/load_balancers/monitors', 'references')
    self.add('AUTH', 'user/load_balancers/preview')
    self.add('AUTH', 'user/load_balancers/pools')
    self.add('AUTH', 'user/load_balancers/pools', 'health')
    self.add('AUTH', 'user/load_balancers/pools', 'preview')
    self.add('AUTH', 'user/load_balancers/pools', 'references')

def user_audit_logs(self):
    """ :meta private: """

    self.add('AUTH', 'user/audit_logs')

def user_load_balancing_analytics(self):
    """ :meta private: """

    self.add('AUTH', 'user/load_balancing_analytics/events')

def user_tokens_verify(self):
    """ :meta private: """

    self.add('AUTH', 'user/tokens')
    self.add('AUTH', 'user/tokens/permission_groups')
    self.add('AUTH', 'user/tokens/verify')
    self.add('AUTH', 'user/tokens', 'value')

def accounts(self):
    """ :meta private: """

    self.add('AUTH', 'accounts')
    self.add('AUTH', 'accounts', 'billing/profile')
    self.add('AUTH', 'accounts', 'brand-protection/submit')
    self.add('AUTH', 'accounts', 'brand-protection/url-info')
    self.add('AUTH', 'accounts', 'cfd_tunnel')
    self.add('AUTH', 'accounts', 'cfd_tunnel', 'configurations')
    self.add('AUTH', 'accounts', 'cfd_tunnel', 'connectors')
    self.add('AUTH', 'accounts', 'cfd_tunnel', 'connections')
    self.add('AUTH', 'accounts', 'cfd_tunnel', 'management')
    self.add('AUTH', 'accounts', 'cfd_tunnel', 'token')
    self.add('AUTH', 'accounts', 'custom_pages')

    self.add('AUTH', 'accounts', 'dlp/datasets')
    self.add('AUTH', 'accounts', 'dlp/datasets', 'upload', content_type={'POST':'application/octet-stream'})
    self.add('AUTH', 'accounts', 'dlp/patterns/validate')
    self.add('AUTH', 'accounts', 'dlp/payload_log')
    self.add('AUTH', 'accounts', 'dlp/profiles')
    self.add('AUTH', 'accounts', 'dlp/profiles/custom')
    self.add('AUTH', 'accounts', 'dlp/profiles/predefined')

    self.add('AUTH', 'accounts', 'members')
    self.add('AUTH', 'accounts', 'mnm/config')
    self.add('AUTH', 'accounts', 'mnm/config/full')
    self.add('AUTH', 'accounts', 'mnm/rules')
    self.add('AUTH', 'accounts', 'mnm/rules', 'advertisement')
    self.add('AUTH', 'accounts', 'railguns')
    self.add('AUTH', 'accounts', 'railguns', 'connections')
    self.add('AUTH', 'accounts', 'registrar/domains')
    self.add('AUTH', 'accounts', 'registrar/contacts')
    self.add('AUTH', 'accounts', 'roles')
    self.add('AUTH', 'accounts', 'rules/lists')
    self.add('AUTH', 'accounts', 'rules/lists', 'items')
    self.add('AUTH', 'accounts', 'rules/lists/bulk_operations')
    self.add('AUTH', 'accounts', 'rulesets')
    self.add('AUTH', 'accounts', 'rulesets', 'versions')
    self.add('AUTH', 'accounts', 'rulesets', 'versions', 'by_tag')
    self.add('AUTH', 'accounts', 'rulesets', 'versions', 'by_tag/wordpress')
    self.add('AUTH', 'accounts', 'rulesets', 'rules')
#   self.add('AUTH', 'accounts', 'rulesets/import')
    self.add('AUTH', 'accounts', 'rulesets/phases', 'entrypoint')
    self.add('AUTH', 'accounts', 'rulesets/phases', 'entrypoint/versions')
    self.add('AUTH', 'accounts', 'rulesets/phases', 'versions')

    self.add('AUTH', 'accounts', 'rum/site_info')
    self.add('AUTH', 'accounts', 'rum/site_info/list')
    self.add('AUTH', 'accounts', 'rum/v2', 'rule')
    self.add('AUTH', 'accounts', 'rum/v2', 'rules')

    self.add('AUTH', 'accounts', 'storage/analytics')
    self.add('AUTH', 'accounts', 'storage/analytics/stored')
    self.add('AUTH', 'accounts', 'storage/kv/namespaces')
    self.add('AUTH', 'accounts', 'storage/kv/namespaces', 'bulk')
    self.add('AUTH', 'accounts', 'storage/kv/namespaces', 'keys')
    self.add('AUTH', 'accounts', 'storage/kv/namespaces', 'values', content_type={'PUT':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'storage/kv/namespaces', 'metadata')

    self.add('AUTH', 'accounts', 'subscriptions')
    self.add('AUTH', 'accounts', 'tunnels')
    self.add('AUTH', 'accounts', 'tunnels', 'connections')

    self.add('AUTH', 'accounts', 'vectorize/index')
    self.add('AUTH', 'accounts', 'vectorize/indexes')
    self.add('AUTH', 'accounts', 'vectorize/indexes', 'delete-by-ids')
    self.add('AUTH', 'accounts', 'vectorize/indexes', 'get-by-ids')
    self.add('AUTH', 'accounts', 'vectorize/indexes', 'insert', content_type={'POST':'application/x-ndjson'})
    self.add('AUTH', 'accounts', 'vectorize/indexes', 'query')
    self.add('AUTH', 'accounts', 'vectorize/indexes', 'upsert', content_type={'POST':'application/x-ndjson'})

    self.add('AUTH', 'accounts', 'virtual_dns')
    self.add('AUTH', 'accounts', 'virtual_dns', 'dns_analytics/report')
    self.add('AUTH', 'accounts', 'virtual_dns', 'dns_analytics/report/bytime')

    self.add('AUTH', 'accounts', 'workers/account-settings')
    self.add('AUTH', 'accounts', 'workers/deployments/by-script')
    self.add('AUTH', 'accounts', 'workers/deployments/by-script', 'detail')
    self.add('AUTH', 'accounts', 'workers/dispatch/namespaces')
    self.add('AUTH', 'accounts', 'workers/dispatch/namespaces', 'scripts')
    self.add('AUTH', 'accounts', 'workers/dispatch/namespaces', 'scripts', 'bindings')
    self.add('AUTH', 'accounts', 'workers/dispatch/namespaces', 'scripts', 'content', content_type={'PUT':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'workers/dispatch/namespaces', 'scripts', 'secrets')
    self.add('AUTH', 'accounts', 'workers/dispatch/namespaces', 'scripts', 'settings')
    self.add('AUTH', 'accounts', 'workers/dispatch/namespaces', 'scripts', 'tags')
    self.add('AUTH', 'accounts', 'workers/domains')
    self.add('AUTH', 'accounts', 'workers/durable_objects/namespaces')
    self.add('AUTH', 'accounts', 'workers/durable_objects/namespaces', 'objects')
    self.add('AUTH', 'accounts', 'workers/queues')
    self.add('AUTH', 'accounts', 'workers/queues', 'consumers')
    self.add('AUTH', 'accounts', 'workers/scripts')
    self.add('AUTH', 'accounts', 'workers/scripts', 'content', content_type={'PUT':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'workers/scripts', 'content/v2')
    self.add('AUTH', 'accounts', 'workers/scripts', 'deployments')
    self.add('AUTH', 'accounts', 'workers/scripts', 'schedules')
    self.add('AUTH', 'accounts', 'workers/scripts', 'script-settings')
    self.add('AUTH', 'accounts', 'workers/scripts', 'settings', content_type={'PATCH':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'workers/scripts', 'tails')
    self.add('AUTH', 'accounts', 'workers/scripts', 'usage-model')
    self.add('AUTH', 'accounts', 'workers/scripts', 'versions')
    self.add('AUTH', 'accounts', 'workers/services', 'environments', 'content', content_type={'PUT':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'workers/services', 'environments', 'settings')

    self.add('AUTH', 'accounts', 'workers/subdomain')

def accounts_addressing(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'addressing/address_maps')
    self.add('AUTH', 'accounts', 'addressing/address_maps', 'accounts')
    self.add('AUTH', 'accounts', 'addressing/address_maps', 'ips')
    self.add('AUTH', 'accounts', 'addressing/address_maps', 'zones')
    self.add('AUTH', 'accounts', 'addressing/loa_documents', content_type={'POST':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'addressing/loa_documents', 'download')
    self.add('AUTH', 'accounts', 'addressing/prefixes')
    self.add('AUTH', 'accounts', 'addressing/prefixes', 'bgp/prefixes')
    self.add('AUTH', 'accounts', 'addressing/prefixes', 'bgp/status')
    self.add('AUTH', 'accounts', 'addressing/prefixes', 'bindings')
    self.add('AUTH', 'accounts', 'addressing/prefixes', 'delegations')
    self.add('AUTH', 'accounts', 'addressing/services')

def accounts_audit_logs(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'audit_logs')

def accounts_load_balancers(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'load_balancers/preview')
    self.add('AUTH', 'accounts', 'load_balancers/monitors')
    self.add('AUTH', 'accounts', 'load_balancers/monitors', 'preview')
    self.add('AUTH', 'accounts', 'load_balancers/monitors', 'references')
    self.add('AUTH', 'accounts', 'load_balancers/pools')
    self.add('AUTH', 'accounts', 'load_balancers/pools', 'health')
    self.add('AUTH', 'accounts', 'load_balancers/pools', 'preview')
    self.add('AUTH', 'accounts', 'load_balancers/pools', 'references')
    self.add('AUTH', 'accounts', 'load_balancers/regions')
    self.add('AUTH', 'accounts', 'load_balancers/search')


def accounts_firewall(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'firewall/access_rules/rules')

def accounts_secondary_dns(self):
    """ :meta private: """

#   self.add('AUTH', 'accounts', 'secondary_dns/masters')
    self.add('AUTH', 'accounts', 'secondary_dns/primaries')
    self.add('AUTH', 'accounts', 'secondary_dns/tsigs')
    self.add('AUTH', 'accounts', 'secondary_dns/acls')
    self.add('AUTH', 'accounts', 'secondary_dns/peers')

def accounts_stream(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'stream')
    self.add('AUTH', 'accounts', 'stream', 'audio')
    self.add('AUTH', 'accounts', 'stream', 'audio/copy')
    self.add('AUTH', 'accounts', 'stream', 'captions', content_type={'PUT':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'stream', 'embed')
    self.add('AUTH', 'accounts', 'stream', 'downloads')
    self.add('AUTH', 'accounts', 'stream', 'token')
    self.add('AUTH', 'accounts', 'stream/clip')
    self.add('AUTH', 'accounts', 'stream/copy')
    self.add('AUTH', 'accounts', 'stream/direct_upload')
    self.add('AUTH', 'accounts', 'stream/keys')
#   self.add('AUTH', 'accounts', 'stream/preview')
    self.add('AUTH', 'accounts', 'stream/watermarks', content_type={'POST':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'stream/webhook')
    self.add('AUTH', 'accounts', 'stream/live_inputs')
    self.add('AUTH', 'accounts', 'stream/live_inputs', 'outputs')
    self.add('AUTH', 'accounts', 'stream/live_inputs', 'outputs', 'enabled')

def zones_media(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'media')
    self.add('AUTH', 'zones', 'media', 'embed')
    self.add('AUTH', 'zones', 'media', 'preview')

def memberships(self):
    """ :meta private: """

    self.add('AUTH', 'memberships')

def graphql(self):
    """ :meta private: """

    self.add('AUTH', 'graphql')

def zones_access(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'access/apps')
    self.add('AUTH', 'zones', 'access/apps', 'policies')
    self.add('AUTH', 'zones', 'access/apps', 'revoke_tokens')
    self.add('AUTH', 'zones', 'access/bookmarks')
    self.add('AUTH', 'zones', 'access/certificates')
    self.add('AUTH', 'zones', 'access/certificates/settings')
#   self.add('AUTH', 'zones', 'access/apps/ca')
    self.add('AUTH', 'zones', 'access/apps', 'ca')
    self.add('AUTH', 'zones', 'access/apps', 'user_policy_checks')
    self.add('AUTH', 'zones', 'access/groups')
    self.add('AUTH', 'zones', 'access/identity_providers')
    self.add('AUTH', 'zones', 'access/organizations')
    self.add('AUTH', 'zones', 'access/organizations/revoke_user')
    self.add('AUTH', 'zones', 'access/service_tokens')

def accounts_access(self):
    """ :meta private: """

#   self.add('AUTH', 'accounts', 'access/bookmarks') # deprecated 2023-03-19
    self.add('AUTH', 'accounts', 'access/custom_pages')
    self.add('AUTH', 'accounts', 'access/gateway_ca')
    self.add('AUTH', 'accounts', 'access/groups')
    self.add('AUTH', 'accounts', 'access/identity_providers')
    self.add('AUTH', 'accounts', 'access/organizations')
#   self.add('AUTH', 'accounts', 'access/organizations/doh') # deprecated 2020-02-04 - expired!
    self.add('AUTH', 'accounts', 'access/organizations/revoke_user')
    self.add('AUTH', 'accounts', 'access/service_tokens')
    self.add('AUTH', 'accounts', 'access/service_tokens', 'refresh')
    self.add('AUTH', 'accounts', 'access/service_tokens', 'rotate')
    self.add('AUTH', 'accounts', 'access/apps')
#   self.add('AUTH', 'accounts', 'access/apps/ca')
    self.add('AUTH', 'accounts', 'access/apps', 'ca')
    self.add('AUTH', 'accounts', 'access/apps', 'policies')
    self.add('AUTH', 'accounts', 'access/apps', 'revoke_tokens')
    self.add('AUTH', 'accounts', 'access/apps', 'user_policy_checks')
    self.add('AUTH', 'accounts', 'access/certificates')
    self.add('AUTH', 'accounts', 'access/certificates/settings')
    self.add('AUTH', 'accounts', 'access/keys')
    self.add('AUTH', 'accounts', 'access/keys/rotate')
    self.add('AUTH', 'accounts', 'access/logs/access_requests')
    self.add('AUTH', 'accounts', 'access/policies')
    self.add('AUTH', 'accounts', 'access/seats')
    self.add('AUTH', 'accounts', 'access/tags')
    self.add('AUTH', 'accounts', 'access/users')
    self.add('AUTH', 'accounts', 'access/users', 'failed_logins')
    self.add('AUTH', 'accounts', 'access/users', 'active_sessions')
    self.add('AUTH', 'accounts', 'access/users', 'last_seen_identity')


def accounts_diagnostics(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'diagnostics/traceroute')

def zones_waiting_rooms(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'waiting_rooms')
    self.add('AUTH', 'zones', 'waiting_rooms', 'events')
    self.add('AUTH', 'zones', 'waiting_rooms', 'events', 'details')
    self.add('AUTH', 'zones', 'waiting_rooms', 'rules')
    self.add('AUTH', 'zones', 'waiting_rooms', 'status')
    self.add('AUTH', 'zones', 'waiting_rooms/preview')
    self.add('AUTH', 'zones', 'waiting_rooms/settings')

def accounts_ai(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'ai-gateway/gateways')
    self.add('AUTH', 'accounts', 'ai-gateway/gateways', 'logs')

    self.add('AUTH', 'accounts', 'ai/authors/search')
    self.add('AUTH', 'accounts', 'ai/finetunes')
    self.add('AUTH', 'accounts', 'ai/finetunes', 'finetune-assets', content_type={'POST':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'ai/finetunes/public')
    self.add('AUTH', 'accounts', 'ai/models/search')

    self.add('AUTH', 'accounts', 'ai/run', content_type={'POST':['application/json','application/octet-stream']})

    self.add('AUTH', 'accounts', 'ai/run/@cf/baai/bge-base-en-v1.5')
    self.add('AUTH', 'accounts', 'ai/run/@cf/baai/bge-large-en-v1.5')
    self.add('AUTH', 'accounts', 'ai/run/@cf/baai/bge-small-en-v1.5')
    self.add('AUTH', 'accounts', 'ai/run/@cf/bytedance/stable-diffusion-xl-lightning')
    self.add('AUTH', 'accounts', 'ai/run/@cf/deepseek-ai/deepseek-coder-7b-instruct-v1.5')
    self.add('AUTH', 'accounts', 'ai/run/@cf/deepseek-ai/deepseek-math-7b-base')
    self.add('AUTH', 'accounts', 'ai/run/@cf/deepseek-ai/deepseek-math-7b-instruct')
    self.add('AUTH', 'accounts', 'ai/run/@cf/defog/sqlcoder-7b-2')
    self.add('AUTH', 'accounts', 'ai/run/@cf/facebook/bart-large-cnn')
    self.add('AUTH', 'accounts', 'ai/run/@cf/facebook/detr-resnet-50', content_type={'POST':'application/octet-stream'})
    self.add('AUTH', 'accounts', 'ai/run/@cf/fblgit/una-cybertron-7b-v2-bf16')
    self.add('AUTH', 'accounts', 'ai/run/@cf/google/gemma-2b-it-lora')
    self.add('AUTH', 'accounts', 'ai/run/@cf/google/gemma-7b-it-lora')
    self.add('AUTH', 'accounts', 'ai/run/@cf/huggingface/distilbert-sst-2-int8')
    self.add('AUTH', 'accounts', 'ai/run/@cf/inml/inml-roberta-dga')
    self.add('AUTH', 'accounts', 'ai/run/@cf/jpmorganchase/roberta-spam')
    self.add('AUTH', 'accounts', 'ai/run/@cf/llava-hf/llava-1.5-7b-hf')
    self.add('AUTH', 'accounts', 'ai/run/@cf/lykon/dreamshaper-8-lcm')
    self.add('AUTH', 'accounts', 'ai/run/@cf/m-a-p/opencodeinterpreter-ds-6.7b')
    self.add('AUTH', 'accounts', 'ai/run/@cf/meta-llama/llama-2-7b-chat-hf-lora')
    self.add('AUTH', 'accounts', 'ai/run/@cf/meta/detr-resnet-50')
    self.add('AUTH', 'accounts', 'ai/run/@cf/meta/llama-2-7b-chat-fp16')
    self.add('AUTH', 'accounts', 'ai/run/@cf/meta/llama-2-7b-chat-int8')
    self.add('AUTH', 'accounts', 'ai/run/@cf/meta/llama-3-8b-instruct')
    self.add('AUTH', 'accounts', 'ai/run/@cf/meta/m2m100-1.2b')
    self.add('AUTH', 'accounts', 'ai/run/@cf/microsoft/phi-2')
    self.add('AUTH', 'accounts', 'ai/run/@cf/microsoft/phi-3-mini-4k-instruct')
    self.add('AUTH', 'accounts', 'ai/run/@cf/microsoft/resnet-50', content_type={'POST':'application/octet-stream'})
    self.add('AUTH', 'accounts', 'ai/run/@cf/mistral/mistral-7b-instruct-v0.1')
    self.add('AUTH', 'accounts', 'ai/run/@cf/mistral/mistral-7b-instruct-v0.1-vllm')
    self.add('AUTH', 'accounts', 'ai/run/@cf/mistral/mistral-7b-instruct-v0.2-lora')
    self.add('AUTH', 'accounts', 'ai/run/@cf/mistral/mixtral-8x7b-instruct-v0.1-awq')
    self.add('AUTH', 'accounts', 'ai/run/@cf/nexaaidev/octopus-v2')
    self.add('AUTH', 'accounts', 'ai/run/@cf/openai/whisper', content_type={'POST':'application/octet-stream'})
    self.add('AUTH', 'accounts', 'ai/run/@cf/openai/whisper-sherpa', content_type={'POST':'application/octet-stream'})
    self.add('AUTH', 'accounts', 'ai/run/@cf/openai/whisper-tiny-en', content_type={'POST':'application/octet-stream'})
    self.add('AUTH', 'accounts', 'ai/run/@cf/openchat/openchat-3.5-0106')
    self.add('AUTH', 'accounts', 'ai/run/@cf/qwen/qwen1.5-0.5b-chat')
    self.add('AUTH', 'accounts', 'ai/run/@cf/qwen/qwen1.5-1.8b-chat')
    self.add('AUTH', 'accounts', 'ai/run/@cf/qwen/qwen1.5-14b-chat-awq')
    self.add('AUTH', 'accounts', 'ai/run/@cf/qwen/qwen1.5-7b-chat-awq')
    self.add('AUTH', 'accounts', 'ai/run/@cf/runwayml/stable-diffusion-v1-5-img2img')
    self.add('AUTH', 'accounts', 'ai/run/@cf/runwayml/stable-diffusion-v1-5-inpainting')
    self.add('AUTH', 'accounts', 'ai/run/@cf/stabilityai/stable-diffusion-xl-base-1.0')
    self.add('AUTH', 'accounts', 'ai/run/@cf/stabilityai/stable-diffusion-xl-turbo')
    self.add('AUTH', 'accounts', 'ai/run/@cf/sven/test')
    self.add('AUTH', 'accounts', 'ai/run/@cf/thebloke/discolm-german-7b-v1-awq')
    self.add('AUTH', 'accounts', 'ai/run/@cf/thebloke/yarn-mistral-7b-64k-awq')
    self.add('AUTH', 'accounts', 'ai/run/@cf/tiiuae/falcon-7b-instruct')
    self.add('AUTH', 'accounts', 'ai/run/@cf/tinyllama/tinyllama-1.1b-chat-v1.0')
    self.add('AUTH', 'accounts', 'ai/run/@cf/unum/uform-gen2-qwen-500m')

    self.add('AUTH', 'accounts', 'ai/run/@hf/baai/bge-base-en-v1.5')
    self.add('AUTH', 'accounts', 'ai/run/@hf/baai/bge-m3')
    self.add('AUTH', 'accounts', 'ai/run/@hf/google/gemma-7b-it')
    self.add('AUTH', 'accounts', 'ai/run/@hf/meta-llama/meta-llama-3-8b-instruct')
    self.add('AUTH', 'accounts', 'ai/run/@hf/mistral/mistral-7b-instruct-v0.2')
    self.add('AUTH', 'accounts', 'ai/run/@hf/nexusflow/starling-lm-7b-beta')
    self.add('AUTH', 'accounts', 'ai/run/@hf/nousresearch/hermes-2-pro-mistral-7b')
    self.add('AUTH', 'accounts', 'ai/run/@hf/sentence-transformers/all-minilm-l6-v2')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/codellama-7b-instruct-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/deepseek-coder-6.7b-base-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/deepseek-coder-6.7b-instruct-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/llama-2-13b-chat-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/llamaguard-7b-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/mistral-7b-instruct-v0.1-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/neural-chat-7b-v3-1-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/openchat_3.5-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/openhermes-2.5-mistral-7b-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/orca-2-13b-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/starling-lm-7b-alpha-awq')
    self.add('AUTH', 'accounts', 'ai/run/@hf/thebloke/zephyr-7b-beta-awq')

    self.add('AUTH', 'accounts', 'ai/run/proxy')
    self.add('AUTH', 'accounts', 'ai/tasks/search')

def accounts_extras(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'alerting/v3/available_alerts')
    self.add('AUTH', 'accounts', 'alerting/v3/destinations/eligible')
    self.add('AUTH', 'accounts', 'alerting/v3/destinations/pagerduty')
    self.add('AUTH', 'accounts', 'alerting/v3/destinations/pagerduty/connect')
    self.add('AUTH', 'accounts', 'alerting/v3/destinations/webhooks')
    self.add('AUTH', 'accounts', 'alerting/v3/history')
    self.add('AUTH', 'accounts', 'alerting/v3/policies')

    self.add('AUTH', 'accounts', 'calls/apps')
    self.add('AUTH', 'accounts', 'calls/turn_keys')

    self.add('AUTH', 'accounts', 'custom_ns')
    self.add('AUTH', 'accounts', 'custom_ns/availability')
    self.add('AUTH', 'accounts', 'custom_ns/verify')

    self.add('AUTH', 'accounts', 'devices')
    self.add('AUTH', 'accounts', 'devices', 'override_codes')
    self.add('AUTH', 'accounts', 'devices/dex_tests')
    self.add('AUTH', 'accounts', 'devices/networks')
    self.add('AUTH', 'accounts', 'devices/policies')
    self.add('AUTH', 'accounts', 'devices/policy')
    self.add('AUTH', 'accounts', 'devices/policy', 'exclude')
#   self.add('AUTH', 'accounts', 'devices/policy/exclude')
    self.add('AUTH', 'accounts', 'devices/policy', 'fallback_domains')
#   self.add('AUTH', 'accounts', 'devices/policy/fallback_domains')
    self.add('AUTH', 'accounts', 'devices/policy', 'include')
#   self.add('AUTH', 'accounts', 'devices/policy/include')
    self.add('AUTH', 'accounts', 'devices/posture')
    self.add('AUTH', 'accounts', 'devices/posture/integration')
    self.add('AUTH', 'accounts', 'devices/revoke')
    self.add('AUTH', 'accounts', 'devices/settings')
    self.add('AUTH', 'accounts', 'devices/unrevoke')

    self.add('AUTH', 'accounts', 'dex/colos')
    self.add('AUTH', 'accounts', 'dex/fleet-status/devices')
    self.add('AUTH', 'accounts', 'dex/fleet-status/live')
    self.add('AUTH', 'accounts', 'dex/fleet-status/over-time')
    self.add('AUTH', 'accounts', 'dex/http-tests')
    self.add('AUTH', 'accounts', 'dex/http-tests', 'percentiles')
    self.add('AUTH', 'accounts', 'dex/tests')
    self.add('AUTH', 'accounts', 'dex/tests/unique-devices')
    self.add('AUTH', 'accounts', 'dex/traceroute-test-results', 'network-path')
    self.add('AUTH', 'accounts', 'dex/traceroute-tests')
    self.add('AUTH', 'accounts', 'dex/traceroute-tests', 'network-path')
    self.add('AUTH', 'accounts', 'dex/traceroute-tests', 'percentiles')

    self.add('AUTH', 'accounts', 'dns_firewall')
    self.add('AUTH', 'accounts', 'dns_firewall', 'dns_analytics/report')
    self.add('AUTH', 'accounts', 'dns_firewall', 'dns_analytics/report/bytime')

    self.add('AUTH', 'accounts', 'gateway')
    self.add('AUTH', 'accounts', 'gateway/app_types')
    self.add('AUTH', 'accounts', 'gateway/audit_ssh_settings')
    self.add('AUTH', 'accounts', 'gateway/categories')
    self.add('AUTH', 'accounts', 'gateway/configuration')
    self.add('AUTH', 'accounts', 'gateway/lists')
    self.add('AUTH', 'accounts', 'gateway/lists', 'items')
    self.add('AUTH', 'accounts', 'gateway/locations')
    self.add('AUTH', 'accounts', 'gateway/logging')
    self.add('AUTH', 'accounts', 'gateway/proxy_endpoints')
    self.add('AUTH', 'accounts', 'gateway/rules')

    self.add('AUTH', 'accounts', 'images/v1', content_type={'POST':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'images/v1', 'blob')
    self.add('AUTH', 'accounts', 'images/v1/config')
    self.add('AUTH', 'accounts', 'images/v1/keys')
    self.add('AUTH', 'accounts', 'images/v1/stats')
    self.add('AUTH', 'accounts', 'images/v1/variants')
    self.add('AUTH', 'accounts', 'images/v2')
    self.add('AUTH', 'accounts', 'images/v2/direct_upload', content_type={'POST':'multipart/form-data'})

    self.add('AUTH', 'accounts', 'intel-phishing/predict')
    self.add('AUTH', 'accounts', 'intel/asn')
    self.add('AUTH', 'accounts', 'intel/asn', 'subnets')
    self.add('AUTH', 'accounts', 'intel/attack-surface-report', 'dismiss')
    self.add('AUTH', 'accounts', 'intel/attack-surface-report/issue-types')
    self.add('AUTH', 'accounts', 'intel/attack-surface-report/issues')
    self.add('AUTH', 'accounts', 'intel/attack-surface-report/issues/class')
    self.add('AUTH', 'accounts', 'intel/attack-surface-report/issues/severity')
    self.add('AUTH', 'accounts', 'intel/attack-surface-report/issues/type')
    self.add('AUTH', 'accounts', 'intel/dns')
    self.add('AUTH', 'accounts', 'intel/domain')
    self.add('AUTH', 'accounts', 'intel/domain-history')
    self.add('AUTH', 'accounts', 'intel/domain/bulk')
    self.add('AUTH', 'accounts', 'intel/indicator-feeds')
    self.add('AUTH', 'accounts', 'intel/indicator-feeds', 'data')
    self.add('AUTH', 'accounts', 'intel/indicator-feeds', 'snapshot', content_type={'PUT':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'intel/indicator-feeds/permissions/add')
    self.add('AUTH', 'accounts', 'intel/indicator-feeds/permissions/remove')
    self.add('AUTH', 'accounts', 'intel/indicator-feeds/permissions/view')
    self.add('AUTH', 'accounts', 'intel/ip')
    self.add('AUTH', 'accounts', 'intel/ip-list')
    self.add('AUTH', 'accounts', 'intel/miscategorization')
    self.add('AUTH', 'accounts', 'intel/sinkholes')
    self.add('AUTH', 'accounts', 'intel/whois')

    self.add('AUTH', 'accounts', 'magic/cf_interconnects')
    self.add('AUTH', 'accounts', 'magic/gre_tunnels')
    self.add('AUTH', 'accounts', 'magic/ipsec_tunnels')
    self.add('AUTH', 'accounts', 'magic/ipsec_tunnels', 'psk_generate')
    self.add('AUTH', 'accounts', 'magic/routes')
    self.add('AUTH', 'accounts', 'magic/sites')
    self.add('AUTH', 'accounts', 'magic/sites', 'acls')
    self.add('AUTH', 'accounts', 'magic/sites', 'lans')
    self.add('AUTH', 'accounts', 'magic/sites', 'wans')

    self.add('AUTH', 'accounts', 'pages/projects')
    self.add('AUTH', 'accounts', 'pages/projects', 'deployments', content_type={'POST':'multipart/form-data'})
    self.add('AUTH', 'accounts', 'pages/projects', 'deployments', 'history/logs')
    self.add('AUTH', 'accounts', 'pages/projects', 'deployments', 'retry')
    self.add('AUTH', 'accounts', 'pages/projects', 'deployments', 'rollback')
    self.add('AUTH', 'accounts', 'pages/projects', 'domains')
    self.add('AUTH', 'accounts', 'pages/projects', 'purge_build_cache')

    self.add('AUTH', 'accounts', 'pcaps')
    self.add('AUTH', 'accounts', 'pcaps', 'download')
    self.add('AUTH', 'accounts', 'pcaps/ownership')
    self.add('AUTH', 'accounts', 'pcaps/ownership/validate')

    self.add('AUTH', 'accounts', 'queues')
    self.add('AUTH', 'accounts', 'queues', 'consumers')
    self.add('AUTH', 'accounts', 'queues', 'messages/ack')
    self.add('AUTH', 'accounts', 'queues', 'messages/pull')

    self.add('AUTH', 'accounts', 'teamnet/routes')
    self.add('AUTH', 'accounts', 'teamnet/routes/ip')
    self.add('AUTH', 'accounts', 'teamnet/routes/network')
    self.add('AUTH', 'accounts', 'teamnet/virtual_networks')

    self.add('AUTH', 'accounts', 'urlscanner/scan')
    self.add('AUTH', 'accounts', 'urlscanner/scan', 'har')
    self.add('AUTH', 'accounts', 'urlscanner/scan', 'screenshot')

    self.add('AUTH', 'accounts', 'hyperdrive/configs')

    self.add('AUTH', 'accounts', 'warp_connector')
    self.add('AUTH', 'accounts', 'warp_connector', 'token')

    self.add('AUTH', 'accounts', 'zerotrust/connectivity_settings')

    self.add('AUTH', 'accounts', 'd1/database')
    self.add('AUTH', 'accounts', 'd1/database', 'query')

    self.add('AUTH', 'accounts', 'zt_risk_scoring')
    self.add('AUTH', 'accounts', 'zt_risk_scoring', 'reset')
    self.add('AUTH', 'accounts', 'zt_risk_scoring/behaviors')
    self.add('AUTH', 'accounts', 'zt_risk_scoring/summary')

def zones_extras(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'acm/total_tls')

    self.add('AUTH', 'zones', 'cache/cache_reserve')
    self.add('AUTH', 'zones', 'cache/cache_reserve_clear')
    self.add('AUTH', 'zones', 'cache/origin_post_quantum_encryption')
    self.add('AUTH', 'zones', 'cache/regional_tiered_cache')
    self.add('AUTH', 'zones', 'cache/tiered_cache_smart_topology_enable')
    self.add('AUTH', 'zones', 'cache/variants')

    self.add('AUTH', 'zones', 'managed_headers')
    self.add('AUTH', 'zones', 'page_shield')
    self.add('AUTH', 'zones', 'page_shield/policies')
    self.add('AUTH', 'zones', 'page_shield/scripts')
    self.add('AUTH', 'zones', 'page_shield/connections')
    self.add('AUTH', 'zones', 'rulesets')
    self.add('AUTH', 'zones', 'rulesets', 'rules')
    self.add('AUTH', 'zones', 'rulesets', 'versions')
    self.add('AUTH', 'zones', 'rulesets/phases', 'entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases', 'entrypoint/versions')
    self.add('AUTH', 'zones', 'rulesets/phases', 'versions')
    self.add('AUTH', 'zones', 'rulesets/phases/http_custom_errors/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_config_settings/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_request_dynamic_redirect/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_request_origin/entrypoint')

    self.add('AUTH', 'zones', 'url_normalization')

    self.add('AUTH', 'zones', 'hostnames/settings')
    self.add('AUTH', 'zones', 'snippets', content_type={'PUT':'multipart/form-data'})
    self.add('AUTH', 'zones', 'snippets', 'content')
    self.add('AUTH', 'zones', 'snippets/snippet_rules')

    self.add('AUTH', 'zones', 'speed_api/availabilities')
    self.add('AUTH', 'zones', 'speed_api/pages')
    self.add('AUTH', 'zones', 'speed_api/pages', 'tests')
    self.add('AUTH', 'zones', 'speed_api/pages', 'trend')
    self.add('AUTH', 'zones', 'speed_api/schedule')

    self.add('AUTH', 'zones', 'dcv_delegation/uuid')

def zones_web3(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'web3/hostnames')
    self.add('AUTH', 'zones', 'web3/hostnames', 'ipfs_universal_path/content_list')
    self.add('AUTH', 'zones', 'web3/hostnames', 'ipfs_universal_path/content_list/entries')

def accounts_email(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'email/routing/addresses')

def accounts_r2(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'r2/buckets')
    self.add('AUTH', 'accounts', 'r2/buckets', 'usage')
    self.add('AUTH', 'accounts', 'r2/buckets', 'objects')
    self.add('AUTH', 'accounts', 'r2/buckets', 'sippy')

    self.add('AUTH', 'accounts', 'event_notifications/r2', 'configuration')
    self.add('AUTH', 'accounts', 'event_notifications/r2', 'configuration/queues')


def zones_email(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'email/routing')
    self.add('AUTH', 'zones', 'email/routing/disable')
    self.add('AUTH', 'zones', 'email/routing/dns')
    self.add('AUTH', 'zones', 'email/routing/enable')
    self.add('AUTH', 'zones', 'email/routing/rules')
    self.add('AUTH', 'zones', 'email/routing/rules/catch_all')

def zones_api_gateway(self):
    """ :meta private: """

    self.add('AUTH', 'zones', 'api_gateway/configuration')
    self.add('AUTH', 'zones', 'api_gateway/discovery')
    self.add('AUTH', 'zones', 'api_gateway/discovery/operations')
    self.add('AUTH', 'zones', 'api_gateway/operations')
    self.add('AUTH', 'zones', 'api_gateway/operations', 'schema_validation')
#   self.add('AUTH', 'zones', 'api_gateway/operations/schema_validation')
    self.add('AUTH', 'zones', 'api_gateway/schemas')
    self.add('AUTH', 'zones', 'api_gateway/settings/schema_validation')
    self.add('AUTH', 'zones', 'api_gateway/user_schemas', content_type={'POST':'multipart/form-data'})
    self.add('AUTH', 'zones', 'api_gateway/user_schemas', 'operations')

def radar(self):
    """ :meta private: """

    self.add('AUTH', 'radar/alerts')
    self.add('AUTH', 'radar/alerts/locations')
    self.add('AUTH', 'radar/annotations/outages')
    self.add('AUTH', 'radar/annotations/outages/locations')

    self.add('AUTH', 'radar/datasets')
    self.add('AUTH', 'radar/datasets/download')

    self.add('AUTH', 'radar/dns/top/ases')
    self.add('AUTH', 'radar/dns/top/locations')

    self.add('AUTH', 'radar/entities/asns')
    self.add('AUTH', 'radar/entities/asns', 'rel')
    self.add('AUTH', 'radar/entities/asns/ip')
    self.add('AUTH', 'radar/entities/ip')
    self.add('AUTH', 'radar/entities/locations')

    self.add('AUTH', 'radar/netflows/timeseries')
    self.add('AUTH', 'radar/netflows/top/ases')
    self.add('AUTH', 'radar/netflows/top/locations')

    self.add('AUTH', 'radar/performance/iqi/summary')
    self.add('AUTH', 'radar/performance/iqi/timeseries_groups')

    self.add('AUTH', 'radar/quality/iqi/summary')
    self.add('AUTH', 'radar/quality/iqi/timeseries_groups')
    self.add('AUTH', 'radar/quality/speed/histogram')
    self.add('AUTH', 'radar/quality/speed/summary')
    self.add('AUTH', 'radar/quality/speed/top/ases')
    self.add('AUTH', 'radar/quality/speed/top/locations')

    self.add('AUTH', 'radar/ranking/domain')
    self.add('AUTH', 'radar/ranking/timeseries')
    self.add('AUTH', 'radar/ranking/timeseries_groups')
    self.add('AUTH', 'radar/ranking/top')

    self.add('AUTH', 'radar/search/global')

    self.add('AUTH', 'radar/specialevents')

    self.add('AUTH', 'radar/verified_bots/top/bots')
    self.add('AUTH', 'radar/verified_bots/top/categories')

    self.add('AUTH', 'radar/connection_tampering/summary')
    self.add('AUTH', 'radar/connection_tampering/timeseries_groups')
    self.add('AUTH', 'radar/traffic_anomalies')
    self.add('AUTH', 'radar/traffic_anomalies/locations')

def radar_as112(self):
    """ :meta private: """

    self.add('AUTH', 'radar/as112/summary/dnssec')
    self.add('AUTH', 'radar/as112/summary/edns')
    self.add('AUTH', 'radar/as112/summary/ip_version')
    self.add('AUTH', 'radar/as112/summary/protocol')
    self.add('AUTH', 'radar/as112/summary/query_type')
    self.add('AUTH', 'radar/as112/summary/response_codes')

    self.add('AUTH', 'radar/as112/timeseries')
    self.add('AUTH', 'radar/as112/timeseries/dnssec')
    self.add('AUTH', 'radar/as112/timeseries/edns')
    self.add('AUTH', 'radar/as112/timeseries/ip_version')
    self.add('AUTH', 'radar/as112/timeseries/protocol')
    self.add('AUTH', 'radar/as112/timeseries/query_type')
    self.add('AUTH', 'radar/as112/timeseries/response_codes')

    self.add('AUTH', 'radar/as112/timeseries_groups/dnssec')
    self.add('AUTH', 'radar/as112/timeseries_groups/edns')
    self.add('AUTH', 'radar/as112/timeseries_groups/ip_version')
    self.add('AUTH', 'radar/as112/timeseries_groups/protocol')
    self.add('AUTH', 'radar/as112/timeseries_groups/query_type')
    self.add('AUTH', 'radar/as112/timeseries_groups/response_codes')

    self.add('AUTH', 'radar/as112/top/locations')
    self.add('AUTH', 'radar/as112/top/locations/dnssec')
    self.add('AUTH', 'radar/as112/top/locations/edns')
    self.add('AUTH', 'radar/as112/top/locations/ip_version')

def radar_attacks(self):
    """ :meta private: """

    self.add('AUTH', 'radar/attacks/layer3/summary')
    self.add('AUTH', 'radar/attacks/layer3/timeseries')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups')
    self.add('AUTH', 'radar/attacks/layer3/summary/bitrate')
    self.add('AUTH', 'radar/attacks/layer3/summary/duration')
    self.add('AUTH', 'radar/attacks/layer3/summary/ip_version')
    self.add('AUTH', 'radar/attacks/layer3/summary/protocol')
    self.add('AUTH', 'radar/attacks/layer3/summary/vector')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups/bitrate')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups/duration')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups/industry')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups/ip_version')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups/protocol')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups/vector')
    self.add('AUTH', 'radar/attacks/layer3/timeseries_groups/vertical')
    self.add('AUTH', 'radar/attacks/layer3/top/attacks')
    self.add('AUTH', 'radar/attacks/layer3/top/industry')
    self.add('AUTH', 'radar/attacks/layer3/top/locations/origin')
    self.add('AUTH', 'radar/attacks/layer3/top/locations/target')
    self.add('AUTH', 'radar/attacks/layer3/top/vertical')

    self.add('AUTH', 'radar/attacks/layer7/summary')
    self.add('AUTH', 'radar/attacks/layer7/summary/http_method')
    self.add('AUTH', 'radar/attacks/layer7/summary/http_version')
    self.add('AUTH', 'radar/attacks/layer7/summary/ip_version')
    self.add('AUTH', 'radar/attacks/layer7/summary/managed_rules')
    self.add('AUTH', 'radar/attacks/layer7/summary/mitigation_product')
    self.add('AUTH', 'radar/attacks/layer7/timeseries')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups/http_method')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups/http_version')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups/industry')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups/ip_version')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups/managed_rules')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups/mitigation_product')
    self.add('AUTH', 'radar/attacks/layer7/timeseries_groups/vertical')
    self.add('AUTH', 'radar/attacks/layer7/top/ases/origin')
    self.add('AUTH', 'radar/attacks/layer7/top/attacks')
    self.add('AUTH', 'radar/attacks/layer7/top/industry')
    self.add('AUTH', 'radar/attacks/layer7/top/locations/origin')
    self.add('AUTH', 'radar/attacks/layer7/top/locations/target')
    self.add('AUTH', 'radar/attacks/layer7/top/vertical')

def radar_bgp(self):
    """ :meta private: """

    self.add('AUTH', 'radar/bgp/leaks/events')
    self.add('AUTH', 'radar/bgp/timeseries')
    self.add('AUTH', 'radar/bgp/top/ases')
    self.add('AUTH', 'radar/bgp/top/ases/prefixes')
    self.add('AUTH', 'radar/bgp/top/prefixes')
    self.add('AUTH', 'radar/bgp/hijacks/events')
    self.add('AUTH', 'radar/bgp/routes/moas')
    self.add('AUTH', 'radar/bgp/routes/pfx2as')
    self.add('AUTH', 'radar/bgp/routes/stats')
    self.add('AUTH', 'radar/bgp/routes/timeseries')

def radar_email(self):
    """ :meta private: """

    self.add('AUTH', 'radar/email/routing/summary/arc')
    self.add('AUTH', 'radar/email/routing/summary/dkim')
    self.add('AUTH', 'radar/email/routing/summary/dmarc')
    self.add('AUTH', 'radar/email/routing/summary/encrypted')
    self.add('AUTH', 'radar/email/routing/summary/ip_version')
    self.add('AUTH', 'radar/email/routing/summary/spf')
    self.add('AUTH', 'radar/email/routing/timeseries_groups/arc')
    self.add('AUTH', 'radar/email/routing/timeseries_groups/dkim')
    self.add('AUTH', 'radar/email/routing/timeseries_groups/dmarc')
    self.add('AUTH', 'radar/email/routing/timeseries_groups/encrypted')
    self.add('AUTH', 'radar/email/routing/timeseries_groups/ip_version')
    self.add('AUTH', 'radar/email/routing/timeseries_groups/spf')

    self.add('AUTH', 'radar/email/security/summary/arc')
    self.add('AUTH', 'radar/email/security/summary/dkim')
    self.add('AUTH', 'radar/email/security/summary/dmarc')
    self.add('AUTH', 'radar/email/security/summary/malicious')
    self.add('AUTH', 'radar/email/security/summary/spam')
    self.add('AUTH', 'radar/email/security/summary/spf')
    self.add('AUTH', 'radar/email/security/summary/spoof')
    self.add('AUTH', 'radar/email/security/summary/threat_category')
    self.add('AUTH', 'radar/email/security/summary/tls_version')

    self.add('AUTH', 'radar/email/security/timeseries/arc')
    self.add('AUTH', 'radar/email/security/timeseries/dkim')
    self.add('AUTH', 'radar/email/security/timeseries/dmarc')
    self.add('AUTH', 'radar/email/security/timeseries/malicious')
    self.add('AUTH', 'radar/email/security/timeseries/spam')
    self.add('AUTH', 'radar/email/security/timeseries/spf')
    self.add('AUTH', 'radar/email/security/timeseries/threat_category')
    self.add('AUTH', 'radar/email/security/timeseries_groups/arc')
    self.add('AUTH', 'radar/email/security/timeseries_groups/dkim')
    self.add('AUTH', 'radar/email/security/timeseries_groups/dmarc')
    self.add('AUTH', 'radar/email/security/timeseries_groups/malicious')
    self.add('AUTH', 'radar/email/security/timeseries_groups/spam')
    self.add('AUTH', 'radar/email/security/timeseries_groups/spf')
    self.add('AUTH', 'radar/email/security/timeseries_groups/spoof')
    self.add('AUTH', 'radar/email/security/timeseries_groups/threat_category')
    self.add('AUTH', 'radar/email/security/timeseries_groups/tls_version')

    self.add('AUTH', 'radar/email/security/top/ases')
    self.add('AUTH', 'radar/email/security/top/ases/arc')
    self.add('AUTH', 'radar/email/security/top/ases/dkim')
    self.add('AUTH', 'radar/email/security/top/ases/dmarc')
    self.add('AUTH', 'radar/email/security/top/ases/malicious')
    self.add('AUTH', 'radar/email/security/top/ases/spam')
    self.add('AUTH', 'radar/email/security/top/ases/spf')
    self.add('AUTH', 'radar/email/security/top/locations')
    self.add('AUTH', 'radar/email/security/top/locations/arc')
    self.add('AUTH', 'radar/email/security/top/locations/dkim')
    self.add('AUTH', 'radar/email/security/top/locations/dmarc')
    self.add('AUTH', 'radar/email/security/top/locations/malicious')
    self.add('AUTH', 'radar/email/security/top/locations/spam')
    self.add('AUTH', 'radar/email/security/top/locations/spf')
    self.add('AUTH', 'radar/email/security/top/tlds')
    self.add('AUTH', 'radar/email/security/top/tlds/malicious')
    self.add('AUTH', 'radar/email/security/top/tlds/spam')
    self.add('AUTH', 'radar/email/security/top/tlds/spoof')

def radar_http(self):
    """ :meta private: """


    self.add('AUTH', 'radar/http/summary/bot_class')
    self.add('AUTH', 'radar/http/summary/device_type')
    self.add('AUTH', 'radar/http/summary/http_protocol')
    self.add('AUTH', 'radar/http/summary/http_version')
    self.add('AUTH', 'radar/http/summary/ip_version')
    self.add('AUTH', 'radar/http/summary/os')
    self.add('AUTH', 'radar/http/summary/post_quantum')
    self.add('AUTH', 'radar/http/summary/tls_version')

    self.add('AUTH', 'radar/http/timeseries/bot_class')
    self.add('AUTH', 'radar/http/timeseries/browser')
    self.add('AUTH', 'radar/http/timeseries/browser_family')
    self.add('AUTH', 'radar/http/timeseries/device_type')
    self.add('AUTH', 'radar/http/timeseries/http_protocol')
    self.add('AUTH', 'radar/http/timeseries/http_version')
    self.add('AUTH', 'radar/http/timeseries/ip_version')
    self.add('AUTH', 'radar/http/timeseries/os')
    self.add('AUTH', 'radar/http/timeseries/tls_version')

    self.add('AUTH', 'radar/http/timeseries_groups/bot_class')
    self.add('AUTH', 'radar/http/timeseries_groups/browser')
    self.add('AUTH', 'radar/http/timeseries_groups/browser_family')
    self.add('AUTH', 'radar/http/timeseries_groups/device_type')
    self.add('AUTH', 'radar/http/timeseries_groups/http_protocol')
    self.add('AUTH', 'radar/http/timeseries_groups/http_version')
    self.add('AUTH', 'radar/http/timeseries_groups/ip_version')
    self.add('AUTH', 'radar/http/timeseries_groups/os')
    self.add('AUTH', 'radar/http/timeseries_groups/post_quantum')
    self.add('AUTH', 'radar/http/timeseries_groups/tls_version')

    self.add('AUTH', 'radar/http/top/ases')
    self.add('AUTH', 'radar/http/top/ases/bot_class')
    self.add('AUTH', 'radar/http/top/ases/browser_family')
    self.add('AUTH', 'radar/http/top/ases/device_type')
    self.add('AUTH', 'radar/http/top/ases/http_protocol')
    self.add('AUTH', 'radar/http/top/ases/http_version')
    self.add('AUTH', 'radar/http/top/ases/ip_version')
    self.add('AUTH', 'radar/http/top/ases/os')
    self.add('AUTH', 'radar/http/top/ases/tls_version')
    self.add('AUTH', 'radar/http/top/browsers')
    self.add('AUTH', 'radar/http/top/browser_families')
    self.add('AUTH', 'radar/http/top/locations')
    self.add('AUTH', 'radar/http/top/locations/bot_class')
    self.add('AUTH', 'radar/http/top/locations/browser_family')
    self.add('AUTH', 'radar/http/top/locations/device_type')
    self.add('AUTH', 'radar/http/top/locations/http_protocol')
    self.add('AUTH', 'radar/http/top/locations/http_version')
    self.add('AUTH', 'radar/http/top/locations/ip_version')
    self.add('AUTH', 'radar/http/top/locations/os')
    self.add('AUTH', 'radar/http/top/locations/tls_version')

def from_developers(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'analytics_engine/sql')

    self.add('AUTH', 'accounts', 'logpush/jobs')
    self.add('AUTH', 'accounts', 'logpush/datasets', 'fields')
    self.add('AUTH', 'accounts', 'logpush/datasets', 'jobs')
    self.add('AUTH', 'accounts', 'logpush/ownership')
    self.add('AUTH', 'accounts', 'logpush/ownership/validate')
    self.add('AUTH', 'accounts', 'logpush/validate/destination/exists')
    self.add('AUTH', 'accounts', 'logpush/validate/origin')

    self.add('AUTH', 'accounts', 'logs/retrieve')
    self.add('AUTH', 'accounts', 'logs/control/cmb/config')

    self.add('AUTH', 'accounts', 'magic/advanced_tcp_protection/configs/allowlist')
    self.add('AUTH', 'accounts', 'magic/advanced_tcp_protection/configs/prefixes')
    self.add('AUTH', 'accounts', 'magic/advanced_tcp_protection/configs/prefixes/bulk')
    self.add('AUTH', 'accounts', 'magic/advanced_tcp_protection/configs/syn_protection/rules')
    self.add('AUTH', 'accounts', 'magic/advanced_tcp_protection/configs/tcp_flow_protection/rules')
    self.add('AUTH', 'accounts', 'magic/advanced_tcp_protection/configs/tcp_protection_status')

    self.add('AUTH', 'accounts', 'pubsub/namespaces')
    self.add('AUTH', 'accounts', 'pubsub/namespaces', 'brokers')
    self.add('AUTH', 'accounts', 'pubsub/namespaces', 'brokers', 'credentials')

    self.add('AUTH', 'accounts', 'rulesets/phases/ddos_l4/entrypoint')
    self.add('AUTH', 'accounts', 'rulesets/phases/ddos_l7/entrypoint')
    self.add('AUTH', 'accounts', 'rulesets/phases/http_request_firewall_custom/entrypoint')
    self.add('AUTH', 'accounts', 'rulesets/phases/http_request_firewall_managed/entrypoint')

    self.add('AUTH', 'accounts', 'stream', 'captions', 'vtt')
    self.add('AUTH', 'accounts', 'stream/analytics/views')
    self.add('AUTH', 'accounts', 'stream/live_inputs', 'videos')
    self.add('AUTH', 'accounts', 'stream/storage-usage')

#   self.add('AUTH', 'organizations', 'load_balancers/monitors')

    self.add('AUTH', 'users')

    self.add('AUTH', 'zones', 'content-upload-scan/disable')
    self.add('AUTH', 'zones', 'content-upload-scan/enable')
    self.add('AUTH', 'zones', 'content-upload-scan/payloads')
    self.add('AUTH', 'zones', 'content-upload-scan/settings')

    self.add('AUTH', 'zones', 'phases/http_request_firewall_managed/entrypoint')

    self.add('AUTH', 'zones', 'rulesets/phases/ddos_l7/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_ratelimit/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_request_cache_settings/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_request_firewall_custom/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_request_firewall_managed/entrypoint')
    self.add('AUTH', 'zones', 'rulesets/phases/http_request_firewall_managed/entrypoint/versions')

    self.add('AUTH', 'zones', 'certificate_authorities/hostname_associations')
    self.add('AUTH', 'zones', 'hold')

    self.add('AUTH', 'accounts', 'challenges/widgets')
    self.add('AUTH', 'accounts', 'challenges/widgets', 'rotate_secret')
    self.add('AUTH', 'accounts', 'mtls_certificates')
    self.add('AUTH', 'accounts', 'mtls_certificates', 'associations')
    self.add('AUTH', 'accounts', 'request-tracer/trace')

def accounts_cloudforce_one(self):
    """ :meta private: """

    self.add('AUTH', 'accounts', 'cloudforce-one/requests')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests', 'message')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests', 'message/new')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests/constants')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests/new')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests/priority')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests/priority/new')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests/priority/quota')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests/quota')
    self.add('AUTH', 'accounts', 'cloudforce-one/requests/types')
python-cloudflare-2.20.0/CloudFlare/cloudflare.py000066400000000000000000001537061461736615400217750ustar00rootroot00000000000000""" Cloudflare v4 API

A Python interface Cloudflare's v4 API.

See README.md for detailed/further reading.

Copyright (c) 2016 thru 2024, Cloudflare. All rights reserved.
"""

import json
import keyword

from .network import CFnetwork, CFnetworkError
from .logging_helper import CFlogger
from .utils import user_agent, build_curl
from .read_configs import read_configs, ReadConfigError
from .api_v4 import api_v4
from .api_extras import api_extras
from .api_decode_from_openapi import api_decode_from_openapi
from .exceptions import CloudFlareAPIError, CloudFlareInternalError
from .warning_2_20 import warning_2_20, warn_warning_2_20, indent_warning_2_20

BASE_URL = 'https://api.cloudflare.com/client/v4'
OPENAPI_URL = 'https://github.com/cloudflare/api-schemas/raw/main/openapi.json'

DEFAULT_GLOBAL_REQUEST_TIMEOUT = 5
DEFAULT_MAX_REQUEST_RETRIES = 5

class CloudFlare():
    """ A Python interface Cloudflare's v4 API.

    :param email: Authentication email (if not provided by config methods).
    :param key: Authentication key (if not provided by config methods).
    :param token: Authentication token (if not provided by config methods).
    :param certtoken: Authentication certtoken (if not provided by config methods).
    :param debug: Debug is enabled by setting to True.
    :param raw: Set to True to force raw responses so you can see paging.
    :param use_sessions: The default is True; rarely needs changing.
    :param profile: Profile name (default is "CloudFlare").
    :param base_url: Rarely changed Cloudflare API URL.
    :param global_request_timeout: Timeout value (default is 5 seconds).
    :param max_request_retries: Number of retry times (default is 5 times).
    :param http_headers: Additional HTTP headers (as a list).
    :return: New instance of CloudFlare()

    A Python interface Cloudflare's v4 API.
    """

    class _v4base():
        """ :meta private: """

        def __init__(self, config, warnings=True):
            """ :meta private: """

            self.network = None
            self.config = config

            self.api_email = config['email'] if 'email' in config else None
            self.api_key = config['key'] if 'key' in config else None
            self.api_token = config['token'] if 'token' in config else None
            self.api_certtoken = config['certtoken'] if 'certtoken' in config else None

            # We must have a base_url value
            self.base_url = config['base_url'] if 'base_url' in config else BASE_URL

            # The modern-day API definition comes from here (soon)
            self.openapi_url = config['openapi_url'] if 'openapi_url' in config else OPENAPI_URL

            self.raw = config['raw']
            self.use_sessions = config['use_sessions']
            self.global_request_timeout = config['global_request_timeout'] if 'global_request_timeout' in config else DEFAULT_GLOBAL_REQUEST_TIMEOUT
            self.max_request_retries = config['max_request_retries'] if 'max_request_retries' in config else DEFAULT_MAX_REQUEST_RETRIES
            try:
                self.global_request_timeout = int(self.global_request_timeout)
            except (TypeError, ValueError):
                self.global_request_timeout = DEFAULT_GLOBAL_REQUEST_TIMEOUT
            try:
                self.max_request_retries = int(self.max_request_retries)
            except (TypeError, ValueError):
                self.max_request_retries = DEFAULT_MAX_REQUEST_RETRIES
            self.additional_http_headers = config['http_headers'] if 'http_headers' in config else None
            self.profile = config['profile']
            self.network = CFnetwork(
                use_sessions=self.use_sessions,
                global_request_timeout=self.global_request_timeout,
                max_request_retries=self.max_request_retries
            )
            self.user_agent = user_agent()

            self.logger = CFlogger(config['debug']).getLogger() if 'debug' in config and config['debug'] else None

            if warnings:
                # After 2.20.* there is a warning message posted to handle un-pinned versions
                warning = warning_2_20()
                if warning:
                    # we are running 2.20.* or above and hence it's time to warn the user
                    if self.logger:
                        self.logger.warning(indent_warning_2_20(warning))
                    else:
                        warn_warning_2_20(indent_warning_2_20(warning))

        def __del__(self):
            if self.network:
                del self.network
                self.network = None

        def _add_headers(self, method, data, files, content_type=None):
            """ Add default headers """
            self.headers = {}
            self.headers['User-Agent'] = self.user_agent
            if method == 'GET':
                # no content type needed - except we throw in a default just for grin's
                self.headers['Content-Type'] = 'application/json'
            elif content_type is not None and method in content_type:
                # this api endpoint and this method requires a specific content type.
                ct = content_type[method]
                if isinstance(ct, list):
                    # How do we choose from more than one content type?
                    found = False
                    for t in ct:
                        # we have to match against the data type - arggg!
                        if 'application/octet-stream' == t and isinstance(data, (bytes,bytearray)):
                            self.headers['Content-Type'] = t
                            found = True
                            break
                        if 'application/json' == t and isinstance(data, (list,dict)):
                            self.headers['Content-Type'] = t
                            found = True
                            break
                        if 'application/javascript' == t and isinstance(data, str):
                            self.headers['Content-Type'] = t
                            found = True
                            break
                    if not found:
                        # punt - pick first - we can't do anything else!
                        self.headers['Content-Type'] = ct[0]
                else:
                    self.headers['Content-Type'] = ct
            else:
                # default choice
                self.headers['Content-Type'] = 'application/json'

            # now adjust Content-Type based on data and files
            if method != 'GET':
                if self.headers['Content-Type'] == 'application/json' and isinstance(data, str):
                    # passing javascript vs JSON
                    self.headers['Content-Type'] = 'application/javascript'
                if self.headers['Content-Type'] == 'application/json' and isinstance(data, (bytes,bytearray)):
                    # passing binary file vs JSON
                    self.headers['Content-Type'] = 'application/octet-stream'
                if data and len(data) > 0 and self.headers['Content-Type'] == 'multipart/form-data':
                    # convert from params to files (i.e multipart/form-data)
                    if files is None:
                        files = set()
                    for k,v in data.items():
                        if isinstance(v, (dict, list)):
                            files.add((k, (None, json.dumps(v), 'application/json')))
                        else:
                            files.add((k, (None, v)))
                    # we have replaced data's values into files
                    data = None
                if data is not None and len(data) == 0:
                    data = None
                if files is not None and len(files) == 0:
                    files = None
                if data is None and files is None and self.headers['Content-Type'] == 'multipart/form-data':
                    # can't have zero length multipart/form-data and as there's no data or files; we don't need it
                    del self.headers['Content-Type']
                if files:
                    # overwrite Content-Type as we are uploading data
                    self.headers['Content-Type'] = 'multipart/form-data'
                    # however something isn't right and this works ... look at again later!
                    del self.headers['Content-Type']
            if self.additional_http_headers:
                for h in self.additional_http_headers:
                    t, v = h.split(':', 1)
                    t = t.strip()
                    v = v.strip()
                    if len(v) > 0 and ((v[0] == '"' and v[-1] == '"') or (v[0] == "'" and v[-1] == "'")):
                        v = v[1:-1]
                    self.headers[t] = v
            return data, files

        def _add_auth_headers(self, method):
            """ Add authentication headers """

            v = 'email' + '.' + method.lower()
            api_email = self.config[v] if v in self.config else self.api_email
            v = 'key' + '.' + method.lower()
            api_key = self.config[v] if v in self.config else self.api_key
            v = 'token' + '.' + method.lower()
            api_token = self.config[v] if v in self.config else self.api_token

            if api_email is None and api_key is None and api_token is None:
                if self.logger:
                    self.logger.debug('neither email/key or token defined')
                raise CloudFlareAPIError(0, 'neither email/key or token defined')

            if api_key is not None and api_token is not None:
                if self.logger:
                    self.logger.debug('confused info - both key and token defined')
                raise CloudFlareAPIError(0, 'confused info - both key and token defined')

            if api_email is not None and api_key is None and api_token is None:
                if self.logger:
                    self.logger.debug('email defined however neither key or token defined')
                raise CloudFlareAPIError(0, 'email defined however neither key or token defined')

            # We know at this point that at-least one api_* is set and no confusion!

            if api_email is None and api_token is not None:
                # post issue-114 - token is used
                self.headers['Authorization'] = 'Bearer %s' % (api_token)
            elif api_email is None and api_key is not None:
                # pre issue-114 - key is used vs token - backward compat
                self.headers['Authorization'] = 'Bearer %s' % (api_key)
            elif api_email is not None and api_key is not None:
                # boring old school email/key methodology (token ignored)
                self.headers['X-Auth-Email'] = api_email
                self.headers['X-Auth-Key'] = api_key
            elif api_email is not None and api_token is not None:
                # boring old school email/key methodology (token ignored)
                self.headers['X-Auth-Email'] = api_email
                self.headers['X-Auth-Key'] = api_token
            else:
                raise CloudFlareInternalError(0, 'coding issue!')

        def _add_certtoken_headers(self, method):
            """ Add authentication headers """

            v = 'certtoken' + '.' + method.lower()
            if v in self.config:
                api_certtoken = self.config[v] # use specific value for this method
            else:
                api_certtoken = self.api_certtoken # use generic value for all methods

            if api_certtoken is None:
                if self.logger:
                    self.logger.debug('no cert token defined')
                raise CloudFlareAPIError(0, 'no cert token defined')
            self.headers['X-Auth-User-Service-Key'] = api_certtoken

        def do_not_available(self, method, parts, identifiers, params=None, data=None, files=None, content_type=None):
            """ Cloudflare v4 API"""

            # base class simply returns not available - no processing of any arguments
            if self.logger:
                self.logger.debug('call for this method not available')
            raise CloudFlareAPIError(0, 'call for this method not available')

        def do_no_auth(self, method, parts, identifiers, params=None, data=None, files=None, content_type=None):
            """ Cloudflare v4 API"""

            data, files = self._add_headers(method, data, files, content_type)
            # We decide at this point if we are sending json or string data
            if isinstance(data, (str,bytes,bytearray)):
                return self._call(method, parts, identifiers, params, data, None, files)
            return self._call(method, parts, identifiers, params, None, data, files)

        def do_auth(self, method, parts, identifiers, params=None, data=None, files=None, content_type=None):
            """ Cloudflare v4 API"""

            data, files = self._add_headers(method, data, files, content_type)
            self._add_auth_headers(method)
            # We decide at this point if we are sending json or string data
            if isinstance(data, (str,bytes,bytearray)):
                return self._call(method, parts, identifiers, params, data, None, files)
            return self._call(method, parts, identifiers, params, None, data, files)

        def do_auth_unwrapped(self, method, parts, identifiers, params=None, data=None, files=None, content_type=None):
            """ Cloudflare v4 API"""

            data, files = self._add_headers(method, data, files, content_type)
            self._add_auth_headers(method)
            # We decide at this point if we are sending json or string data
            if isinstance(data, (str,bytes,bytearray)):
                return self._call_unwrapped(method, parts, identifiers, params, data, None, files)
            return self._call_unwrapped(method, parts, identifiers, params, None, data, files)

        def do_certauth(self, method, parts, identifiers, params=None, data=None, files=None, content_type=None):
            """ Cloudflare v4 API"""

            data, files = self._add_headers(method, data, files, content_type)
            self._add_certtoken_headers(method)
            # We decide at this point if we are sending json or string data
            if isinstance(data, (str,bytes,bytearray)):
                return self._call(method, parts, identifiers, params, data, None, files)
            return self._call(method, parts, identifiers, params, None, data, files)

        def _call_network(self, method, headers, parts, identifiers, params, data_str, data_json, files):
            """ Cloudflare v4 API"""

            if (method is None) or (parts[0] is None):
                # should never happen
                raise CloudFlareInternalError(0, 'You must specify a method and endpoint')

            if len(parts) > 1 and parts[1] is not None or (data_str is not None and method == 'GET'):
                if identifiers[0] is None:
                    raise CloudFlareAPIError(0, 'You must specify first identifier')
                if identifiers[1] is None:
                    url = (self.base_url + '/'
                           + parts[0] + '/'
                           + str(identifiers[0]) + '/'
                           + parts[1])
                else:
                    url = (self.base_url + '/'
                           + parts[0] + '/'
                           + str(identifiers[0]) + '/'
                           + parts[1] + '/'
                           + str(identifiers[1]))
            else:
                if identifiers[0] is None:
                    url = (self.base_url + '/'
                           + parts[0])
                else:
                    url = (self.base_url + '/'
                           + parts[0] + '/'
                           + str(identifiers[0]))

            if len(parts) > 2 and parts[2]:
                url += '/' + parts[2]
                if identifiers[2]:
                    url += '/' + str(identifiers[2])
                if len(parts) > 3 and parts[3]:
                    url += '/' + parts[3]
                    if identifiers[3]:
                        url += '/' + str(identifiers[3])
                    if len(parts) > 4 and parts[4]:
                        url += '/' + parts[4]

            if self.logger:
                msg = build_curl(method, url, headers, params, data_str, data_json, files)
                self.logger.debug('Call: emulated curl command ...\n%s', msg)

            try:
                response = self.network(method, url, headers, params, data_str, data_json, files)
            except CFnetworkError as e:
                if self.logger:
                    self.logger.debug('Call: network error: %s', e)
                raise CloudFlareAPIError(0, str(e)) from None
            except Exception as e:
                if self.logger:
                    self.logger.debug('Call: network exception! %s', e)
                raise CloudFlareAPIError(0, 'network exception: %s' % (e)) from None

            # Create response_{type|code|data}
            try:
                response_type = response.headers['Content-Type']
                if ';' in response_type:
                    # remove the ;paramaters part (like charset=, etc.)
                    response_type = response_type[0:response_type.rfind(';')]
                response_type = response_type.strip().lower()
            except KeyError:
                # API should always response; but if it doesn't; here's the default
                response_type = 'application/octet-stream'
            response_code = response.status_code
            response_data = response.content
            if not isinstance(response_data, (str, bytes, bytearray)):
                # the more I think about it; then less likely this will ever be called
                try:
                    response_data = response_data.decode('utf-8')
                except UnicodeDecodeError:
                    pass

            if self.logger:
                if 'text/' == response_type[0:5] or response_type in ['application/javascript', 'application/json']:
                    if len(response_data) > 180:
                        self.logger.debug('Response: %d, %s, %s...', response_code, response_type, response_data[0:180])
                    else:
                        self.logger.debug('Response: %d, %s, %s', response_code, response_type, response_data)
                else:
                    self.logger.debug('Response: %d, %s, %s', response_code, response_type, '...')

            if response_code == 429:
                # 429 Too Many Requests
                # The HTTP 429 Too Many Requests response status code indicates the user
                # has sent too many requests in a given amount of time ("rate limiting").
                # A Retry-After header might be included to this response indicating how
                # long to wait before making a new request.
                try:
                    retry_after = response.headers['Retry-After']
                except (KeyError,IndexError):
                    retry_after = ''
                # XXX/TODO no processing for now - but could try again within library
                if self.logger:
                    self.logger.debug('Response: 429 Header Retry-After: %s', retry_after)

            # if response_code in [400,401,403,404,405,412,500]:
            if 400 <= response_code <= 499 or response_code == 500:
                # The /certificates API call insists on a 500 error return and yet has valid error data
                # Other API calls can return 400 or 4xx with valid response data
                # lets check and convert if able
                try:
                    j = json.loads(response_data)
                    if len(j) == 2 and 'code' in j and 'error' in j:
                        # This is an incorrect response from the API (happens on 404's) - but we can handle it cleanly here
                        # {\n  "code": 1000,\n  "error": "not_found"\n}
                        response_data = '{"errors": [{"code": %d, "message": "%s"}], "success": false, "result": null}' % (j['code'], j['error'])
                        response_data = response_data.encode()
                        response_code = 200
                    elif 'success' in j and 'errors' in j:
                        # yippe - try to continue by allowing to process fully
                        response_code = 200
                    else:
                        # no go - it's not a Cloudflare error format
                        pass
                except (ValueError, json.decoder.JSONDecodeError):
                    # ignore - maybe a real error that's not json, let proceed!
                    pass

            if 500 <= response_code <= 599:
                # 500 Internal Server Error
                # 501 Not Implemented
                # 502 Bad Gateway
                # 503 Service Unavailable
                # 504 Gateway Timeout
                # 505 HTTP Version Not Supported
                # 506 Variant Also Negotiates
                # 507 Insufficient Storage
                # 508 Loop Detected
                # 509 Unassigned
                # 510 Not Extended
                # 511 Network Authentication Required

                # the libary doesn't deal with these errors, just pass upwards!
                # there's no value to add and the returned data is questionable or not useful
                response.raise_for_status()

                # should not be reached
                raise CloudFlareInternalError(0, 'internal error in status code processing')

            # if 400 <= response_code <= 499:
            #    # 400 Bad Request
            #    # 401 Unauthorized
            #    # 403 Forbidden
            #    # 405 Method Not Allowed
            #    # 415 Unsupported Media Type
            #    # 429 Too many requests
            #
            #    # don't deal with these errors, just pass upwards!
            #    response.raise_for_status()

            # if 300 <= response_code <= 399:
            #    # 304 Not Modified
            #
            #    # don't deal with these errors, just pass upwards!
            #    response.raise_for_status()

            # should be a 200 response at this point

            return [response_type, response_code, response_data]

        def _raw(self, method, headers, parts, identifiers, params, data_str, data_json, files):
            """ Cloudflare v4 API"""

            [response_type, response_code, response_data] = self._call_network(method,
                                                                               headers, parts,
                                                                               identifiers,
                                                                               params, data_str, data_json, files)

            # API can return HTTP code OK, CREATED, ACCEPTED, or NO-CONTENT - all of which are a-ok.
            if response_code not in [200, 201, 202, 204]:
                # 3xx & 4xx errors (5xx's handled above)
                response_data = {'success': False,
                                 'errors': [{'code': response_code, 'message':'HTTP response code %d' % response_code}],
                                 'result': str(response_data)}

                # it would be nice to return the error code and content type values; but not quite yet
                return response_data

            if response_type == 'application/json':
                # API says it's JSON; so it better be parsable as JSON
                # NDJSON is returned by Enterprise Log Share i.e. /zones/:id/logs/received
                if hasattr(response_data, 'decode'):
                    try:
                        response_data = response_data.decode('utf-8')
                    except UnicodeDecodeError:
                        # clearly not a string that can be decoded!
                        if self.logger:
                            self.logger.debug('Response: decode(utf-8) failed, reverting to binary response')
                        # return binary
                        return {'success': True, 'result': response_data}
                try:
                    if response_data == '':
                        # This should really be 'null' but it isn't. Even then, it's wrong!
                        response_data = None
                    else:
                        response_data = json.loads(response_data)
                except (ValueError,json.decoder.JSONDecodeError):
                    # Lets see if it's NDJSON data
                    # NDJSON is a series of JSON elements with newlines between each element
                    try:
                        r = []
                        for line in response_data.splitlines():
                            r.append(json.loads(line))
                        response_data = r
                    except (ValueError, json.decoder.JSONDecodeError):
                        # While this should not happen; it's always possible
                        if self.logger:
                            self.logger.debug('Response data not JSON: %r', response_data)
                        raise CloudFlareAPIError(0, 'JSON parse failed - report to Cloudflare.') from None

                if isinstance(response_data, dict) and 'success' in response_data:
                    return response_data
                # if it's not a dict then it's not going to have 'success'
                return {'success': True, 'result': response_data}

            if response_type in ['text/plain', 'application/octet-stream']:
                # API says it's text; but maybe it's actually JSON? - should be fixed in API
                if hasattr(response_data, 'decode'):
                    try:
                        response_data = response_data.decode('utf-8')
                    except UnicodeDecodeError:
                        # clearly not a string that can be decoded!
                        if self.logger:
                            self.logger.debug('Response: decode(utf-8) failed, reverting to binary response')
                        # return binary
                        return {'success': True, 'result': response_data}
                try:
                    if response_data == '':
                        # This should really be 'null' but it isn't. Even then, it's wrong!
                        response_data = None
                    else:
                        response_data = json.loads(response_data)
                except (ValueError, json.decoder.JSONDecodeError):
                    # So it wasn't JSON - moving on as if it's text!
                    pass
                if isinstance(response_data, dict) and 'success' in response_data:
                    return response_data
                return {'success': True, 'result': response_data}

            if response_type in ['text/javascript', 'application/javascript', 'text/html', 'text/css', 'text/csv']:
                # used by Cloudflare workers etc
                if hasattr(response_data, 'decode'):
                    try:
                        response_data = response_data.decode('utf-8')
                    except UnicodeDecodeError:
                        # clearly not a string that can be decoded!
                        if self.logger:
                            self.logger.debug('Response: decode(utf-8) failed, reverting to binary response')
                        # return binary
                        return {'success': True, 'result': response_data}
                return {'success': True, 'result': str(response_data)}

            if response_type in ['application/pdf', 'application/zip'] or response_type[0:6] in ['audio/', 'image/', 'video/']:
                # it's raw/binary - just pass thru
                return {'success': True, 'result': response_data}

            # Assuming nothing - but continuing anyway as if its a string
            if hasattr(response_data, 'decode'):
                try:
                    response_data = response_data.decode('utf-8')
                except UnicodeDecodeError:
                    # clearly not a string that can be decoded!
                    if self.logger:
                        self.logger.debug('Response: decode(utf-8) failed, reverting to binary response')
                    # return binary
                    return {'success': True, 'result': response_data}
            return {'success': True, 'result': str(response_data)}

        def _call(self, method, parts, identifiers, params, data_str, data_json, files):
            """ Cloudflare v4 API"""

            response_data = self._raw(method, self.headers, parts, identifiers, params, data_str, data_json, files)

            # Sanatize the returned results - just in case API is messed up
            if 'success' not in response_data:
                # { "data": null, "errors": [ { "message": "request must be a POST", "path": null, "extensions": { "timestamp": "20...
                # XXX/TODO should be retested and aybe recoded/deleted
                if 'errors' in response_data:
                    if response_data['errors'] is None:
                        # Only happens on /graphql call
                        if self.logger:
                            self.logger.debug('Response: assuming success = "True"')
                        response_data['success'] = True
                    else:
                        if self.logger:
                            self.logger.debug('Response: assuming success = "False"')
                        # The following only happens on /graphql call
                        try:
                            message = response_data['errors'][0]['message']
                        except KeyError:
                            message = ''
                        try:
                            location = str(response_data['errors'][0]['location'])
                        except KeyError:
                            location = ''
                        try:
                            path = '>'.join(response_data['errors'][0]['path'])
                        except KeyError:
                            path = ''
                        response_data['errors'] = [{'code': 99999, 'message': message + ' - ' + location + ' - ' + path}]
                        response_data['success'] = False
                else:
                    if 'result' not in response_data:
                        # Only happens on /certificates call
                        # should be fixed in /certificates API
                        # may well be fixed by now
                        if self.logger:
                            self.logger.debug('Response: assuming success = "False"')
                        r = response_data
                        response_data['errors'] = []
                        response_data['errors'].append(r)
                        response_data['success'] = False
                    else:
                        if self.logger:
                            self.logger.debug('Response: assuming success = "True"')
                        response_data['success'] = True

            if response_data['success'] is False:
                if 'errors' in response_data and response_data['errors'] is not None:
                    errors = response_data['errors'][0]
                else:
                    errors = {}
                if 'code' in errors:
                    code = errors['code']
                else:
                    code = 99998
                if 'message' in errors:
                    message = errors['message']
                elif 'error' in errors:
                    message = errors['error']
                else:
                    message = ''
                # if 'messages' in response_data:
                #     errors['error_chain'] = response_data['messages']
                if 'error_chain' in errors:
                    error_chain = errors['error_chain']
                    for error in error_chain:
                        if self.logger:
                            self.logger.debug('Response: error %d %s - chain', error['code'], error['message'])
                    if self.logger:
                        self.logger.debug('Response: error %d %s', code, message)
                    raise CloudFlareAPIError(code, message, error_chain)

                if self.logger:
                    self.logger.debug('Response: error %d %s', code, message)
                raise CloudFlareAPIError(code, message)

            if self.raw:
                result = {}
                # theres always a result value - unless it's a graphql query
                try:
                    result['result'] = response_data['result']
                except KeyError:
                    result['result'] = response_data
                # theres may not be a result_info on every call
                if 'result_info' in response_data:
                    result['result_info'] = response_data['result_info']
                # no need to return success, errors, or messages as they return via an exception
            else:
                # theres always a result value - unless it's a graphql query
                try:
                    result = response_data['result']
                except KeyError:
                    result = response_data

            if self.logger:
                if isinstance(result, (str, dict, list)):
                    if len(str(result)) > 180:
                        self.logger.debug('Response: %s...', str(result)[0:180].replace('\n', ' '))
                    else:
                        self.logger.debug('Response: %s', str(result).replace('\n', ' '))
                elif isinstance(result, (bytes,bytearray)):
                    self.logger.debug('Response: %s', result[0:180])
                else:
                    self.logger.debug('Response: %s', '...')
            return result

        def _call_unwrapped(self, method, parts, identifiers, params, data_str, data_json, files):
            """ Cloudflare v4 API"""

            response_data = self._raw(method, self.headers, parts, identifiers, params, data_str, data_json, files)
            if self.logger:
                self.logger.debug('Response: %s', response_data)
            result = response_data
            return result

        def api_from_openapi(self, url=None):
            """ Cloudflare v4 API"""

            if url is None:
                url = self.openapi_url

            try:
                v = self._read_from_web(url)
            except Exception as e:
                if self.logger:
                    self.logger.debug('OpenAPI read from web failed: %s', e)
                raise CloudFlareAPIError(0, 'OpenAPI read from web failed: %s' % (e)) from None

            try:
                v, openapi_version, cloudflare_version, cloudflare_url = api_decode_from_openapi(v)
            except SyntaxError as e:
                if self.logger:
                    self.logger.debug('OpenAPI bad json file: %s', e)
                raise CloudFlareAPIError(0, 'OpenAPI bad json file: %s' % (e)) from None

            # if self.base_url != cloudflare_url:
            #    # XXX/TODO should this be recorded or throw an error?
            #    pass

            if self.logger:
                self.logger.debug('OpenAPI version: %s, Cloudflare API version: %s url: %s', openapi_version, cloudflare_version, cloudflare_url)
            return v

        def _read_from_web(self, url):
            """ Cloudflare v4 API"""
            try:
                if self.logger:
                    self.logger.debug('Call: doit!')
                response = self.network('GET', url)
                if self.logger:
                    self.logger.debug('Call: done!')
            except Exception as e:
                if self.logger:
                    self.logger.debug('Call: exception! "%s"', e)
                raise CloudFlareAPIError(0, 'connection failed.') from None

            return response.text

    class _CFbase():
        """ :meta private: """

        def __init__(self, base, parts, content_type=None):
            """ Cloudflare v4 API"""

            self._base = base
            self._parts = parts
            if content_type:
                self._content_type = content_type
            self._do = self._base.do_not_available

        def __call__(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None):
            """ Cloudflare v4 API"""

            # This is the same as a get()
            return self.get(identifier1, identifier2, identifier3, identifier4, params=params, data=data)

        def __str__(self):
            """ Cloudflare v4 API"""

            return '[' + '/' + '/:id/'.join(self._parts) + ']'

        def __repr__(self):
            """ Cloudflare v4 API"""

            return '[' + '/' + '/:id/'.join(self._parts) + ']'

        def get(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._do('GET', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, self._content_type)
                return self._do('GET', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

        def patch(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._do('PATCH', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, self._content_type)
                return self._do('PATCH', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

        def post(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None, files=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._do('POST', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, files, self._content_type)
                return self._do('POST', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, files)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

        def put(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None, files=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._do('PUT', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, files, self._content_type)
                return self._do('PUT', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, files)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

        def delete(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._do('DELETE', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, self._content_type)
                return self._do('DELETE', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

    class _CFbaseUnused(_CFbase):
        """ :meta private: """

        def __init__(self, base, parts, content_type):
            """ Cloudflare v4 API"""

            super().__init__(base, parts, content_type)
            self._do = self._base.do_not_available

    class _CFbaseNoAuth(_CFbase):
        """ :meta private: """

        def __init__(self, base, parts, content_type):
            """ Cloudflare v4 API"""

            super().__init__(base, parts, content_type)
            self._do = self._base.do_no_auth
            self._valid = True

        def patch(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._base.do_not_available('PATCH', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, self._content_type)
                return self._base.do_not_available('PATCH', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

        def post(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None, files=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._base.do_not_available('POST', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, files, self._content_type)
                return self._base.do_not_available('POST', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, files)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

        def put(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None, files=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._base.do_not_available('PUT', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, self._content_type)
                return self._base.do_not_available('PUT', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

        def delete(self, identifier1=None, identifier2=None, identifier3=None, identifier4=None, params=None, data=None):
            """ Cloudflare v4 API"""

            try:
                if getattr(self, '_content_type', False):
                    return self._base.do_not_available('DELETE', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data, self._content_type)
                return self._base.do_not_available('DELETE', self._parts, [identifier1, identifier2, identifier3, identifier4], params, data)
            except CloudFlareAPIError as e:
                raise CloudFlareAPIError(e=e) from None

    class _CFbaseAuth(_CFbase):
        """ :meta private: """

        def __init__(self, base, parts, content_type):
            """ Cloudflare v4 API"""

            super().__init__(base, parts, content_type)
            self._do = self._base.do_auth
            self._valid = True

    class _CFbaseAuthUnwrapped(_CFbase):
        """ :meta private: """

        def __init__(self, base, parts, content_type):
            """ Cloudflare v4 API"""

            super().__init__(base, parts, content_type)
            self._do = self._base.do_auth_unwrapped
            self._valid = True

    class _CFbaseAuthCert(_CFbase):
        """ :meta private: """

        def __init__(self, base, parts, content_type):
            """ Cloudflare v4 API"""

            super().__init__(base, parts, content_type)
            self._do = self._base.do_certauth
            self._valid = True

    @classmethod
    def sanitize_verb(cls, v):
        """ sanitize_verb """
        # keywords are also changed to have underscore appended so it can used with Python code
        if keyword.iskeyword(v):
            v = v + '_'
        # AI functions introduce '@' symbol - i.e .../@cf/... they are replaced with at_
        if '@' == v[0]:
            v = 'at_' + v[1:]
        # AI functions introduce '.' symbol - i.e 1.0 they are replaced with underscore
        if '.' in v:
            v = v.replace('.','_')
        # dashes (vs underscores) cause issues in Python and other languages. they are replaced with underscores
        if '-' in v:
            v = v.replace('-','_')
        return v

    def add_carefully(self, t, *parts, content_type=None):
        """ add_carefully()
        """
        self.add(t, parts, content_type, auto=False)

    def add(self, t, *parts, content_type=None, auto=True):
        """ add()

        :param t: type of API call.
        :param p1: part1 of API call.
        :param p2: part1 of API call.
        :param p3: part1 of API call.
        :param p4: part1 of API call.
        :param p5: part1 of API call.
        :param content_type: optional value for the HTTP Content-Type for an API call.

        add() is the core fuction that creates a new API endpoint that can be called later on.
        """

        api_sections = []
        for p in parts:
            api_sections += p.split('/')

        branch = self
        for api_part in api_sections[0:-1]:
            try:
                branch = getattr(branch, CloudFlare.sanitize_verb(api_part))
            except AttributeError:
                # missing path - should never happen unless api_v4 is a busted file or add_all() used
                if not auto:
                    raise CloudFlareAPIError(0, 'api load: api_part **%s** missing when adding path /%s' % (api_part, '/'.join(api_sections))) from None
                # create intermediate path as required
                f = self._CFbaseUnused(self._base, parts, content_type=None)
                setattr(branch, CloudFlare.sanitize_verb(api_part), f)
                branch = getattr(branch, CloudFlare.sanitize_verb(api_part))

        api_part = api_sections[-1]
        try:
            branch = getattr(branch, CloudFlare.sanitize_verb(api_part))
            # we only are here becuase the name already exists - don't let it overwrite - should never happen unless api_v4 is a busted file
            raise CloudFlareAPIError(0, 'api load: duplicate api_part found: %s/**%s**' % ('/'.join(api_sections[0:-1]), api_part))
        except AttributeError:
            # this is the required behavior - i.e. it's a new node to create
            pass

        if t == 'VOID':
            f = self._CFbaseUnused(self._base, parts, content_type=None)
        elif t == 'OPEN':
            f = self._CFbaseNoAuth(self._base, parts, content_type=content_type)
        elif t == 'AUTH':
            f = self._CFbaseAuth(self._base, parts, content_type=content_type)
        elif t == 'AUTH_UNWRAPPED':
            f = self._CFbaseAuthUnwrapped(self._base, parts, content_type=content_type)
        elif t == 'CERT':
            f = self._CFbaseAuthCert(self._base, parts, content_type=content_type)
        else:
            # should never happen
            raise CloudFlareAPIError(0, 'api load type mismatch')

        setattr(branch, CloudFlare.sanitize_verb(api_part), f)

    def find(self, cmd):
        """ find()

        :param cmd: API in slash format
        :return: fuction to call for that API

        You can use this call to convert a string API command into the actual function call
        """
        m = self
        for verb in cmd.split('/'):
            if verb == '' or verb[0] == ':':
                continue
            try:
                m = getattr(m, CloudFlare.sanitize_verb(verb))
            except AttributeError:
                raise AttributeError('%s: not found' % (verb)) from None
        return m

    def api_list(self):
        """ api_list()

        :return: list of API calls

        A recursive walk of the api tree returning a list of api calls
        """
        return self._api_list(m=self)

    def _api_list(self, m=None, s=''):
        """ :meta private: """
        w = []
        for n in sorted(dir(m)):
            if n[0] == '_':
                # internal
                continue
            if n in ['delete', 'get', 'patch', 'post', 'put']:
                # gone too far
                continue
            try:
                a = getattr(m, n)
            except AttributeError:
                # really should not happen!
                raise CloudFlareAPIError(0, '%s: not found - should not happen' % (n)) from None
            d = dir(a)
            if '_base' not in d:
                continue
            # it's a known api call - lets show the result and continue down the tree
            if '_parts' in d and '_valid' in d:
                if 'delete' in d or 'get' in d or 'patch' in d or 'post' in d or 'put' in d:
                    # only show the result if a call exists for this part
                    if n[-1] == '_':
                        if keyword.iskeyword(n[:-1]):
                            # should always be a keyword - but now nothing needs to be done
                            pass
                        # remove the extra keyword postfix'ed with underscore
                        w.append(str(a)[1:-1])
                    else:
                        # handle underscores by returning the actual API call vs the method name
                        w.append(str(a)[1:-1])
            # now recurse downwards into the tree
            w = w + self._api_list(a, s + '/' + n)
        return w

    def api_from_openapi(self, url=None):
        """ api_from_openapi()

        :param url: OpenAPI URL or None if you use the built official URL

        """

        return self._base.api_from_openapi(url)

    def __init__(self, email=None, key=None, token=None, certtoken=None, debug=False, raw=False, use_sessions=True, profile=None, base_url=None, global_request_timeout=None, max_request_retries=None, http_headers=None, warnings=True):
        """ :meta private: """

        self._base = None

        if email is not None and not isinstance(email, str):
            raise TypeError('email is %s - must be str' % (type(email)))
        if key is not None and not isinstance(key, str):
            raise TypeError('key is %s - must be str' % (type(key)))
        if token is not None and not isinstance(token, str):
            raise TypeError('token is %s - must be str' % (type(token)))
        if certtoken is not None and not isinstance(certtoken, str):
            raise TypeError('certtoken is %s - must be str' % (type(certtoken)))

        try:
            config = read_configs(profile)
        except ReadConfigError as e:
            raise e

        # class creation values override all configuration values
        if email is not None:
            config['email'] = email
        if key is not None:
            config['key'] = key
        if token is not None:
            config['token'] = token
        if certtoken is not None:
            config['certtoken'] = certtoken
        if debug is not None:
            config['debug'] = debug
        if raw is not None:
            config['raw'] = raw
        if use_sessions is not None:
            config['use_sessions'] = use_sessions
        if profile is not None:
            config['profile'] = profile
        if base_url is not None:
            config['base_url'] = base_url
        if global_request_timeout is not None:
            config['global_request_timeout'] = global_request_timeout
        if max_request_retries is not None:
            config['max_request_retries'] = max_request_retries
        if http_headers is not None:
            if not isinstance(http_headers, list):
                raise TypeError('http_headers is not a list')
            for h in http_headers:
                try:
                    t, v = h.split(':', 1)
                except ValueError:
                    # clearly a bad header syntax
                    raise TypeError('http_headers bad syntax') from None
                if len(t.strip()) == 0:
                    raise TypeError('http_headers bad syntax') from None
            config['http_headers'] = http_headers

        # we do not need to handle item.call values - they pass straight thru

        for k,v in config.items():
            if v == '':
                config[k] = None

        self._base = self._v4base(config, warnings=warnings)

        # add the API calls
        try:
            api_v4(self)
            if 'extras' in config and config['extras']:
                api_extras(self, config['extras'])
        except Exception as e:
            raise e

    def __del__(self):
        """ :meta private: """

        if self._base:
            del self._base
            self._base = None

    def __call__(self):
        """ :meta private: """

        raise TypeError('object is not callable')

    def __enter__(self):
        """ :meta private: """
        return self

    def __exit__(self, t, v, tb):
        """ :meta private: """
        if t is None:
            return True
        # pretend we didn't deal with raised error - which is true
        return False

    def __str__(self):
        """ :meta private: """

        if self._base.api_email is None:
            s = '["%s","%s"]' % (self._base.profile, 'REDACTED')
        else:
            s = '["%s","%s","%s"]' % (self._base.profile, self._base.api_email, 'REDACTED')
        return s

    def __repr__(self):
        """ :meta private: """

        if self._base.api_email is None:
            s = '%s,%s("%s","%s","%s","%s",%s,"%s")' % (
                self.__module__, type(self).__name__,
                self._base.profile, 'REDACTED', 'REDACTED',
                self._base.base_url, self._base.raw, self._base.user_agent
            )
        else:
            s = '%s,%s("%s","%s","%s","%s","%s",%s,"%s")' % (
                self.__module__, type(self).__name__,
                self._base.profile, self._base.api_email, 'REDACTED', 'REDACTED',
                self._base.base_url, self._base.raw, self._base.user_agent
            )
        return s

    def __getattr__(self, key):
        """ :meta private: """

        # this code will expand later
        if key in dir(self):
            return self[key]
        # this is call to a non-existent endpoint
        raise AttributeError(key)

class Cloudflare(CloudFlare):
    """ A Python interface Cloudflare's v4 API.

    Alternate upper/lowercase version.
    """

class cloudflare(CloudFlare):
    """ A Python interface Cloudflare's v4 API.

    Alternate upper/lowercase version.
    """
python-cloudflare-2.20.0/CloudFlare/exceptions.py000066400000000000000000000063641461736615400220330ustar00rootroot00000000000000""" errors for Cloudflare API"""

class CloudFlareError(Exception):
    """ errors for Cloudflare API"""

    class _CodeMessage():
        """ a small class to save away an interger and string (the code and the message)"""

        def __init__(self, code, message):
            self._code = code
            self._message = message

        def __int__(self):
            return self._code

        def __str__(self):
            return self._message

        def __repr__(self):
            return '[%d:"%s"]' % (int(self._code), str(self._message))

    def __init__(self, code=0, message=None, error_chain=None, e=None):
        """ errors for Cloudflare API"""

        if e and isinstance(e, CloudFlareAPIError):
            # create fresh values (i.e copies)
            self._evalue = CloudFlareError._CodeMessage(int(e), str(e))
            if getattr(e, '_error_chain', False):
                self._error_chain = [CloudFlareError._CodeMessage(int(v), str(v)) for v in e._error_chain]
            return

        self._evalue = CloudFlareError._CodeMessage(int(code), str(message))
        if error_chain is not None:
            self._error_chain = []
            for evalue in error_chain:
                if isinstance(evalue, CloudFlareError._CodeMessage):
                    v = evalue
                else:
                    v = CloudFlareError._CodeMessage(int(evalue['code']), str(evalue['message']))
                self._error_chain.append(v)
        # As we are built off Exception, we need to get our superclass all squared away
        # super().__init__(message)

    def __bool__(self):
        """ bool value for Cloudflare API errors"""

        # required because there's a len() function below that can return 0
        # see https://docs.python.org/3/library/stdtypes.html#truth-value-testing
        return True

    def __int__(self):
        """ integer value for Cloudflare API errors"""

        return int(self._evalue)

    def __str__(self):
        """ string value for Cloudflare API errors"""

        return str(self._evalue)

    def __repr__(self):
        """ string value for Cloudflare API errors"""

        s = '[%d:"%s"]' % (int(self._evalue), str(self._evalue))
        if getattr(self, '_error_chain', False):
            for evalue in self._error_chain:
                s += ' [%d:"%s"]' % (int(evalue), str(evalue))
        return s

    def __len__(self):
        """ Cloudflare API errors can contain a chain of errors"""

        try:
            return len(getattr(self, '_error_chain'))
        except AttributeError:
            return 0

    def __getitem__(self, ii):
        """ Cloudflare API errors can contain a chain of errors"""

        return self._error_chain[ii]

    def __iter__(self):
        """ Cloudflare API errors can contain a chain of errors"""

        if getattr(self, '_error_chain', False):
            for evalue in self._error_chain:
                yield evalue
        return

    def next(self):
        """ Cloudflare API errors can contain a chain of errors"""

        if getattr(self, '_error_chain', False) is False:
            raise StopIteration

class CloudFlareAPIError(CloudFlareError):
    """ errors for Cloudflare API"""

class CloudFlareInternalError(CloudFlareError):
    """ errors for Cloudflare API"""
python-cloudflare-2.20.0/CloudFlare/logging_helper.py000066400000000000000000000031151461736615400226260ustar00rootroot00000000000000""" Logging for Cloudflare API"""
import logging

# try:
#     import http.client as http_client
# except ImportError:
#     # Python 2
#     import httplib as http_client

DEBUG = 0
INFO = 1

class CFlogger():
    """ Logging for Cloudflare API"""

    logger = None
    request_logger = None

    def __init__(self, level):
        """ Logging for Cloudflare API"""
        self.logger_level = self._get_logging_level(level)
        # logging.basicConfig(level=self.logger_level)
        if CFlogger.request_logger is None:
            CFlogger.request_logger = logging.getLogger("requests.packages.urllib3")
            CFlogger.request_logger.setLevel(self.logger_level)
            CFlogger.request_logger.propagate = level

    def getLogger(self):
        """ Logging for Cloudflare API"""
        # create logger
        if CFlogger.logger is None:
            CFlogger.logger = logging.getLogger('Python Cloudflare API v4')
            CFlogger.logger.setLevel(self.logger_level)

            ch = logging.StreamHandler()
            ch.setLevel(self.logger_level)

            # create formatter
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

            # add formatter to ch
            ch.setFormatter(formatter)

            # add ch to logger
            CFlogger.logger.addHandler(ch)

            # http_client.HTTPConnection.debuglevel = 1

        return CFlogger.logger

    def _get_logging_level(self, level):
        """ Logging for Cloudflare API"""
        if level is True:
            return logging.DEBUG
        return logging.INFO
python-cloudflare-2.20.0/CloudFlare/network.py000066400000000000000000000076061461736615400213430ustar00rootroot00000000000000""" Network for Cloudflare API"""

from urllib.parse import urlparse

from requests import Session, RequestException, ConnectionError as requests_ConnectionError
from requests.exceptions import Timeout
from requests.adapters import HTTPAdapter

class CFnetworkError(Exception):
    """ errors for network calls """

class CFnetwork():
    """ CFnetwork """

    def __init__(self, use_sessions=True, global_request_timeout=5, max_request_retries=5):
        """ CFnetwork """

        self.use_sessions = use_sessions
        self.global_request_timeout = global_request_timeout
        self.max_request_retries = max_request_retries
        self.session = None

    def __call__(self, method, url, headers=None, params=None, data_str=None, data_json=None, files=None):
        """ __call__ """

        if self.use_sessions:
            if self.session is None:
                s = Session()
                if self.max_request_retries is not None:
                    prefix = 'https://%s' % (urlparse(url).netloc)
                    s.mount(prefix, HTTPAdapter(max_retries=self.max_request_retries))
                self.session = s
        else:
            # only now do we import all of requests ... it's a rare case
            import requests
            self.session = requests

        try:
             r = self._do_network(method, url, headers, params, data_str, data_json, files)
        except Timeout as e:
            raise CFnetworkError('network request timeout error: %s' % (e)) from None
        except requests_ConnectionError as e:
            raise CFnetworkError('network request connection error: %s' % (e)) from None
        except RequestException as e:
            raise CFnetworkError('network request exception error: %s' % (e)) from None

        return r

    def _do_network(self, method, url, headers, params, data_str, data_json, files):
        """ _do_network """
        method = method.upper()

        # https://docs.python-requests.org/en/latest/user/quickstart/#post-a-multipart-encoded-file
        # Note, the json parameter is ignored if either data or files is passed.
        # This should have been handled well before here (it is!)

        if method == 'GET':
            # no data or files
            r = self.session.get(
                url,
                headers=headers,
                params=params,
                timeout=self.global_request_timeout,
            )
        elif method == 'POST':
            r = self.session.post(
                url,
                headers=headers,
                params=params,
                data=data_str,
                json=data_json,
                files=files,
                timeout=self.global_request_timeout,
            )
        elif method == 'PUT':
            r = self.session.put(
                url,
                headers=headers,
                params=params,
                data=data_str,
                json=data_json,
                files=files,
                timeout=self.global_request_timeout,
            )
        elif method == 'DELETE':
            r = self.session.delete(
                url,
                headers=headers,
                params=params,
                data=data_str,
                json=data_json,
                timeout=self.global_request_timeout,
            )
        elif method == 'PATCH':
            r = self.session.request(
                'PATCH',
                url,
                headers=headers,
                params=params,
                data=data_str,
                json=data_json,
                timeout=self.global_request_timeout,
            )
        else:
            # should never happen
            raise CFnetworkError('internal error - http method invalid: %s' % (method))
        # success!
        return r

    def __del__(self):
        """ __del__ """

        if self.use_sessions and self.session:
            self.session.close()
            self.session = None
python-cloudflare-2.20.0/CloudFlare/read_configs.py000066400000000000000000000127651461736615400222770ustar00rootroot00000000000000""" reading the config file for Cloudflare API"""

import os
import re
try:
    # py3
    import configparser
except ImportError:
    # py2
    import ConfigParser as configparser # type: ignore

class ReadConfigError(Exception):
    """ errors for read_configs"""

def read_configs(profile=None):
    """ reading the config file for Cloudflare API"""

    # We return all these values
    config = {'email': None, 'key': None, 'token': None, 'certtoken': None, 'extras': None, 'base_url': None, 'openapi_url': None, 'profile': None}

    # envioronment variables override config files - so setup first
    config['email'] = os.getenv('CLOUDFLARE_EMAIL') if os.getenv('CLOUDFLARE_EMAIL') is not None else os.getenv('CF_API_EMAIL')
    config['key'] = os.getenv('CLOUDFLARE_API_KEY') if os.getenv('CLOUDFLARE_API_KEY') is not None else os.getenv('CF_API_KEY')
    config['token'] = os.getenv('CLOUDFLARE_API_TOKEN') if os.getenv('CLOUDFLARE_API_TOKEN') is not None else os.getenv('CF_API_TOKEN')
    config['certtoken'] = os.getenv('CLOUDFLARE_API_CERTKEY') if os.getenv('CLOUDFLARE_API_CERTKEY') is not None else os.getenv('CF_API_CERTKEY')
    config['extras'] = os.getenv('CLOUDFLARE_API_EXTRAS') if os.getenv('CLOUDFLARE_API_EXTRAS') is not None else os.getenv('CF_API_EXTRAS')
    config['base_url'] = os.getenv('CLOUDFLARE_API_URL') if os.getenv('CLOUDFLARE_API_URL') is not None else os.getenv('CF_API_URL')
    config['openapi_url'] = os.getenv('CLOUDFLARE_OPENAPI_URL') if os.getenv('CLOUDFLARE_OPENAPI_URL') is not None else os.getenv('CF_OPENAPI_URL')

    config['global_request_timeout'] = os.getenv('CLOUDFLARE_GLOBAL_REQUEST_TIMEOUT')
    config['max_request_retries'] = os.getenv('CLOUDFLARE_MAX_REQUEST_RETRIES')
    config['http_headers'] = os.getenv('CLOUDFLARE_HTTP_HEADERS')

    # grab values from config files
    cp = configparser.ConfigParser()
    try:
        cp.read([
            '.cloudflare.cfg',
            os.path.expanduser('~/.cloudflare.cfg'),
            os.path.expanduser('~/.cloudflare/cloudflare.cfg')
        ])
    except OSError:
        raise ReadConfigError("%s: configuration file error" % ('.cloudflare.cfg')) from None

    if len(cp.sections()) == 0 and profile is not None and len(profile) > 0:
        # no config file and yet a config name provided - not acceptable!
        raise ReadConfigError("%s: configuration section provided however config file missing" % (profile)) from None

    # Is it CloudFlare or Cloudflare? (A legacy issue)
    if profile is None:
        if cp.has_section('CloudFlare'):
            profile = 'CloudFlare'
        if cp.has_section('Cloudflare'):
            profile = 'Cloudflare'

    # still not found - then set to to CloudFlare for legacy reasons
    if profile is None:
        profile = "CloudFlare"

    config['profile'] = profile

    if len(profile) > 0 and len(cp.sections()) > 0:
        # we have a configuration file - lets use it

        if not cp.has_section(profile):
            raise ReadConfigError("%s: configuration section missing - configuration file only has these sections: %s" % (profile, ','.join(cp.sections()))) from None

        for option in ['email', 'key', 'token', 'certtoken', 'extras', 'base_url', 'openapi_url', 'global_request_timeout', 'max_request_retries', 'http_headers']:
            try:
                config_value = cp.get(profile, option)
                if option == 'extras':
                    # we join all values together as one space seperated strings
                    config[option] = re.sub(r"\s+", ' ', config_value)
                elif option == 'http_headers':
                    # we keep lines as is for now
                    config[option] = config_value
                else:
                    config[option] = re.sub(r"\s+", '', config_value)
                if config[option] is None or config[option] == '':
                    config.pop(option)
            except (configparser.NoOptionError, configparser.NoSectionError):
                pass

            # do we have an override for specific calls? (i.e. token.post or email.get etc)
            for method in ['get', 'patch', 'post', 'put', 'delete']:
                option_for_method = option + '.' + method
                try:
                    config_value = cp.get(profile, option_for_method)
                    config[option_for_method] = re.sub(r"\s+", '', config_value)
                    if config[option] is None or config[option] == '':
                        config.pop(option_for_method)
                except (configparser.NoOptionError, configparser.NoSectionError):
                    pass

    # do any final cleanup - only needed for extras and http_headers (which are multiline)
    if 'extras' in config and config['extras'] is not None:
        config['extras'] = config['extras'].strip().split(' ')
    if 'http_headers' in config and config['http_headers'] is not None:
        config['http_headers'] = [h for h in config['http_headers'].split('\n') if len(h) > 0]
        for h in config['http_headers']:
            try:
                t, v = h.split(':', 1)
            except ValueError:
                # clearly a bad header syntax
                raise ReadConfigError('%s: header syntax error' % (h)) from None
            if len(t.strip()) == 0:
                raise ReadConfigError('%s: header syntax error' % (h)) from None

    # remove blank entries
    for x in sorted(config.keys()):
        if config[x] is None or config[x] == '':
            try:
                config.pop(x)
            except KeyError:
                pass

    return config
python-cloudflare-2.20.0/CloudFlare/tests/000077500000000000000000000000001461736615400204315ustar00rootroot00000000000000python-cloudflare-2.20.0/CloudFlare/tests/__init__.py000066400000000000000000000000611461736615400225370ustar00rootroot00000000000000""" __init__.py to make pytest coverage work """
python-cloudflare-2.20.0/CloudFlare/tests/dummy_loa_document.pdf000066400000000000000000000320571461736615400250170ustar00rootroot00000000000000%PDF-1.4
%Óëéá
1 0 obj
<>
endobj
3 0 obj
<>
endobj
7 0 obj
<> stream
xœíÁ1 nëÊ@/øC]
endstream
endobj
8 0 obj
<> stream
xœÅYÛŠ#7}ï¯Ðs`4ª*]!<;ö>o0$ï›Ý…À$dòÿênµïÇRÏ„]l£j©Nºj†ŒÓ÷éG*l>¿ÿ6…iuùÖE2ãû×fþñúmxü(æÛ¿Ã(Ï
¹Íë—áëðéâ„Ä&øñË!gsú©ç]/Ï~Ú;o|6û¯>YÊRJ&³Q!YŽÞìÿ0?;Ó/fÿç@Ù¦˜]wÌ‚T&…]8¬;šÖ·{ 1ëœ)-Í\âmÍ9©æ£jÚÍouÝùt<¹Y­‹~ävÄy‡Uã\,og³‹
?QN‹rñþTùf³Ø}ÃgDÁ†Ém¹86–\QIŠarÛÕâ…Û‚!oãøJÞK6ú¤æÊ >çÕŽháÄO°Ô}ERJG¼ìî"Ý,¥ zšx·Â	àQÛ.Ð9Ùº±šª
$  õÇ7ØoªÊÑŽÜQˆ\FªV“CIƒÝvÈ[ýA;Ì;šºv牙xÑX)	nblb„5ïb9p?·Øb³PöùªÅ’³ÚâŸ4ß}ÅF}(_öXÝB¢Ù鮚,±â“ö[«v²¡ˆ+rl¿µË*°à‚+ÇÇ.{×ôLVB)ì›`Ø®žÌ¬ž®fƒkÔ•Î8R”9§&:ª*.>j/ïpPùÐ"ˆº#)þ&ˆÊy²I}!,m" uð¨0gÍpŽ)èÈØŒŸîå“@x{Fé®ìL¯‹ŒÒuGU0±De|-•aqY)1û—ÕÊp-à¼VP›ßâc×ëÎLΗØÏ@!ØŽ»@è¼_7¾ƒ;Þ tÛÿm¸£ Á®‹
¤S§6¦ÔAAmâ+(¸º Š,Ú'“„»Õ!?à£üjºûŽÇ°W¤¹ÇPH*‘Þ—“¬Wœ#wää2f¶ŽÔ¾¥7áRºÂëi­]Ðåux\®Ïž0Vû”»"F¯öl#ÁÞl×Ë–zìÁ¤"ØÔW„¼fEf¿z@¬OÿÕÑÓ*1-{ÂôÚœ¤ÃÜ`64êB«Ì=l·ÂkEÓÃlÃ4ÂêÆxÅ®èÜô#n,x\¯s¯^L2ÇÓ™3ÞÙ—ñ×™ë$Öä7

d	 † àNE…Áá"Ëa¦HãN˜Ûà(L	Öë!RŽ-‡$ÂI)ÇÍem	º%¨ Èû¢„#ß&ñLPn–Iêz!Á‰]ÊÏf#c|ý2üö“ùKñq¿Lÿªrc‰œËâãïÉ<ÿ­Ú>
ÿ
;£
endstream
endobj
2 0 obj
<>
/XObject <>
/Font <>>>
/MediaBox [0 0 612 792]
/Contents 8 0 R
/StructParents 0
/Parent 9 0 R>>
endobj
9 0 obj
<>
endobj
10 0 obj
<>
endobj
11 0 obj
<>
endobj
12 0 obj
<> stream
xœµV{L[×ÿν~` `Fš‚Áƒ؆‡€ÉâÇñÚ|—B¤¦QR
iõ’!ËÉaâ,ç\çâeýfüM<ßMl‹¯®áàì|&›L€´«Ã×C&‚±å	­B°'R\Œ²„µ^<7‘4	BID"1™=ƒ×Ó¾I±ñ ñ%êáüæ’,ÿ‰OÿòçBüýÖï—^@LP¼ªÔAT#y)ØÏÁ¢Ïg¤È{’ovœìÀ­ù­^‘ÐlŒ.‘‡z“9+­l¾”Ø—E€;e¯¸¨Qnj‰$j̱`T0²ŒÚF^€Q7À(0ŠŒŽŒæFgFÏŒ.Œ–ò@¢6ú”g=j:("È‚Nh½´›º¨WTÙCm|H$—ľ¤³ä#-â¹1Ú›ÑDÅ<¼?¯ÂVO5ÿBøì-xî#áûÀßnƒ÷–½&`ÛÌÜœËÀ[kð÷UH†Ôn”£ã’ý’—ê—t¯p=MG°ÃɽcÐI¢wøž_á–ÌGg Ù¿Šçòª”X®¦OvZsç+q=Ó>ñ¯Û­Ú8p=-§=%>Ïã~³ß[Yñ¾b
ˇšÑ¢{2Ïäê‰L÷—,úKºMI·ô—:£¿¤ŒþRŠþR‰þâ5ÍÉè^•à•o¶àUF`Ì,ê–,ÁÂûSCÓ¼;Œ/еԘa~KØßË©±Œÿjj¬ÀWðS©±’ú¨25Va>79fɯMr,ž~t×1D6J1tþÍR&èY)M˜…DL}¦¡?†ø#øv¹‘_-9ÈEÛ`u«yf°8€Y5âv6p…è5P5숷r-žGiè&¥k[”kylíe&SÎ]_ïrÆ:·Éd,S©:}a¡ËY__Wç2èU*–Ý7\Q1ÜçÛo×TÚ5èmf[eöxù€¦ó±=¬$ðX[ÃèîÖvSÕgüG-Þ’ªNGsóÇ·"BT‡½‰s¨ˆg"œé@k4:îÕØº0xÌj
õvîØîÎÑÖ7µÈ–­‘@|±ïDË>Ç#¦n}¸Ñs®O.^ƒQçw+kúÉùó?“-=ñøãW÷¥,ç`©É´le?~öYÙ²/~Ý?%ªñ{Õ°ÜS5Œu.ÑŒ‚ï@UÅp¯oÈ2–WmsZstŽšdžðkÞí®lÏãý,ÿ'wºŽùKŠëڶغœñ_4=l²ZüñèµIüÇ$zQì/¯‰®=Êw)÷IÌ+“óÈíøë²eÙÌsBoHýÐå&wšo¶,ã¯%?úùg"ù4gvœÖÀ8*}áÆ¢ä[~‘ß)Î+éZ¸
@k”u­{¯ìü3¬¯Ia]µ|{ÅOÇ_P×V„o™yeÃÒ…ayÖê` p¨ºúPÀjÓjmV«]«µkvÌú™Ë?Ó²}Ú¿Üu³¿¢ÍÊF¬£ÑcŸ³¶UPÆÙȧ╳?j$fÔéDÕÝütœß7æÒ×”ÏLŽ
T;š7¹÷:eKõð^ûÞºÜø{<þ{ícMn/§#3r¸„¬DýÈÀ¼µDxÌfäãF6%R‘Ú&!ƒ¾°¨(é2Þ1äP‘
Žê«ÓtºÒttÀ±»ö!å!yƒÕb±o:cëpökšÇ:¬ö»jm[ÛÊæŒÝ-û»»ê6V¶UYk*Ås•Q_s?ÿE„ÞfŸ}‰¼Œuü~å;yJZìéY¶ð8ç,éJ»VNâ˜i–MäÙí[ÞxÙ¦ãd¯T¼ü¶»ÏéìsK%ËK½';ˆþì}€Ä
endstream
endobj
13 0 obj
<>
endobj
14 0 obj
<>
/W [0 [682 492] 31 [515] 37 [527] 46 [407] 69 [391 0 535] 103 [397] 111 [660 0 528] 121 [539] 156 [519] 162 [473] 170 [413] 176 [543]]
/DW 248>>
endobj
15 0 obj
<> stream
xœ]ÑÍjÃ0ໟBÇîPòÓ¦¥YÚBûaÙ ±•ΰ8Æqyû9Rè`†>$Ù–Uõ¹6ÚCôîFÙ ‡^åpïN"txÓF$)(-ý*úË¡µ"
ÅÍ)%WÖŽ”f¬=ëÂÊHû5v`YGR¶cH‡5V±ÖΤãzú…ub]I§Š”¥¤2eñÍÊ’Å7{ŽYµ¼ö¶4¿<Òc²òî\*½$Ms™£6øxl;Ú¥jù~  •Æ
endstream
endobj
4 0 obj
<>
endobj
16 0 obj
<> stream
xœíX{l[Wÿν7vâ8Mœø‘؎ߎñ+¾±6u'nkÞ&¥Í;iÓÕN²ÄihÑè˜(kWØÒiE€¨P+Q„`Lµûƒiªh5ñGµ
„
¡‹4@üÑ:|çú6MÒR†@¢Bþ>÷9ßùßù¾s-€"Ì8€–øžfb!fF޽
-Ý]}ñTûÑcÛÐÒ7иZpŠÁöElWwõùƒÃ×°Žó¿Œí±¾þXÿïÛÞ]Àv;¶ã“Éñùœg™°½†íÔáñÅyº®Wb™{8±r裯ïû3@·Ò7f¦Ç§tÖ_àþä=Ï`‡„Ï‘`û¶m3ÉÔò®’ËêwŠks“ãžnܯà@ž!9¾<ϽÍÁù»q¾iv<9½/wù<;…NÍÏ-¦Ö¯ãéøüÂôüŒäìóئç•^#ëϿƽˆM ëëP”èQI¼¹½r)K8–JÛ9òwpÃ,H„Ù„“ ëW!˜™½M°‡ýyšò3´X°Îís¸s"† *'`0L®,$@{xaú(hã©YÐf¦nBF×îkzåÚÕÑÂÝ–ývÿR¿x‰–ï¿j¸¸Hïbœ
çJé‹uO›yاD%Ès+ÐT.¨ÄÜÌ«Q‰ ¹‚5¥Û€9~x&EìGRã	Ò$Xã‰?À*TüÌè9˜!,sÀ̶÷fJ2AbÀÞ|6åX†á¶3‹7vÁEij’ᔳ‘c@¾!ØþsžÞ¸p#Œp;#+X ˆÆ„²{8´Ð
}pŽ@æa	VV½Ð‚½ã0ƒ½³°@{YÅúQß| ¸ãŒäÃt7.ë7HP¬0¥XG¾Ñ3uvÁm±Î¾-Ös`
šÅºÙµdêȯÔbÝÿÀò-GßÉÔãXè…9Hâ)f¡
RX&ð”“0ÓXƳ¥ðŒsxæì†&¬OaÙí9d óöìþXÄ5shÉUàC/¨‚¨EsheR9¶1æÃ²
Â8Nw?Šëé¬CX.c2¡wû „)ŒyX°µ‡i‰IÄbÚÀ²yïı¿kŸä÷ËÇïtE@Gw`Í+¶‚B+†3ç`B˜Ý+ÈH
ëI,MÈï¬ÀÂúÍÇ…±)q
<§°øQêÃ÷­ù„•IsWG¦S¸Ê„÷7Ö)ûÇDkÍÓ)¬u¢…$Åb*Ý|&SÚÃ'c˜Æ‹Q-‡bP,*Ì
;ïpX¥ÒˆCÃG"V#£°2ÅéË—œNÎubtô2Wi¼`\aÎßK蘘(¸þÆühmí•ëd‘ÆŸgý/Œ•¹€oì±TT„ªÃa>¨ÖH+*¬‰D¥T«ù`8²†x…DBûNww±¿qÄØ£
:Üq‡¥™/±ÊF?0'å{_™M~¥›7Più‘XÓhP&›_N¿g¤¯K"/eÎb!b^Å£5+jÃêë««¯3gïÞ½—"Eé5œD<Äãð8"Âþ¡êŠ
‡ÃÇlEGÁi4F¥DdîÆÏ…ìûý~o©ÏØokpD¶Ô.xöZâ›O0vyê,µÏȾiC…©L£SØvø÷ƒ!{¸Lg(/Ñ*å–"Ü>Xƒ±Erøm€Á3CI‘:êD¡V)¬«Dâ üì`(šßÛœ¯¾Æ–z-¥^_mÙÛãnž
/çsöO1æ]Ú¶“Â(wÆÝ}C2I‘¯Ú1¦4(ª&ÛÓ¿«Ñ;tÊ^NmQªe¸+îïbÞÂ;¶e˜°J­Èœ”PÓ3ãÆ3ˆIªR«I¯¥ÅÀÉWsØò[ýþ@tb¿Î£U:L¥^Ê+·˜ÂÌ[ßéÔêžiX©_6Äxo­AUvSQ„¼7Q?Àó–?Ö4Ô:O¶u?×^;fŒ—‡ôîF›µÞåˆéj=Iyt©·w)j7P–X*+,ú’a;~CÀÖ‹ð4J|ñ¨ýûæyõÊbˆ²¸±‘ÅèDȵ»”“®.糺¶²Ê¥K­óé‚ò3+=©z}YÇ•{M¼Î¶Œ'(.4Dým{¨?GÄsضœƒò%5‡ÌÒꇎóTÇɶ§Ž5vÌ„r˜ôi¢ÒðU¶°ÁX¢æªÊz¦ž¥ºú£q¥=oR¿;jjò1S±b¿ÎH¿D½˜µ17ñË#DãVUPĬºãï¬l7êKùr¿/ýÇÓd'¹·s8¬%òdnoš!'“![Ñ–^¸s!JÄ»F7S°¥ŠÖÕ\VßSÓß±jv¼Z¼ÎÑrob$ýSbúteé«ô÷C'šz“ùXRAðÕp/z©my1âwY‘á_a
™Ê
Î}•ø‚†b»ÆTQ/Ÿ!_XLƒÏš››ÞG†¬*dš‡¡›újú;3ÀÈZ£É·—è.´QƇîf³/·C<±Tkk*–É-^ÆkrÑÇ2ùñŒ—er¢¦UØCˆšÁ
¤ÂVDUÉÖ¨A䬱ÃYGÃ¥Æ\¯å¤úAsùVPg­[hX®×k{^"¶­Q“ÂMm¸_ñf^ÄÛ—*RËrÖ~¤ÖìR4v½o¿‰¬%¢µ2Ùg¤ÒpS:ëCÈ	åÕ½õµó1ž­xÿ©s6žò×{§Ã®Š|ß>=ôé§›’ŽF{¿Kc(ª®oßgŽ$å^ãÁrC™"¿°@ž«nßÙ[y@­5wÉ#±ï%%.,~×îSÉrŸ–)¨½:´W@Ö¨ŸR/ãESˆQþëöU£Kï)àæÌÝòÄ	¦ô•šIsº¨ÅîCÆõ:¦mè"'‘Ëkð¹G† »ƒù’º¢H+SæUVÉe?{¹oG©œ“•æ·yGx[Âͳ¬]¯%ܱîuØÛmwÒùuý‰it\Aëøût\¢þ	9@N.¦‹0vš‰š)&Ÿ•H¸Ì¯Z±Mmö…̪fò.QÏÌБ[L9Ç|L¿h1‹xWx;Ä㮩qWF"L]•ÓÅó.gUæ÷£{›ã·ýQúÃmú7â#Ãäer‹¬‘5¦„1Ee^bn¢þu±=좨ßÌjVÿ#ý~V³šÕÿ#½žÕ'@odõ‘ú>ý÷”þ·ñÏÇ=˜7gÛüà¿bjÅÔ„É)‚©W£ý›æò›úéÜV±Â¢{aªÛfßøI±f%+YÉÊ[6¿aYÉJV²’•¬dåÉüF7ÿ¯1d%+O‚dc!+ÿŽ ¿Üú°æ
endstream
endobj
17 0 obj
<>
endobj
18 0 obj
<>
/DW 600>>
endobj
19 0 obj
<> stream
xœ]QÛjÃ0}÷Wè±{(¹õ
!°¥äa–íR[é‹c÷!?[êZ˜À6GÒ9’¥¤nÑ’w7Ê=ôÚ(‡Óxqá„gmD–ƒÒÒ_Ýrè¬H¹'CcúQ”%@ò¢“w3,Õx‘¼9…N›3,¾ê6àöbíh<¤¢ª@a”^:ûÚ
	Ñ–
qíçeàÜ3>g‹θ9*œl'Ñu挢LƒUP>«õ/¾bÖ©—ß‹ÙÙ*d§éº¨"Ê·„65¡‚cÛ}DEq$”H÷ª°þÓ»—ßQZº§gµ%¥¬fnÆò+ØÉU6;v2/bç‘BEÊýä׸füd\Æm‚òâ\mŒ¦ç¥
Þ–jGYñü	•^
endstream
endobj
5 0 obj
<>
endobj
20 0 obj
<> stream
xœíYl[Õ>ç^Çn7‰c;ŽÇöõulÇŽíÄ×<ça;im;‰ÓŽ¤ÍœWK^sÒ¦£ëØè´U©ÐS·±µÚC°	4!:Qi¬šP7ؤM€Ð €@J;†+1•ÚûϽ®›¤€è`<$ÿÎûœïœÿß{ƒ0B¨2B¡p;z	áÓÐÛÒÑ×ýwߪ!ªÚ;:¢ƒm/=xöa„èƒÐ~¶7êtÝt䌟€ö®è@ëÀ…óO†aý{ÐÞŸ[ÈÕçB•z²Û¦Æˆ4W@¹ijfe’û×­¤kB¹ÏMOŒ—þÆÀ8Á÷NC‡˜ËQBû&h§g—öG6mþ1Bê—*®›™=õÛ?žAH|ÖkgÇö/ˆÎR»a¾æëçÆf'/ì/ã†A‡£ó‹K©Óˆƒq?_HL,L‹ßm)´%§qêö‡DG ‰p*…
qËQ°ÅŠæ˜´Ö(H<¥A.„6Œ#¡‡~.ùÈN5§šiõÆ9¢£|xýì@tg¡ÿDÓÉÈšÅS¤<¯öþTs²V‹ŒÐƒÔ6’¦ëÚ‰‚¾`Œ6cðsäd„ÜŒlÛQ
än`Ìó&MÁçFD-OM/áJ„v/Íà Œ+ *h()ÊȤyy>Å™t_à(ÔÞ3€Êâ+‰T6•˜¸•ÍŒ-Í¡2[॔`ëdb,’…•Æ„r¤<”“|úÊωnôkÄ=Ô0ÈÉIwR°ƒ¨[(ñ(ra-ôæÓ¹´ˆ¦(ÑFï·†ÚzÑã`ïŠà‘?±áñF¼HÝCv?m‰`‘‚·ñÏAY¾£ÁÛNÔ€ZQõ ^´- ½hì@€|­Í „ÐO½™ú]šPK®‹"4@[èÑŒÿÜhôcøæçCÜÏÍÏ"#ïɲ¹½³	¤Èx@°J˜Iâa
¡6À®ñ³>u	lZÄîRHÅiïý‡‰ú$ìµUbt˜]Ó§@·¬›q?ÃÂk’‡t¹žÀ]é:FZ\®S¨öQ¨Ó¨½˜®‹=”®zÒu1œDƒP‡8•‚g…zêyézjEÐ<š…™Ch‰ß¡Ý(ŽbhÊ)4
}Q˜±ö-}A¨CÙíyØÛÔ§`tV&À/ÂÊyÀÓ£ZЮò:ÔHó€çGöeÆPÖ"/Œn†õdÖ$”û!éá¶p $/ä^k­6úŒ>ú´Fú
­Õ€DaDaã
X´¾õѲÁëK¤Õ@Ížn¹øV+ÌœG_ågGÑ
øh	ê³PêÁïs¼_pjH>ÆÃˆ…÷ÿô7Àùr¢ež0ã*šƒ_9cU¨V,ƒï—`•à‡E~?ö¥ÑÚyß/Am Ì]ô¥kmÒ«¸‡çù
|€ç‡>„ßÄÇñïñß©Qê[Ôaž ^¢1ÍÑ7ßE?I¿%R¦9œå,"Èr–³üç=Àû>&ø„|äÃ'?‚üLùqàs7ÄÏdùùux:Ê78D•Á»ráyºÁ{÷¢Œ‘U2f3+‘øÌ*Îçcu”,€_HøÓ-òî=xðQmÛ?ÚÆ¾IÝse¦á;SS}«3º¼|dÿžïõ€YGƒ÷Q É99K›LfV,–Ð>£ÔŸ=?œ1…¢"½lQ‡íÔ±+xÞ7[_?ëKÞ}.Œ`TFž ç*)Q*ÄbÖ̹¼^ÛdbÙØOâßm‹vvÇ]®xw'u¬ûøôî{¶…š÷F#KðÖqP*(þm0dŒòP‡AÜ£ä}©&õ6e¡NÀ;¿¡°Ádò¸½Þ«’ÌWåÄb¥¢¤Ww%ZZ]Bîꮪêvñ¹4rßÌž{ûúîÝ3s_¤³ue º¯¹y_t`¥•Èè4¾„´ðŠb‚Ÿƒ"¨s€â¥ÉXâèóPDÖ»öžêSg°ºÎ©kò©ô¥á>[0fJh˸Ö©š^®ÕÖJ™€-2XÈÔ1²ò‚º"FéÚÞ”|¶Mïh3æêkõålÈùêxÑÈÛ»ÊÊ8¥„û@K%J0´ÅØo %mšfb¶¶ަñ-Ö Yã¯e:+Ù)«óRçÎŽ,MóÝÑ}°#æ÷ôÛYÓ%]x¼䕃½ë<ª’\s$l O%ã=+á®[ºü1m•¶Å\7ätÖÙ»µ•–¸ÔŸè‹$üÖ
O™Ö9XçpÕ£¬ñ§ÞÆ—Á¼ü«ðœÙíñbF.ܹÔS¼-ˆ$ô†Ð—m63[:Æ=Uf¹N«Q³žW¥ûÃY©kÐ#·)ŠžšÙѯ´­ë8­–ÓÁŸŽ«ªTûêv*+‹šþ—õ~[E}‰(ߤSÕÊEò›g›M”¹+ÜÝ–ÜÜ|L£qìÛœøa›5pœuk’wiªÕl©H$¯RšìäëÃùÄÇ69ÝWcŽ“Œß‰l$’CW×E"n¿-dƒ°]´øÆw%ÿŠÙp°º:ùKò}e@ýzJlƒä[›èH
Á&çV
Ø2!Îd,˜"³ÙßÛùé{n›²º\É4H·áW‚WžñÔäKRYÝš9`¼f*A1Õ5õxí2j6´I(¹Ëjtä)9+Wq«Eª\]YŒW[V«ÕlßÚ™<…‡+MÉûñÕFÊŒ@Žb­œµID¦kÀ«Ñ*ï:û…3EiãcÞ‚-³Áàl‹;ÛM¦v§3l6‡ÓgÖŸˆô%ü!瀯nÐ)œ]ÄßB/ƒ¿…bM3áǪ”òõ·hN¶ÛÛ¶;ãL/C‹n³…2—П¨_ôÖ¦ùžÈ>8À7Ââõ·ÐqªyÅëü/œ&‰ì8`›‚åιÚXᛨƫ‹¡ÜüŽÜMÍ=ÉWa}ø¤Ö[…;ÓÇsÛA™y÷¬¿ÑTZŠØ½ߨ6Y'ƒ¦|ßµsçäxÝ´ÑÂ8¬µÊ`WŒqí’:´^­Ñ¡-(-Qä)BÞÆÞJµ§´\®—ëØB™Õ[iiµ}
ƒ|uR§wÄÃz|>N	Gví•sgG„½ãp~è7J¼N¶®¬˜é”r;ü«œ'‚l~­4Ï/ÄãV¸7ÿƒWIœÄH4_eYúÖ|7‰rMÖ°%”ˆ*‡¥ã»°#y>´9q²lÐêŒÚ”“2F9‘ö‰ÏGsò’âaŸœ£¨o±›eyª|®¥¸ðÕèr®P´¹\ºgÛ³ÅÞÞ¿ˆE­´¨ÑnÄ’oé;l'ƒ7_¹T³µšèHÎáQÀ‡ßê18nŒû_Án|2˜$;újî¬í‡4©’RÒ¤-éþæ6¤ûÉÜ‘tÿqH¶nÀ¯½}²”¥,eéÓ¤µwX–²”¥,e)KYºq‚ßÒç×–Ÿ"î৉÷?Èžò¿ˆ>¹üY®û2ئY[þŸd|®g!K_.‚xyí¿B:
endstream
endobj
21 0 obj
<>
endobj
22 0 obj
<>
/DW 600>>
endobj
23 0 obj
<> stream
xœ]‘Mnƒ0…÷>Å,ÛE˜¤i$„”’DbÑ•öÄRKÅXÆYpûššJµÒç™7<Þ$U}¨­	¼ùA5 3V{‡«Wg¼+2	Ú¨°½Uß:‘Dq3ûÚvƒ(
€ä=VÇà'¸ÛëáŒ÷"yõ½±¸û¬šÈÍÕ¹oìÑHEY‚Æ.NznÝKÛ#$$[Õ:ÖM˜VQó×ñ19Iœ±5h]«Ð·ö‚¢Hã)¡8ÅS
´ú_}Ǫs§¾ZOÝ2v§é:+‰6Lk¦SE”¥L'¢|M´ÝÍ”ËG"ydÚå{¢üȵÑf!v·øÈ]Ý~"«¸¿òÀîä–/Ÿø’mI6™³»­\æò¤9€yQ·tÕÕû,m“³4owƒ›UóóYŸšÿ
endstream
endobj
6 0 obj
<>
endobj
xref
0 24
0000000000 65535 f 
0000000015 00000 n 
0000001533 00000 n 
0000000109 00000 n 
0000005017 00000 n 
0000008550 00000 n 
0000012648 00000 n 
0000000146 00000 n 
0000000371 00000 n 
0000001784 00000 n 
0000001839 00000 n 
0000002016 00000 n 
0000002078 00000 n 
0000004100 00000 n 
0000004302 00000 n 
0000004651 00000 n 
0000005163 00000 n 
0000007761 00000 n 
0000007965 00000 n 
0000008183 00000 n 
0000008700 00000 n 
0000011848 00000 n 
0000012055 00000 n 
0000012276 00000 n 
trailer
<>
startxref
12801
%%EOF
python-cloudflare-2.20.0/CloudFlare/tests/test_add.py000066400000000000000000000036751461736615400226050ustar00rootroot00000000000000""" add to api tests """

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test API list fetches from Cloudflare website

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_add_invalid():
    """ test_add_invalid """
    """add API commands"""
    cf.add('OPEN', 'invalid')
    try:
        results = cf.invalid()
        print('error - should not reach here', file=sys.stderr)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # error 7000 No route for that URI
        print('Error expected: %d=%s' % (int(e), str(e)), file=sys.stderr)
        assert int(e) == 7000
        assert str(e) == 'No route for that URI'

def test_add_invalid_with_underscore():
    """add API commands"""
    cf.add('OPEN', 'in_valid')
    try:
        results = cf.in_valid()
        print('error - should not reach here', file=sys.stderr)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # error 7000 No route for that URI
        print('Error expected: %d=%s' % (int(e), str(e)), file=sys.stderr)
        assert int(e) == 7000
        assert str(e) == 'No route for that URI'

def test_add_invalid_with_dash():
    """add API commands"""
    cf.add('OPEN', 'in-val-id')
    try:
        results = cf.in_val_id()
        print('error - should not reach here', file=sys.stderr)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # error 7000 No route for that URI
        print('Error expected: %d=%s' % (int(e), str(e)), file=sys.stderr)
        assert int(e) == 7000
        assert str(e) == 'No route for that URI'

if __name__ == '__main__':
    test_cloudflare(debug=True)
    test_add_invalid()
    test_add_invalid_with_underscore()
    test_add_invalid_with_dash()
python-cloudflare-2.20.0/CloudFlare/tests/test_api_dump.py000066400000000000000000000037061461736615400236460ustar00rootroot00000000000000""" test dump calls """

import os
import sys
import re

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test API list fetches from Cloudflare website

cf = None

OPENAPI_URL = "https://github.com/cloudflare/api-schemas/raw/main/openapi.json"

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

verb_only = re.compile('^[a-zA-Z0-9][-a-zA-Z0-9_]*[a-zA-Z0-9]$')

def check_cmd_syntax(cmd):
    """ check_cmd_syntax """
    assert '/' == cmd[0]
    for verb in cmd[1:].split('/'):
        if verb[0] == '@':
            # don't want to check the rest of the api - it's an AI one
            break
        if verb[0] == ':':
            # :id or equiv
            assert bool(verb_only.match(verb[1:]))
        else:
            # just a verb
            assert bool(verb_only.match(verb))

def check_method_syntax(method):
    """ check_method_syntax """
    assert method in ['GET', 'POST', 'PATCH', 'PUT', 'DELETE']

def test_api_list():
    """dump a tree of all the known API commands"""
    api_list = cf.api_list()
    assert len(api_list) > 0
    for api in api_list:
        check_cmd_syntax(api)

def test_api_from_openapi():
    """dump a tree of all the known API commands - from web"""
    api_list = cf.api_from_openapi()
    assert len(api_list) > 0
    for api in api_list:
        # {'action': 'GET', 'cmd': '/accounts', 'deprecated': ...
        assert 'action' in api
        assert 'cmd' in api
        check_method_syntax(api['action'])
        check_cmd_syntax(api['cmd'])

def test_api_from_openapi_with_url():
    """dump a tree of all the known API commands - from web"""
    api_list = cf.api_from_openapi(OPENAPI_URL)
    assert len(api_list) > 0

if __name__ == '__main__':
    test_cloudflare(debug=True)
    test_api_list()
    test_api_from_openapi()
    test_api_from_openapi_with_url()
python-cloudflare-2.20.0/CloudFlare/tests/test_certificates.py000066400000000000000000000042611461736615400245120ustar00rootroot00000000000000""" certificates tests """

import os
import sys
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test /centificates - a weird call (will fail if you don't have a certtoken)

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

def test_certificates():
    """ test_certificates """
    params = {'zone_id':zone_id}
    try:
        certificates = cf.certificates(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s - can not run this test on this domain - no worries - skipping' % (zone_name, int(e), str(e)), file=sys.stderr)
        return

    assert isinstance(certificates, list)
    if len(certificates) == 0:
        # no cert's for this domain - which is the norm
        print('no cert(s) returned', file=sys.stderr)
        return
    for c in certificates:
        assert isinstance(c, dict)
        assert 'id' in c
        assert 'expires_on' in c
        assert 'hostnames' in c
        assert 'certificate' in c
        print('%s: %48s %29s %s' % (zone_name, c['id'], c['expires_on'], c['hostnames']), file=sys.stderr)

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_certificates()
python-cloudflare-2.20.0/CloudFlare/tests/test_cloudflare.py000077500000000000000000000066411461736615400241740ustar00rootroot00000000000000""" test global_request_timeout and max_request_retries """

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips1():
    """ test_ips1 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_with_global_request_timeout(debug=False):
    """ test_cloudflare_with_global_request_timeout """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug, global_request_timeout=10)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips2():
    """ test_ips2 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_with_max_request_retries(debug=False):
    """ test_cloudflare_with_max_request_retries """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug, max_request_retries=2)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips3():
    """ test_ips3 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_with_global_request_timeout_and_max_request_retries(debug=False):
    """ test_cloudflare_with_global_request_timeout_and_max_request_retries """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug, global_request_timeout=10, max_request_retries=2)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips4():
    """ test_ips4 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_with_global_request_timeout_invalid(debug=False):
    """ test_cloudflare_with_global_request_timeout_invalid """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug, global_request_timeout='STRING')
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips5():
    """ test_ips5 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_with_max_request_retries_invalid(debug=False):
    """ test_cloudflare_with_max_request_retries_invalid """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug, max_request_retries='STRING')
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips6():
    """ test_ips6 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_with_global_request_timeout_and_max_request_retries_invalid(debug=False):
    """ test_cloudflare_with_global_request_timeout_and_max_request_retries_invalid """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug, global_request_timeout='STRING', max_request_retries='STRING')
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips7():
    """ test_ips7 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

if __name__ == '__main__':
    test_cloudflare(debug=True)
    test_ips1()
    test_cloudflare_with_global_request_timeout(debug=True)
    test_ips2()
    test_cloudflare_with_max_request_retries(debug=True)
    test_ips3()
    test_cloudflare_with_global_request_timeout_and_max_request_retries(debug=True)
    test_ips4()
    test_cloudflare_with_global_request_timeout_invalid(debug=True)
    test_ips5()
    test_cloudflare_with_max_request_retries_invalid(debug=True)
    test_ips6()
    test_cloudflare_with_global_request_timeout_and_max_request_retries_invalid(debug=True)
    test_ips7()
python-cloudflare-2.20.0/CloudFlare/tests/test_cloudflare_calls.py000066400000000000000000000135371461736615400253510ustar00rootroot00000000000000""" class calling tests """

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test Cloudflare init param (ie. debug, raw, etc)

cf = None

def test_cloudflare():
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare()
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_percent_s():
    """ test_percent_s """
    s = '%s' % cf
    assert len(s) > 0 and isinstance(s, str)

def test_percent_r():
    """ test_percent_r """
    s = '%r' % cf
    assert len(s) > 0 and isinstance(s, str)

def test_percent_ips_s():
    """ test_percent_ips_s """
    s = '%s' % cf.ips
    assert len(s) > 0 and isinstance(s, str)

def test_percent_ips_r():
    """ test_percent_ips_r """
    s = '%r' % cf.ips
    assert len(s) > 0 and isinstance(s, str)

def test_percent_cf_accounts_billing_s():
    """ test_percent_cf_accounts_billing_s """
    s = '%s' % cf.accounts.billing
    assert len(s) > 0 and isinstance(s, str)

def test_percent_cf_accounts_billing_r():
    """ test_percent_cf_accounts_billing_r """
    s = '%r' % cf.accounts.billing
    assert len(s) > 0 and isinstance(s, str)

def test_percent_cf_zones_waiting_rooms_events_details_s():
    """ test_percent_cf_accounts_billing_s """
    s = '%s' % cf.zones.waiting_rooms.events.details
    assert len(s) > 0 and isinstance(s, str)

def test_percent_cf_zones_waiting_rooms_events_details_r():
    """ test_percent_cf_accounts_billing_r """
    s = '%r' % cf.zones.waiting_rooms.events.details
    assert len(s) > 0 and isinstance(s, str)

def test_ips1():
    """ test_ips1 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_debug():
    """ test_cloudflare_debug """
    global cf
    cf = CloudFlare.CloudFlare(debug=True)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips2():
    """ test_ips2 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_raw():
    """ test_cloudflare_raw """
    global cf
    cf = CloudFlare.CloudFlare(raw=False)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips3():
    """ test_ips3 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_no_sessions():
    """ test_cloudflare_no_sessions """
    global cf
    cf = CloudFlare.CloudFlare(use_sessions=False)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips4():
    """ test_ips4 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_ips5():
    """ test_ips5 """
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert len(ips) > 0

def test_cloudflare_url_invalid():
    """ test_cloudflare_url_invalid """
    global cf
    cf = CloudFlare.CloudFlare(base_url='blah blah blah blah ...')
    # this does not fail yet - so we wait

def test_ips6_should_fail():
    """ test_ips6_should_fail """
    try:
        ips = cf.ips()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s %d %s' % (type(e).__name__, int(e), str(e)), file=sys.stderr)
        pass
    except Exception as e:
        print('Error expected: %s %s' % (type(e).__name__, e), file=sys.stderr)
        pass

def test_cloudflare_url_wrong():
    """ test_cloudflare_url_wrong """
    global cf
    cf = CloudFlare.CloudFlare(base_url='http://example.com/')
    # this does not fail yet - so we wait

def test_ips7_should_fail():
    """ test_ips7_should_fail """
    try:
        ips = cf.ips()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s %d %s' % (type(e).__name__, int(e), str(e)), file=sys.stderr)
        pass
    except Exception as e:
        print('Error expected: %s %s' % (type(e).__name__, e), file=sys.stderr)
        pass

def test_cloudflare_email_invalid():
    """ test_cloudflare_email_invalid """
    global cf
    try:
        cf = CloudFlare.CloudFlare(email=int(0))
        assert False
    except TypeError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

def test_cloudflare_key_invalid():
    """ test_cloudflare_key_invalid """
    global cf
    try:
        cf = CloudFlare.CloudFlare(key=int(0))
        assert False
    except TypeError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

def test_cloudflare_token_invalid():
    """ test_cloudflare_token_invalid """
    global cf
    try:
        cf = CloudFlare.CloudFlare(token=int(0))
        assert False
    except TypeError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

def test_cloudflare_certtoken_invalid():
    """ test_cloudflare_certtoken_invalid """
    global cf
    try:
        cf = CloudFlare.CloudFlare(certtoken=int(0))
        assert False
    except TypeError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

def test_cloudflare_context():
    """ test_cloudflare_context """
    global cf
    cf = None
    with CloudFlare.CloudFlare() as cf:
        assert isinstance(cf, CloudFlare.CloudFlare)
        ips = cf.ips()
        assert isinstance(ips, dict)
        assert len(ips) > 0

if __name__ == '__main__':
    test_cloudflare()
    test_percent_s()
    test_percent_r()
    test_percent_ips_s()
    test_percent_ips_r()
    test_percent_cf_accounts_billing_s()
    test_percent_cf_accounts_billing_r()
    test_percent_cf_zones_waiting_rooms_events_details_s()
    test_percent_cf_zones_waiting_rooms_events_details_r()
    test_ips1()
    test_cloudflare_debug()
    test_ips2()
    test_cloudflare_raw()
    test_ips3()
    test_cloudflare_no_sessions()
    test_ips4()
    test_ips5()

    test_cloudflare_url_wrong()
    test_ips6_should_fail()

    test_cloudflare_url_invalid()
    test_ips7_should_fail()

    test_cloudflare_email_invalid()
    test_cloudflare_key_invalid()
    test_cloudflare_token_invalid()
    test_cloudflare_certtoken_invalid()

    test_cloudflare_context()
python-cloudflare-2.20.0/CloudFlare/tests/test_dns_import_export.py000066400000000000000000000073051461736615400256260ustar00rootroot00000000000000""" dns import/export test """

import os
import sys
import uuid
import time
import random
import tempfile

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test IMPORT EXPORT

cf = None

def test_cloudflare():
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare()
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

def test_dns_import():
    """ test_dns_import """
    # IMPORT
    # create a zero length file
    fp = tempfile.TemporaryFile(mode='w+b')
    fp.seek(0)
    while True:
        try:
            results = cf.zones.dns_records.import_.post(zone_id, files={'file':fp})
            break
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            print('cf.zones.dns_records.import: returned %d "%s"' % (int(e), str(e))) # 429 or 99998
            time.sleep(.5) # This is sadly needed as import seems to be rate limited
            fp.seek(0)
    # {"recs_added": 0, "recs_added_by_type": {}, "total_records_parsed": 0}
    assert len(results) > 0
    assert results['recs_added'] == 0
    assert len(results['recs_added_by_type']) == 0
    assert results['total_records_parsed'] == 0

def test_dns_export():
    """ test_dns_export """
    # EXPORT
    dns_records = cf.zones.dns_records.export.get(zone_id)
    assert len(dns_records) > 0
    assert isinstance(dns_records, str)
    assert 'SOA' in dns_records
    assert 'NS' in dns_records

def test_cloudflare_with_debug():
    """ test_cloudflare_with_debug """
    global cf
    cf = CloudFlare.CloudFlare(debug=True)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_dns_import_with_debug():
    """ test_dns_import_with_debug """
    # IMPORT
    # create a zero length file
    fp = tempfile.TemporaryFile(mode='w+b')
    fp.seek(0)
    while True:
        try:
            results = cf.zones.dns_records.import_.post(zone_id, files={'file':fp})
            break
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            print('cf.zones.dns_records.import: returned %d "%s"' % (int(e), str(e))) # 429 or 99998
            time.sleep(.5) # This is sadly needed as import seems to be rate limited
            fp.seek(0)
    # {"recs_added": 0, "recs_added_by_type": {}, "total_records_parsed": 0}
    assert len(results) > 0
    assert results['recs_added'] == 0
    assert len(results['recs_added_by_type']) == 0
    assert results['total_records_parsed'] == 0

def test_dns_export_with_debug():
    """ test_dns_export_with_debug """
    # EXPORT
    dns_records = cf.zones.dns_records.export.get(zone_id)
    assert len(dns_records) > 0
    assert isinstance(dns_records, str)
    assert 'SOA' in dns_records
    assert 'NS' in dns_records

if __name__ == '__main__':
    test_cloudflare()
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_dns_import()
    test_dns_export()
    test_cloudflare_with_debug()
    test_dns_import_with_debug()
    test_dns_export_with_debug()
python-cloudflare-2.20.0/CloudFlare/tests/test_dns_records.py000066400000000000000000000131721461736615400243530ustar00rootroot00000000000000""" get/post/delete/etc dns based tests """

import os
import sys
import uuid
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test GET POST PUT PATCH & DELETE - but not in that order

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        exit(0)
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

dns_name = None
dns_type = None
dns_content1 = None
dns_content2 = None
dns_content3 = None

def test_dns_records_create_values():
    """ test_dns_records_create_values """
    global dns_name, dns_type, dns_content1, dns_content2, dns_content3
    dns_name = str(uuid.uuid1())
    dns_type = 'TXT'
    dns_content1 = 'temp pytest element 1'
    dns_content2 = 'temp pytest element 2'
    dns_content3 = 'temp pytest element 3'
    print('dns_record: %s' % (dns_name), file=sys.stderr)

def test_dns_records_port_invalid():
    """ test_dns_records_port_invalid """
    # create an invalid DNS record - i.e. txt value for A record IP address
    dns_record = {'name':dns_name, 'type':'A', 'content':'NOT-A-VALID-IP-ADDRESS'}
    try:
        dns_result = cf.zones.dns_records.post(zone_id, data=dns_record)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # more than one error returned by the API - a specific error and a generic error
        print('Error expected: %r' % (e))
        assert len(e) > 0
        print('Error expected (chain): %s' % (' '.join(['%r' % (v) for v in e])))
        assert True

def test_dns_records_get1():
    """ test_dns_records_get1 """
    # GET
    params = {'name':dns_name + '.' + zone_name, 'match':'all', 'type':dns_type}
    dns_results = cf.zones.dns_records.get(zone_id, params=params)
    assert len(dns_results) == 0

dns_id = None

def test_dns_records_post():
    """ test_dns_records_post """
    global dns_id
    # POST
    dns_record = {'name':dns_name, 'type':dns_type, 'content':dns_content1}
    dns_result = cf.zones.dns_records.post(zone_id, data=dns_record)
    assert dns_result['name'] == dns_name + '.' + zone_name
    assert dns_result['type'] == dns_type
    assert dns_result['content'] == dns_content1

    dns_id = dns_result['id']
    assert len(dns_id) == 32
    print('dns_record: %s %s' % (dns_name, dns_id), file=sys.stderr)

def test_dns_records_get2():
    """ test_dns_records_get2 """
    # GET
    params = {'name':dns_name + '.' + zone_name, 'match':'all', 'type':dns_type}
    dns_results = cf.zones.dns_records.get(zone_id, params=params)
    assert len(dns_results) == 1
    assert dns_results[0]['name'] == dns_name + '.' + zone_name
    assert dns_results[0]['type'] == dns_type
    assert dns_results[0]['content'] == dns_content1

def test_dns_records_get3():
    """ test_dns_records_get3 """
    # GET
    dns_result = cf.zones.dns_records.get(zone_id, dns_id)
    assert dns_result['name'] == dns_name + '.' + zone_name
    assert dns_result['type'] == dns_type
    assert dns_result['content'] == dns_content1

def test_dns_records_patch():
    """ test_dns_records_patch """
    # PATCH
    dns_record = {'content':dns_content2}
    dns_result = cf.zones.dns_records.patch(zone_id, dns_id, data=dns_record)
    assert dns_result['name'] == dns_name + '.' + zone_name
    assert dns_result['type'] == dns_type
    assert dns_result['content'] == dns_content2

def test_dns_records_put():
    """ test_dns_records_put """
    # PUT
    dns_record = {'name':dns_name, 'type':dns_type, 'content':dns_content3}
    dns_result = cf.zones.dns_records.put(zone_id, dns_id, data=dns_record)
    assert dns_result['name'] == dns_name + '.' + zone_name
    assert dns_result['type'] == dns_type
    assert dns_result['content'] == dns_content3

def test_dns_records_get4():
    """ test_dns_records_get4 """
    # GET
    dns_result = cf.zones.dns_records.get(zone_id, dns_id)
    assert dns_result['name'] == dns_name + '.' + zone_name
    assert dns_result['type'] == dns_type
    assert dns_result['content'] == dns_content3

def test_dns_records_delete():
    """ test_dns_records_delete """
    # DELETE
    dns_result = cf.zones.dns_records.delete(zone_id, dns_id)
    assert dns_result['id'] == dns_id

def test_dns_records_get5():
    """ test_dns_records_get5 """
    # GET
    params = {'name':dns_name + '.' + zone_name, 'match':'all', 'type':dns_type}
    dns_results = cf.zones.dns_records.get(zone_id, params=params)
    assert len(dns_results) == 0

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_dns_records_create_values()
    test_dns_records_port_invalid()
    test_dns_records_get1()
    test_dns_records_post()
    test_dns_records_get2()
    test_dns_records_get3()
    test_dns_records_patch()
    test_dns_records_put()
    test_dns_records_get4()
    test_dns_records_delete()
    test_dns_records_get5()
python-cloudflare-2.20.0/CloudFlare/tests/test_find.py000066400000000000000000000021641461736615400227650ustar00rootroot00000000000000""" find api tests """

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test API list fetches from Cloudflare website

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_find():
    """ test_find """
    ips_call = cf.find('/ips')
    assert True

def test_find_call():
    """ test_find """
    ips_call = cf.find('/ips')
    ips = ips_call()
    assert isinstance(ips, dict)
    assert isinstance(ips['ipv4_cidrs'], list)
    assert isinstance(ips['ipv6_cidrs'], list)
    assert len(ips['ipv4_cidrs']) > 0
    assert len(ips['ipv6_cidrs']) > 0

def test_find_invalid():
    """ test_find """
    try:
        invalid_endpoint_call = cf.find('/invalid-endpoint')
        print('error - should not reach here', file=sys.stderr)
        assert False
    except AttributeError as e:
        assert True

if __name__ == '__main__':
    test_cloudflare(debug=True)
    test_find()
    test_find_call()
    test_find_invalid()
python-cloudflare-2.20.0/CloudFlare/tests/test_graphql.py000066400000000000000000000151021461736615400234770ustar00rootroot00000000000000""" graphql tests """

import os
import sys
import random
import datetime
import pytz
import json

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test /graphql

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

def rfc3339_iso8601_time(hour_delta=0, with_hms=False):
    # format time (with an hour offset in RFC3339 or ISO8601 format (and do it UTC time)
    if sys.version_info[:3][0] <= 3 and sys.version_info[:3][1] <= 10:
        dt = datetime.datetime.utcnow().replace(microsecond=0, tzinfo=pytz.UTC)
    else:
        dt = datetime.datetime.now(datetime.UTC).replace(microsecond=0)
    dt += datetime.timedelta(hours=hour_delta)
    if with_hms:
        return dt.isoformat().replace('+00:00', 'Z')
    return dt.strftime('%Y-%m-%d')

def test_graphql_get():
    """ /graphql_get test """
    try:
        # graphql is alwatys a post - this should fail (but presently doesn't)
        r = cf.graphql.get()
        if r is not None and 'data' in r and 'errors' in r:
            # still an invalid API!
            print('Error in API (but proceeding) r=', r, file=sys.stderr)
            assert True
        else:
            assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)
        assert True

def test_graphql_patch():
    """ /graphql_patch test """
    try:
        # graphql is alwatys a post - this should fail (but presently doesn't)
        r = cf.graphql.patch()
        if r is not None and 'data' in r and 'errors' in r:
            # still an invalid API!
            print('Error in API (but proceeding) r=', r, file=sys.stderr)
            assert True
        else:
            assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)
        assert True

def test_graphql_put():
    """ /graphql_put test """
    try:
        # graphql is alwatys a post - this should fail (but presently doesn't)
        r = cf.graphql.put()
        if r is not None and 'data' in r and 'errors' in r:
            # still an invalid API!
            print('Error in API (but proceeding) r=', r, file=sys.stderr)
            assert True
        else:
            assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)
        assert True

def test_graphql_delete():
    """ /graphql_delete test """
    try:
        # graphql is alwatys a post - this should fail (but presently doesn't)
        r = cf.graphql.delete()
        if r is not None and 'data' in r and 'errors' in r:
            # still an invalid API!
            print('Error in API (but proceeding) r=', r, file=sys.stderr)
            assert True
        else:
            assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)
        assert True

def test_graphql_post_empty():
    """ /graphql_post_empty test """
    try:
        # graphql requires data - this should fail (but presently doesn't)
        r = cf.graphql.post(data={})
        if r is not None and 'data' in r and 'errors' in r:
            # still an invalid API!
            print('Error in API (but proceeding) r=', r, file=sys.stderr)
            assert True
        else:
            assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)
        assert True

def test_graphql_post():
    """ /graphql_post test """
    date_before = rfc3339_iso8601_time(0) # now
    date_after = rfc3339_iso8601_time(-3 * 24) # 3 days worth

    query = """
      query {
        viewer {
            zones(filter: {zoneTag: "%s"} ) {
            httpRequests1dGroups(limit:40, filter:{date_lt: "%s", date_gt: "%s"}) {
              sum { countryMap { bytes, requests, clientCountryName } }
              dimensions { date }
            }
          }
        }
      }
    """ % (zone_id, date_before, date_after)

    # remove whitespace from query - this isn't needed; but helps debug
    query = '\n'.join([s.strip() for s in query.splitlines()]).strip()

    # graphql query is always a post
    try:
        r = cf.graphql.post(data={'query':query})
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % ('/graphql.post', int(e), str(e)), file=sys.stderr)
        assert False

    # success - lets confirm it's graphql results as-per query above

    # basic graphql results
    assert 'data' in r
    assert 'errors' in r
    assert r['errors'] is None

    # viewer and zones from above
    assert 'viewer' in r['data']
    assert 'zones' in r['data']['viewer']

    # only one zone
    zones = r['data']['viewer']['zones']
    assert len(zones) == 1
    zone_info = zones[0]

    # the data
    assert 'httpRequests1dGroups' in zone_info
    httpRequests1dGroups = zone_info['httpRequests1dGroups']
    assert isinstance(httpRequests1dGroups, list)
    for h in httpRequests1dGroups:
        assert 'dimensions' in h
        assert 'date' in h['dimensions']
        result_date = h['dimensions']['date']
        assert 'sum' in h
        result_sum = h['sum']
        assert 'countryMap' in result_sum
        countryMap = h['sum']['countryMap']
        for element in countryMap:
            assert 'bytes' in element
            assert 'requests' in element
            assert 'clientCountryName' in element

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_graphql_get()
    test_graphql_put()
    test_graphql_patch()
    test_graphql_delete()
    test_graphql_post_empty()
    test_graphql_post()
python-cloudflare-2.20.0/CloudFlare/tests/test_images_v2_direct_upload.py000066400000000000000000000133771461736615400266270ustar00rootroot00000000000000""" radar returning CSV test """

import os
import sys
import json
import random
import datetime
import pytz

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test /accounts/:id/images/v2/direct_upload - this uses forms in port

cf = None

def rfc3339_iso8601_time(hour_delta=0, with_hms=False):
    # format time (with an hour offset in RFC3339 or ISO8601 format (and do it UTC time)
    if sys.version_info[:3][0] <= 3 and sys.version_info[:3][1] <= 10:
        dt = datetime.datetime.utcnow().replace(microsecond=0, tzinfo=pytz.UTC)
    else:
        dt = datetime.datetime.now(datetime.UTC).replace(microsecond=0)
    dt += datetime.timedelta(hours=hour_delta)
    if with_hms:
        return dt.isoformat().replace('+00:00', 'Z')
    return dt.strftime('%Y-%m-%d')

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

account_name = None
account_id = None

def test_find_account(find_name=None):
    """ test_find_account """
    global account_name, account_id
    # grab a random account identifier from the first 10 accounts
    if find_name:
        params = {'per_page':1, 'name':find_name}
    else:
        params = {'per_page':10}
    try:
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (find_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(accounts) > 0 and len(accounts) <= 10
    # n = random.randrange(len(accounts))
    # stop using a random account - use the primary account (i.e. the zero'th one)
    n = 0
    account_name = accounts[n]['name']
    account_id = accounts[n]['id']
    assert len(account_id) == 32
    print('account: %s %s' % (account_id, account_name), file=sys.stderr)

# simple metadata in json string form
metadata_values = json.dumps({
    'item1': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...',
    'item2': 'Ignore item number one',
})

# format future time in RFC3339 format (and do it UTC time)
time_plus_one_hour_in_iso = rfc3339_iso8601_time(1, True)

def test_images_v2_direct_upload():
    """ test_images_v2_direct_upload """

    try:
        r = cf.accounts.images.v2.direct_upload.post(account_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error unexpected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert False

    assert isinstance(r, dict)
    assert len(r['id']) > 0
    assert len(r['uploadURL']) > 0

    image_id = r['id']
    image_url = r['uploadURL']
    print('%s %s' % (image_id, image_url), file=sys.stderr)

def test_images_v2_direct_upload_data():
    """ test_images_v2_direct_upload """

    data = {
        'metadata': metadata_values,
        'expiry': time_plus_one_hour_in_iso,
    }
    try:
        r = cf.accounts.images.v2.direct_upload.post(account_id, data=data)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error unexpected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert False

    assert isinstance(r, dict)
    assert len(r['id']) > 0
    assert len(r['uploadURL']) > 0

    image_id = r['id']
    image_url = r['uploadURL']
    print('%s %s' % (image_id, image_url), file=sys.stderr)

def test_images_v2_direct_upload_files():
    """ test_images_v2_direct_upload """

    files = {
        ('metadata', (None, metadata_values)),
        ('expiry', (None, time_plus_one_hour_in_iso))
    }
    try:
        r = cf.accounts.images.v2.direct_upload.post(account_id, files=files)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error unexpected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert False

    assert isinstance(r, dict)
    assert len(r['id']) > 0
    assert len(r['uploadURL']) > 0

    image_id = r['id']
    image_url = r['uploadURL']
    print('%s %s' % (image_id, image_url), file=sys.stderr)

def test_images_v2_direct_upload_data_and_files():
    """ test_images_v2_direct_upload """

    data = {
        'metadata': metadata_values,
        'expiry': time_plus_one_hour_in_iso,
    }
    files = {
        ('metadata', (None, metadata_values)),
        ('expiry', (None, time_plus_one_hour_in_iso))
    }
    try:
        r = cf.accounts.images.v2.direct_upload.post(account_id, data=data, files=files)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error unexpected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert False

    assert isinstance(r, dict)
    assert len(r['id']) > 0
    assert len(r['uploadURL']) > 0

    image_id = r['id']
    image_url = r['uploadURL']
    print('%s %s' % (image_id, image_url), file=sys.stderr)

def test_images_v2_direct_upload_files_len_zero():
    """ test_images_v2_direct_upload """

    files = set() # zero length set
    try:
        r = cf.accounts.images.v2.direct_upload.post(account_id, files=files)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # this can trigger an error from the Cloudflare API backend - which is wrong; however, should be coded around.
        print('Error unexpected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert False

    assert isinstance(r, dict)
    assert len(r['id']) > 0
    assert len(r['uploadURL']) > 0

    image_id = r['id']
    image_url = r['uploadURL']
    print('%s %s' % (image_id, image_url), file=sys.stderr)

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_account(sys.argv[1])
    else:
        test_find_account()
    test_images_v2_direct_upload()
    test_images_v2_direct_upload_data()
    test_images_v2_direct_upload_files()
    test_images_v2_direct_upload_data_and_files()
    test_images_v2_direct_upload_files_len_zero()
python-cloudflare-2.20.0/CloudFlare/tests/test_ips.py000066400000000000000000000042161461736615400226400ustar00rootroot00000000000000""" ips tests """

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

def test_ips():
    """ test_ips """
    # no auth required
    ips = cf.ips()
    assert isinstance(ips, dict)
    assert isinstance(ips['ipv4_cidrs'], list)
    assert isinstance(ips['ipv6_cidrs'], list)
    assert len(ips['ipv4_cidrs']) > 0
    assert len(ips['ipv6_cidrs']) > 0

def test_ips_plus_jdcloud():
    """ test_ips_plus_jdcloud """
    # no auth required
    params = {'networks':'jdcloud'}
    ips = cf.ips(params=params)
    assert isinstance(ips, dict)
    assert isinstance(ips['ipv4_cidrs'], list)
    assert isinstance(ips['ipv6_cidrs'], list)
    assert isinstance(ips['jdcloud_cidrs'], list)
    assert len(ips['ipv4_cidrs']) > 0
    assert len(ips['ipv6_cidrs']) > 0
    assert len(ips['jdcloud_cidrs']) > 0

def test_ips_patch():
    """ test_ips_patch """
    # should fail!
    try:
        cf.ips.patch()
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

def test_ips_post():
    """ test_ips_post """
    # should fail!
    try:
        cf.ips.post()
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

def test_ips_put():
    """ test_ips_put """
    # should fail!
    try:
        cf.ips.put()
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

def test_ips_delete():
    """ test_ips_delete """
    # should fail!
    try:
        cf.ips.delete()
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %s' % (e), file=sys.stderr)

if __name__ == '__main__':
    test_cloudflare(debug=True)
    test_ips()
    test_ips_plus_jdcloud()
    test_ips_patch()
    test_ips_post()
    test_ips_put()
    test_ips_delete()
python-cloudflare-2.20.0/CloudFlare/tests/test_issue114.py000066400000000000000000000217211461736615400234230ustar00rootroot00000000000000""" issue-114 tests """

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# CloudFlare(email=None, key=None, token=None, certtoken=None, debug=False, ...)

cf = None

debug = False

class TestCloudflare:
    """ TestCloudflare """

    def test_email_key_token000(self):
        """ test_email_key_token### """
        self._run(0, 0, 0)

    def test_email_key_token001(self):
        """ test_email_key_token### """
        self._run(0, 0, 1)

    def test_email_key_token002(self):
        """ test_email_key_token### """
        self._run(0, 0, 2)

    def test_email_key_token010(self):
        """ test_email_key_token### """
        self._run(0, 1, 0)

    def test_email_key_token011(self):
        """ test_email_key_token### """
        self._run(0, 1, 1)

    def test_email_key_token012(self):
        """ test_email_key_token### """
        self._run(0, 1, 2)

    def test_email_key_token020(self):
        """ test_email_key_token### """
        self._run(0, 2, 0)

    def test_email_key_token021(self):
        """ test_email_key_token### """
        self._run(0, 2, 1)

    def test_email_key_token022(self):
        """ test_email_key_token### """
        self._run(0, 2, 2)

    def test_email_key_token100(self):
        """ test_email_key_token### """
        self._run(1, 1, 0)

    def test_email_key_token101(self):
        """ test_email_key_token### """
        self._run(1, 1, 1)

    def test_email_key_token102(self):
        """ test_email_key_token### """
        self._run(1, 1, 2)

    def test_email_key_token110(self):
        """ test_email_key_token### """
        self._run(1, 1, 1)

    def test_email_key_token111(self):
        """ test_email_key_token### """
        self._run(1, 1, 1)

    def test_email_key_token112(self):
        """ test_email_key_token### """
        self._run(1, 1, 2)

    def test_email_key_token120(self):
        """ test_email_key_token### """
        self._run(1, 2, 1)

    def test_email_key_token121(self):
        """ test_email_key_token### """
        self._run(1, 2, 1)

    def test_email_key_token122(self):
        """ test_email_key_token### """
        self._run(1, 2, 2)

    def test_email_key_token200(self):
        """ test_email_key_token### """
        self._run(2, 0, 0)

    def test_email_key_token201(self):
        """ test_email_key_token### """
        self._run(2, 0, 1)

    def test_email_key_token202(self):
        """ test_email_key_token### """
        self._run(2, 0, 2)

    def test_email_key_token210(self):
        """ test_email_key_token### """
        self._run(2, 1, 2)

    def test_email_key_token211(self):
        """ test_email_key_token### """
        self._run(2, 1, 1)

    def test_email_key_token212(self):
        """ test_email_key_token### """
        self._run(2, 1, 2)

    def test_email_key_token220(self):
        """ test_email_key_token### """
        self._run(2, 2, 2)

    def test_email_key_token221(self):
        """ test_email_key_token### """
        self._run(2, 2, 1)

    def test_email_key_token222(self):
        """ test_email_key_token### """
        self._run(2, 2, 2)

    def _run(self, token_index, key_index, email_index):
        """ _run """
        global cf
        try:
            profile = self._profile
        except AttributeError:
            # Always clear environment
            self._setup()
            assert self._email or self._key or self._token
            # if not self._email and not self._key and not self._token:
            #     assert 'EMAIL/KEY/TOKEN all needed in order to run this test' == ''
            profile = self._profile

        # select combination
        email = [None, self._email, 'example@example.com'][email_index]
        key = [None, self._key, self._token][key_index]
        token = [None, self._token, self._key][token_index]

        try:
            cf = CloudFlare.CloudFlare(email=email, key=key, token=token, debug=debug, profile=profile)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            print('%s: Error %d=%s' % ('CloudFlare', int(e), str(e)), file=sys.stderr)
            # don't know what to do; but, lets continue anyway
            return
        assert isinstance(cf, CloudFlare.CloudFlare)

        try:
            r = cf.zones.get(params={'per_page':1})
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            print('%s: Error %d=%s' % ('/zones', int(e), str(e)), file=sys.stderr)
            r = None

        if email is None and key is None and token == self._token:
            print('SUCCESS (as expeced): email = ', self._obfuscate(email), 'key = ', self._obfuscate(key), 'token = ', self._obfuscate(token), file=sys.stderr)
            assert isinstance(r, list)
            assert len(r) == 1
            assert isinstance(r[0], dict)
            return

        if email is None and key == self._token and token is None:
            print('SUCCESS (as expeced): email = ', self._obfuscate(email), 'key = ', self._obfuscate(key), 'token = ', self._obfuscate(token), file=sys.stderr)
            assert isinstance(r, list)
            assert isinstance(r, list)
            assert len(r) == 1
            assert isinstance(r[0], dict)
            return

        if email == self._email and key == self._key and token is None:
            print('SUCCESS (as expeced): email = ', self._obfuscate(email), 'key = ', self._obfuscate(key), 'token = ', self._obfuscate(token), file=sys.stderr)
            assert isinstance(r, list)
            assert len(r) == 1
            assert isinstance(r[0], dict)
            return

        if email == self._email and key is None and token == self._key:
            print('SUCCESS (as expeced): email = ', self._obfuscate(email), 'key = ', self._obfuscate(key), 'token = ', self._obfuscate(token), file=sys.stderr)
            assert isinstance(r, list)
            assert len(r) == 1
            assert isinstance(r[0], dict)
            return

        # Nothing else should work!
        print('FAILED  (as expeced): email = ', self._obfuscate(email), 'key = ', self._obfuscate(key), 'token = ', self._obfuscate(token), file=sys.stderr)
        assert r is None

    def _setup(self):
        """ setup """
        # Force no profile to be picked
        self._profile = ''
        # read in email/key/token from config file(s)
        _config_files = [
            '.cloudflare.cfg',
            os.path.expanduser('~/.cloudflare.cfg'),
            os.path.expanduser('~/.cloudflare/cloudflare.cfg')
        ]
        email = None
        key = None
        token = None
        for filename in _config_files:
            try:
                with open(filename, 'r') as fd:
                    for line in fd:
                        if email and key and token:
                            break
                        if line[0] == '#':
                            continue
                        a = line.split()
                        if len(a) < 3:
                            continue
                        if a[1] != '=':
                            continue
                        if not email and a[0] == 'email':
                            email = a[2]
                            continue
                        if not key and a[0] == 'key':
                            key = a[2]
                            continue
                        if not token and a[0] == 'token':
                            token = a[2]
                            continue
                break
            except FileNotFoundError:
                pass
        self._email = email
        self._key = key
        self._token = token

        # now remove all env variables!
        for env in ['CLOUDFLARE_EMAIL', 'CLOUDFLARE_API_KEY', 'CLOUDFLARE_API_TOKEN']:
            try:
                del os.environ[env]
            except KeyError:
                pass
        for env in ['CF_API_EMAIL', 'CF_API_KEY', 'CF_API_TOKEN']:
            try:
                del os.environ[env]
            except KeyError:
                pass

    def _obfuscate(self, s):
        """ _obfuscate """
        return 'â–ˆ' if s is None else 'â–ˆ' * len(s)

if __name__ == '__main__':
    debug = True
    t = TestCloudflare()
    t.test_email_key_token000()
    t.test_email_key_token001()
    t.test_email_key_token002()
    t.test_email_key_token010()
    t.test_email_key_token011()
    t.test_email_key_token012()
    t.test_email_key_token020()
    t.test_email_key_token021()
    t.test_email_key_token022()
    t.test_email_key_token100()
    t.test_email_key_token101()
    t.test_email_key_token102()
    t.test_email_key_token110()
    t.test_email_key_token111()
    t.test_email_key_token112()
    t.test_email_key_token120()
    t.test_email_key_token121()
    t.test_email_key_token122()
    t.test_email_key_token200()
    t.test_email_key_token201()
    t.test_email_key_token202()
    t.test_email_key_token210()
    t.test_email_key_token211()
    t.test_email_key_token212()
    t.test_email_key_token220()
    t.test_email_key_token221()
    t.test_email_key_token222()
python-cloudflare-2.20.0/CloudFlare/tests/test_loa_documents.py000066400000000000000000000145601461736615400247040ustar00rootroot00000000000000""" loa_documents tests """

import os
import sys
import random
import tempfile

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

from CloudFlare.tests.utils import dummy_loa_document

# test /accounts/:id/addressing/prefixes
# test /accounts/:id/addressing/loa_documents
# test /accounts/:id/addressing/loa_documents/:id/download

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

account_name = None
account_id = None

def test_find_account(find_name=None):
    """ test_find_account """
    global account_name, account_id
    # grab a random account identifier from the first 10 accounts
    if find_name:
        params = {'per_page':1, 'name':find_name}
    else:
        params = {'per_page':10}
    try:
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (find_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(accounts) > 0 and len(accounts) <= 10
    # n = random.randrange(len(accounts))
    # stop using a random account - use the primary account (i.e. the zero'th one)
    n = 0
    account_name = accounts[n]['name']
    account_id = accounts[n]['id']
    assert len(account_id) == 32
    print('account: %s %s' % (account_id, account_name), file=sys.stderr)

def test_addressing_prefixs():
    """ test_addressing_prefixs """
    prefixes = cf.accounts.addressing.prefixes(account_id)
    assert isinstance(prefixes, list)
    for p in prefixes:
        assert 'id' in p
        assert 'cidr' in p
        assert 'asn' in p
        assert 'advertised' in p
        assert 'approved' in p
        print('%s: cidr=%s asn=%s advertised=%s approved=%s' % (
            p['id'],
            p['cidr'],
            p['asn'],
            p['advertised'],
            p['approved']
        ), file=sys.stderr)

def test_addressing_loa_documents():
    """ test_addressing_loa_documents """
    loa_documents = cf.accounts.addressing.loa_documents(account_id)
    assert isinstance(loa_documents, list)
    for loa_document in loa_documents[-4:]:
        assert isinstance(loa_document, dict)
        assert 'id' in loa_document
        assert 'created' in loa_document
        assert 'filename' in loa_document
        assert 'verified' in loa_document
        assert 'size_bytes' in loa_document
        print('%s: %s filename=%s size_bytes=%d verified=%s' % (
           loa_document['id'],
           loa_document['created'],
           loa_document['filename'],
           loa_document['size_bytes'],
           loa_document['verified']
        ), file=sys.stderr)

def test_addressing_loa_documents_upload(filename=None):
    """ test_addressing_loa_documents_upload """
    if filename:
        # use provided file
        try:
            pdf_file = open(filename, 'rb')
        except (FileNotFoundError, IsADirectoryError, PermissionError) as e:
            print('%s: %s' % (filename, e), file=sys.stderr)
            assert False
    else:
        # create a dummy temporary file
        pdf_file = tempfile.NamedTemporaryFile(mode='w+b', prefix='dummy-loa-document-', suffix='.pdf', delete=False)
        pdf_file.write(dummy_loa_document.encode())
        pdf_file.seek(0)

    size_bytes = os.fstat(pdf_file.fileno()).st_size
    print('filename=%s size_bytes=%d' % (pdf_file.name, size_bytes), file=sys.stderr)

    files = {'loa_document': pdf_file}
    try:
        loa_document = cf.accounts.addressing.loa_documents.post(account_id, files=files)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (filename, int(e), str(e)), file=sys.stderr)
        assert False
    assert isinstance(loa_document, dict)
    assert 'id' in loa_document
    assert 'filename' in loa_document
    assert 'verified' in loa_document
    assert 'size_bytes' in loa_document
    print('%s: filename=%s size_bytes=%d verified=%s' % (
        loa_document['id'],
        loa_document['filename'],
        loa_document['size_bytes'],
        loa_document['verified']
    ), file=sys.stderr)
    assert size_bytes == loa_document['size_bytes']

def ispdf(s):
    """ ispdf """
    if isinstance(s, str):
        s = s.encode()
    idx = 0
    while s[idx] in [b'\r', b'\n']:
        idx += 1
    # maybe ... \xef\xbb\xbf%PDF- ... which is  U+FEFF - the byte order mark, or BOM
    if s[idx:idx+3] == b'\xef\xbb\xbf':
        idx += 3
    # Simple %PDF- starter
    if s[idx:idx+5] == b'%PDF-':
        return True
    # check further down the file - which seems messy and in-fact is!
    # https://stackoverflow.com/questions/77753113/pdf-not-at-start-of-file-but-why
    if b'%PDF-' in s[0:1024]:
        return True
    # give up!
    print('ispdf: failing with content="%s..."' % (s[0:50]), file=sys.stderr)
    return False

def test_addressing_loa_documents_download():
    """ test_addressing_loa_documents_download """
    loa_documents = cf.accounts.addressing.loa_documents(account_id)
    assert isinstance(loa_documents, list)
    for loa_document in loa_documents[-4:]:
        assert isinstance(loa_document, dict)
        assert 'id' in loa_document
        assert 'created' in loa_document
        assert 'filename' in loa_document
        assert 'verified' in loa_document
        assert 'size_bytes' in loa_document
        assert isinstance(loa_document['size_bytes'], int)
        print('%s: %s filename=%s size_bytes=%d verified=%s' % (
            loa_document['id'],
            loa_document['created'],
            loa_document['filename'],
            loa_document['size_bytes'],
            loa_document['verified']
        ), file=sys.stderr)
        loa_document_identifier = loa_document['id']
        size_bytes = loa_document['size_bytes']
        pdf_content = cf.accounts.addressing.loa_documents.download(account_id, loa_document_identifier)
        assert size_bytes == len(pdf_content)
        assert ispdf(pdf_content)

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_account(sys.argv[1])
    else:
        test_find_account()
    test_addressing_prefixs()
    test_addressing_loa_documents()
    if len(sys.argv) > 2:
        test_addressing_loa_documents_upload(sys.argv[2])
    else:
        test_addressing_loa_documents_upload()
    test_addressing_loa_documents_download()
python-cloudflare-2.20.0/CloudFlare/tests/test_load_balancers.py000066400000000000000000000247651461736615400250110ustar00rootroot00000000000000""" workers tests """

import os
import sys
import uuid
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test /accounts/:id/workers/scripts

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

account_name = None
account_id = None

def test_find_account(find_name=None):
    """ test_find_account """
    global account_name, account_id
    # grab a random account identifier from the first 10 accounts
    if find_name:
        params = {'per_page':1, 'name':find_name}
    else:
        params = {'per_page':10}
    try:
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (find_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(accounts) > 0 and len(accounts) <= 10
    # n = random.randrange(len(accounts))
    # stop using a random account - use the primary account (i.e. the zero'th one)
    n = 0
    account_name = accounts[n]['name']
    account_id = accounts[n]['id']
    assert len(account_id) == 32
    print('account: %s %s' % (account_id, account_name), file=sys.stderr)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

def test_load_balancers_list_regions():
    """ test_load_balancers_list_regions """
    regions = cf.accounts.load_balancers.regions(account_id)
    assert isinstance(regions, dict)
    assert 'regions' in regions
    assert 'iso_standard' in regions
    assert isinstance(regions['regions'], list)
    assert isinstance(regions['iso_standard'], str)
    for region in regions['regions']:
        assert 'countries' in region
        assert 'region_code' in region
        assert isinstance(region['countries'], list)
        assert isinstance(region['region_code'], str)
        countries = ','.join([v['country_code_a2'] for v in region['countries']])
        print('/accounts/load_balancers/regions: %s: %s' % (region['region_code'], countries), file=sys.stderr)

def test_load_balancers_get_regions():
    """ test_load_balancers_get_regions """
    regions = cf.accounts.load_balancers.regions(account_id, 'WNAM')
    assert isinstance(regions, dict)
    assert 'regions' in regions
    assert 'iso_standard' in regions
    assert isinstance(regions['regions'], list)
    assert isinstance(regions['iso_standard'], str)
    for region in regions['regions']:
        assert 'countries' in region
        assert 'region_code' in region
        assert isinstance(region['countries'], list)
        assert isinstance(region['region_code'], str)
        countries = ','.join([v['country_code_a2'] for v in region['countries']])
        print('/accounts/load_balancers/regions: %s: %s' % (region['region_code'], countries), file=sys.stderr)

def test_load_balancers_search():
    """ test_load_balancers_search """
    r = cf.accounts.load_balancers.search(account_id)
    assert isinstance(r, dict)
    assert 'resources' in r
    assert isinstance(r['resources'], list)
    if len(r['resources']) == 0:
        print('/account/load_balancers/search: returns zero results', file=sys.stderr)
        return
    for resource in r['resources']:
        assert 'reference_type' in r
        assert isinstance(r['reference_type'], str)
        assert 'resource_id' in r
        assert isinstance(r['resource_id'], str)
        assert 'resource_name' in r
        assert isinstance(r['resource_name'], str)
        assert 'resource_type' in r
        assert isinstance(r['resource_type'], str)
        print('/account/load_balancers/search: %s: %s' % (r['resource_id'], r['resource_name']), file=sys.stderr)

def test_load_balancers_pools():
    """ test_load_balancers_pools """
    pools = cf.accounts.load_balancers.pools(account_id)
    assert isinstance(pools, list)
    for pool in pools:
        assert isinstance(pool, dict)
        assert 'id' in pool
        print('/accounts/load_balancers/pools: %s: %s length=%d' % (pool['id'], pool['name'], len(pool['origins'])), file=sys.stderr)

pool_id = None

def test_load_balancers_pool_create():
    """ test_load_balancers_pool_create """
    global pool_id
    origin_name = str(uuid.uuid1())
    pool_name = str(uuid.uuid1())
    origins_data = [
        # Yes yes yes - we know these are Google's addresses - but that's ok
        {'address':'8.8.8.101', 'name':origin_name + '_1'},
        {'address':'8.8.8.102', 'name':origin_name + '_2'},
        {'address':'8.8.8.103', 'name':origin_name + '_3'},
    ]
    pool_data = {'description':'testing123', 'name':pool_name, 'origins':origins_data}
    try:
        pool = cf.accounts.load_balancers.pools.post(account_id, data=pool_data)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # this happens when the account isn't setup for load balancers
        pool_id = None
        return
    assert isinstance(pool, dict)
    assert 'id' in pool
    print('/accounts/load_balancers/pools: POST: %s: %s length=%d' % (pool['id'], pool['name'], len(pool['origins'])), file=sys.stderr)
    pool_id = pool['id']
    print('pool_id =', pool_id)

def test_load_balancers_pool_details():
    """ test_load_balancers_pool_details """
    if pool_id is None:
        print('/accounts/load_balancers/pools: skip', file=sys.stderr)
        return
    pool = cf.accounts.load_balancers.pools(account_id, pool_id)
    assert isinstance(pool, dict)
    assert 'id' in pool
    print('/accounts/load_balancers/pools: %s: %s length=%d' % (pool['id'], pool['name'], len(pool['origins'])), file=sys.stderr)
    assert pool_id == pool['id']

load_balancer_id = None

def test_load_balancers_create():
    """ test_load_balancers_create """
    global load_balancer_id
    if pool_id is None:
        print('/zones/load_balancers: POST: skip', file=sys.stderr)
        load_balancer_id = None
        return
    dns_name = str(uuid.uuid1())
    balancer_data = {
        'default_pools': [pool_id],
        'fallback_pool': pool_id,
        'name': dns_name + '.' + zone_name,
        'description': dns_name,
    }
    try:
        load_balancer = cf.zones.load_balancers.post(zone_id, data=balancer_data)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # this happens when the zone isn't setup for load balancers
        print('/zones/load_balancers: POST: skip', file=sys.stderr)
        load_balancer_id = None
        return
    assert isinstance(load_balancer, dict)
    assert 'default_pools' in load_balancer
    assert 'fallback_pool' in load_balancer
    assert 'id' in load_balancer
    load_balancer_id = load_balancer['id']
    print('/zones/load_balancers: POST: %s %s' % (load_balancer['id'], load_balancer['name']), file=sys.stderr)

def test_load_balancers_pool_health():
    """ test_load_balancers_pool_health """
    if pool_id is None:
        print('/accounts/load_balancers/pools/health: skip', file=sys.stderr)
        return
    try:
        health = cf.accounts.load_balancers.pools.health(account_id, pool_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # the new loadbalancer will not be ready - which is ok
        print('/accounts/load_balancers/pools/health: Error expected: %d %s' % (int(e), str(e)), file=sys.stderr)
        return
    assert isinstance(health, dict)
    assert 'pool_id' in health
    assert isinstance(health['pool_id'], str)
    assert isinstance(health['pop_health'], dict)
    print('/accounts/load_balancers/pools/health: %s: length=%d' % (health['pool_id'], len(health['pop_health'])), file=sys.stderr)
    assert pool_id == health['pool_id']

def test_load_balancers_pool_delete_should_fail():
    """ test_load_balancers_pool_delete_should_fail """
    if pool_id is None or load_balancer_id is None:
        print('/accounts/load_balancers/pools: DELETE: skip', file=sys.stderr)
        return
    try:
        pool = cf.accounts.load_balancers.pools.delete(account_id, pool_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # the new loadbalancer can not be deleted as it's in use with a zone
        print('/accounts/load_balancers/pools: DELETE: Error expected: %d %s' % (int(e), str(e)), file=sys.stderr)
        return
    assert 'id' in pool
    print('/accounts/load_balancers/pools: DELETE: %s: deleted' % (pool['id']), file=sys.stderr)
    assert pool_id == pool['id']
    assert False

def test_load_balancers_delete():
    """ test_load_balancers_delete """
    if load_balancer_id is None:
        print('/zones/load_balancers: DELETE: skip', file=sys.stderr)
        return
    r = cf.zones.load_balancers.delete(zone_id, load_balancer_id)
    assert isinstance(r, dict)
    assert 'id' in r
    print('/zones/load_balancers: DELETE: %s: deleted' % (r['id']), file=sys.stderr)
    assert load_balancer_id == r['id']

def test_load_balancers_pool_delete():
    """ test_load_balancers_pool_delete """
    if pool_id is None:
        print('/accounts/load_balancers/pools: DELETE: skip', file=sys.stderr)
        return
    pool = cf.accounts.load_balancers.pools.delete(account_id, pool_id)
    assert isinstance(pool, dict)
    assert 'id' in pool
    print('/accounts/load_balancers/pools: DELETE: %s: deleted' % (pool['id']), file=sys.stderr)
    assert pool_id == pool['id']

if __name__ == '__main__':
    test_cloudflare(debug=False)
    if len(sys.argv) > 1:
        test_find_account(sys.argv[1])
    else:
        test_find_account()
    if len(sys.argv) > 2:
        test_find_zone(sys.argv[2])
    else:
        test_find_zone()
    test_load_balancers_list_regions()
    test_load_balancers_get_regions()
    test_load_balancers_search()
    test_load_balancers_pools()
    test_load_balancers_pool_create()
    test_load_balancers_pool_details()
    test_load_balancers_create()
    test_load_balancers_pool_health()
    test_load_balancers_pool_delete_should_fail()
    test_load_balancers_delete()
    test_load_balancers_pool_delete()
python-cloudflare-2.20.0/CloudFlare/tests/test_log_received.py000066400000000000000000000033631461736615400244760ustar00rootroot00000000000000""" graphql tests """

import os
import sys
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test /zones/:id/logs/received

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

def test_logs_received():
    """ /zones/:id/logs/received test """

    # python -m cli4 -v zones/:$zone/logs/received
    try:
        r = cf.zones.logs.received.get(zone_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % ('/zones.logs.received.get', int(e), str(e)), file=sys.stderr)
        assert False
    # XXX/TODO - sadly this call returns all manner of weird stuff - we punt for now
    assert r is not None

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_logs_received()
python-cloudflare-2.20.0/CloudFlare/tests/test_paging_thru_zones.py000066400000000000000000000056161461736615400255770ustar00rootroot00000000000000""" paging thru zones tests """

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test paging thru zones with raw option

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(raw=True, debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

def paging_thru_zones(name=None):
    """ paging_thru_zones """
    count_received = 0
    total_count = 0 # we want to confirm this total later
    page_number = 0
    while True:
        page_number += 1
        params = {'per_page':10,'page':page_number,'name':name}
        try:
            raw_results = cf.zones.get(params=params)
        except CloudFlare.exceptions.CloudFlareAPIError:
            assert False

        assert 'result_info' in raw_results
        assert 'result' in raw_results

        results_info = raw_results['result_info']
        results = raw_results['result']

        assert 'count' in results_info
        assert 'page' in results_info
        assert 'per_page' in results_info
        assert 'total_count' in results_info
        assert 'total_pages' in results_info

        count = results_info['count']
        page = results_info['page']
        per_page = results_info['per_page']
        total_count = results_info['total_count']
        total_pages = results_info['total_pages']

        assert isinstance(count, int)
        assert isinstance(page, int)
        assert isinstance(per_page, int)
        assert isinstance(total_count, int)
        assert isinstance(total_pages, int)

        assert page_number == page

        assert len(results) == count
        assert isinstance(results, list)

        count_received += count

        domains = []
        for zone in results:
            assert 'id' in zone
            assert 'name' in zone
            zone_name = zone['name']
            domains.append(zone_name)
        print("COUNT=%d PAGE=%d PER_PAGE=%d TOTAL_COUNT=%d TOTAL_PAGES=%d -- %s" % (
            count,
            page,
            per_page,
            total_count,
            total_pages,
            ','.join(domains)
        ), file=sys.stderr)

        if count == 0 or page_number >= total_pages:
            # finished
            break

    # did we receive all the info?
    assert count_received == total_count

def test_paging_thru_zones():
    """ test_paging_thru_zones """
    paging_thru_zones(None)

def test_paging_thru_zones_match_com():
    """ test_paging_thru_zones_match_com """
    # we assume your account has one of these domains
    paging_thru_zones('ends_with:.com')

def test_paging_thru_zones_match_nothing():
    """ test_paging_thru_zones_match_nothing """
    paging_thru_zones('QWERTYUIOOP')

if __name__ == '__main__':
    test_cloudflare(debug=True)
    test_paging_thru_zones()
    test_paging_thru_zones_match_com()
    test_paging_thru_zones_match_nothing()
python-cloudflare-2.20.0/CloudFlare/tests/test_purge_cache.py000066400000000000000000000046731461736615400243210ustar00rootroot00000000000000""" get/post/delete/etc zone ruleset based tests """

import os
import sys
import uuid
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test purge_cache

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None
zone_type = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id, zone_type
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    zone_type = zones[0]['plan']['name']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

def create_purge_zone_data():
    """ create_purge_zone_data """
    if 'Enterprise' in zone_type:
        # Enterprise accounts can do all things ...
        fake_tag = 'tag-' + str(uuid.uuid1())
        data = {
            # 'purge_everything': True,
            'hosts': [zone_name],
            'tags': [fake_tag],
            'prefixes': [zone_name + '/' + 'index.html'],
        }
    else:
        # Free, Pro, Business accounts can only do this ...
        data = {
            'purge_everything': True
        }
    return data

def test_purge_cache_post():
    """ test_purge_cache_post """
    r = cf.zones.purge_cache.post(zone_id, data=create_purge_zone_data())
    assert isinstance(r, dict)
    assert 'id' in r
    assert r['id'] == zone_id

def test_purge_cache_delete():
    """ test_purge_cache_delete """
    # delete method is not in documents; however, it works
    r = cf.zones.purge_cache.delete(zone_id, data=create_purge_zone_data())
    assert isinstance(r, dict)
    assert 'id' in r
    assert r['id'] == zone_id

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_purge_cache_post()
    test_purge_cache_delete()
python-cloudflare-2.20.0/CloudFlare/tests/test_radar_returning_csv.py000066400000000000000000000026301461736615400261040ustar00rootroot00000000000000""" radar returning CSV test """

import os
import sys
import uuid

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test radar - this tests CSV responses

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

aliases = None

def test_radar_datasets_ranking():
    """ test_radar_datasets_ranking """
    # get the list of aliases - we only need to grab 12 values
    global aliases
    params = {'limit':12}
    results = cf.radar.datasets(params=params)
    assert len(results) > 0
    assert 'datasets' in results
    aliases = []
    for v in results['datasets']:
        aliases.append(
            (v['id'], v['alias'], v['meta']['top'])
        )
    aliases = sorted(aliases, key=lambda v: v[2], reverse=False)

def test_radar_datasets_ranking_two_aliases():
    """ test_radar_datasets_ranking_two_aliases """
    for v in aliases[0:2]:
        alias = v[1]
        n_lines = v[2]
        results = cf.radar.datasets(alias)
        # produces CSV results
        assert len(results) > 0
        lines = results.splitlines()
        assert lines[0] == 'domain'
        assert len(lines) >= n_lines + 1

if __name__ == '__main__':
    test_cloudflare(debug=True)
    test_radar_datasets_ranking()
    test_radar_datasets_ranking_two_aliases()
python-cloudflare-2.20.0/CloudFlare/tests/test_rulesets.py000066400000000000000000000160751461736615400237210ustar00rootroot00000000000000""" get/post/delete/etc zone ruleset based tests """

import os
import sys
import uuid
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test GET POST PUT PATCH & DELETE - but not in that order

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

ruleset_name = None
ruleset_ref = None
ruleset_content = None

def test_ruleset_create_values():
    """ test_accounts_ruleset_create_values """
    global ruleset_name, ruleset_ref, ruleset_content
    ruleset_name = str(uuid.uuid1())
    ruleset_ref = str(uuid.uuid1())
    ruleset_content = """
        {
          "description": "Testing 1 2 3 ...",
          "kind": "zone",
          "name": "%s",
          "phase": "http_request_firewall_custom",
          "rules": [
            {
              "action": "block",
              "description": "Block when the IP address is not 1.1.1.1",
              "enabled": false,
              "expression": "ip.src ne 1.1.1.1",
              "ref": "%s"
            }
          ]
        }
    """ % (ruleset_name, ruleset_ref)
    ruleset_content = ''.join([s.strip() for s in ruleset_content.splitlines()]).strip()

    print('ruleset: name=%s content=%s' % (ruleset_name, ruleset_content), file=sys.stderr)
    assert True

ruleset_id = None

def test_zones_rulesets_get():
    """ test_zones_rulesets_get """
    # GET
    ruleset_results = cf.zones.rulesets.get(zone_id)
    assert isinstance(ruleset_results, list)
    for ruleset in ruleset_results:
        assert isinstance(ruleset, dict)
        assert 'id' in ruleset
        assert 'kind' in ruleset
        assert 'phase' in ruleset
        assert 'name' in ruleset
        print('ruleset: %s: name=%s kind=%s phase=%s' % (ruleset['id'], ruleset['name'], ruleset['kind'], ruleset['phase']), file=sys.stderr)
    assert True

def test_zones_ruleset_post():
    """ test_zones_rulesets_post """
    global ruleset_id
    # POST
    ruleset = cf.zones.rulesets.post(zone_id, data=ruleset_content)
    assert isinstance(ruleset, dict)
    assert 'id' in ruleset
    assert 'kind' in ruleset
    assert 'phase' in ruleset
    assert 'name' in ruleset
    assert 'rules' in ruleset
    assert 'ref' in ruleset['rules'][0]
    assert ruleset['name'] == ruleset_name
    assert ruleset['rules'][0]['ref'] == ruleset_ref
    ruleset_id = ruleset['id']
    print('ruleset: %s: name=%s kind=%s phase=%s' % (ruleset['id'], ruleset['name'], ruleset['kind'], ruleset['phase']), file=sys.stderr)

def test_zones_rulesets_get_specific():
    """ test_zones_rulesets_get_specific """
    # GET
    ruleset = cf.zones.rulesets.get(zone_id, ruleset_id)
    assert isinstance(ruleset, dict)
    assert 'id' in ruleset
    assert 'kind' in ruleset
    assert 'phase' in ruleset
    assert 'name' in ruleset
    assert 'rules' in ruleset
    assert ruleset['id'] == ruleset_id
    print('ruleset: %s: name=%s kind=%s phase=%s' % (ruleset['id'], ruleset['name'], ruleset['kind'], ruleset['phase']), file=sys.stderr)

def test_zones_ruleset_delete():
    """ test_zones_rulesets_delete """
    # DELETE
    ruleset_response = cf.zones.rulesets.delete(zone_id, ruleset_id)
    # None is returned - not quite the same response as other delete's in the API
    assert ruleset_response is None

account_name = None
account_id = None

def test_find_account(find_name=None):
    """ test_find_account """
    global account_name, account_id
    # grab a random account identifier from the first 10 accounts
    if find_name:
        params = {'per_page':1, 'name':find_name}
    else:
        params = {'per_page':10}
    try:
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (find_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(accounts) > 0 and len(accounts) <= 10
    # n = random.randrange(len(accounts))
    # stop using a random account - use the primary account (i.e. the zero'th one)
    n = 0
    account_name = accounts[n]['name']
    account_id = accounts[n]['id']
    assert len(account_id) == 32
    print('account: %s %s' % (account_id, account_name), file=sys.stderr)

ruleset_id = None
ruleset_version = None

def test_accounts_ruleset():
    """ test_accounts_ruleset """
    global ruleset_id, ruleset_version
    ruleset_results = results = cf.accounts.rulesets(account_id)
    assert isinstance(ruleset_results, list)
    for ruleset in ruleset_results:
        assert isinstance(ruleset, dict)
        assert 'id' in ruleset
        assert 'version' in ruleset
        ruleset_id = ruleset['id']
        ruleset_version = ruleset['version']
        # we only need one!
        break
    print('account ruleset: %s %s' % (ruleset_id, ruleset_name), file=sys.stderr)

def test_accounts_rulesets_versions():
    """ test_accounts_ruleset_versions """
    ruleset = cf.accounts.rulesets.versions(account_id, ruleset_id, ruleset_version)
    assert isinstance(ruleset, dict)
    assert 'id' in ruleset
    assert 'version' in ruleset
    assert ruleset['id'] == ruleset_id
    assert ruleset['version'] == ruleset_version

def test_accounts_rulesets_versions_by_tag():
    """ test_accounts_rulesets_versions_by_tag """
    # List an account ruleset version's rules by tag
    # /accounts/{account_id}/rulesets/{ruleset_id}/versions/{ruleset_version}/by_tag/{rule_tag}
    # four id's passed on call!
    rule_tag = 'QWERTYUIOP'
    try:
        results = cf.accounts.rulesets.versions.by_tag(account_id, ruleset_id, ruleset_version, rule_tag)
        print('results=', results, file=sys.stderr)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert True

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_ruleset_create_values()
    test_zones_rulesets_get()
    test_zones_ruleset_post()
    test_zones_rulesets_get_specific()
    test_zones_ruleset_delete()
    if len(sys.argv) > 2:
        test_find_account(sys.argv[2])
    else:
        test_find_account()
    test_accounts_ruleset()
    test_accounts_rulesets_versions()
    test_accounts_rulesets_versions_by_tag()
python-cloudflare-2.20.0/CloudFlare/tests/test_urlscanner.py000066400000000000000000000073601461736615400242240ustar00rootroot00000000000000""" urlscanner tests - PNG data retured """

import os
import sys
import random
import tempfile

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

account_name = None
account_id = None

def test_find_account(find_name=None):
    """ test_find_account """
    global account_name, account_id
    # grab a random account identifier from the first 10 accounts
    if find_name:
        params = {'per_page':1, 'name':find_name}
    else:
        params = {'per_page':10}
    try:
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (find_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(accounts) > 0 and len(accounts) <= 10
    # n = random.randrange(len(accounts))
    # stop using a random account - use the primary account (i.e. the zero'th one)
    n = 0
    account_name = accounts[n]['name']
    account_id = accounts[n]['id']
    assert len(account_id) == 32
    print('account: %s %s' % (account_id, account_name), file=sys.stderr)

scan_uuid = None
scan_url = None

def test_urlscanner():
    """ test_urlscanner """
    global scan_uuid, scan_url
    scans = cf.accounts.urlscanner.scan.get(account_id, params={'limit':10})
    assert isinstance(scans, dict)
    assert 'tasks' in scans
    assert isinstance(scans['tasks'], list)
    tasks = scans['tasks']
    for task in tasks:
        assert isinstance(task, dict)
        assert 'success' in task
        assert 'time' in task
        assert 'uuid' in task
        assert 'url' in task
        print('%s: %s %s %s' % (task['uuid'], task['success'], task['time'], task['url']), file=sys.stderr)
    n = random.randrange(len(tasks))
    scan_uuid = tasks[n]['uuid']
    scan_url = tasks[n]['url']

def test_urlscanner_scan():
    """ test_urlscanner_scan """
    scan = cf.accounts.urlscanner.scan.get(account_id, scan_uuid)
    assert isinstance(scan, dict)
    assert 'scan' in scan
    assert isinstance(scan['scan'], dict)
    assert 'task' in scan['scan']
    task = scan['scan']['task']
    assert 'success' in task
    assert 'time' in task
    assert 'url' in task
    assert 'uuid' in task
    assert 'visibility' in task
    print('%s: %s %s %s' % (task['uuid'], task['success'], task['time'], task['url']), file=sys.stderr)

# https://www.w3.org/TR/png/#5PNG-file-signature
# PNG signature 89 50 4E 47 0D 0A 1A 0A
def ispng(s):
    """ ispng """
    if b'\x89PNG\x0d\x0a\x1a\x0a' == s[0:8]:
        return True
    return False

# we don't write out the image - we have no interest in doing this
def write_png_file(s):
    """ write_png_file """
    hostname = scan_url.split('/')[2].replace('.', '_')
    with tempfile.NamedTemporaryFile(mode='wb', prefix='screenshot-' + hostname + '-', suffix='.png', delete=False) as fp:
        fp.write(s)
        print('%s' % (fp.name), file=sys.stderr)

def test_urlscanner_scan_screenshot():
    """ test_urlscanner_scan_screenshot """
    # the real test - returning bytes as this is an image
    png_content = cf.accounts.urlscanner.scan.screenshot.get(account_id, scan_uuid)
    assert isinstance(png_content, bytes)
    print('%s: %s png_content: len=%d sig="%s"' % (scan_uuid, scan_url, len(png_content), png_content[0:8]), file=sys.stderr)
    assert ispng(png_content)
    # write_png_file(png_content)

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_account(sys.argv[1])
    else:
        test_find_account()
    test_urlscanner()
    test_urlscanner_scan()
    test_urlscanner_scan_screenshot()
python-cloudflare-2.20.0/CloudFlare/tests/test_waiting_room.py000066400000000000000000000071471461736615400245510ustar00rootroot00000000000000""" waiting_room tests """

import os
import sys
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test waiting_rooms

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

zone_name = None
zone_id = None

def test_find_zone(domain_name=None):
    """ test_find_zone """
    global zone_name, zone_id
    # grab a random zone identifier from the first 10 zones
    if domain_name:
        params = {'per_page':1, 'name':domain_name}
    else:
        params = {'per_page':10}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (domain_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(zones) > 0 and len(zones) <= 10
    n = random.randrange(len(zones))
    zone_name = zones[n]['name']
    zone_id = zones[n]['id']
    assert len(zone_id) == 32
    print('zone: %s %s' % (zone_id, zone_name), file=sys.stderr)

def test_waiting_room_s():
    """ test_waiting_room_s """
    s = str(cf.zones.waiting_rooms.events.details)
    assert isinstance(s, str)

def test_waiting_room():
    """ test_waiting_room """
    waiting_rooms = cf.zones.waiting_rooms(zone_id)
    assert isinstance(waiting_rooms, list)
    for waiting_room in waiting_rooms:
        assert isinstance(waiting_room, dict)

def test_waiting_room_settings():
    """ test_waiting_room_settings """
    settings = cf.zones.waiting_rooms.settings(zone_id)
    assert isinstance(settings, dict)
    assert 'search_engine_crawler_bypass' in settings

def test_waiting_room_preview():
    """ test_waiting_room_preview """
    # we expect failure
    try:
        r = cf.zones.waiting_rooms.preview(zone_id)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert True

def test_waiting_room_events_details():
    """ test_waiting_room_events_details """
    waiting_room_id = '00000000000000000000000000000000'
    event_id = '00000000000000000000000000000000'
    # we expect failure - we are mainly testing three id style calls!
    try:
        r = cf.zones.waiting_rooms.events.details(zone_id, waiting_room_id, event_id)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert True

def test_waiting_room_post():
    """ test_waiting_room_post """
    # we expect failure - we don't expect to create a waiting_room
    waiting_room_data = {
        'host': 'example.com',
        'path': 'waiting_room.html',
        'name': 'waiting_room_testing',
        'description': 'Waiting Room Testing',
        'suspended': True,
        'new_users_per_minute': 1e6,
        'total_active_users': 2e6
    }
    try:
        new_waitng_room = cf.zones.waiting_rooms.post(zone_id, data=waiting_room_data)
        print('new_waiting_room=', new_waiting_room, file=sys.stderr)
        assert False
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('Error expected: %d %s' % (int(e), str(e)), file=sys.stderr)
        assert True

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_zone(sys.argv[1])
    else:
        test_find_zone()
    test_waiting_room_s()
    test_waiting_room()
    test_waiting_room_settings()
    test_waiting_room_preview()
    test_waiting_room_events_details()
    test_waiting_room_post()
python-cloudflare-2.20.0/CloudFlare/tests/test_workers.py000066400000000000000000000103201461736615400235320ustar00rootroot00000000000000""" workers tests """

import os
import sys
import uuid
import random

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

# test /accounts/:id/workers/scripts

cf = None

def test_cloudflare(debug=False):
    """ test_cloudflare """
    global cf
    cf = CloudFlare.CloudFlare(debug=debug)
    assert isinstance(cf, CloudFlare.CloudFlare)

account_name = None
account_id = None

def test_find_account(find_name=None):
    """ test_find_account """
    global account_name, account_id
    # grab a random account identifier from the first 10 accounts
    if find_name:
        params = {'per_page':1, 'name':find_name}
    else:
        params = {'per_page':10}
    try:
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('%s: Error %d=%s' % (find_name, int(e), str(e)), file=sys.stderr)
        assert False
    assert len(accounts) > 0 and len(accounts) <= 10
    # n = random.randrange(len(accounts))
    # stop using a random account - use the primary account (i.e. the zero'th one)
    n = 0
    account_name = accounts[n]['name']
    account_id = accounts[n]['id']
    assert len(account_id) == 32
    print('account: %s %s' % (account_id, account_name), file=sys.stderr)

sample_script_content = """
addEventListener("fetch", event => {
        event.respondWith(fetchAndModify(event.request));
    }
);

async function fetchAndModify(request) {
    console.log("got a request:", request);

    // Send the request on to the origin server.
    const response = await fetch(request);

    // Read response body.
    const text = await response.text();

    // Modify it.
    const modified = text.replace(
          "",
          ""
    );

    // Return modified response.
    return new Response(modified, {
            status: response.status,
            statusText: response.statusText,
            headers: response.headers
        }
    );
}
"""

sample_script_content = '\n'.join([s.strip() for s in sample_script_content.splitlines() if s != '']).strip()

script_id = None
script_tag = None

def test_workers_script_put():
    """ test_workers_script_put """
    global script_id, script_tag

    script_id = str(uuid.uuid1())

    r = cf.accounts.workers.scripts.put(account_id, script_id, data=sample_script_content)
    assert isinstance(r, dict)
    assert 'id' in r
    assert 'tag' in r
    assert script_id == r['id']
    script_tag = r['tag']

def test_workers_find():
    """ test_workers_find """
    workers = cf.accounts.workers.scripts(account_id)
    assert len(workers) > 0
    assert isinstance(workers, list)
    found = False
    for w in workers:
        assert 'id' in w
        if script_id == w['id']:
            found = True
            break
    assert found is True

def test_workers_find_all():
    """ test_workers_find_all """
    workers = cf.accounts.workers.scripts(account_id)
    assert len(workers) >= 0
    assert isinstance(workers, list)
    if len(workers) == 0:
        return
    for w in workers:
        assert 'id' in w
        assert 'tag' in w
        this_script_name = w['id']
        this_script_tag = w['tag']
        assert isinstance(this_script_name, str)
        assert len(this_script_tag) == 32
        this_script_content = cf.accounts.workers.scripts(account_id, this_script_name)
        assert isinstance(this_script_content, str)
        assert len(this_script_content) > 0
        # print('%s: %s -> %s' % (this_script_tag, this_script_name, this_script_content.replace('\n','')[0:50]), file=sys.stderr)
        # just do one ... that's all that's needed for testing
        break

def test_workers_script_delete():
    """ test_workers_script_delete """
    r = cf.accounts.workers.scripts.delete(account_id, script_id)
    assert isinstance(r, dict)
    assert 'id' in r
    # note that 'id' and 'tag' are inconsistently used in DELETE vs PUT. Sigh.
    assert script_tag == r['id']

if __name__ == '__main__':
    test_cloudflare(debug=True)
    if len(sys.argv) > 1:
        test_find_account(sys.argv[1])
    else:
        test_find_account()
    test_workers_script_put()
    test_workers_find()
    test_workers_find_all()
    test_workers_script_delete()
python-cloudflare-2.20.0/CloudFlare/tests/utils.py000066400000000000000000000027771461736615400221600ustar00rootroot00000000000000""" misc utilities for Cloudflare test code """

dummy_loa_document = """%PDF-1.4
1 0 obj << /Length 559 >> stream
 1 0 0 rg
 BT /F1 24 Tf 72 680 Td (LOA DOCUMENT) Tj ET
 .75 .75 .75 RG 1 w
 72 676 m 540 676 l s
 0 0 0 rg
 BT /F1 12 Tf 72 648 Td (See /tests/ folder under python-cloudflare on GitHub) Tj ET
 BT /F1 12 Tf 72 624 Td (THIS DOCUMENT IS ONLY USED FOR TESTING) Tj ET
 BT /F1 12 Tf 72 604 Td (Please ignore and delete upon receipt) Tj ET
 BT /F1 12 Tf 72 584 Td (See URL:) Tj ET
 0 0 1 rg
 BT /F2 12 Tf 130 584 Td (http://github.com/cloudflare/python-cloudflare) Tj ET
 .75 .75 .75 RG 1 w
 72 560 m 540 560 l s
 .75 .75 .75 RG 1 w
 36 756 m 576 756 l 576 36 l 36 36 l 36 756 l s
endstream
endobj
2 0 obj << /Type /Catalog /Pages 3 0 R >> endobj
3 0 obj << /Type /Pages /Kids [4 0 R ] /Count 1 >> endobj
4 0 obj << /Type /Page /Parent 3 0 R /MediaBox [0 0 612 792] /Contents 1 0 R
 /Resources <<
  /ProcSet 5 0 R
  /Font << /F1 6 0 R >>
  /Font << /F2 7 0 R >>
 >>
>>
endobj
5 0 obj [/PDF /Text] endobj
6 0 obj << /Type /Font /Subtype /Type1 /Name /F1 /BaseFont /Arial >> endobj
7 0 obj << /Type /Font /Subtype /Type1 /Name /F2 /BaseFont /Courier >> endobj
8 0 obj << /Creator (https://github.com/cloudflare/python-cloudflare)
 /Producer (Hand coded for python-cloudflare)
 /Title (dummy_loa_document.pdf)
 /Author (Martin J Levy)
 /Subject (Dummy LOA Document - please delete)
 /Keywords (LOA)
 /CreationDate (D:20240101120000Z)
 /ModDate (D:20240101120000Z)
>> endobj
trailer << /Size 8 /Root 2 0 R /Info 8 0 R >>
%%EOF"""

python-cloudflare-2.20.0/CloudFlare/utils.py000066400000000000000000000077611461736615400210140ustar00rootroot00000000000000""" misc utilities  for Cloudflare API"""
import sys
import json
from requests import __version__ as requests__version__

from . import __version__

def user_agent():
    """ misc utilities  for Cloudflare API"""
    # the default User-Agent is something like 'python-requests/2.11.1'
    # this additional data helps support @ Cloudflare help customers
    return ('python-cloudflare/' + __version__ + '/' +
            'python-requests/' + str(requests__version__) + '/' +
            'python/' + '.'.join([str(v) for v in sys.version_info[:3]]))

def sanitize_secrets(secrets):
    """ misc utilities  for Cloudflare API"""
    redacted_phrase = 'REDACTED'

    if secrets is None:
        return None

    secrets_copy = secrets.copy()
    if 'password' in secrets_copy:
        secrets_copy['password'] = redacted_phrase
    elif 'X-Auth-Key' in secrets_copy:
        secrets_copy['X-Auth-Key'] = redacted_phrase
    elif 'X-Auth-User-Service-Key' in secrets_copy:
        secrets_copy['X-Auth-User-Service-Key'] = redacted_phrase
    elif 'Authorization' in secrets_copy:
        secrets_copy['Authorization'] = redacted_phrase

    return secrets_copy

def build_curl(method, url, headers, params, data_str, data_json, files):
    """ misc utilities  for Cloudflare API"""

    msg = []
    # url
    url_full = url
    if params is not None:
        for k in params:
            if k is None:
                continue
            url_full += '&%s=%s' % (k, params[k])
        url_full = url_full.replace('&', '?', 1)
    msg.append('       curl \\')
    msg.append('            --url "%s" \\' % (str(url_full)))
    msg.append('            --request %s \\' % (str(method)))
    # headers
    h = sanitize_secrets(headers)
    for k in h:
        if k is None:
            continue
        msg.append('            --header "%s: %s" \\' % (k, h[k]))
    # data_str
    if data_str is not None:
        if isinstance(data_str, (bytes,bytearray)):
            if len(data_str) > 180:
                msg.append('            --data-binary \'%s ...\' \\' % (str(data_str[0:180]).replace('\n', '\n')))
            else:
                msg.append('            --data-binary \'%s\' \\' % (str(data_str).replace('\n', '\n')))
        else:
            if len(data_str) > 180:
                msg.append('            --data \'%s ...\' \\' % (str(data_str[0:180]).replace('\n', ' ')))
            else:
                msg.append('            --data \'%s\' \\' % (str(data_str).replace('\n', ' ')))
    # data_json
    if data_json is not None:
        try:
            s = json.dumps(data_json)
        except (TypeError, ValueError, RecursionError):
            s = str(data_json)
        if len(s) > 180:
            msg.append('            --data \'%s ...\' \\' % (s[0:180].replace('\n', ' ')))
        else:
            msg.append('            --data \'%s\' \\' % (s.replace('\n', ' ')))
    # files
    if files is not None:
        if isinstance(files, (dict)):
            for k, v in files.items():
                if isinstance(v, (list, tuple)):
                    if v[0] is None:
                        msg.append('            --form %s="%s" \\' % (k, v[1]))
                    else:
                        msg.append('            --form %s="%s" \\' % (k, v[0]))
                else:
                    msg.append('            --form %s="%s" \\' % (k,v))
        elif isinstance(files, (set, list, tuple)):
            for f in files:
                if isinstance(f, (list, tuple)):
                    if f[1][0] is None:
                        # not a file
                        msg.append('            --form %s="%s" \\' % (f[0], f[1][1]))
                    else:
                        # a file
                        msg.append('            --form %s="@%s" \\' % (f[0], f[1][0]))
                else:
                    msg.append('            --form "%s" \\' % (f,))
        else:
            msg.append('            --form file="@%s" \\' % (files))

    # remove the last \ from the last line.
    msg[-1] = msg[-1][:-1]

    return '\n'.join(msg)
python-cloudflare-2.20.0/CloudFlare/warning_2_20.py000066400000000000000000000043111461736615400220270ustar00rootroot00000000000000""" warning message if version is 2.20 or above (technically, there's no version above 2.20.0) """

import sys
import warnings

from . import __version__

MAJOR_VERSION_WARNING = """\
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   WARNING  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! You're seeing this warning because you've upgraded the Python package 'cloudflare' to version  !!
!! 2.20.* via an automated upgrade without version pinning. Version 2.20.0 exists to catch any    !!
!! of these upgrades before Cloudflare releases a new major release under the release number 3.x. !!
!!                                                                                                !!
!! Should you determine that you need to revert this upgrade and pin to v2.19.* it is recommended !!
!! you do the following: pip install --upgrade cloudflare==2.19.* or equivilant.                  !!
!!                                                                                                !!
!! Or you can upgrade to v3.x. NOTE: Release 3.x will not be code-compatible or call-compatible   !!
!! with previous releases. To see more about upgrading to next major version, please see:         !!
!! https://github.com/cloudflare/python-cloudflare/discussions/191                                !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\
"""

def warning_2_20():
    """ warning_2_20 """

    if __version__ < '2.20.0':
        return None
    return MAJOR_VERSION_WARNING

#def print_warning_2_20(warning):
#    """ print_warning_2_20 """
#    # boring stderr message printing - however, warn_ form is prefered
#    print(warning, file=sys.stderr)
#    pass

def warn_warning_2_20(warning):
    """ warn_warning_2_20 """
    # force these warnings to be shown (even if -Wd isn't used on python command line)
    warnings.simplefilter('always', PendingDeprecationWarning)
    # stacklevel=4 cleanly upstacks the calls in cloudflare.py (and hence should chanhge if cloudflare.py changes)
    warnings.warn(warning,  PendingDeprecationWarning, stacklevel=4)

def indent_warning_2_20(warning):
    """ indent_warning_2_20 """
    return ''.join(['\n       ' + v for v in warning.split('\n')])
python-cloudflare-2.20.0/LICENSE000066400000000000000000000021061461736615400162530ustar00rootroot00000000000000The MIT License (MIT)

Copyright (c) 2016 Felix Wong and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
python-cloudflare-2.20.0/MANIFEST.in000066400000000000000000000001231461736615400170010ustar00rootroot00000000000000include LICENSE
recursive-include examples *.sh *.py
#recursive-include cli4 *.man
python-cloudflare-2.20.0/Makefile000066400000000000000000000114031461736615400167060ustar00rootroot00000000000000
PYTHON = python
# PANDOC = pandoc
PYLINT = pylint
TWINE = twine
PYTEST = pytest

SPHINX_RELEASE = 2.20.0
SPHINX_AUTHOR = Martin J. Levy
SPHINX_COPYRIGHT = Copyright (c) 2016 thru 2024, Cloudflare. All rights reserved.

EMAIL = "mahtin@mahtin.com"
NAME = "cloudflare"

#all:	README.rst CHANGELOG.md build
all:	CHANGELOG.md build

# README.rst: README.md
# 	$(PANDOC) --wrap=none --from=markdown --to=rst < README.md > README.rst 

CHANGELOG.md: FORCE
	@ tmp=/tmp/_$$$$.md ; \
	( \
		cp /dev/null $$tmp ; \
		echo '# Change Log' ; \
		echo '' ; \
		git log --date=iso-local --pretty=format:' - %ci [%h](../../commit/%H) %s' ; \
		echo '' ; \
	)  >> $$tmp ; \
	diff $$tmp CHANGELOG.md || ( cp $$tmp CHANGELOG.md ; echo "CHANGELOG.md - updated" ) ; \
	rm $$tmp
FORCE:

build: setup.py
	$(PYTHON) setup.py -q build

install: build
	sudo $(PYTHON) setup.py -q install
	sudo rm -rf ${NAME}.egg-info

test: all
	@if pip show pytest-cov > /dev/null; \
	then \
		if [ ! -s .coverage ] ; then touch -t 202001011200 .coverage ; fi ; \
		if [ `find CloudFlare CloudFlare/tests cli4 examples setup.py setup.cfg -type f -depth 1 -name '*.py' -newer .coverage | wc -l` != 0 ]; \
		then \
			echo $(PYTEST) --cov=CloudFlare ; \
			$(PYTEST) --cov=CloudFlare ; \
			coverage html ; \
		else \
			true ; \
		fi ; \
	else \
		echo $(PYTEST) -vv ; \
		$(PYTEST) -vv ; \
	fi

cli4test: all
	$(PYTHON) -m cli4 /ips > /dev/null

sdist: all
	make clean
	make test
	$(PYTHON) setup.py -q sdist
	$(TWINE) check dist/*
	@rm -rf ${NAME}.egg-info

bdist: all
	make clean
	make test
	$(PYTHON) setup.py -q bdist
	$(TWINE) check dist/*
	@rm -rf ${NAME}.egg-info

bdist_wheel: all
	make clean
	make test
	$(PYTHON) setup.py -q bdist_wheel
	$(TWINE) check dist/*
	@rm -rf ${NAME}.egg-info

upload: clean all tag upload-github upload-pypi

upload-github:
	git push
	git push origin --tags

upload-pypi:
	## $(PYTHON) setup.py -q sdist bdist_wheel upload # --sign --identity="$(EMAIL)"
	$(TWINE) upload -r pypi --repository cloudflare dist/*

showtag: sdist
	@ v=`ls -r dist | head -1 | sed -e 's/cloudflare-\([0-9.]*\)\.tar.*/\1/'` ; echo "\tDIST VERSION =" $$v ; (git tag | fgrep -q "$$v") && echo "\tGIT TAG EXISTS"

tag: sdist
	@ v=`ls -r dist | head -1 | sed -e 's/cloudflare-\([0-9][0-9.][0-9]*[.rc0-9]*\)\.tar.*/\1/'` ; echo "\tDIST VERSION =" $$v ; (git tag | fgrep -q "$$v") || git tag "$$v"

sign:
	v=`ls -r dist | head -1 | sed -e 's/cloudflare-\([0-9.]*\)\.tar.*/\1/'` ; echo "\tDIST VERSION =" $$v ; \
	mkdir -p tarball ; \
	rm -f tarball/$$v.tar.gz.asc tarball/$$v.zip.asc ; \
	curl -sS -o tarball/$$v.tar.gz https://codeload.github.com/cloudflare/python-cloudflare/tar.gz/$$v ; \
	curl -sS -o tarball/$$v.zip https://codeload.github.com/cloudflare/python-cloudflare/zip/$$v ; \
	gpg --default-key ${EMAIL} --armor --detach-sign tarball/$$v.tar.gz ; \
	gpg --default-key ${EMAIL} --armor --detach-sign tarball/$$v.zip ; \
	ls -l tarball/$$v.tar.gz tarball/$$v.zip ; \
	ls -l tarball/$$v.tar.gz.asc tarball/$$v.zip.asc ;

docs: all
	@mkdir -p docs/_build docs/_static
	sphinx-apidoc --force --module-first --separate --ext-autodoc -A "$(SPHINX_AUTHOR)" -R "$(SPHINX_RELEASE)" -V "$(SPHINX_RELEASE)" -o docs . 'setup.*'
	sphinx-build -a -E -j auto -b html docs docs/_build/html

clean-docs: all
	rm -rf docs/CloudFlare*.rst docs/cli4*.rst docs/examples*.rst docs/modules*.rst docs/_build docs/_static

lint:
	$(PYLINT) CloudFlare cli4

openapi:
	@tmp=/tmp/_$$$$_ ; \
	$(PYTHON) -m cli4 --dump | sort > $$tmp.1 ; \
	$(PYTHON) -m cli4 --openapi '' | tee $$tmp.5 | sed -e 's/^[A-Z][A-Z]*  *//' -e 's/?.*//' -e 's/\/:[a-z][A-Za-z_]*/\/:id/g' -e 's/\/:[a-z][A-Za-z_]*}/\/:id/g' -e 's/:id\/:id/:id/' -e 's/\/:id$$//' -e 's/\/:id$$//' -e 's/\/:id ;/ ;/' -e 's/ ; Content-Type: .*//' -e 's/\/$$//' | sort -u > $$tmp.2 ; \
	egrep -v '; deprecated' < $$tmp.2 | sed -e 's/ ; .*//' | diff $$tmp.1 - > $$tmp.3 ; \
	echo "In code:" ; \
	egrep '< ' < $$tmp.3 | sed -e 's/< /    /' | sort | tee $$tmp.4 ; \
	echo "In docs:" ; \
	egrep '> ' < $$tmp.3 | sed -e 's/> /    /' | sort | sed -e "s/\//self.add('AUTH', '/" -e "s/$$/'\)/" -e "s/\/:id\//', '/g" ; \
	echo "Deprecated:" ; \
	egrep '; deprecated' < $$tmp.2 | while read cmd x deprecated deprecated_date ; do egrep "$$cmd" $$tmp.4 | sed -e "s/$$/ ; deprecated $$deprecated_date/" ; done | sort | uniq ; \
	echo "Content-Type's:" ; \
	egrep ';' < $$tmp.5 | egrep -v '; deprecated' | egrep -v ' ; Content-Type: application/json' | sed -e 's/^/    /' ; \
	rm $$tmp.?

TUNA_CLI4_TEST_COMMAND = "--openapi="
TUNA_CLI4_TEST_COMMAND = "/ips"

tuna:
	@tmp=/tmp/_$$$$_ ; \
	$(PYTHON) -X importtime -m cli4 $(TUNA_CLI4_TEST_COMMAND) > /dev/null 2> $$tmp.1 ; \
	tuna $$tmp.1 2> /dev/null & \
	tunapid=$$! ; \
	sleep 10 ; \
	kill $$tunapid ; \
	rm $$tmp.?

clean:
	rm -rf build
	rm -rf dist
	mkdir build dist
	$(PYTHON) setup.py -q clean
	rm -rf ${NAME}.egg-info

python-cloudflare-2.20.0/README.md000066400000000000000000001504551461736615400165400ustar00rootroot00000000000000# cloudflare-python

> [!WARNING]
> Soon there will be two Python packages for accessing Cloudflare's API.
>
> 1. This original [package](https://github.com/cloudflare/python-cloudflare), which was initially introduced [here](https://blog.cloudflare.com/python-cloudflare/).
> 2. A ground-up rewrite of the SDK, released under `3.*`, at some point in the future. See [here](https://github.com/cloudflare/python-cloudflare/discussions/191)
>
> If you like using this package in it's present form, it is highly recommended that you pin to the `2.*` releases now.
>
> ```bash
> $ cat ${YOUR_PROJECT}/requirements.txt
> cloudflare==2.19.*
> $
> ```
>
> For manual upgrades; the following will work cleanly:
> ```bash
> $ pip install --upgrade cloudflare==2.19.*
> ...
> Successfully installed cloudflare-2.19.4
> $

> [!WARNING]
> Release `2.20.*` is now available and it will produce a warning message explaining all this via stderr (the standard error output).
> This messages does not stop the program from operating, it's just a warning.
> If you wish to surpress this message (which is a bad idea because pinning to `2.19.*` is the right thing to do), then do the following in your code:
> ```python
>     cf = CloudFlare.CloudFlare(..., warnings=False)
> ```
> Or, if you use `cli4`, then the following.
> ```bash
> $ cli4 -w False ...
>```

> [!WARNING]
> Release `3.*` will not be code-compatible/call-compatible with previous releases (i.e. release `1.*` and `2.*`).

When you see this README complete change you will know that `3.*` has been released; however, until then, this code will be released under a `2.19.*` release number.

## Package stats

[![Downloads](https://static.pepy.tech/badge/cloudflare)](https://pepy.tech/project/cloudflare)
[![Downloads](https://static.pepy.tech/badge/cloudflare/month)](https://pepy.tech/project/cloudflare)
[![Downloads](https://static.pepy.tech/badge/cloudflare/week)](https://pepy.tech/project/cloudflare)
[![Downloads](https://static.pepy.tech/badge/cloudflare/week)](https://pepy.tech/project/cloudflare)
[![Downloads](https://img.shields.io/pypi/pyversions/cloudflare.svg)](https://pepy.tech/project/cloudflare)

## Instant how-to-use example

If you want to call the following API call:
```
    https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{dns_record_id}
```

It would translates to the following Python code:
```python
    results = cf.zones.dns_records(zone_id, dns_record_id)
```

Many more examples are below and/or in the `examples` folder.

## Installation

Two methods are provided to install this software.
Use PyPi (see [package](https://pypi.python.org/pypi/cloudflare) details) or GitHub (see [package](https://github.com/cloudflare/python-cloudflare) details).

### Via PyPI

```bash
$ sudo pip install cloudflare
$
```

Yes - that simple! (the sudo may not be needed in some cases).

### Via github

```bash
$ git clone https://github.com/cloudflare/python-cloudflare
$ cd python-cloudflare
$ ./setup.py build
$ sudo ./setup.py install
$
```

Or whatever variance of that you want to use.
There is a Makefile included.

## Cloudflare name change - dropping the capital F

In Sepember/October 2016 the company modified its company name and dropped the capital F.
However, for now (and for backward compatibility reasons) the class name stays the same.

## Cloudflare API version 4

The Cloudflare API can be found [here](https://api.cloudflare.com/).
Each API call is provided via a similarly named function within the **CloudFlare** class.
A full list is provided below.

## Example code

All example code is available on GitHub (see [package](https://github.com/cloudflare/python-cloudflare) in the [examples](https://github.com/cloudflare/python-cloudflare/tree/master/examples) folder).

## Blog

This package was initially introduced [here](https://blog.cloudflare.com/python-cloudflare/) via Cloudflare's [blog](https://blog.cloudflare.com/).

## Getting Started

A very simple listing of zones within your account; including the IPv6 status of the zone.

```python
import CloudFlare

def main():
    cf = CloudFlare.CloudFlare()
    zones = cf.zones.get()
    for zone in zones:
        zone_id = zone['id']
        zone_name = zone['name']
        print("zone_id=%s zone_name=%s" % (zone_id, zone_name))

if __name__ == '__main__':
    main()
```

This example works when there are less than 50 zones (50 is the default number of values returned from a query like this).

Now lets expand on that and add code to show the IPv6 and SSL status of the zones. Lets also query 100 zones.

```python
import CloudFlare

def main():
    cf = CloudFlare.CloudFlare()
    zones = cf.zones.get(params = {'per_page':100})
    for zone in zones:
        zone_id = zone['id']
        zone_name = zone['name']

        settings_ssl = cf.zones.settings.ssl.get(zone_id)
        ssl_status = settings_ssl['value']

        settings_ipv6 = cf.zones.settings.ipv6.get(zone_id)
        ipv6_status = settings_ipv6['value']

        print("zone_id=%s zone_name=%s" % (zone_id, zone_name))
        print("ssl_status=%s ipv6_status=%s" % (ssl_status, ipv6_status))

if __name__ == '__main__':
    main()
```

In order to query more than a single page of zones, we would have to use the raw mode (described more below).
We can loop over many get calls and pass the page parameter to facilitate the paging.

Raw mode is only needed when a get request has the possibility of returning many items.

```python
import CloudFlare

def main():
    cf = CloudFlare.CloudFlare(raw=True)
    page_number = 0
    while True:
        page_number += 1
        raw_results = cf.zones.get(params={'per_page':5,'page':page_number})
        zones = raw_results['result']

        for zone in zones:
            zone_id = zone['id']
            zone_name = zone['name']
            print("zone_id=%s zone_name=%s" % (zone_id, zone_name))

        total_pages = raw_results['result_info']['total_pages']
        if page_number == total_pages:
            break

if __name__ == '__main__':
    main()
```

A more complex example follows.

```python
import CloudFlare

def main():
    zone_name = 'example.com'

    cf = CloudFlare.CloudFlare()

    # query for the zone name and expect only one value back
    try:
        zones = cf.zones.get(params = {'name':zone_name,'per_page':1})
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('No zones found')

    # extract the zone_id which is needed to process that zone
    zone = zones[0]
    zone_id = zone['id']

    # request the DNS records from that zone
    try:
        dns_records = cf.zones.dns_records.get(zone_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones/dns_records.get %d %s - api call failed' % (e, e))

    # print the results - first the zone name
    print("zone_id=%s zone_name=%s" % (zone_id, zone_name))

    # then all the DNS records for that zone
    for dns_record in dns_records:
        r_name = dns_record['name']
        r_type = dns_record['type']
        r_value = dns_record['content']
        r_id = dns_record['id']
        print('\t', r_id, r_name, r_type, r_value)

    exit(0)

if __name__ == '__main__':
    main()
```

## Providing Cloudflare Username and API Key

When you create a **CloudFlare** class you can pass some combination of these four core parameters.

 * `email` - The account email (only if an API Key is being used)
 * `api` - The API Key (if coding prior to Issue-114 being merged)
 * `token` - The API Token (if coding after to Issue-114)
 * `certtoken` - Optional Origin-CA Certificate Token

This parameter controls how the data is returned from a successful call (see notes below).

 * `raw` - An optional Raw flag (True/False) - defaults to False

Timeouts (10s) and Retries (5) are configured by default. Should you wish to override them, use these settings:
* `global_request_timeout` - How long before each API call to Cloudflare should time out (in seconds)
* `max_requests_retries` - How many times to retry an API call when DNS lookups, socket connections, or connect timeouts occur.

> NOTE: `max_request_retries` is only available when `use_sessions` is not disabled.

The following paramaters are for debug and/or development usage

 * `debug` - An optional Debug flag (True/False) - defaults to False
 * `use_sessions` - An optional Use-Sessions flag (True/False) - defaults to True
 * `profile` - An optional Profile name (the default is `Cloudflare`)
 * `base_url` - An optional Base URL (only used for development)

email=None, key=None, token=None, certtoken=None, debug=False, raw=False, use_sessions=True, profile=None, base_url=None):

### Issue-114

After [Issue-114](https://github.com/cloudflare/python-cloudflare/issues/114) was coded and merged, the use of `token` and `key` changed; however, is backward compatible (amazingly!).

If you are using only the API Token, then don't include the API Email. If you are coding prior to Issue-114, then the API Key can also be used as an API Token if the API Email is not used.

### Python code to create class

```python
import CloudFlare

# A minimal call - reading values from environment variables or configuration file
cf = CloudFlare.CloudFlare()

# A minimal call with debug enabled
cf = CloudFlare.CloudFlare(debug=True)

# An authenticated call using an API Token (note the missing email)
cf = CloudFlare.CloudFlare(token='00000000000000000000000000000000')

# An authenticated call using an API Email and API Key
cf = CloudFlare.CloudFlare(email='user@example.com', key='00000000000000000000000000000000')

# An authenticated call using an API Token and CA-Origin info
cf = CloudFlare.CloudFlare(token='00000000000000000000000000000000', certtoken='v1.0-...')

# An authenticated call using an API Email, API Key, and CA-Origin info
cf = CloudFlare.CloudFlare(email='user@example.com', key='00000000000000000000000000000000', certtoken='v1.0-...')

# An authenticated call using using a stored profile (see below)
cf = CloudFlare.CloudFlare(profile="CompanyX"))
```

If the account email and API key are not passed when you create the class, then they are retrieved from either the users exported shell environment variables or the .cloudflare.cfg or ~/.cloudflare.cfg or ~/.cloudflare/cloudflare.cfg files, in that order.

If you're using an API Token, any `cloudflare.cfg` file must either not contain an `email` and `key` attribute (or they can be zero length strings) and the `CLOUDFLARE_EMAIL` `CLOUDFLARE_API_KEY` environment variable must be unset (or zero length strings), otherwise the token (`CLOUDFLARE_API_TOKEN` or `token` attribute) will not be used.

There is one call that presently doesn't need any email or token certification (the */ips* call); hence you can test without any values saved away.

### Using shell environment variables

Note (for latest version of code):

 * `CLOUDFLARE_EMAIL` has replaced `CF_API_EMAIL`.
 * `CLOUDFLARE_API_KEY` has replaced `CF_API_KEY`.
 * `CLOUDFLARE_API_TOKEN` has replaced `CF_API_TOKEN`.
 * `CLOUDFLARE_API_CERTKEY` has replaced `CF_API_CERTKEY`.

Additionally, these two variables are available for testing purposes:

 * `CLOUDFLARE_API_EXTRAS` has replaced `CF_API_EXTRAS`.
 * `CLOUDFLARE_API_URL` has replaced `CF_API_URL`.

The older environment variable names can still be used.

```bash
$ export CLOUDFLARE_EMAIL='user@example.com'
$ export CLOUDFLARE_API_KEY='00000000000000000000000000000000'
$ export CLOUDFLARE_API_CERTKEY='v1.0-...'
$
```

Or if using API Token.

```bash
$ export CLOUDFLARE_API_TOKEN='00000000000000000000000000000000'
$ export CLOUDFLARE_API_CERTKEY='v1.0-...'
$
```
These are optional environment variables; however, they do override the values set within a configuration file.

### Using configuration file to store email and keys

The default profile name is `Cloudflare` for obvious reasons.

```bash
$ cat ~/.cloudflare/cloudflare.cfg
[Cloudflare]
email = user@example.com # Do not set if using an API Token
key = 00000000000000000000000000000000
certtoken = v1.0-...
extras =
$
```

More than one profile can be stored within that file.
Here's an example for a work and home setup (in this example work has an API Token and home uses email/key).

```bash
$ cat ~/.cloudflare/cloudflare.cfg
[Work]
token = 00000000000000000000000000000000
[Home]
email = home@example.com
key = 00000000000000000000000000000000
$
```

To select a profile, use the `--profile profile-name` option for `cli4` command or use `profile="profile-name"` in the library call.

```bash
$ cli4 --profile Work /zones | jq '.[]|.name' | wc -l
      13
$

$ cli4 --profile Home /zones | jq '.[]|.name' | wc -l
       1
$
```

Here is the same in code.

```python
#!/usr/bin/env python

import CloudFlare

def main():
    cf = CloudFlare.CloudFlare(profile="Work")
    ...
```

### Passing your own HTTP headers to API calls

There are very specific case where a user of the library needs to add custom headers to all HTTP calls.
This is rarly needed.

The addition headers can be passed via the confuration file as follows:

```bash
$ cat ~/.cloudflare/cloudflare.cfg
...
http_headers =
        X-Header1:value
        X-Header2: value1 value2 value3
        X-Header3: "this is life as we know it"
        X-Header4: 'two single quotes'
        X-Header5:
...
$
```
Each line should have a header noun, a colon, and a verb.

You can also pass these via Python calls.
```python
    import CloudFlare

    http_headers = [
        'X-Header1:value',
        'X-Header2: value1 value2 value3',
        'X-Header3: "this is life as we know it"',
        'X-Header4: \'two single quotes\'',
        'X-Header5:',
    ]
    cf = CloudFlare.CloudFlare(http_headers=http_headers)
...
```

These header values can also be passed via `cli4` command (many times) - use the `-v` option to see the debug messages:
```
$ cli4 -v --header 'X-something:' --header 'X-whatever:whatever' /zones > /tmp/results.json
...
            --header "X-something: " \
            --header "X-whatever: whatever " \
...
$
```

### Advanced use of configuration file for authentication based on method

The configuration file can have values that are both generic and specific to the method.
Here's an example where a project has a different API Token for reading and writing values.

```bash
$ cat ~/.cloudflare/cloudflare.cfg
[Work]
token = 0000000000000000000000000000000000000000
token.get = 0123456789012345678901234567890123456789
$
```

When a GET call is processed then the second token is used. For all other calls the first token is used.
Here's a more explict verion of that config:

```bash
$ cat ~/.cloudflare/cloudflare.cfg
[Work]
token.delete = 0000000000000000000000000000000000000000
token.get = 0123456789012345678901234567890123456789
token.patch = 0000000000000000000000000000000000000000
token.post = 0000000000000000000000000000000000000000
token.put = 0000000000000000000000000000000000000000
$
```

This can be used with email values also.

### About /certificates and certtoken

The *CLOUDFLARE_API_CERTKEY* or *certtoken* values are used for the Origin-CA */certificates* API calls.
You can leave *certtoken* in the configuration with a blank value (or omit the option variable fully).

The *extras* values are used when adding API calls outside of the core codebase.
Technically, this is only useful for internal testing within Cloudflare.
You can leave *extras* in the configuration with a blank value (or omit the option variable fully).

## Exceptions and return values

### Response data

The response is build from the JSON in the API call.
It contains the **results** values; but does not contain the paging values.

You can return all the paging values by calling the class with raw=True. Here's an example without paging.

```python
#!/usr/bin/env python

import json
import CloudFlare

def main():
    cf = CloudFlare.CloudFlare()
    zones = cf.zones.get(params={'per_page':5})
    print("len=%d" % (zones.length()))

if __name__ == '__main__':
    main()
```

The results are as follows.

```
5
```

When you add the raw option; the APIs full structure is returned. This means the paging values can be seen.

```python
#!/usr/bin/env python

import json
import CloudFlare

def main():
    cf = CloudFlare.CloudFlare(raw=True)
    zones = cf.zones.get(params={'per_page':5})
    print("len=%d" % (zones.length()))
    print(json.dumps(zones, indent=4, sort_keys=True))

if __name__ == '__main__':
    main()
```

This produces.

```
5
{
    "result": [
        ...
    ],
    "result_info": {
        "count": 5,
        "page": 1,
        "per_page": 5,
        "total_count": 31,
        "total_pages": 7
    }
}
```

A full example of paging is provided below.

### Exceptions

The library will raise **CloudFlareAPIError** when the API call fails.
The exception returns both an integer and textual message in one value.

```python
import CloudFlare

    ...
    try
        r = ...
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('api error: %d %s' % (e, e))
    ...
```

The other raised response is **CloudFlareInternalError** which can happen when calling an invalid method.

In some cases more than one error is returned. In this case the return value `e` is also an array.
You can iterate over that array to see the additional error.

```python
import sys
import CloudFlare

    ...
    try
        r = ...
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        if len(e) > 0:
            sys.stderr.write('api error - more than one error value returned!\n')
            for x in e:
                sys.stderr.write('api error: %d %s\n' % (x, x))
        exit('api error: %d %s' % (e, e))
    ...
```

### Exception handling

Here's code using the CLI command `cli4` of the responses passed back in exceptions.

First a simple get with a clean (non-error) response.

```bash
$ cli4 /zones/:example.com/dns_records | jq -c '.[]|{"name":.name,"type":.type,"content":.content}'
{"name":"example.com","type":"MX","content":"something.example.com"}
{"name":"something.example.com","type":"A","content":"10.10.10.10"}
$
```

Next a simple/single error response.
This is simulated by providing incorrect authentication information.

```bash
$ CLOUDFLARE_EMAIL='someone@example.com' cli4 /zones/
cli4: /zones - 9103 Unknown X-Auth-Key or X-Auth-Email
$
```

More than one call can be done on the same command line. In this mode, the connection is preserved between calls.

```bash
$ cli4 /user/organizations /user/invites
...
$
```
Note that the output is presently two JSON structures one after the other - so less useful that you may think.

Finally, a command that provides more than one error response.
This is simulated by passing an invalid IPv4 address to a DNS record creation.

```bash
$ cli4 --post name='foo' type=A content="NOT-A-VALID-IP-ADDRESS" /zones/:example.com/dns_records
cli4: /zones/:example.com/dns_records - 9005 Content for A record is invalid. Must be a valid IPv4 address
cli4: /zones/:example.com/dns_records - 1004 DNS Validation Error
$
```

## Included example code

The [examples](https://github.com/cloudflare/python-cloudflare/tree/master/examples) folder contains many examples in both simple and verbose formats.

You can see the installed path of these files directly via `cli4 -e` (or `cli4 --examples`) command.

```bash
$ cli4 -e
Python .py files:
	...
	/opt/homebrew/lib/python3.11/site-packages/examples/example_always_use_https.py
	...
Bash .sh files:
	...
	/opt/homebrew/lib/python3.11/site-packages/examples/example_paging_thru_zones.sh
	...
$
```

The exact path will vary depending on your system.
The above example is MacOS and Python 3.9 hence the `/opt/homebrew/lib/python3.11/site-packages/` path.
One Linux, the Python pip command may install the code is a system location like `/usr/lib/python3/dist-packages` or `~/.local/lib/python3.9/site-packages/` or something different.
The `cli4 -e` command will try to decode the location and display the example files.

If you are running release before Python 3.9 then you will be asked to install the following:

```bash
$ pip install importlib_resources
...
$
```

It will show up if you are running on an older system. For example, this is the results from running on Win7:

```bash
U:\Users\Bobby>cli4 -e
Module "importlib_resources" missing - please "pip install importlib_resources" as your Python version is lower than 3.9

U:\Users\Bobby>python -V
Python 3.8.3

U:\Users\Bobby>
```

Upgrading from an older version of Python is always recommended. Upgrading from Win7 is by-default even more important!

## A DNS zone code example

```python
#!/usr/bin/env python

import sys
import CloudFlare

def main():
    zone_name = sys.argv[1]
    cf = CloudFlare.CloudFlare()
    zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name})
    zone_id = zone_info['id']

    dns_records = [
        {'name':'foo', 'type':'AAAA', 'content':'2001:d8b::1'},
        {'name':'foo', 'type':'A', 'content':'192.168.0.1'},
        {'name':'duh', 'type':'A', 'content':'10.0.0.1', 'ttl':120},
        {'name':'bar', 'type':'CNAME', 'content':'foo'},
        {'name':'shakespeare', 'type':'TXT', 'content':"What's in a name? That which we call a rose by any other name ..."}
    ]

    for dns_record in dns_records:
        r = cf.zones.dns_records.post(zone_id, data=dns_record)
    exit(0)

if __name__ == '__main__':
    main()
```

## A DNS zone delete code example (be careful)

```python
#!/usr/bin/env python

import sys
import CloudFlare

def main():
    zone_name = sys.argv[1]
    cf = CloudFlare.CloudFlare()
    zone_info = cf.zones.get(params={'name': zone_name})
    zone_id = zone_info['id']

    dns_name = sys.argv[2]
    dns_records = cf.zones.dns_records.get(zone_id, params={'name':dns_name + '.' + zone_name})
    for dns_record in dns_records:
        dns_record_id = dns_record['id']
        r = cf.zones.dns_records.delete(zone_id, dns_record_id)
    exit(0)

if __name__ == '__main__':
    main()
```

## CLI

All API calls can be called from the command line via the `cli4` command.
Additionally, the `cli4` command will convert domain name or account name prefixed with a colon (`:`) into the correct identifier.
e.g. to view `example.com` you can use `cli4 /zones/:example.com`.
You can pass the zone identifier (or account identifier or any identifier) with a colon followed by the identifier as a hex number 32 characters long.

```bash
$ cli4 [-V|--version] [-h|--help] [-v|--verbose] \
    [-e|--examples] \
    [-q|--quiet] \
    [-j|--json] [-y|--yaml] [-n|--ndjson] [-i|--image] \
    [-r|--raw] \
    [-d|--dump] \
    [-A|--openapi url] \
    [-b|--binary] \
    [-p|--profile profile-name] \
    [-h|--header additional-header] \
    [-w|--warnings [True|False]] \
    [--get|--patch|--post|--put|--delete] \
    [item=value|item=@filename|@filename ...] /command ...
```

### CLI parameters for POST/PUT/PATCH

For API calls that need to pass data or parameters there is various formats to use.

The simplest form is `item=value`. This passes the value as a string within the APIs JSON data.

If you need a numeric value passed then `==` can be used to force the value to be treated as a numeric value within the APIs JSON data.
For example: `item==value`.

if you need to pass a list of items; then `[]` can be used. For example:

```bash
pool_id1="11111111111111111111111111111111"
pool_id2="22222222222222222222222222222222"
pool_id3="33333333333333333333333333333333"
cli4 --post global_pools="[ ${pool_id1}, ${pool_id2}, ${pool_id3} ]" region_pools="[ ]" /user/load_balancers/maps
```

Data or parameters can be either named or unnamed.
It can not be both.
Named is the majority format; as described above.
Unnamed parameters simply don't have anything before the `=` sign, as in `=value`.
This format is presently only used by the Cloudflare Load Balancer API calls.
For example:

```bash
cli4 --put ="00000000000000000000000000000000" /user/load_balancers/maps/:00000000000000000000000000000000/region/:WNAM
```

Data can also be uploaded from file contents. Using the `item=@filename` format will open the file and the contents uploaded in the POST.

### CLI output

The default output from the CLI command is in JSON.
It can also output YAML format (i.e. human readable).
This is controled by the `--yaml` or `--json` flags (JSON is the default).
There is also a `--ndjson` flag for use with line based JSON data - this is mainly used for log data.

Additonally the output can be plain text or binary image format depending on the results from the API call (some calls results in non JSON results).
The `--image` flag will return the data in the same format as the API's results.

### Simple CLI calls

 * `cli4 /user/billing/profile`
 * `cli4 /user/invites`

 * `cli4 /zones/:example.com`
 * `cli4 /zones/:example.com/dnssec`
 * `cli4 /zones/:example.com/settings/ipv6`
 * `cli4 --put /zones/:example.com/activation_check`
 * `cli4 /zones/:example.com/keyless_certificates`

 * `cli4 /zones/:example.com/analytics/dashboard`

### More complex CLI calls

Here is the creation of a DNS entry, followed by a listing of that entry and then the deletion of that entry.

```bash
$ $ cli4 --post name="test" type="A" content="10.0.0.1" /zones/:example.com/dns_records
{
    "id": "00000000000000000000000000000000",
    "name": "test.example.com",
    "type": "A",
    "content": "10.0.0.1",
    ...
}
$

$ cli4 /zones/:example.com/dns_records/:test.example.com | jq '{"id":.id,"name":.name,"type":.type,"content":.content}'
{
  "id": "00000000000000000000000000000000",
  "name": "test.example.com",
  "type": "A",
  "content": "10.0.0.1"
}

$ cli4 --delete /zones/:example.com/dns_records/:test.example.com | jq -c .
{"id":"00000000000000000000000000000000"}
$
```

There's the ability to handle dns entries with multiple values.
This produces more than one API call within the command.

```bash
$ cli4 /zones/:example.com/dns_records/:test.example.com | jq -c '.[]|{"id":.id,"name":.name,"type":.type,"content":.content}'
{"id":"00000000000000000000000000000000","name":"test.example.com","type":"A","content":"192.168.0.1"}
{"id":"00000000000000000000000000000000","name":"test.example.com","type":"AAAA","content":"2001:d8b::1"}
$
```

Here are the cache purging commands.

```bash
$ cli4 --delete purge_everything=true /zones/:example.com/purge_cache | jq -c .
{"id":"00000000000000000000000000000000"}
$

$ cli4 --delete files='[http://example.com/css/styles.css]' /zones/:example.com/purge_cache | jq -c .
{"id":"00000000000000000000000000000000"}
$

$ cli4 --delete files='[http://example.com/css/styles.css,http://example.com/js/script.js]' /zones/:example.com/purge_cache | jq -c .
{"id":"00000000000000000000000000000000"}
$

$ cli4 --delete tags='[tag1,tag2,tag3]' /zones/:example.com/purge_cache | jq -c .
cli4: /zones/:example.com/purge_cache - 1107 Only enterprise zones can purge by tag.
$
```

A somewhat useful listing of available plans for a specific zone.

```bash
$ cli4 /zones/:example.com/available_plans | jq -c '.[]|{"id":.id,"name":.name}'
{"id":"00000000000000000000000000000000","name":"Pro Website"}
{"id":"00000000000000000000000000000000","name":"Business Website"}
{"id":"00000000000000000000000000000000","name":"Enterprise Website"}
{"id":"0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee","name":"Free Website"}
$
```

### Cloudflare CA CLI calls

Here's some Cloudflare CA calls. Note the need of the `zone_id=` parameter with the basic `/certificates` call.

```bash
$ cli4 /zones/:example.com | jq -c '.|{"id":.id,"name":.name}'
{"id":"12345678901234567890123456789012","name":"example.com"}
$

$ cli4 zone_id=12345678901234567890123456789012 /certificates | jq -c '.[]|{"id":.id,"expires_on":.expires_on,"hostnames":.hostnames,"certificate":.certificate}'
{"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-29 22:36:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "}
{"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:23:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "}
{"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:20:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "}
$
```

A certificate can be viewed via a simple GET request.

```bash
$ cli4 /certificates/:123456789012345678901234567890123456789012345678
{
    "certificate": "-----BEGIN CERTIFICATE-----\n ... ",
    "expires_on": "2032-01-29 22:36:00 +0000 UTC",
    "hostnames": [
        "*.example.com",
        "example.com"
    ],
    "id": "123456789012345678901234567890123456789012345678",
    "request_type": "origin-rsa"
}
$
```

Creating a certificate. This is done with a `POST` request. Note the use of `==` in order to pass a decimal number (vs. string) in JSON. The CSR is not shown for simplicity sake.

```bash
$ CSR=`cat example.com.csr`
$ cli4 --post hostnames='["example.com","*.example.com"]' requested_validity==365 request_type="origin-ecc" csr="$CSR" /certificates
{
    "certificate": "-----BEGIN CERTIFICATE-----\n ... ",
    "csr": "-----BEGIN CERTIFICATE REQUEST-----\n ... ",
    "expires_on": "2018-09-27 21:47:00 +0000 UTC",
    "hostnames": [
        "*.example.com",
        "example.com"
    ],
    "id": "123456789012345678901234567890123456789012345678",
    "request_type": "origin-ecc",
    "requested_validity": 365
}
$
```

Deleting a certificate can be done with a `DELETE` call.

```bash
$ cli4 --delete /certificates/:123456789012345678901234567890123456789012345678
{
    "id": "123456789012345678901234567890123456789012345678",
    "revoked_at": "0000-00-00T00:00:00Z"
}
$
```

### Paging CLI calls

The `--raw` command provides access to the paging returned values.
See the API documentation for all the info.
Here's an example of how to page thru a list of zones (it's included in the examples folder as `example_paging_thru_zones.sh`).
Note the use of `==` to pass a number vs a string as paramater.

```bash
:
tmp=/tmp/$$_
trap "rm ${tmp}; exit 0" 0 1 2 15
PAGE=0
while true
do
        cli4 --raw per_page==5 page==${PAGE} /zones > ${tmp}
        domains=`jq -c '.|.result|.[]|.name' < ${tmp} | tr -d '"'`
        result_info=`jq -c '.|.result_info' < ${tmp}`
        COUNT=`      echo "${result_info}" | jq .count`
        PAGE=`       echo "${result_info}" | jq .page`
        PER_PAGE=`   echo "${result_info}" | jq .per_page`
        TOTAL_COUNT=`echo "${result_info}" | jq .total_count`
        TOTAL_PAGES=`echo "${result_info}" | jq .total_pages`
        echo COUNT=${COUNT} PAGE=${PAGE} PER_PAGE=${PER_PAGE} TOTAL_COUNT=${TOTAL_COUNT} TOTAL_PAGES=${TOTAL_PAGES} -- ${domains}
        if [ "${PAGE}" == "${TOTAL_PAGES}" ]
        then
                ## last section
                break
        fi
        # grab the next page
        PAGE=`expr ${PAGE} + 1`
done
```

It produces the following results.

```bash
COUNT=5 PAGE=1 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- accumsan.example auctor.example consectetur.example dapibus.example elementum.example
COUNT=5 PAGE=2 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- felis.example iaculis.example ipsum.example justo.example lacus.example
COUNT=5 PAGE=3 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- lectus.example lobortis.example maximus.example morbi.example pharetra.example
COUNT=5 PAGE=4 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- porttitor.example potenti.example pretium.example purus.example quisque.example
COUNT=5 PAGE=5 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- sagittis.example semper.example sollicitudin.example suspendisse.example tortor.example
COUNT=1 PAGE=7 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- varius.example vehicula.example velit.example velit.example vitae.example
COUNT=5 PAGE=6 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- vivamus.example
```

### Paging thru lists (using cursors)

Some API calls use cursors to read beyond the initally returned values. See the API page in order to see which API calls do this.

```bash
$ ACCOUNT_ID="00000000000000000000000000000000"
$ LIST_ID="00000000000000000000000000000000"
$
$ cli4 --raw /accounts/::${ACCOUNT_ID}/rules/lists/::${LIST_ID}/items > /tmp/page1.json
$ after=`jq -r '.result_info.cursors.after' < /tmp/page1.json`
$ echo "after=$after"
after=Mxm4GVmKjYbFjy2VxMPipnJigm1M_s6lCS9ABR9wx-RM2A
$
```

Once we have the `after` value, we can pass it along in order to read the next hunk of values. We finish when `after` returns as null (or isn't present).

```bash
$ cli4 --raw cursor="$after" /accounts/::${ACCOUNT_ID}/rules/lists/::${LIST_ID}/items > /tmp/page2.json
$ after=`jq -r '.result_info.cursors.after' < /tmp/page2.json`
$ echo "after=$after"
after=null
$
```

We can see the results now in two files.

```bash
$ jq -c '.result[]' < /tmp/page1.json | wc -l
      25
$

$ jq -c '.result[]' < /tmp/page2.json | wc -l
       5
$

$ for f in /tmp/page?.json ; do jq -r '.result[]|.id,.ip,.comment' < $f | paste - - - ; done | column -s'   ' -t
0fe44928258549feb47126a966fbf4a0  0.0.0.0           all zero
2e1e02120f5e466f8c0e26375e4cf4c8  1.0.0.1           Cloudflare DNS a
9ca5fd0ac6f54fdbb9dedd3fb72ce2da  1.1.1.1           Cloudflare DNS b
b3654987446743738c782f36ebe074f5  10.0.0.0/8        RFC1918 space
90bec8ce37d242faa2e27d1e78c1d8e2  103.21.244.0/22   Cloudflare IP
970a3c810cda41af9bef2c36a1892f7e  103.22.200.0/22   Cloudflare IP
3ec8516158bf4f3cac18210f611ee541  103.31.4.0/22     Cloudflare IP
ee9d268367204e6bb8e5e4c907f22de8  104.16.0.0/12     Cloudflare IP
93ae02eda9774c45840af367a02fe529  108.162.192.0/18  Cloudflare IP
62891ebf6db44aa494d79a6401af185e  131.0.72.0/22     Cloudflare IP
cac40cd940cc470582b8c912a8a12bea  141.101.64.0/18   Cloudflare IP
f6d5eacd81a2407f8e0d81caee21e7f8  162.158.0.0/15    Cloudflare IP
3d538dfc38ab471d9d3fe78332acfa4e  172.16.0.0/12     RFC1918 space
f353cb8f98424837ad35382a22b9debe  172.64.0.0/13     Cloudflare IP
78f3e1a0bafc41f88d4d40ad49a642e0  173.245.48.0/20   Cloudflare IP
c23a545475c54c32a7681c6b508d3e80  188.114.96.0/20   Cloudflare IP
f693237c9e294fe481221cbc2d7c20ef  190.93.240.0/20   Cloudflare IP
6d465ab3a0994c07827ebdcf8f34d977  192.168.0.0/16    RFC1918 space
1ad1e634b3664bac939086185c62faf7  197.234.240.0/22  Cloudflare IP
5d2968e7b3114d8e869a379d71c8ba86  198.41.128.0/17   Cloudflare IP
6a69de60b31448fa864f0a3ac5abe8d0  224.0.0.0/24      Multicast
30749cce89af4ab3a80e308294f46a46  240.0.0.0/4       Class E
2b32c67ea4d044628abe39f28662d8f0  255.255.255.255   all ones
cc7cd828b2fb4bcfb9391c2d3ef8d068  2400:cb00::/32    Cloudflare IP
b30d4cbd7dcd48729e8ebeda552e48a8  2405:8100::/32    Cloudflare IP
49db60758c8344959c338a74afc9748a  2405:b500::/32    Cloudflare IP
96e9eca1923c40d5a84865145f5a5d6a  2606:4700::/32    Cloudflare IP
21bc52a26e10405d89b7180ddcf49302  2803:f800::/32    Cloudflare IP
ff78f842188e4b869eb5389ae9ab8f41  2a06:98c0::/29    Cloudflare IP
0880cdfc40b14f6fa0639522a728859d  2c0f:f248::/32    Cloudflare IP
$
```

The `result_info.cursors` area also contains a `before` value for reverse scrolling.

As with `per_page` scrolling, raw mode is used.

### DNSSEC CLI calls

```bash
$ cli4 /zones/:example.com/dnssec | jq -c '{"status":.status}'
{"status":"disabled"}
$

$ cli4 --patch status=active /zones/:example.com/dnssec | jq -c '{"status":.status}'
{"status":"pending"}
$

$ cli4 /zones/:example.com/dnssec
{
    "algorithm": "13",
    "digest": "41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194",
    "digest_algorithm": "SHA256",
    "digest_type": "2",
    "ds": "example.com. 3600 IN DS 2371 13 2 41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194",
    "flags": 257,
    "key_tag": 2371,
    "key_type": "ECDSAP256SHA256",
    "modified_on": "2016-05-01T22:42:15.591158Z",
    "public_key": "mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==",
    "status": "pending"
}
$
```

### Zone file upload (i.e. import) CLI calls (uses BIND format files)

Refer to [Import DNS records](https://api.cloudflare.com/#dns-records-for-a-zone-import-dns-records) on API documentation for this feature.

```bash
$ cat zone.txt
example.com.            IN      SOA     somewhere.example.com. someone.example.com. (
                                2017010101
                                3H
                                15
                                1w
                                3h
                        )

record1.example.com.    IN      A       10.0.0.1
record2.example.com.    IN      AAAA    2001:d8b::2
record3.example.com.    IN      CNAME   record1.example.com.
record4.example.com.    IN      TXT     "some text"
$

$ cli4 --post file=@zone.txt /zones/:example.com/dns_records/import
{
    "recs_added": 4,
    "total_records_parsed": 4
}
$
```

### Zone file upload (i.e. import) Python calls (uses BIND format files)

Because `import` is a keyword (or reserved word) in Python we append a `_` (underscore) to the verb in order to use.
The `cli4` command does not need this edit.

```python
    #
    # "import" is a reserved word and hence we add '_' to the end of verb.
    #
    r = cf.zones.dns_records.import_.post(zone_id, files={'file':fd})
```

See [examples/example_dns_import.py](https://github.com/cloudflare/python-cloudflare/tree/master/examples/example_dns_import.py) for working code.

### Zone file download (i.e. export) CLI calls (uses BIND format files)

The following is documented within the **Advanced** option of the DNS page within the Cloudflare portal.

```bash
$ cli4 /zones/:example.com/dns_records/export | egrep -v '^;;|^$'
$ORIGIN .
@       3600    IN      SOA     example.com.    root.example.com.       (
                2025552311      ; serial
                7200            ; refresh
                3600            ; retry
                86400           ; expire
                3600)           ; minimum
example.com.    300     IN      NS      REPLACE&ME$WITH^YOUR@NAMESERVER.
record4.example.com.    300     IN      TXT     "some text"
record3.example.com.    300     IN      CNAME   record1.example.com.
record1.example.com.    300     IN      A       10.0.0.1
record2.example.com.    300     IN      AAAA    2001:d8b::2
$
```

The egrep is used for documentation brevity.

This can also be done via Python code with the following example.

```python
#!/usr/bin/env python
import sys
import CloudFlare

def main():
    zone_name = sys.argv[1]
    cf = CloudFlare.CloudFlare()

    zones = cf.zones.get(params={'name': zone_name})
    zone_id = zones[0]['id']

    dns_records = cf.zones.dns_records.export.get(zone_id)
    for l in dns_records.splitlines():
        if len(l) == 0 or l[0] == ';':
            continue
        print(l)
    exit(0)

if __name__ == '__main__':
    main()
```

### Cloudflare Workers

Cloudflare Workers are described on the Cloudflare blog at
[here](https://blog.cloudflare.com/introducing-cloudflare-workers/) and
[here](https://blog.cloudflare.com/code-everywhere-cloudflare-workers/), with the beta release announced
[here](https://blog.cloudflare.com/cloudflare-workers-is-now-on-open-beta/).

The Python libraries now support the Cloudflare Workers API calls. The following javascript is lifted from [https://cloudflareworkers.com/](https://cloudflareworkers.com/) and slightly modified.

```bash
$ cat modify-body.js
addEventListener("fetch", event => {
  event.respondWith(fetchAndModify(event.request));
});

async function fetchAndModify(request) {
  console.log("got a request:", request);

  // Send the request on to the origin server.
  const response = await fetch(request);

  // Read response body.
  const text = await response.text();

  // Modify it.
  const modified = text.replace(
  "",
  "");

  // Return modified response.
  return new Response(modified, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });
}
$
```

Here's the website with it's simple `` statement

```bash
$ curl -sS https://example.com/ | fgrep '
$
```

Now lets add the script. Looking above, you will see that it's simple action is to modify the `` statement and make the background yellow.

```bash
$ cli4 --put @- /zones/:example.com/workers/script < modify-body.js
{
    "etag": "1234567890123456789012345678901234567890123456789012345678901234",
    "id": "example-com",
    "modified_on": "2018-02-15T00:00:00.000000Z",
    "script": "addEventListener(\"fetch\", event => {\n  event.respondWith(fetchAndModify(event.request));\n});\n\nasync function fetchAndModify(request) {\n  console.log(\"got a request:\", request);\n\n  // Send the request on to the origin server.\n  const response = await fetch(request);\n\n  // Read response body.\n  const text = await response.text();\n\n  // Modify it.\n  const modified = text.replace(\n  \"\",\n  \"\");\n\n  // Return modified response.\n  return new Response(modified, {\n    status: response.status,\n    statusText: response.statusText,\n    headers: response.headers\n  });\n}\n",
    "size": 603
}
$
```

The following call checks that the script is associated with the zone. In this case, it's the only script added by this user.

```bash
$ cli4 /user/workers/scripts
[
    {
        "created_on": "2018-02-15T00:00:00.000000Z",
        "etag": "1234567890123456789012345678901234567890123456789012345678901234",
        "id": "example-com",
        "modified_on": "2018-02-15T00:00:00.000000Z"
    }
]
$
```

Next step is to make sure a route is added for that script on that zone.

```bash
$ cli4 --post pattern="example.com/*" script="example-com" /zones/:example.com/workers/routes
{
    "id": "12345678901234567890123456789012"
}
$

$ cli4 /zones/:example.com/workers/routes
[
    {
        "id": "12345678901234567890123456789012",
        "pattern": "example.com/*",
        "script": "example-com"
    }
]
$
```

With that script added to the zone and the route added, we can now see the website has been modified because of the Cloudflare Worker.

```bash
$ curl -sS https://example.com/ | fgrep '
$
```

All this can be removed; hence bringing the website back to its initial state.

```bash
$ cli4 --delete /zones/:example.com/workers/script
12345678901234567890123456789012
$ cli4 --delete /zones/:example.com/workers/routes/:12345678901234567890123456789012
true
$

$ curl -sS https://example.com/ | fgrep '
$
```

Refer to the Cloudflare Workers API documentation for more information.

## Cloudflare Instant Logs

Please see https://developers.cloudflare.com/logs/instant-logs for all the information on how to use this feature.
The `cli4` command along with the Python libaries can be used to control the instant logs; however, the websocket reading is outside the scope of this library.

To query the states of the instant logs:

```bash
$ cli4 /zones/:example.com/logpush/edge/jobs | jq .
[]
$
```

To add monitoring:

```bash
$ cli4 --post \
        ='{
                "fields": "ClientIP,ClientRequestHost,ClientRequestMethod,ClientRequestURI,EdgeEndTimestamp,EdgeResponseBytes,EdgeResponseStatus,EdgeStartTimestamp,RayID",
                "sample": 1,
                "filter": "",
                "kind": "instant-logs"
        }' \
        /zones/:example.com/logpush/edge/jobs | jq .
{
  "destination_conf": "wss://logs.cloudflare.com/instant-logs/ws/sessions/00000000000000000000000000000000",
  "fields": "ClientIP,ClientRequestHost,ClientRequestMethod,ClientRequestURI,EdgeEndTimestamp,EdgeResponseBytes,EdgeResponseStatus,EdgeStartTimestamp,RayID",
  "filter": "",
  "kind": "instant-logs",
  "sample": 1,
  "session_id": "00000000000000000000000000000000"
}
$
```

To see the results:

```bash
$ cli4 /zones/:example.com/logpush/edge/jobs | jq .
[
  {
    "fields": "ClientIP,ClientRequestHost,ClientRequestMethod,ClientRequestURI,EdgeEndTimestamp,EdgeResponseBytes,EdgeResponseStatus,EdgeStartTimestamp,RayID",
    "filter": "",
    "kind": "instant-logs",
    "sample": 1,
    "session_id": "00000000000000000000000000000000"
  }
]
$
```

## Cloudflare GraphQL

The GraphQL interface can be accessed via the command line or via Python.

```
    query="""
      query {
        viewer {
            zones(filter: {zoneTag: "%s"} ) {
            httpRequests1dGroups(limit:40, filter:{date_lt: "%s", date_gt: "%s"}) {
              sum { countryMap { bytes, requests, clientCountryName } }
              dimensions { date }
            }
          }
        }
      }
    """ % (zone_id, date_before[0:10], date_after[0:10])

    r = cf.graphql.post(data={'query':query})

    httpRequests1dGroups = zone_info = r['data']['viewer']['zones'][0]['httpRequests1dGroups']
```

See the [examples/example_graphql.sh](examples/example_graphql.sh) and [examples/example_graphql.py](https://github.com/cloudflare/python-cloudflare/tree/master/examples/example_graphql.py) files for working examples.
Here is the working example of the shell version:

```bash
$ examples/example_graphql.sh example.com
2020-07-14T02:00:00Z    34880
2020-07-14T03:00:00Z    18953
2020-07-14T04:00:00Z    28700
2020-07-14T05:00:00Z    2358
2020-07-14T06:00:00Z    34905
2020-07-14T07:00:00Z    779
2020-07-14T08:00:00Z    35450
2020-07-14T10:00:00Z    17803
2020-07-14T11:00:00Z    32678
2020-07-14T12:00:00Z    19947
2020-07-14T13:00:00Z    4956
2020-07-14T14:00:00Z    34585
2020-07-14T15:00:00Z    3022
2020-07-14T16:00:00Z    5224
2020-07-14T18:00:00Z    79482
2020-07-14T21:00:00Z    10609
2020-07-14T22:00:00Z    5740
2020-07-14T23:00:00Z    2545
2020-07-15T01:00:00Z    10777
$
```

For more information on how to use GraphQL at Cloudflare, refer to the [Cloudflare GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api).
It contains a full overview of Cloudflare's GraphQL features and keywords.

## Cloudflare AI

See https://blog.cloudflare.com/workers-ai-update-stable-diffusion-code-llama-workers-ai-in-100-cities/ for the introduction,
along with https://developers.cloudflare.com/workers-ai/models/ for the nitty gritty details.

There are three AI calls included within the example folder.

### Image creation.

```bash
$ python examples/example_ai_images.py A happy llama running through an orange cloud > /tmp/image.png
$
$ file /tmp/image.png
/tmp/image.png: PNG image data, 1024 x 1024, 8-bit/color RGB, non-interlaced
$
```

### Translation.

```bash
$ python examples/example_ai_translate.py I\'ll have an order of the moule frites
Je vais avoir une commande des frites de moule
$
```

### Speech Recognition with the openai/whisper model.

The following downloads a speech as an mp3 file and passes it to the AI API.
It does a very good job transcribing; however, there's a good chance these mp3 files were use for training.
That said, the example code is here to show how the API works vs testing the AI/ML quality.

```bash
$ python examples/example_ai_speechrecognition.py
mp3 received: length=700367
My fellow Americans, Michelle and I have been so touched by all the well wishes that we've received over the past few weeks. But tonight, tonight it's my turn to say thanks.
$
```

This is presently work-in-progress because of the non-Python calling method. The syntax could change in the future.

They can also be called via `cli4`.

```bash
$ cli4 --image --post text="I'll have an order of the moule frites" source_lang=english target_lang=french /accounts/:AccountID/ai/run/@cf/meta/m2m100-1.2b
{'translated_text': 'Je vais avoir une commande des frites de moule'}
$
```

Presently you will need the following in your `cloudflare.cfg` file.

```bash
$ cat ~/.cloudflare/cloudflare.cfg
[CloudFlare]
global_request_timeout = 120
max_request_retries = 1
extras =
    /accounts/:id/ai/run/@cf/meta/llama-2-7b-chat-fp16
    /accounts/:id/ai/run/@cf/meta/llama-2-7b-chat-int8
    /accounts/:id/ai/run/@cf/mistral/mistral-7b-instruct-v0.1
    /accounts/:id/ai/run/@cf/openai/whisper
    /accounts/:id/ai/run/@cf/meta/m2m100-1.2b
    /accounts/:id/ai/run/@cf/huggingface/distilbert-sst-2-int8
    /accounts/:id/ai/run/@cf/microsoft/resnet-50
    /accounts/:id/ai/run/@cf/stabilityai/stable-diffusion-xl-base-1.0
    /accounts/:id/ai/run/@cf/baai/bge-base-en-v1.5
    /accounts/:id/ai/run/@cf/baai/bge-large-en-v1.5
    /accounts/:id/ai/run/@cf/baai/bge-small-en-v1.5

$
```

As the `@` (at) symbol and the `.` (dot) symbol aren't allowed in python variable names; you'll have the replace `@cf` with `at_cf` and `.` with `_`.
There's already notes above that state that `-` (dash) is replaced with `_` in the code.
That will be needed with some model names.

The `cli4` command does not need this edit. It is done on the fly!

For example, the following code is valid:
```python
    r = cf.accounts.ai.run.at_cf.openai.whisper.post(account_id, data=audio_data)
    r = cf.accounts.ai.run.at_cf.meta.m2m100_1_2b.post(account_id, data=translate_data)
    r = cf.accounts.ai.run.at_cf.stabilityai.stable_diffusion_xl_base_1_0.post(account_id, data=image_create_data)
```

Or you can use the `find()` call can will do this conversion for you.
```python
    translate_data = {'text':"I'll have an order of the moule frites", 'source_lang':'english', 'target_lang':'french'}

    m = cf.find('/accounts/:id/ai/run/@cf/meta/m2m100-1.2b')
    r = m.post(account_id, data=translate_data)
    print(r['translated_text'])
```

You will also have to run with a version of the library above `2.18.2`.

## Implemented API calls

The `--dump` argument to cli4 will produce a list of all the call implemented within the library.

```bash
$ cli4 --dump
/certificates
/ips
/organizations
...
/zones/ssl/analyze
/zones/ssl/certificate_packs
/zones/ssl/verification
$
```

### Table of commands

An automatically generated table of commands is provided [here](TABLE-OF-COMMANDS.md).

## Adding extra API calls manually

Extra API calls can be added via the configuration file

```bash
$ cat ~/.cloudflare/cloudflare.cfg
[Cloudflare]
extras =
    /client/v4/command
    /client/v4/command/:command_identifier
    /client/v4/command/:command_identifier/settings
$
```

While it's easy to call anything within Cloudflare's API, it's not very useful to add items in here as they will simply return API URL errors.
Technically, this is only useful for internal testing within Cloudflare.

## Issues

The following error can be caused by an out of date SSL/TLS library and/or out of date Python.

```bash
/usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:318: SNIMissingWarning: An HTTPS request has been made, but the SNI (Subject Name Indication) extension to TLS is not available on this platform. This may cause the server to present an incorrect TLS certificate, which can cause validation failures. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#snimissingwarning.
  SNIMissingWarning
/usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:122: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
  InsecurePlatformWarning
```

The solution can be found [here](https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning) and/or [here](http://stackoverflow.com/questions/35144550/how-to-install-cryptography-on-ubuntu).

## Python 2.x vs 3.x support

As of May/June 2016 the code is now tested against pylint.
This was required in order to move the codebase into Python 3.x.
The motivation for this came from [Danielle Madeley (danni)](https://github.com/danni).

~~While the codebase has been edited to run on Python 3.x, there's not been enough Python 3.x testing performed.~~
~~If you can help in this regard; please contact the maintainers.~~

As of January 2020 the code is Python3 clean.

As of January 2020 the code is shipped up to pypi with Python2 support removed.

As of January 2020 the code is Python3.8 clean. The new `SyntaxWarning` messages (i.e. `SyntaxWarning: "is" with a literal. Did you mean "=="?`) meant minor edits were needed.

As of late 2023 the code is Python3.11 clean.

As of April 2024 the code is officially marked as 3.x only (3.6 and above to be specific) such that it can become PEP561 specific.

## pypi and GitHub signed releases

As of October/2022, the code is signed by the maintainers personal email address: `mahtin@mahtin.com` `7EA1 39C4 0C1C 842F 9D41 AAF9 4A34 925D 0517 2859`

## Credit

This is based on work by [Felix Wong (gnowxilef)](https://github.com/gnowxilef) found [here](https://github.com/cloudflare-api/python-cloudflare-v4).
It has been seriously expanded upon.

## Changelog

An automatically generated CHANGELOG is provided [here](CHANGELOG.md).

## Copyright

Copyright (c) 2016 thru 2024, Cloudflare. All rights reserved.
Previous portions copyright [Felix Wong (gnowxilef)](https://github.com/gnowxilef).
python-cloudflare-2.20.0/TABLE-OF-COMMANDS.md000066400000000000000000001522731461736615400201330ustar00rootroot00000000000000# cloudflare-python

## Table of commands

|`GET`   |`PUT`   |`POST`  |`PATCH` |`DELETE`|API call|
|--------|--------|--------|--------|--------|:-------|
|`GET`   |-       |`PUT`   |-       |-       | /accounts |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/apps |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/access/apps/:id/ca |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/apps/:id/policies |
|-       |`POST`  |-       |-       |-       | /accounts/:id/access/apps/:id/revoke_tokens |
|`GET`   |-       |-       |-       |-       | /accounts/:id/access/apps/:id/user_policy_checks |
|`GET`   |-       |-       |-       |-       | /accounts/:id/access/apps/ca |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/certificates |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/access/certificates/settings |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/custom_pages |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/groups |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/identity_providers |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/access/keys |
|-       |`POST`  |-       |-       |-       | /accounts/:id/access/keys/rotate |
|`GET`   |-       |-       |-       |-       | /accounts/:id/access/logs/access_requests |
|`GET`   |`POST`  |`PUT`   |-       |-       | /accounts/:id/access/organizations |
|-       |`POST`  |-       |-       |-       | /accounts/:id/access/organizations/revoke_user |
|-       |-       |-       |`PATCH` |-       | /accounts/:id/access/seats |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/service_tokens |
|-       |`POST`  |-       |-       |-       | /accounts/:id/access/service_tokens/:id/refresh |
|-       |`POST`  |-       |-       |-       | /accounts/:id/access/service_tokens/:id/rotate |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/access/tags |
|`GET`   |-       |-       |-       |-       | /accounts/:id/access/users |
|`GET`   |-       |-       |-       |-       | /accounts/:id/access/users/:id/active_sessions |
|`GET`   |-       |-       |-       |-       | /accounts/:id/access/users/:id/failed_logins |
|`GET`   |-       |-       |-       |-       | /accounts/:id/access/users/:id/last_seen_identity |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/addressing/address_maps |
|-       |-       |`PUT`   |-       |`DELETE`| /accounts/:id/addressing/address_maps/:id/accounts |
|-       |-       |`PUT`   |-       |`DELETE`| /accounts/:id/addressing/address_maps/:id/ips |
|-       |-       |`PUT`   |-       |`DELETE`| /accounts/:id/addressing/address_maps/:id/zones |
|-       |`POST`  |-       |-       |-       | /accounts/:id/addressing/loa_documents |
|`GET`   |-       |-       |-       |-       | /accounts/:id/addressing/loa_documents/:id/download |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/addressing/prefixes |
|`GET`   |-       |-       |`PATCH` |-       | /accounts/:id/addressing/prefixes/:id/bgp/prefixes |
|`GET`   |-       |-       |`PATCH` |-       | /accounts/:id/addressing/prefixes/:id/bgp/status |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/addressing/prefixes/:id/bindings |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/addressing/prefixes/:id/delegations |
|`GET`   |-       |-       |-       |-       | /accounts/:id/addressing/services |
|-       |`POST`  |-       |-       |-       | /accounts/:id/ai/run |
|-       |`POST`  |-       |-       |-       | /accounts/:id/ai/run/proxy |
|`GET`   |-       |-       |-       |-       | /accounts/:id/alerting/v3/available_alerts |
|`GET`   |-       |-       |-       |-       | /accounts/:id/alerting/v3/destinations/eligible |
|`GET`   |-       |-       |-       |`DELETE`| /accounts/:id/alerting/v3/destinations/pagerduty |
|`GET`   |`POST`  |-       |-       |-       | /accounts/:id/alerting/v3/destinations/pagerduty/connect |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/alerting/v3/destinations/webhooks |
|`GET`   |-       |-       |-       |-       | /accounts/:id/alerting/v3/history |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/alerting/v3/policies |
|`GET`   |-       |-       |-       |-       | /accounts/:id/audit_logs |
|-       |`POST`  |-       |-       |-       | /accounts/:id/brand-protection/submit |
|`GET`   |-       |-       |-       |-       | /accounts/:id/brand-protection/url-info |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/cfd_tunnel |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/cfd_tunnel/:id/configurations |
|`GET`   |-       |-       |-       |`DELETE`| /accounts/:id/cfd_tunnel/:id/connections |
|`GET`   |-       |-       |-       |-       | /accounts/:id/cfd_tunnel/:id/connectors |
|-       |`POST`  |-       |-       |-       | /accounts/:id/cfd_tunnel/:id/management |
|`GET`   |-       |-       |-       |-       | /accounts/:id/cfd_tunnel/:id/token |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/challenges/widgets |
|-       |`POST`  |-       |-       |-       | /accounts/:id/challenges/widgets/:id/rotate_secret |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/custom_ns |
|`GET`   |-       |-       |-       |-       | /accounts/:id/custom_ns/availability |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/custom_pages |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/d1/database |
|-       |`POST`  |-       |-       |-       | /accounts/:id/d1/database/:id/query |
|`GET`   |-       |-       |-       |-       | /accounts/:id/devices |
|`GET`   |-       |-       |-       |-       | /accounts/:id/devices/:id/override_codes |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/devices/dex_tests |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/devices/networks |
|`GET`   |-       |-       |-       |-       | /accounts/:id/devices/policies |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/devices/policy |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/devices/policy/:id/exclude |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/devices/policy/:id/fallback_domains |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/devices/policy/:id/include |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/devices/policy/exclude |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/devices/policy/fallback_domains |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/devices/policy/include |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/devices/posture |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/devices/posture/integration |
|-       |`POST`  |-       |-       |-       | /accounts/:id/devices/revoke |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/devices/settings |
|-       |`POST`  |-       |-       |-       | /accounts/:id/devices/unrevoke |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/colos |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/fleet-status/devices |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/fleet-status/live |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/fleet-status/over-time |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/http-tests |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/http-tests/:id/percentiles |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/tests |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/tests/unique-devices |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/traceroute-test-results/:id/network-path |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/traceroute-tests |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/traceroute-tests/:id/network-path |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dex/traceroute-tests/:id/percentiles |
|-       |`POST`  |-       |-       |-       | /accounts/:id/diagnostics/traceroute |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/dlp/datasets |
|-       |`POST`  |-       |-       |-       | /accounts/:id/dlp/datasets/:id/upload |
|-       |`POST`  |-       |-       |-       | /accounts/:id/dlp/patterns/validate |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/dlp/payload_log |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dlp/profiles |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/dlp/profiles/custom |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/dlp/profiles/predefined |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/dns_firewall |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dns_firewall/:id/dns_analytics/report |
|`GET`   |-       |-       |-       |-       | /accounts/:id/dns_firewall/:id/dns_analytics/report/bytime |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/email/routing/addresses |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/firewall/access_rules/rules |
|`GET`   |`POST`  |-       |-       |-       | /accounts/:id/gateway |
|`GET`   |-       |-       |-       |-       | /accounts/:id/gateway/app_types |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/gateway/audit_ssh_settings |
|`GET`   |-       |-       |-       |-       | /accounts/:id/gateway/categories |
|`GET`   |-       |`PUT`   |`PATCH` |-       | /accounts/:id/gateway/configuration |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /accounts/:id/gateway/lists |
|`GET`   |-       |-       |-       |-       | /accounts/:id/gateway/lists/:id/items |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/gateway/locations |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/gateway/logging |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/gateway/proxy_endpoints |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/gateway/rules |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/hyperdrive/configs |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/images/v1 |
|`GET`   |-       |-       |-       |-       | /accounts/:id/images/v1/:id/blob |
|`GET`   |-       |-       |-       |-       | /accounts/:id/images/v1/keys |
|`GET`   |-       |-       |-       |-       | /accounts/:id/images/v1/stats |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/images/v1/variants |
|`GET`   |-       |-       |-       |-       | /accounts/:id/images/v2 |
|-       |`POST`  |-       |-       |-       | /accounts/:id/images/v2/direct_upload |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/asn |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/asn/:id/subnets |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/dns |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/domain |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/domain-history |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/domain/bulk |
|`GET`   |`POST`  |-       |-       |-       | /accounts/:id/intel/indicator-feeds |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/indicator-feeds/:id/data |
|-       |-       |`PUT`   |-       |-       | /accounts/:id/intel/indicator-feeds/:id/snapshot |
|-       |-       |`PUT`   |-       |-       | /accounts/:id/intel/indicator-feeds/permissions/add |
|-       |-       |`PUT`   |-       |-       | /accounts/:id/intel/indicator-feeds/permissions/remove |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/indicator-feeds/permissions/view |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/ip |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/ip-list |
|-       |`POST`  |-       |-       |-       | /accounts/:id/intel/miscategorization |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/sinkholes |
|`GET`   |-       |-       |-       |-       | /accounts/:id/intel/whois |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /accounts/:id/load_balancers/monitors |
|-       |`POST`  |-       |-       |-       | /accounts/:id/load_balancers/monitors/:id/preview |
|`GET`   |-       |-       |-       |-       | /accounts/:id/load_balancers/monitors/:id/references |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /accounts/:id/load_balancers/pools |
|`GET`   |-       |-       |-       |-       | /accounts/:id/load_balancers/pools/:id/health |
|-       |`POST`  |-       |-       |-       | /accounts/:id/load_balancers/pools/:id/preview |
|`GET`   |-       |-       |-       |-       | /accounts/:id/load_balancers/pools/:id/references |
|`GET`   |-       |-       |-       |-       | /accounts/:id/load_balancers/preview |
|`GET`   |-       |-       |-       |-       | /accounts/:id/load_balancers/regions |
|`GET`   |-       |-       |-       |-       | /accounts/:id/load_balancers/search |
|`GET`   |-       |-       |-       |-       | /accounts/:id/logpush/datasets/:id/fields |
|`GET`   |-       |-       |-       |-       | /accounts/:id/logpush/datasets/:id/jobs |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/logpush/jobs |
|-       |`POST`  |-       |-       |-       | /accounts/:id/logpush/ownership |
|-       |`POST`  |-       |-       |-       | /accounts/:id/logpush/ownership/validate |
|-       |`POST`  |-       |-       |-       | /accounts/:id/logpush/validate/destination/exists |
|-       |`POST`  |-       |-       |-       | /accounts/:id/logpush/validate/origin |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/logs/control/cmb/config |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/magic/cf_interconnects |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/magic/gre_tunnels |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/magic/ipsec_tunnels |
|-       |`POST`  |-       |-       |-       | /accounts/:id/magic/ipsec_tunnels/:id/psk_generate |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/magic/routes |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/members |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /accounts/:id/mnm/config |
|`GET`   |-       |-       |-       |-       | /accounts/:id/mnm/config/full |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /accounts/:id/mnm/rules |
|-       |-       |-       |`PATCH` |-       | /accounts/:id/mnm/rules/:id/advertisement |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/mtls_certificates |
|`GET`   |-       |-       |-       |-       | /accounts/:id/mtls_certificates/:id/associations |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/pages/projects |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/pages/projects/:id/deployments |
|`GET`   |-       |-       |-       |-       | /accounts/:id/pages/projects/:id/deployments/:id/history/logs |
|-       |`POST`  |-       |-       |-       | /accounts/:id/pages/projects/:id/deployments/:id/retry |
|-       |`POST`  |-       |-       |-       | /accounts/:id/pages/projects/:id/deployments/:id/rollback |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/pages/projects/:id/domains |
|-       |`POST`  |-       |-       |-       | /accounts/:id/pages/projects/:id/purge_build_cache |
|`GET`   |`POST`  |-       |-       |-       | /accounts/:id/pcaps |
|`GET`   |-       |-       |-       |-       | /accounts/:id/pcaps/:id/download |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/pcaps/ownership |
|-       |`POST`  |-       |-       |-       | /accounts/:id/pcaps/ownership/validate |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/r2/buckets |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/registrar/domains |
|-       |`POST`  |-       |-       |-       | /accounts/:id/request-tracer/trace |
|`GET`   |-       |-       |-       |-       | /accounts/:id/roles |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/rules/lists |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/rules/lists/:id/items |
|`GET`   |-       |-       |-       |-       | /accounts/:id/rules/lists/bulk_operations |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/rulesets |
|-       |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/rulesets/:id/rules |
|`GET`   |-       |-       |-       |`DELETE`| /accounts/:id/rulesets/:id/versions |
|`GET`   |-       |-       |-       |-       | /accounts/:id/rulesets/:id/versions/:id/by_tag |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/rulesets/phases/:id/entrypoint |
|`GET`   |-       |-       |-       |-       | /accounts/:id/rulesets/phases/:id/entrypoint/versions |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/rum/site_info |
|`GET`   |-       |-       |-       |-       | /accounts/:id/rum/site_info/list |
|-       |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/rum/v2/:id/rule |
|`GET`   |`POST`  |-       |-       |-       | /accounts/:id/rum/v2/:id/rules |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/secondary_dns/acls |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/secondary_dns/peers |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/secondary_dns/tsigs |
|`GET`   |-       |-       |-       |-       | /accounts/:id/storage/analytics |
|`GET`   |-       |-       |-       |-       | /accounts/:id/storage/analytics/stored |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/storage/kv/namespaces |
|-       |-       |`PUT`   |-       |`DELETE`| /accounts/:id/storage/kv/namespaces/:id/bulk |
|`GET`   |-       |-       |-       |-       | /accounts/:id/storage/kv/namespaces/:id/keys |
|`GET`   |-       |-       |-       |-       | /accounts/:id/storage/kv/namespaces/:id/metadata |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /accounts/:id/storage/kv/namespaces/:id/values |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/stream |
|`GET`   |-       |-       |`PATCH` |`DELETE`| /accounts/:id/stream/:id/audio |
|-       |`POST`  |-       |-       |-       | /accounts/:id/stream/:id/audio/copy |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /accounts/:id/stream/:id/captions |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/stream/:id/downloads |
|`GET`   |-       |-       |-       |-       | /accounts/:id/stream/:id/embed |
|-       |`POST`  |-       |-       |-       | /accounts/:id/stream/:id/token |
|-       |`POST`  |-       |-       |-       | /accounts/:id/stream/clip |
|-       |`POST`  |-       |-       |-       | /accounts/:id/stream/copy |
|-       |`POST`  |-       |-       |-       | /accounts/:id/stream/direct_upload |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/stream/keys |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/stream/live_inputs |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/stream/live_inputs/:id/outputs |
|`GET`   |-       |-       |-       |-       | /accounts/:id/stream/storage-usage |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/stream/watermarks |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /accounts/:id/stream/webhook |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/subscriptions |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/teamnet/routes |
|`GET`   |-       |-       |-       |-       | /accounts/:id/teamnet/routes/ip |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/teamnet/virtual_networks |
|`GET`   |-       |-       |-       |-       | /accounts/:id/tunnels |
|`GET`   |`POST`  |-       |-       |-       | /accounts/:id/urlscanner/scan |
|`GET`   |-       |-       |-       |-       | /accounts/:id/urlscanner/scan/:id/har |
|`GET`   |-       |-       |-       |-       | /accounts/:id/urlscanner/scan/:id/screenshot |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/vectorize/indexes |
|-       |`POST`  |-       |-       |-       | /accounts/:id/vectorize/indexes/:id/insert |
|-       |`POST`  |-       |-       |-       | /accounts/:id/vectorize/indexes/:id/query |
|-       |`POST`  |-       |-       |-       | /accounts/:id/vectorize/indexes/:id/upsert |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /accounts/:id/warp_connector |
|`GET`   |-       |-       |-       |-       | /accounts/:id/warp_connector/:id/token |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/workers/account-settings |
|`GET`   |-       |-       |-       |-       | /accounts/:id/workers/deployments/by-script |
|`GET`   |-       |-       |-       |-       | /accounts/:id/workers/deployments/by-script/:id/detail |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /accounts/:id/workers/dispatch/namespaces/:id/scripts |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/workers/dispatch/namespaces/:id/scripts/:id/content |
|`GET`   |-       |-       |`PATCH` |-       | /accounts/:id/workers/dispatch/namespaces/:id/scripts/:id/settings |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /accounts/:id/workers/domains |
|`GET`   |-       |-       |-       |-       | /accounts/:id/workers/durable_objects/namespaces |
|`GET`   |-       |-       |-       |-       | /accounts/:id/workers/durable_objects/namespaces/:id/objects |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/workers/queues |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /accounts/:id/workers/queues/:id/consumers |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /accounts/:id/workers/scripts |
|-       |-       |`PUT`   |-       |-       | /accounts/:id/workers/scripts/:id/content |
|`GET`   |-       |-       |-       |-       | /accounts/:id/workers/scripts/:id/content/v2 |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/workers/scripts/:id/schedules |
|`GET`   |-       |-       |`PATCH` |-       | /accounts/:id/workers/scripts/:id/settings |
|`GET`   |`POST`  |-       |-       |`DELETE`| /accounts/:id/workers/scripts/:id/tails |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/workers/scripts/:id/usage-model |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/workers/services/:id/environments/:id/content |
|`GET`   |-       |-       |`PATCH` |-       | /accounts/:id/workers/services/:id/environments/:id/settings |
|`GET`   |-       |`PUT`   |-       |-       | /accounts/:id/workers/subdomain |
|`GET`   |-       |-       |`PATCH` |-       | /accounts/:id/zerotrust/connectivity_settings |
|`GET`   |`POST`  |-       |-       |`DELETE`| /certificates |
|`GET`   |-       |-       |-       |-       | /ips |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /memberships |
|`GET`   |-       |-       |-       |-       | /radar/annotations/outages |
|`GET`   |-       |-       |-       |-       | /radar/annotations/outages/locations |
|`GET`   |-       |-       |-       |-       | /radar/as112/summary/dnssec |
|`GET`   |-       |-       |-       |-       | /radar/as112/summary/edns |
|`GET`   |-       |-       |-       |-       | /radar/as112/summary/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/as112/summary/protocol |
|`GET`   |-       |-       |-       |-       | /radar/as112/summary/query_type |
|`GET`   |-       |-       |-       |-       | /radar/as112/summary/response_codes |
|`GET`   |-       |-       |-       |-       | /radar/as112/timeseries |
|`GET`   |-       |-       |-       |-       | /radar/as112/timeseries_groups/dnssec |
|`GET`   |-       |-       |-       |-       | /radar/as112/timeseries_groups/edns |
|`GET`   |-       |-       |-       |-       | /radar/as112/timeseries_groups/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/as112/timeseries_groups/protocol |
|`GET`   |-       |-       |-       |-       | /radar/as112/timeseries_groups/query_type |
|`GET`   |-       |-       |-       |-       | /radar/as112/timeseries_groups/response_codes |
|`GET`   |-       |-       |-       |-       | /radar/as112/top/locations |
|`GET`   |-       |-       |-       |-       | /radar/as112/top/locations/dnssec |
|`GET`   |-       |-       |-       |-       | /radar/as112/top/locations/edns |
|`GET`   |-       |-       |-       |-       | /radar/as112/top/locations/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/summary/bitrate |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/summary/duration |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/summary/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/summary/protocol |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/summary/vector |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries_groups/bitrate |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries_groups/duration |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries_groups/industry |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries_groups/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries_groups/protocol |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries_groups/vector |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/timeseries_groups/vertical |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/top/attacks |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/top/industry |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/top/locations/origin |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/top/locations/target |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer3/top/vertical |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/summary/http_method |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/summary/http_version |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/summary/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/summary/managed_rules |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/summary/mitigation_product |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries_groups/http_method |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries_groups/http_version |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries_groups/industry |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries_groups/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries_groups/managed_rules |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries_groups/mitigation_product |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/timeseries_groups/vertical |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/top/ases/origin |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/top/attacks |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/top/industry |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/top/locations/origin |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/top/locations/target |
|`GET`   |-       |-       |-       |-       | /radar/attacks/layer7/top/vertical |
|`GET`   |-       |-       |-       |-       | /radar/bgp/hijacks/events |
|`GET`   |-       |-       |-       |-       | /radar/bgp/leaks/events |
|`GET`   |-       |-       |-       |-       | /radar/bgp/routes/moas |
|`GET`   |-       |-       |-       |-       | /radar/bgp/routes/pfx2as |
|`GET`   |-       |-       |-       |-       | /radar/bgp/routes/stats |
|`GET`   |-       |-       |-       |-       | /radar/bgp/timeseries |
|`GET`   |-       |-       |-       |-       | /radar/bgp/top/ases |
|`GET`   |-       |-       |-       |-       | /radar/bgp/top/ases/prefixes |
|`GET`   |-       |-       |-       |-       | /radar/bgp/top/prefixes |
|`GET`   |-       |-       |-       |-       | /radar/connection_tampering/summary |
|`GET`   |-       |-       |-       |-       | /radar/connection_tampering/timeseries_groups |
|`GET`   |-       |-       |-       |-       | /radar/datasets |
|-       |`POST`  |-       |-       |-       | /radar/datasets/download |
|`GET`   |-       |-       |-       |-       | /radar/dns/top/ases |
|`GET`   |-       |-       |-       |-       | /radar/dns/top/locations |
|`GET`   |-       |-       |-       |-       | /radar/email/security/summary/arc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/summary/dkim |
|`GET`   |-       |-       |-       |-       | /radar/email/security/summary/dmarc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/summary/malicious |
|`GET`   |-       |-       |-       |-       | /radar/email/security/summary/spam |
|`GET`   |-       |-       |-       |-       | /radar/email/security/summary/spf |
|`GET`   |-       |-       |-       |-       | /radar/email/security/summary/threat_category |
|`GET`   |-       |-       |-       |-       | /radar/email/security/timeseries_groups/arc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/timeseries_groups/dkim |
|`GET`   |-       |-       |-       |-       | /radar/email/security/timeseries_groups/dmarc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/timeseries_groups/malicious |
|`GET`   |-       |-       |-       |-       | /radar/email/security/timeseries_groups/spam |
|`GET`   |-       |-       |-       |-       | /radar/email/security/timeseries_groups/spf |
|`GET`   |-       |-       |-       |-       | /radar/email/security/timeseries_groups/threat_category |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/ases |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/ases/arc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/ases/dkim |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/ases/dmarc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/ases/malicious |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/ases/spam |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/ases/spf |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/locations |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/locations/arc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/locations/dkim |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/locations/dmarc |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/locations/malicious |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/locations/spam |
|`GET`   |-       |-       |-       |-       | /radar/email/security/top/locations/spf |
|`GET`   |-       |-       |-       |-       | /radar/entities/asns |
|`GET`   |-       |-       |-       |-       | /radar/entities/asns/:id/rel |
|`GET`   |-       |-       |-       |-       | /radar/entities/asns/ip |
|`GET`   |-       |-       |-       |-       | /radar/entities/ip |
|`GET`   |-       |-       |-       |-       | /radar/entities/locations |
|`GET`   |-       |-       |-       |-       | /radar/http/summary/bot_class |
|`GET`   |-       |-       |-       |-       | /radar/http/summary/device_type |
|`GET`   |-       |-       |-       |-       | /radar/http/summary/http_protocol |
|`GET`   |-       |-       |-       |-       | /radar/http/summary/http_version |
|`GET`   |-       |-       |-       |-       | /radar/http/summary/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/http/summary/os |
|`GET`   |-       |-       |-       |-       | /radar/http/summary/tls_version |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/bot_class |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/browser |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/browser_family |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/device_type |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/http_protocol |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/http_version |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/os |
|`GET`   |-       |-       |-       |-       | /radar/http/timeseries_groups/tls_version |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases/bot_class |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases/device_type |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases/http_protocol |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases/http_version |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases/os |
|`GET`   |-       |-       |-       |-       | /radar/http/top/ases/tls_version |
|`GET`   |-       |-       |-       |-       | /radar/http/top/browser_families |
|`GET`   |-       |-       |-       |-       | /radar/http/top/browsers |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations/bot_class |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations/device_type |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations/http_protocol |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations/http_version |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations/ip_version |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations/os |
|`GET`   |-       |-       |-       |-       | /radar/http/top/locations/tls_version |
|`GET`   |-       |-       |-       |-       | /radar/netflows/timeseries |
|`GET`   |-       |-       |-       |-       | /radar/netflows/top/ases |
|`GET`   |-       |-       |-       |-       | /radar/netflows/top/locations |
|`GET`   |-       |-       |-       |-       | /radar/quality/iqi/summary |
|`GET`   |-       |-       |-       |-       | /radar/quality/iqi/timeseries_groups |
|`GET`   |-       |-       |-       |-       | /radar/quality/speed/histogram |
|`GET`   |-       |-       |-       |-       | /radar/quality/speed/summary |
|`GET`   |-       |-       |-       |-       | /radar/quality/speed/top/ases |
|`GET`   |-       |-       |-       |-       | /radar/quality/speed/top/locations |
|`GET`   |-       |-       |-       |-       | /radar/ranking/domain |
|`GET`   |-       |-       |-       |-       | /radar/ranking/timeseries_groups |
|`GET`   |-       |-       |-       |-       | /radar/ranking/top |
|`GET`   |-       |-       |-       |-       | /radar/search/global |
|`GET`   |-       |-       |-       |-       | /radar/traffic_anomalies |
|`GET`   |-       |-       |-       |-       | /radar/traffic_anomalies/locations |
|`GET`   |-       |-       |-       |-       | /radar/verified_bots/top/bots |
|`GET`   |-       |-       |-       |-       | /radar/verified_bots/top/categories |
|`GET`   |-       |-       |`PATCH` |-       | /user |
|`GET`   |-       |-       |-       |-       | /user/audit_logs |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /user/firewall/access_rules/rules |
|`GET`   |-       |-       |`PATCH` |-       | /user/invites |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /user/load_balancers/monitors |
|-       |`POST`  |-       |-       |-       | /user/load_balancers/monitors/:id/preview |
|`GET`   |-       |-       |-       |-       | /user/load_balancers/monitors/:id/references |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /user/load_balancers/pools |
|`GET`   |-       |-       |-       |-       | /user/load_balancers/pools/:id/health |
|-       |`POST`  |-       |-       |-       | /user/load_balancers/pools/:id/preview |
|`GET`   |-       |-       |-       |-       | /user/load_balancers/pools/:id/references |
|`GET`   |-       |-       |-       |-       | /user/load_balancers/preview |
|`GET`   |-       |-       |-       |-       | /user/load_balancing_analytics/events |
|`GET`   |-       |-       |-       |`DELETE`| /user/organizations |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /user/subscriptions |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /user/tokens |
|-       |-       |`PUT`   |-       |-       | /user/tokens/:id/value |
|`GET`   |-       |-       |-       |-       | /user/tokens/permission_groups |
|`GET`   |-       |-       |-       |-       | /user/tokens/verify |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/access/apps |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/access/apps/:id/ca |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/access/apps/:id/policies |
|-       |`POST`  |-       |-       |-       | /zones/:id/access/apps/:id/revoke_tokens |
|`GET`   |-       |-       |-       |-       | /zones/:id/access/apps/:id/user_policy_checks |
|`GET`   |-       |-       |-       |-       | /zones/:id/access/apps/ca |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/access/certificates |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/access/certificates/settings |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/access/groups |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/access/identity_providers |
|`GET`   |`POST`  |`PUT`   |-       |-       | /zones/:id/access/organizations |
|-       |`POST`  |-       |-       |-       | /zones/:id/access/organizations/revoke_user |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/access/service_tokens |
|`GET`   |`POST`  |-       |-       |-       | /zones/:id/acm/total_tls |
|-       |-       |`PUT`   |-       |-       | /zones/:id/activation_check |
|`GET`   |-       |-       |-       |-       | /zones/:id/analytics/latency |
|`GET`   |-       |-       |-       |-       | /zones/:id/analytics/latency/colos |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/api_gateway/configuration |
|`GET`   |-       |-       |-       |-       | /zones/:id/api_gateway/discovery |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/api_gateway/discovery/operations |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/api_gateway/operations |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/api_gateway/operations/:id/schema_validation |
|-       |-       |-       |`PATCH` |-       | /zones/:id/api_gateway/operations/schema_validation |
|`GET`   |-       |-       |-       |-       | /zones/:id/api_gateway/schemas |
|`GET`   |-       |`PUT`   |`PATCH` |-       | /zones/:id/api_gateway/settings/schema_validation |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/api_gateway/user_schemas |
|`GET`   |-       |-       |-       |-       | /zones/:id/api_gateway/user_schemas/:id/operations |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/argo/smart_routing |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/argo/tiered_caching |
|`GET`   |-       |-       |-       |-       | /zones/:id/available_plans |
|`GET`   |-       |-       |-       |-       | /zones/:id/available_rate_plans |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/bot_management |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/cache/cache_reserve |
|`GET`   |`POST`  |-       |-       |-       | /zones/:id/cache/cache_reserve_clear |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/cache/origin_post_quantum_encryption |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/cache/regional_tiered_cache |
|`GET`   |-       |-       |`PATCH` |`DELETE`| /zones/:id/cache/tiered_cache_smart_topology_enable |
|`GET`   |-       |-       |`PATCH` |`DELETE`| /zones/:id/cache/variants |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/certificate_authorities/hostname_associations |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/client_certificates |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/custom_certificates |
|-       |-       |`PUT`   |-       |-       | /zones/:id/custom_certificates/prioritize |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/custom_hostnames |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /zones/:id/custom_hostnames/fallback_origin |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/custom_ns |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/custom_pages |
|`GET`   |-       |-       |-       |-       | /zones/:id/dcv_delegation/uuid |
|`GET`   |-       |-       |-       |-       | /zones/:id/dns_analytics/report |
|`GET`   |-       |-       |-       |-       | /zones/:id/dns_analytics/report/bytime |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/dns_records |
|`GET`   |-       |-       |-       |-       | /zones/:id/dns_records/export |
|-       |`POST`  |-       |-       |-       | /zones/:id/dns_records/import |
|-       |`POST`  |-       |-       |-       | /zones/:id/dns_records/scan |
|`GET`   |-       |-       |`PATCH` |`DELETE`| /zones/:id/dnssec |
|`GET`   |-       |-       |-       |-       | /zones/:id/email/routing |
|-       |`POST`  |-       |-       |-       | /zones/:id/email/routing/disable |
|`GET`   |-       |-       |-       |-       | /zones/:id/email/routing/dns |
|-       |`POST`  |-       |-       |-       | /zones/:id/email/routing/enable |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/email/routing/rules |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/email/routing/rules/catch_all |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/filters |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/firewall/access_rules/rules |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/firewall/lockdowns |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/firewall/rules |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/firewall/ua_rules |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/firewall/waf/overrides |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/firewall/waf/packages |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/firewall/waf/packages/:id/groups |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/firewall/waf/packages/:id/rules |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/healthchecks |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/healthchecks/preview |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/hold |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /zones/:id/hostnames/settings |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/keyless_certificates |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/load_balancers |
|`GET`   |-       |-       |-       |-       | /zones/:id/logpush/datasets/:id/fields |
|`GET`   |-       |-       |-       |-       | /zones/:id/logpush/datasets/:id/jobs |
|`GET`   |`POST`  |-       |-       |-       | /zones/:id/logpush/edge |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/logpush/jobs |
|-       |`POST`  |-       |-       |-       | /zones/:id/logpush/ownership |
|-       |`POST`  |-       |-       |-       | /zones/:id/logpush/ownership/validate |
|-       |`POST`  |-       |-       |-       | /zones/:id/logpush/validate/destination/exists |
|-       |`POST`  |-       |-       |-       | /zones/:id/logpush/validate/origin |
|`GET`   |`POST`  |-       |-       |-       | /zones/:id/logs/control/retention/flag |
|`GET`   |-       |-       |-       |-       | /zones/:id/logs/rayids |
|`GET`   |-       |-       |-       |-       | /zones/:id/logs/received |
|`GET`   |-       |-       |-       |-       | /zones/:id/logs/received/fields |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/managed_headers |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/origin_tls_client_auth |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/origin_tls_client_auth/hostnames |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/origin_tls_client_auth/hostnames/certificates |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/origin_tls_client_auth/settings |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/page_shield |
|`GET`   |-       |-       |-       |-       | /zones/:id/page_shield/connections |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/page_shield/policies |
|`GET`   |-       |-       |-       |-       | /zones/:id/page_shield/scripts |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/pagerules |
|`GET`   |-       |-       |-       |-       | /zones/:id/pagerules/settings |
|-       |`POST`  |-       |-       |-       | /zones/:id/purge_cache |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/rate_limits |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/rulesets |
|-       |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/rulesets/:id/rules |
|`GET`   |-       |-       |-       |`DELETE`| /zones/:id/rulesets/:id/versions |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/rulesets/phases/:id/entrypoint |
|`GET`   |-       |-       |-       |-       | /zones/:id/rulesets/phases/:id/entrypoint/versions |
|-       |`POST`  |-       |-       |-       | /zones/:id/secondary_dns/force_axfr |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/secondary_dns/incoming |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/secondary_dns/outgoing |
|-       |`POST`  |-       |-       |-       | /zones/:id/secondary_dns/outgoing/disable |
|-       |`POST`  |-       |-       |-       | /zones/:id/secondary_dns/outgoing/enable |
|-       |`POST`  |-       |-       |-       | /zones/:id/secondary_dns/outgoing/force_notify |
|`GET`   |-       |-       |-       |-       | /zones/:id/secondary_dns/outgoing/status |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/0rtt |
|`GET`   |-       |-       |-       |-       | /zones/:id/settings/advanced_ddos |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/always_online |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/always_use_https |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/automatic_https_rewrites |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/automatic_platform_optimization |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/brotli |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/browser_cache_ttl |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/browser_check |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/cache_level |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/challenge_ttl |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/ciphers |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/development_mode |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/early_hints |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/email_obfuscation |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/fonts |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/h2_prioritization |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/hotlink_protection |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/http2 |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/http3 |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/image_resizing |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/ip_geolocation |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/ipv6 |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/min_tls_version |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/minify |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/mirage |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/mobile_redirect |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/nel |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/opportunistic_encryption |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/opportunistic_onion |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/orange_to_orange |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/origin_error_page_pass_thru |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/origin_max_http_version |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/polish |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/prefetch_preload |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/proxy_read_timeout |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/pseudo_ipv4 |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/response_buffering |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/rocket_loader |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/security_header |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/security_level |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/server_side_exclude |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/sort_query_string_for_cache |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/ssl |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/ssl_recommender |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/tls_1_3 |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/tls_client_auth |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/true_client_ip_header |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/waf |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/webp |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/settings/websockets |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/settings/zaraz/v2/config |
|`GET`   |-       |-       |-       |-       | /zones/:id/settings/zaraz/v2/default |
|`GET`   |-       |-       |-       |-       | /zones/:id/settings/zaraz/v2/export |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/settings/zaraz/v2/history |
|`GET`   |-       |-       |-       |-       | /zones/:id/settings/zaraz/v2/history/configs |
|-       |`POST`  |-       |-       |-       | /zones/:id/settings/zaraz/v2/publish |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/settings/zaraz/v2/workflow |
|`GET`   |-       |`PUT`   |-       |`DELETE`| /zones/:id/snippets |
|`GET`   |-       |-       |-       |-       | /zones/:id/snippets/:id/content |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/snippets/snippet_rules |
|`GET`   |-       |-       |-       |-       | /zones/:id/spectrum/analytics/aggregate/current |
|`GET`   |-       |-       |-       |-       | /zones/:id/spectrum/analytics/events/bytime |
|`GET`   |-       |-       |-       |-       | /zones/:id/spectrum/analytics/events/summary |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/spectrum/apps |
|`GET`   |-       |-       |-       |-       | /zones/:id/speed_api/availabilities |
|`GET`   |-       |-       |-       |-       | /zones/:id/speed_api/pages |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/speed_api/pages/:id/tests |
|`GET`   |-       |-       |-       |-       | /zones/:id/speed_api/pages/:id/trend |
|`GET`   |`POST`  |-       |-       |`DELETE`| /zones/:id/speed_api/schedule |
|-       |`POST`  |-       |-       |-       | /zones/:id/ssl/analyze |
|`GET`   |-       |-       |`PATCH` |`DELETE`| /zones/:id/ssl/certificate_packs |
|-       |`POST`  |-       |-       |-       | /zones/:id/ssl/certificate_packs/order |
|`GET`   |-       |-       |-       |-       | /zones/:id/ssl/certificate_packs/quota |
|`GET`   |-       |-       |-       |-       | /zones/:id/ssl/recommendation |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/ssl/universal/settings |
|`GET`   |-       |-       |`PATCH` |-       | /zones/:id/ssl/verification |
|`GET`   |`POST`  |`PUT`   |-       |-       | /zones/:id/subscription |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/url_normalization |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/waiting_rooms |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/waiting_rooms/:id/events |
|`GET`   |-       |-       |-       |-       | /zones/:id/waiting_rooms/:id/events/:id/details |
|`GET`   |`POST`  |`PUT`   |`PATCH` |`DELETE`| /zones/:id/waiting_rooms/:id/rules |
|`GET`   |-       |-       |-       |-       | /zones/:id/waiting_rooms/:id/status |
|-       |`POST`  |-       |-       |-       | /zones/:id/waiting_rooms/preview |
|`GET`   |-       |`PUT`   |`PATCH` |-       | /zones/:id/waiting_rooms/settings |
|`GET`   |`POST`  |-       |`PATCH` |`DELETE`| /zones/:id/web3/hostnames |
|`GET`   |-       |`PUT`   |-       |-       | /zones/:id/web3/hostnames/:id/ipfs_universal_path/content_list |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/web3/hostnames/:id/ipfs_universal_path/content_list/entries |
|`GET`   |`POST`  |`PUT`   |-       |`DELETE`| /zones/:id/workers/routes |

python-cloudflare-2.20.0/cli4/000077500000000000000000000000001461736615400161025ustar00rootroot00000000000000python-cloudflare-2.20.0/cli4/__init__.py000066400000000000000000000000001461736615400202010ustar00rootroot00000000000000python-cloudflare-2.20.0/cli4/__main__.py000077500000000000000000000004061461736615400201770ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API via command line"""
import sys

from .cli4 import cli4

def main(args=None):
    """Cloudflare API via command line"""
    if args is None:
        args = sys.argv[1:]
    cli4(args)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/cli4/cli4.1000066400000000000000000000110021461736615400170110ustar00rootroot00000000000000.TH CLI4 1

.SH NAME
cli4 \- Command line access to Cloudflare v4 API

.SH SYNOPSIS
.B cli4
[\fB\-V\fR|\fB\-\-version]
[\fB\-h\fR|\fB\-\-help]
[\fB\-v\fR|\fB\-\-verbose]
[\fB\-e\fR|\fB\-\-examples]
[\fB\-q\fR|\fB\-\-quiet]
[\fB\-j\fR|\fB\-\-json]
[\fB\-y\fR|\fB\-\-yaml]
[\fB\-n\fR|\fB\-\-ndjson]
[\fB\-r\fR|\fB\-\-raw]
[\fB\ir\fR|\fB\-\-image]
[\fB\-d\fR|\fB\-\-dump]
[\fB\-b\fR|\fB\-\-binary]
[\fB\-A openapi-url\fR|\fB\-\-openapi openapi-url]
[\fB\-p profile-name\fR|\fB\-\-profile profile-name]
[\fBitem\fR=\fIvalue\fR ...]
[=\fIvalue\fR ...]
[\fBitem\fR=@\fIfilename\fR ...]
[=@\fIfilename\fR ...]
[\fB\-G\fR|\fB\-\-get]
[\fB\-P\fR|\fB\-\-patch]
[\fB\-O\fR|\fB\-\-post]
[\fB\-U\fR|\fB\-\-put]
[\fB\-D\fR|\fB\-\-delete]
.IR /command ...

.SH DESCRIPTION
.B cli4
provides command line access to Cloudflare v4 API

.SH OPTIONS
.TP
.IP "[\-V, \-\-version]"
Display program version number and exit.
.IP "[\-h, \-\-help]"
This information (in a terse form).
.IP "[\-v, \-\-verbose]"
Provide some protcol debugging information.
.IP "[\-e, \-\-example]"
Show the path to the examples folder/directory.
.IP "[\-q, \-\-quiet]"
Don't output any JSON/YAML responses.
.IP "[\-j, \-\-json]"
Output response data in JSON format (the default).
.IP "[\-y, \-\-yaml]"
Output response data in YAML format (if yaml package installed).
.IP "[\-n, \-\-ndjson]"
Output response data in NDJSON format (if jsonlines package installed).
.IP "[\-r, \-\-raw]"
Output JSON results in raw mode without splitting out the errors and results.
.IP "[\-i, \-\-image]"
Output results as binary (i.e. it's an image).
This isn't normally needed as the API returns a Content-Type to indicate an image is being returned.
This option can be used to allow the API output to be output without any processing (i.e. JSON formatted).
.IP "[\-d, \-\-dump]"
Output a list of all API calls included in the code.
.IP "[\-b, \-\-binary]"
Open files in binary mode.
.IP "[-A \fIopenapi-url\fR, \-\-openapi \fIopenapi-url\fR]"
Decode Cloudflare's OpenAPI spec and output all the known commands.
.IP "[-p \fIprofile-name\fR, \-\-profile \fIprofile-name\fR]"
Select a \fIprofile-name\fR from the configuration file (hence select custom \fIemail\fR/\fItoken\fR values).
.IP "\-\-get"
Send HTTP request as a \fBGET\fR (the default).
.IP "\-\-patch"
Send HTTP request as a \fBPATCH\fR.
.IP "\-\-post"
Send HTTP request as a \fBPOST\fR.
.IP "\-\-put"
Send HTTP request as a \fBPUT\fR.
.IP "\-\-delete"
Send HTTP request as a \fBDELETE\fR.
.IP "item=\fIvalue\fR"
Set a paramater or data value to send with a \fBGET\fR, \fBPATCH\fR, \fBPOST\fR, \fBPUT\fR or \fBDELETE\fR command. The value is sent as a string.
.IP item:=\fIvalue\fR
Set a paramater or data value to send with a \fBGET\fR, \fBPATCH\fR, \fBPOST\fR, \fBPUT\fR or \fBDELETE\fR command. The value is sent as an interger.
.IP item=@\fIfilename\fR
Set a paramater or data value to send with a \fBPOST\fR or \fBPUT\fR command. The value is based on the content of the file.
.IP "\fI/command ...\fR"
The API command(s) to execute.

.SH COMMAND(S)
The command string uses slash (\fB/\fR) to seperate the verbs in the same way that the Cloudflare v4 API documentation does.
Any verb starting with colon (\fB:\fR) is either converted to zone_id, user_id, organtization_id, or otherwise.
Any verb starting with coloe (\fB:\fR) and followed by 32 hex chracters is passed thru raw.
Any verb starting with two colons (\fB::\fR) is passed thru raw.

.SH RESULTS
The output is either JSON or YAML formatted.

.SH EXAMPLES
.B cli4 /zones
List infomation for all zones.

.B cli4 /zones/:example.com
List specific zone info.

.B cli4 /zones/:example.com/settings
List settings for a specific zone.

.B cli4 --delete purge_everything=true /zones/:example.com/purge_cache
Purge cache for a specific zone.

.B cli4 --delete files='[http://example.com/css/styles.css]' /zones/:example.com/purge_cache
Purge cache for a specific zone.

.B cli4 --delete files='[http://example.com/css/styles.css,http://example.com/js/script.js] /zones/:example.com/purge_cache
Purge cache for a specific zone.

.B cli4 --delete tags='[tag1,tag2,tag3]' /zones/:example.com/purge_cache
Purge cache for a specific zone.

.B cli4 /zones/:example.com/available_plans
List available plans for a zone.

.B cli4 --patch status=active /zones/:example.com/dnssec
Make DNSSEC active for specfic zone.

.B cli4 /zones/:example.com/dnssec
List DNSSEC infomation and status for a specific zone.

.SH SEE ALSO
The Cloudflare API can be found https://api.cloudflare.com/. Each API call is provided via a similarly named function within the Cloudflare class.
python-cloudflare-2.20.0/cli4/cli4.py000066400000000000000000000532111461736615400173110ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API via command line"""

import sys
import re
import getopt
import json

import CloudFlare

from .dump import dump_commands, dump_commands_from_web
from . import converters
from . import examples

my_yaml = None
my_jsonlines = None

class CLI4InternalError(Exception):
    """ errors in cli4 """

def load_and_check_yaml():
    """ load_and_check_yaml() """
    # only called if user uses --yaml flag
    from . import myyaml
    global my_yaml
    try:
        my_yaml = myyaml.myyaml()
    except ImportError:
        sys.exit('cli4: install yaml support via: pip install pyyaml')

def load_and_check_jsonlines():
    """ load_and_check_yaml() """
    # only called if user uses --ndjson flag
    from . import myjsonlines
    global my_jsonlines
    try:
        my_jsonlines = myjsonlines.myjsonlines()
    except ImportError:
        sys.exit('cli4: install jsonlines support via: pip install jsonlines')

def strip_multiline(s):
    """ remove leading/trailing tabs/spaces on each line"""
    # This hack is needed in order to use yaml.safe_load() on JSON text - tabs are not allowed
    return '\n'.join([line.strip() for line in s.splitlines()])

def process_params_content_files(method, binary_file, args):
    """ process_params_content_files() """

    digits_only = re.compile('^-?[0-9]+$')
    floats_only = re.compile('^-?[0-9.]+$')

    params = None
    content = None
    files = None
    # next grab the params. These are in the form of tag=value or =value or @filename
    while len(args) > 0 and ('=' in args[0] or args[0][0] == '@'):
        arg = args.pop(0)
        if arg[0] == '@':
            # a file to be uploaded - used in workers/script etc - only via PUT or POST
            filename = arg[1:]
            if method not in ['PUT','POST']:
                sys.exit('cli4: %s - raw file upload only with PUT or POST' % (filename))
            try:
                if filename == '-':
                    if binary_file:
                        content = sys.stdin.buffer.read()
                    else:
                        content = sys.stdin.read()
                else:
                    if binary_file:
                        with open(filename, 'rb') as f:
                            content = f.read()
                    else:
                        with open(filename, 'r', encoding="utf-8") as f:
                            content = f.read()
            except IOError:
                sys.exit('cli4: %s - file open failure' % (filename))
            continue
        tag_string, value_string = arg.split('=', 1)
        if value_string.lower() == 'true':
            value = True
        elif value_string.lower() == 'false':
            value = False
        elif value_string == '' or value_string.lower() == 'none':
            value = None
        elif value_string[0] == '=' and value_string[1:] == '':
            sys.exit('cli4: %s== - no number value passed' % (tag_string))
        elif value_string[0] == '=' and digits_only.match(value_string[1:]):
            value = int(value_string[1:])
        elif value_string[0] == '=' and floats_only.match(value_string[1:]):
            value = float(value_string[1:])
        elif value_string[0] == '=':
            sys.exit('cli4: %s== - invalid number value passed' % (tag_string))
        elif value_string[0] in '[{' and value_string[-1] in '}]':
            # a json structure - used in pagerules
            try:
                # value = json.loads(value) - changed to yaml code to remove unicode string issues
                load_and_check_yaml()
                # cleanup string before parsing so that yaml.safe.load does not complain about whitespace
                # >>> found character '\t' that cannot start any token <<<
                value_string = strip_multiline(value_string)
                try:
                    value = my_yaml.safe_load(value_string)
                except my_yaml.parser.ParserError:
                    raise ValueError from None
            except ValueError:
                sys.exit('cli4: %s="%s" - can\'t parse json value' % (tag_string, value_string))
        elif value_string[0] == '@':
            # a file to be uploaded - used in dns_records/import etc - only via PUT or POST
            filename = value_string[1:]
            if method not in ['PUT', 'POST']:
                sys.exit('cli4: %s=%s - file upload only with PUT or POST' % (tag_string, filename))
            if files is None:
                files = {}
            if tag_string in files:
                sys.exit('cli4: %s=%s - duplicate name' % (tag_string, filename))
            try:
                if filename == '-':
                    files[tag_string] = sys.stdin
                else:
                    files[tag_string] = open(filename, 'rb')
            except IOError:
                sys.exit('cli4: %s=%s - file open failure' % (tag_string, filename))
            # no need for param code below
            continue
        elif (value_string[0] == '"' and value_string[-1] == '"') or (value_string[0] == '\'' and value_string[-1] == '\''):
            # remove quotes
            value = value_string[1:-1]
        else:
            value = value_string

        if tag_string == '':
            # There's no tag; it's just an unnamed list
            if params is None:
                params = value
            else:
                sys.exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' %
                         (tag_string, value_string))
        else:
            if params is None:
                params = {}
            tag = tag_string
            try:
                params[tag] = value
            except TypeError:
                sys.exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' %
                         (tag_string, value_string))

    if content and params:
        sys.exit('cli4: content and params not allowed together')

    if params and files:
        for k,v in params.items():
            files[k] = (None, v)
        params = None
        # sys.exit('cli4: params and files not allowed together')

    if method != 'GET':
        if params:
            content = params
            params = None

    return (params, content, files)

def run_command(cf, method, command, params=None, content=None, files=None):
    """run the command line"""
    # remove leading and trailing /'s
    if command[0] == '/':
        command = command[1:]
    if command[-1] == '/':
        command = command[:-1]

    # break down command into it's seperate pieces
    # these are then checked against the Cloudflare class
    # to confirm there is a method that matches
    parts = command.split('/')

    cmd = []
    identifier1 = None
    identifier2 = None
    identifier3 = None

    hex_only = re.compile('^[0-9a-fA-F]+$')
    waf_rules = re.compile('^[0-9]+[A-Z]*$')
    uuid_value = re.compile('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') # 8-4-4-4-12

    m = cf
    for element in parts:
        if element[0] == ':':
            element = element[1:]
            if identifier1 is None:
                if len(element) in [32, 40, 48] and hex_only.match(element):
                    # raw identifier - lets just use it as-is
                    identifier1 = element
                elif len(element) == 36 and uuid_value.match(element):
                    # uuid identifier - lets just use it as-is
                    identifier1 = element
                elif element[0] == ':':
                    # raw string - used for workers script_name - use ::script_name
                    identifier1 = element[1:]
                else:
                    try:
                        if cmd[0] == 'certificates':
                            # identifier1 = convert_certificates_to_identifier(cf, element)
                            identifier1 = converters.convert_zones_to_identifier(cf, element)
                        elif cmd[0] == 'zones':
                            identifier1 = converters.convert_zones_to_identifier(cf, element)
                        elif cmd[0] == 'accounts':
                            identifier1 = converters.convert_accounts_to_identifier(cf, element)
                        elif cmd[0] == 'organizations':
                            identifier1 = converters.convert_organizations_to_identifier(cf, element)
                        elif (cmd[0] == 'user') and (cmd[1] == 'organizations'):
                            identifier1 = converters.convert_organizations_to_identifier(cf, element)
                        elif (cmd[0] == 'user') and (cmd[1] == 'invites'):
                            identifier1 = converters.convert_invites_to_identifier(cf, element)
                        elif (cmd[0] == 'user') and (cmd[1] == 'virtual_dns'):
                            identifier1 = converters.convert_virtual_dns_to_identifier(cf, element)
                        elif (cmd[0] == 'user') and (cmd[1] == 'load_balancers') and (cmd[2] == 'pools'):
                            identifier1 = converters.convert_load_balancers_pool_to_identifier(cf, element)
                        else:
                            raise CLI4InternalError("/%s/%s :NOT CODED YET" % ('/'.join(cmd), element))
                    except CLI4InternalError as e:
                        sys.stderr.write('cli4: /%s - %s\n' % (command, e))
                        raise e
                cmd.append(':' + identifier1)
            elif identifier2 is None:
                if len(element) in [32, 40, 48] and hex_only.match(element):
                    # raw identifier - lets just use it as-is
                    identifier2 = element
                elif len(element) == 36 and uuid_value.match(element):
                    # uuid identifier - lets just use it as-is
                    identifier2 = element
                elif element[0] == ':':
                    # raw string - used for workers script_names
                    identifier2 = element[1:]
                else:
                    try:
                        if (cmd[0] and cmd[0] == 'zones') and (cmd[2] and cmd[2] == 'dns_records'):
                            identifier2 = converters.convert_dns_record_to_identifier(cf, identifier1, element)
                        elif (cmd[0] and cmd[0] == 'zones') and (cmd[2] and cmd[2] == 'custom_hostnames'):
                            identifier2 = converters.convert_custom_hostnames_to_identifier(cf, identifier1, element)
                        else:
                            raise CLI4InternalError("/%s/:%s :NOT CODED YET" % ('/'.join(cmd), element))
                    except CLI4InternalError as e:
                        sys.stderr.write('cli4: /%s - %s\n' % (command, e))
                        raise e
                # identifier2 may be an array - this needs to be dealt with later
                if isinstance(identifier2, list):
                    cmd.append(':' + '[' + ','.join(identifier2) + ']')
                else:
                    cmd.append(':' + identifier2)
                    identifier2 = [identifier2]
            else:
                if len(element) in [32, 40, 48] and hex_only.match(element):
                    # raw identifier - lets just use it as-is
                    identifier3 = element
                elif len(element) == 36 and uuid_value.match(element):
                    # uuid identifier - lets just use it as-is
                    identifier3 = element
                elif waf_rules.match(element):
                    identifier3 = element
                elif element[0] == ':':
                    # raw string - used for workers script_names
                    identifier3 = element[1:]
                else:
                    #  /accounts/:id/storage/kv/namespaces/:id/values/:key_name - it's a strange one!
                    if len(cmd) >= 6 and cmd[0] == 'accounts' and cmd[2] == 'storage' and cmd[3] == 'kv' and cmd[4] == 'namespaces' and cmd[6] == 'values':
                        identifier3 = element
                    else:
                        sys.stderr.write('/%s/:%s :NOT CODED YET\n' % ('/'.join(cmd), element))
                        raise e
        else:
            try:
                m = getattr(m, CloudFlare.CloudFlare.sanitize_verb(element))
                cmd.append(element)
            except AttributeError as e:
                # the verb/element was not found
                sys.stderr.write('cli4: /%s - not found\n' % (command))
                raise e

    results = []
    if identifier2 is None:
        identifier2 = [None]
    for i2 in identifier2:
        try:
            if method == 'GET':
                # no content with a GET call
                r = m.get(identifier1=identifier1,
                          identifier2=i2,
                          identifier3=identifier3,
                          params=params)
            elif method == 'PATCH':
                r = m.patch(identifier1=identifier1,
                            identifier2=i2,
                            identifier3=identifier3,
                            data=content)
            elif method == 'POST':
                r = m.post(identifier1=identifier1,
                           identifier2=i2,
                           identifier3=identifier3,
                           data=content, files=files)
            elif method == 'PUT':
                r = m.put(identifier1=identifier1,
                          identifier2=i2,
                          identifier3=identifier3,
                          data=content, files=files)
            elif method == 'DELETE':
                r = m.delete(identifier1=identifier1,
                             identifier2=i2,
                             identifier3=identifier3,
                             data=content)
            else:
                pass
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            if len(e) > 0:
                # more than one error returned by the API
                for x in e:
                    sys.stderr.write('cli4: /%s - %d %s\n' % (command, int(x), str(x)))
            sys.stderr.write('cli4: /%s - %d %s\n' % (command, int(e), str(e)))
            raise e
        except CloudFlare.exceptions.CloudFlareInternalError as e:
            sys.stderr.write('cli4: InternalError: /%s - %d %s\n' % (command, int(e), str(e)))
            raise e
        except Exception as e:
            sys.stderr.write('cli4: /%s - %s - api error\n' % (command, str(e)))
            raise e

        results.append(r)
    return results

def write_results(results, output):
    """dump the results"""

    if output is None:
        return

    if len(results) == 1:
        results = results[0]

    if isinstance(results, (str, bytes, bytearray)):
        # if the results are a simple string, then it should be dumped directly
        # this is only used for /zones/:id/dns_records/export, workers, and other calls
        # or
        # output is image or audio or video or something like that so we dump directly
        pass
    else:
        # anything more complex (dict, list, etc) should be dumped as JSON/YAML
        if output == 'json':
            try:
                results = json.dumps(results,
                                     indent=4,
                                     sort_keys=True,
                                     ensure_ascii=False,
                                     encoding='utf8')
            except TypeError:
                results = json.dumps(results,
                                     indent=4,
                                     sort_keys=True,
                                     ensure_ascii=False)
        elif output == 'yaml':
            results = my_yaml.safe_dump(results)
        elif output == 'ndjson':
            # NDJSON support seems like a hack. There has to be a better way
            try:
                writer = my_jsonlines.Writer(sys.stdout)
                writer.write_all(results)
                writer.close()
            except (BrokenPipeError, IOError):
                pass
            return
        else:
            # None of the above, so pass thru results except something in byte form
            if not isinstance(results, (bytes, bytearray)):
                results = str(results)

    if results:
        try:
            if isinstance(results, (bytes, bytearray)):
                sys.stdout.buffer.write(results)
            else:
                sys.stdout.write(results)
                if not results.endswith('\n'):
                    sys.stdout.write('\n')
        except (BrokenPipeError, IOError):
            pass

def do_it(args):
    """Cloudflare API via command line"""

    verbose = False
    output = 'json'
    example = False
    raw = False
    do_dump = False
    do_openapi = None
    openapi_url = None
    binary_file = False
    profile = None
    http_headers = None
    warnings = True
    method = 'GET'

    usage = ('usage: cli4 '
             + '[-V|--version] [-h|--help] [-v|--verbose] '
             + '[-e|--examples] '
             + '[-q|--quiet] '
             + '[-j|--json] [-y|--yaml] [-n|--ndjson] [-i|--image] '
             + '[-r|--raw] '
             + '[-d|--dump] '
             + '[-A|--openapi url] '
             + '[-b|--binary] '
             + '[-p|--profile profile-name] '
             + '[-h|--header additional-header] '
             + '[-w|--warnings [True|False]] '
             + '[--get|--patch|--post|--put|--delete] '
             + '[item=value|item=@filename|@filename ...] '
             + '/command ...')

    try:
        opts, args = getopt.getopt(args,
                                   'VhveqjynirdA:bp:h:w:GPOUD',
                                   [
                                       'version', 'help', 'verbose',
                                       'examples',
                                       'quiet',
                                       'json', 'yaml', 'ndjson', 'image',
                                       'raw',
                                       'dump',
                                       'openapi=',
                                       'binary',
                                       'profile=',
                                       'header=',
                                       'warnings=',
                                       'get', 'patch', 'post', 'put', 'delete'
                                   ])
    except getopt.GetoptError:
        sys.exit(usage)
    for opt, arg in opts:
        if opt in ('-V', '--version'):
            sys.exit('Cloudflare library version: %s' % (CloudFlare.__version__))
        if opt in ('-h', '--help'):
            sys.exit(usage)
        elif opt in ('-v', '--verbose'):
            verbose = True
        elif opt in ('-q', '--quiet'):
            output = None
        elif opt in ('-e', '--examples'):
            example = True
        elif opt in ('-j', '--json'):
            output = 'json'
        elif opt in ('-y', '--yaml'):
            load_and_check_yaml()
            output = 'yaml'
        elif opt in ('-n', '--ndjson'):
            load_and_check_jsonlines()
            output = 'ndjson'
        elif opt in ('-i', '--image'):
            output = 'image'
        elif opt in ('-r', '--raw'):
            raw = True
        elif opt in ('-p', '--profile'):
            profile = arg
        elif opt in ('-h', '--header'):
            if http_headers is None:
                http_headers = []
            http_headers.append(arg)
        elif opt in ('-w', '--warnings'):
            if arg is None or arg == '':
                warnings = None
            elif arg.lower() in ('yes', 'true', '1'):
                warnings = True
            elif arg.lower() in ('no', 'false', '0'):
                warnings = False
            else:
                sys.exit('cli4: --warnings takes boolean True/False argument')
        elif opt in ('-d', '--dump'):
            do_dump = True
        elif opt in ('-A', '--openapi'):
            do_openapi = True
            openapi_url = arg if arg != '' else None
        elif opt in ('-b', '--binary'):
            binary_file = True
        elif opt in ('-G', '--get'):
            method = 'GET'
        elif opt in ('-P', '--patch'):
            method = 'PATCH'
        elif opt in ('-O', '--post'):
            method = 'POST'
        elif opt in ('-U', '--put'):
            method = 'PUT'
        elif opt in ('-D', '--delete'):
            method = 'DELETE'

    if example:
        try:
            examples.display()
        except ModuleNotFoundError as e:
            sys.exit(e)
        sys.exit(0)

    try:
        cf = CloudFlare.CloudFlare(debug=verbose, raw=raw, profile=profile, http_headers=http_headers, warnings=warnings)
    except Exception as e:
        sys.exit(e)

    if do_dump:
        a = dump_commands(cf)
        # success - just dump results and exit
        sys.stdout.write(a)
        sys.exit(0)

    if do_openapi:
        try:
            a = dump_commands_from_web(cf, openapi_url)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.exit('cli4: %s - Failed' % (e))
        # success - just dump results and exit
        sys.stdout.write(a)
        sys.exit(0)

    # next grab the params. These are in the form of tag=value or =value or @filename
    (params, content, files) = process_params_content_files(method, binary_file, args)

    # what's left is the command itself
    if len(args) < 1:
        sys.exit(usage)
    commands = args

    exit_with_error = False
    for command in commands:
        try:
            results = run_command(cf, method, command, params, content, files)
            write_results(results, output)
        except KeyboardInterrupt as e:
            sys.exit('cli4: %s - Interrupted\n' % (command))
        except Exception as e:
            exit_with_error = True

    if exit_with_error:
        sys.exit(1)

def cli4(args):
    """Cloudflare API via command line"""

    do_it(args)
    sys.exit(0)
python-cloudflare-2.20.0/cli4/converters.py000066400000000000000000000133051461736615400206500ustar00rootroot00000000000000"""Cloudflare API via command line"""
import CloudFlare

class ConverterError(Exception):
    """ errors for converters"""

def convert_zones_to_identifier(cf, zone_name):
    """zone names to numbers"""
    params = {'name':zone_name, 'per_page':1}
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (zone_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (zone_name, e)) from e

    if len(zones) == 1:
        return zones[0]['id']

    raise ConverterError('%s: not found' % (zone_name)) from None

def convert_accounts_to_identifier(cf, account_name):
    """account names to numbers"""
    params = {'name':account_name, 'per_page':1}
    try:
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (account_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (account_name, e)) from e

    if len(accounts) == 1:
        return accounts[0]['id']

    raise ConverterError('%s: not found' % (account_name)) from None

def convert_dns_record_to_identifier(cf, zone_id, dns_name):
    """dns record names to numbers"""
    # this can return an array of results as there can be more than one DNS entry for a name.
    params = {'name':dns_name}
    try:
        dns_records = cf.zones.dns_records.get(zone_id, params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (dns_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (dns_name, e)) from e

    r = []
    for dns_record in dns_records:
        if dns_name == dns_record['name']:
            r.append(dns_record['id'])
    if len(r) > 0:
        return r

    raise ConverterError('%s: not found' % (dns_name)) from None

def convert_certificates_to_identifier(cf, certificate_name):
    """certificate names to numbers"""
    try:
        certificates = cf.certificates.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (certificate_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (certificate_name, e)) from e

    for certificate in certificates:
        if certificate_name in certificate['hostnames']:
            return certificate['id']

    raise ConverterError('%s: not found' % (certificate_name)) from None

def convert_organizations_to_identifier(cf, organization_name):
    """organizations names to numbers"""
    try:
        organizations = cf.user.organizations.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (organization_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (organization_name, e)) from e

    for organization in organizations:
        if organization_name == organization['name']:
            return organization['id']

    raise ConverterError('%s not found' % (organization_name)) from None

def convert_invites_to_identifier(cf, invite_name):
    """invite names to numbers"""
    try:
        invites = cf.user.invites.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (invite_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (invite_name, e)) from e

    for invite in invites:
        if invite_name == invite['organization_name']:
            return invite['id']

    raise ConverterError('%s: not found' % (invite_name)) from None

def convert_virtual_dns_to_identifier(cf, virtual_dns_name):
    """virtual dns names to numbers"""
    try:
        virtual_dnss = cf.user.virtual_dns.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (virtual_dns_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (virtual_dns_name, e)) from e

    for virtual_dns in virtual_dnss:
        if virtual_dns_name == virtual_dns['name']:
            return virtual_dns['id']

    raise ConverterError('%s: not found' % (virtual_dns_name)) from None

def convert_load_balancers_pool_to_identifier(cf, pool_name):
    """load balancer pool names to numbers"""
    try:
        pools = cf.user.load_balancers.pools.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (pool_name, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (pool_name, e)) from e

    for p in pools:
        if pool_name == p['description']:
            return p['id']

    raise ConverterError('%s: not found' % (pool_name)) from None

def convert_custom_hostnames_to_identifier(cf, zone_id, custom_hostname):
    """custom_hostnames to numbers"""
    # this can return an array of results
    params = {'name':custom_hostname}
    try:
        custom_hostnames_records = cf.zones.custom_hostnames.get(zone_id, params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        raise ConverterError(int(e), '%s - %d %s' % (custom_hostname, int(e), e)) from None
    except Exception as e:
        raise ConverterError(0, '%s - %s' % (custom_hostname, e)) from e

    r = []
    for custom_hostnames_record in custom_hostnames_records:
        if custom_hostname == custom_hostnames_record['hostname']:
            r.append(custom_hostnames_record['id'])
    if len(r) > 0:
        return r

    raise ConverterError('%s: not found' % (custom_hostname)) from None
python-cloudflare-2.20.0/cli4/dump.py000066400000000000000000000016151461736615400174240ustar00rootroot00000000000000"""Cloudflare API via command line"""

def dump_commands(cf):
    """dump a tree of all the known API commands"""
    w = cf.api_list()
    return '\n'.join(w) + '\n'

def dump_commands_from_web(cf, url):
    """dump a tree of all the known API commands - from web"""
    w = cf.api_from_openapi(url)

    a = []
    for r in w:
        if r['deprecated']:
            if r['deprecated_already']:
                a.append('%-6s %s ; deprecated %s - expired!' % (r['action'], r['cmd'], r['deprecated_date']))
            else:
                a.append('%-6s %s ; deprecated %s' % (r['action'], r['cmd'], r['deprecated_date']))
        else:
            if 'content_type' in r and r['content_type']:
                a.append('%-6s %s ; Content-Type: %s' % (r['action'], r['cmd'], r['content_type']))
            else:
                a.append('%-6s %s' % (r['action'], r['cmd']))
    return '\n'.join(a) + '\n'
python-cloudflare-2.20.0/cli4/examples.py000066400000000000000000000022701461736615400202730ustar00rootroot00000000000000"""Cloudflare API via command line"""

import os
import sys

if sys.version_info < (3, 9):
    # importlib.resources either doesn't exist or lacks the files()
    # function, so use the PyPI version:
    try:
        import importlib_resources
    except ModuleNotFoundError:
        importlib_resources = None
else:
    # importlib.resources has files(), so use that:
    import importlib.resources as importlib_resources

EXAMPLES_PACKAGE_NAME = 'examples'

def display():
    """ display() """

    if not importlib_resources:
        raise ModuleNotFoundError('Module "importlib_resources" missing - please "pip install importlib_resources" as your Python version is lower than 3.9')

    try:
        pkg = importlib_resources.files(EXAMPLES_PACKAGE_NAME)
    except ModuleNotFoundError as e:
        raise e

    for ext,name in {'c': 'C', 'h': 'C', 'cc': 'C++', 'py':'Python', 'sh':'Bash', 'awk':'AWK'}.items():
        files = sorted(pkg.glob('**/*.' + ext))
        if len(files) == 0:
            continue
        print('%s .%s files:' % (name, ext))
        for file in files:
            if '__init__.py' in os.fspath(file):
                continue
            print('\t%s' % (os.fspath(file)))
python-cloudflare-2.20.0/cli4/myjsonlines.py000066400000000000000000000007431461736615400210320ustar00rootroot00000000000000""" helper functions for jsonlines usage """

class myjsonlines():
    """ myjsonlines """

    _jsonlines = None

    def __init__(self):
        """ __init__ """
        if not myjsonlines._jsonlines:
            try:
                import jsonlines
                myjsonlines._jsonlines = jsonlines
            except ImportError as e:
                raise ImportError from e

    def Writer(self, fd):
        """ Writer() """
        return myjsonlines._jsonlines.Writer(fd)
python-cloudflare-2.20.0/cli4/myyaml.py000066400000000000000000000011571461736615400177700ustar00rootroot00000000000000""" helper functions for yaml usage """

class myyaml():
    """ myyaml """

    _yaml = None
    parser = None

    def __init__(self):
        """ __init__ """
        if not myyaml._yaml:
            try:
                import yaml
                myyaml._yaml = yaml
                myyaml.parser = yaml.parser
            except ImportError as e:
                raise ImportError from e

    def safe_load(self,value_string):
        """ safe_load() """
        return myyaml._yaml.safe_load(value_string)

    def safe_dump(self, results):
        """ safe_dump() """
        return myyaml._yaml.safe_dump(results)
python-cloudflare-2.20.0/docs/000077500000000000000000000000001461736615400161775ustar00rootroot00000000000000python-cloudflare-2.20.0/docs/Makefile000066400000000000000000000011721461736615400176400ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS    ?=
SPHINXBUILD   ?= sphinx-build
SOURCEDIR     = .
BUILDDIR      = _build

# Put it first so that "make" without argument is like "make help".
help:
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
python-cloudflare-2.20.0/docs/conf.py000066400000000000000000000024761461736615400175070ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'python-cloudflare'
copyright = 'Copyright (c) 2016 thru 2024, Cloudflare. All rights reserved.'
author = 'Martin J Levy'

version = '2.20.0'
release = '2.20.0'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.viewcode',
    'sphinx.ext.todo',
    'sphinx.ext.autodoc',
]

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

language = 'en'

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = 'alabaster'
html_static_path = ['_static']

# -- Options for todo extension ----------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration

todo_include_todos = True
python-cloudflare-2.20.0/docs/index.rst000066400000000000000000000027471461736615400200520ustar00rootroot00000000000000.. python-cloudflare documentation master file, created by
   sphinx-quickstart on Sun Mar  3 23:59:20 2024.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

python-cloudflare - A Python-based access to Cloudflare's API's
===============================================================

Release v\ |version|.

.. image:: https://static.pepy.tech/badge/cloudflare/month
    :target: https://pepy.tech/project/cloudflare
    :alt: Requests Downloads Per Month Badge
    
.. image:: https://img.shields.io/pypi/l/cloudflare.svg
    :target: https://pypi.org/project/cloudflare/
    :alt: License Badge

.. image:: https://img.shields.io/pypi/wheel/cloudflare.svg
    :target: https://pypi.org/project/cloudflare/
    :alt: Wheel Support Badge

.. image:: https://img.shields.io/pypi/pyversions/cloudflare.svg
    :target: https://pypi.org/project/cloudflare/
    :alt: Python Version Support Badge

**python-cloudflare** is a Python library for easy access to Cloudflare's API's.

**Trivial example**::

    >>> import CloudFlare
    >>> cf = CloudFlare.cloudflare()
    >>> 
    >>> cf.ips()
    {'ipv4_cidrs': ['173.245.48.0/20', ... ], ... }
    >>>

Refer to the `examples` directory for full examples.

The User Guide
--------------

.. toctree::
   :maxdepth: 4
   :caption: Contents:

   CloudFlare
   cli4
   examples

.. include:: modules.rst

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
python-cloudflare-2.20.0/docs/modules.rst000066400000000000000000000001451461736615400204010ustar00rootroot00000000000000python-cloudflare
=================

.. toctree::
   :maxdepth: 4

   CloudFlare
   cli4
   examples
python-cloudflare-2.20.0/docs/requirements.txt000066400000000000000000000000711461736615400214610ustar00rootroot00000000000000sphinx>=4.0.0
myst_parser>=1.0.0
sphinx_rtd_theme>=1.2.0
python-cloudflare-2.20.0/examples/000077500000000000000000000000001461736615400170655ustar00rootroot00000000000000python-cloudflare-2.20.0/examples/__init__.py000066400000000000000000000000001461736615400211640ustar00rootroot00000000000000python-cloudflare-2.20.0/examples/example_account_rules_lists_items.py000077500000000000000000000053331461736615400264460ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import time

# sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():

    # Code up ...
    # cli4 /accounts/::00000000000000000000000000000000/rules/lists/::00000000000000000000000000000000/items

    try:
        account_id = sys.argv[1]
        list_id = sys.argv[2]
    except IndexError:
        exit('usage: example_account_rules_lists_items.py account_id list_id')

    with CloudFlare.CloudFlare() as cf:

        #
        # Print existing list - showing GET function
        #
        print('EXISTING LIST LOOKS LIKE:')
        items = cf.accounts.rules.lists.items(account_id, list_id)
        for item in items:
            print('%s %s %s %-30s ; %s' % (item['id'], item['created_on'], item['modified_on'], item['ip'], item['comment']))
        print('')

        #
        # Add an element to list - showing POST function
        #
        new_ip_address = '4.4.4.4'
        new_ip_comment = 'all the fours!'
        new_ip_id = None

        print('ADD TO LIST:')
        new_r = cf.accounts.rules.lists.items.post(account_id, list_id, data=[{'ip':new_ip_address,'comment':new_ip_comment}])
        print('new_r = %s' % (new_r))
        print('')

        #
        # So it seems that it takes a while for the database to update; this is delay is a hack
        #
        time.sleep(1)

        #
        # Print the full list again - to show POST worked
        #
        print('NEW LIST LOOKS LIKE:')
        items = cf.accounts.rules.lists.items(account_id, list_id)
        for item in items:
            print('%s %s %s %-30s ; %s' % (item['id'], item['created_on'], item['modified_on'], item['ip'], item['comment']))
            if item['ip'] == new_ip_address:
                new_ip_id = item['id']
        print('')

        #
        # Now remove that element - to show DELETE function (note the use of new_ip_id value
        #
        print('DELETE FROM LIST:')
        if new_ip_id is None:
            exit('    --- NOTHING TO DELETE')
        del_r = cf.accounts.rules.lists.items.delete(account_id, list_id, data={'items':[{'id':new_ip_id}]})
        print('del_r = %s' % (del_r))
        print('')

        #
        # So it seems that it takes a while for the database to update; this is delay is a hack
        #
        time.sleep(1)

        #
        # Print the full list again - to show DELETE worked
        #
        print('FINAL LIST LOOKS LIKE:')
        items = cf.accounts.rules.lists.items(account_id, list_id)
        for item in items:
            print('%s %s %s %-30s ; %s' % (item['id'], item['created_on'], item['modified_on'], item['ip'], item['comment']))
        print('')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_ai_images.py000066400000000000000000000035031461736615400230710ustar00rootroot00000000000000#!/usr/bin/env python

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
import CloudFlare

use_find = False

def doit(account_name, prompt_text):

    # We set the timeout because these AI calls take longer than normal API calls
    cf = CloudFlare.CloudFlare(global_request_timeout=120)

    try:
        if account_name is None or account_name == '':
            params = {'per_page': 1}
        else:
            params = {'name': account_name, 'per_page': 1}
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/accounts %d %s - api call failed' % (e, e))

    try:
        account_id = accounts[0]['id']
    except IndexError:
        exit('%s: account name not found' % (account_name))

    image_create_data = {
        'prompt': prompt_text,
    }

    try:
        if use_find:
            # you can use this format:
            r = cf.find('/accounts/:id/ai/run/@cf/stabilityai/stable-diffusion-xl-base-1.0').post(account_id, data=image_create_data)
        else:
            # or you can use this format:
            # @'s are replaced by at_ so .../@cf/... becomes .../at_cf/... etc
            r = cf.accounts.ai.run.at_cf.stabilityai.stable_diffusion_xl_base_1_0.post(account_id, data=image_create_data)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/ai.run %d %s - api call failed' % (e, e))

    sys.stdout.buffer.write(r)

def main():
    if len(sys.argv) > 1 and sys.argv[1] == '-a':
        del sys.argv[1]
        account_name = sys.argv[1]
        del sys.argv[1]
    else:
        account_name = None
    if len(sys.argv) > 1:
        prompt_text = ' '.join(sys.argv[1:])
    else:
        prompt_text = "A happy llama running through an orange cloud"
    doit(account_name, prompt_text)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_ai_speechrecognition.py000066400000000000000000000110041461736615400253270ustar00rootroot00000000000000#!/usr/bin/env python

import os
import sys
import random
import tempfile
import requests

sys.path.insert(0, os.path.abspath('.'))
import CloudFlare

use_find = False

def user_agent():
    s = random.choice([
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36 Edg/91.0.864.48',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
            'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36',
    ])
    return s

def doit(account_name, audio_data):

    # We set the timeout because these AI calls take longer than normal API calls
    cf = CloudFlare.CloudFlare(global_request_timeout=120)

    try:
        if account_name is None or account_name == '':
            params = {'per_page': 1}
        else:
            params = {'name': account_name, 'per_page': 1}
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/accounts %d %s - api call failed' % (e, e))

    try:
        account_id = accounts[0]['id']
    except IndexError:
        exit('%s: account name not found' % (account_name))

    try:
        if use_find:
            # you can use this format:
            r = cf.find('/accounts/:id/ai/run/@cf/openai/whisper').post(account_id, data=audio_data)
        else:
            # or you can use this format:
            # @'s are replaced by at_ so .../@cf/... becomes .../at_cf/...
            r = cf.accounts.ai.run.at_cf.openai.whisper.post(account_id, data=audio_data)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/ai.run %d %s - api call failed' % (e, e))

    print('%s' % (r['text']))
    # words = [word['word'] for word in r['words']]
    # print('%s' % ('|'.join(words)))

# based on ... thank you to the author
# https://stackoverflow.com/questions/16694907/download-large-file-in-python-with-requests
def download_audio_file(url, referer, n_requested):
    headers = {}
    headers['Referer'] = referer
    headers['User-Agent'] = user_agent()

    # will be deleted once program exits
    fp = tempfile.TemporaryFile(mode='w+b')

    n_received = 0
    # NOTE the stream=True parameter below
    with requests.get(url, headers=headers, stream=True) as r:
        r.raise_for_status()
        for chunk in r.iter_content(chunk_size=16*1024):
            fp.write(chunk)
            n_received += len(chunk)
            if n_received > n_requested:
                break

    # rewind the file so it's ready to read
    fp.seek(0)
    return fp

def default_audio_clip():

    s = random.choice([
        (
            'https://www.americanrhetoric.com/mp3clipsXE/barackobama/barackobamapresidentialfarewellARXE.mp3',
            'https://www.americanrhetoric.com/barackobamaspeeches.htm'
        ),
        (
            'https://archive.org/download/DoNotGoGentleIntoThatGoodNight/gentle.ogg',
	    'https://archive.org/details/DoNotGoGentleIntoThatGoodNight'
	),
        (
            'https://www.nasa.gov/wp-content/uploads/2015/01/590333main_ringtone_eagleHasLanded_extended.mp3',
	    'https://www.nasa.gov/audio-and-ringtones/'
	),
        (
            'https://www.nasa.gov/wp-content/uploads/2015/01/590331main_ringtone_smallStep.mp3',
	    'https://www.nasa.gov/audio-and-ringtones/'
	),
        (
            'https://upload.wikimedia.org/wikipedia/en/7/7f/George_Bush_1988_No_New_Taxes.ogg',
	    'https://en.wikipedia.org/wiki/File:George_Bush_1988_No_New_Taxes.ogg'
	),
        (
            'https://archive.org/download/grand_meaulnes_2004_librivox/grandmeaulnes_01_alainfournier.mp3',
	    'https://archive.org/details/grand_meaulnes_2004_librivox/grandmeaulnes_01_alainfournier_128kb.mp3'
	),
    ])

    return s

def main():
    if len(sys.argv) > 1 and sys.argv[1] == '-a':
        del sys.argv[1]
        account_name = sys.argv[1]
        del sys.argv[1]
    else:
        account_name = None

    if len(sys.argv) > 1:
        url = sys.argv[1]
        referer = url
    else:
        url, referer = default_audio_clip()

    # we only grab the first 680KB of the file - that's enough to show working code
    audio_fp = download_audio_file(url, referer, 680 * 1024)
    audio_data = audio_fp.read()
    print('%s: length=%d' % (url.split('/')[-1:][0], len(audio_data)))

    doit(account_name, audio_data)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_ai_translate.py000066400000000000000000000035121461736615400236210ustar00rootroot00000000000000#!/usr/bin/env python

import os
import sys

sys.path.insert(0, os.path.abspath('.'))
import CloudFlare

use_find = False

def doit(account_name, english_text):

    # We set the timeout because these AI calls take longer than normal API calls
    cf = CloudFlare.CloudFlare(global_request_timeout=120)

    try:
        if account_name is None or account_name == '':
            params = {'per_page': 1}
        else:
            params = {'name': account_name, 'per_page': 1}
        accounts = cf.accounts.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/accounts %d %s - api call failed' % (e, e))

    try:
        account_id = accounts[0]['id']
    except IndexError:
        exit('%s: account name not found' % (account_name))

    translate_data = {
        'text': english_text,
        'source_lang': 'english',
        'target_lang': 'french',
    }

    try:
        if use_find:
            # you can use this format:
            r = cf.find('/accounts/:id/ai/run/@cf/meta/m2m100-1.2b').post(account_id, data=translate_data)
        else:
            # or you can use this format:
            # @'s are replaced by at_ so .../@cf/... becomes .../at_cf/... etc
            r = cf.accounts.ai.run.at_cf.meta.m2m100_1_2b.post(account_id, data=translate_data)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/ai.run %d %s - api call failed' % (e, e))

    print(r['translated_text'])

def main():
    if len(sys.argv) > 1 and sys.argv[1] == '-a':
        del sys.argv[1]
        account_name = sys.argv[1]
        del sys.argv[1]
    else:
        account_name = None
    if len(sys.argv) > 1:
        english_text = ' '.join(sys.argv[1:])
    else:
        english_text = "I'll have an order of the moule frites"
    doit(account_name, english_text)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_always_use_https.py000077500000000000000000000037041461736615400245570ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    update_flag = False

    try:
        if sys.argv[1] == '--off':
            update_flag = True
            new_value = 'off'
            sys.argv.pop(1)
    except IndexError:
        pass

    try:
        if sys.argv[1] == '--on':
            update_flag = True
            new_value = 'on'
            sys.argv.pop(1)
    except IndexError:
        pass

    # Grab the zone name
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        exit('usage: example_always_use_https.py [--on|--off] zone')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones - %s - api call failed' % (e))

    zone_id = zones[0]['id']

    # retrieve present value
    try:
        r = cf.zones.settings.always_use_https.get(zone_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.settings.always_use_https.get %d %s - api call failed' % (e, e))

    present_value = r['value']

    print(zone_id, zone_name, present_value)

    if update_flag and present_value != new_value:
        print('\t', '(now updating... %s -> %s)' % (present_value, new_value))
        try:
            r = cf.zones.settings.always_use_https.patch(zone_id, data={'value':new_value})
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.settings.always_use_https.patch %d %s - api call failed' % (e, e))
        updated_value = r['value']
        if new_value == updated_value:
            print('\t', '... updated!')

if __name__ == '__main__':
    main()
    exit(0)
python-cloudflare-2.20.0/examples/example_are_zones_ipv6.py000077500000000000000000000035231461736615400241110ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Check for update flag
    update_ipv6 = False
    try:
        if sys.argv[1] == '--update':
            update_ipv6 = True
            sys.argv.pop(1)
    except IndexError:
        pass

    # Grab the first argument, if there is one
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        params = {'per_page':50}

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones - %s - api call failed' % (e))

    for zone in sorted(zones, key=lambda v: v['name']):
        zone_name = zone['name']
        zone_id = zone['id']
        try:
            ipv6 = cf.zones.settings.ipv6.get(zone_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.settings.ipv6.get %d %s - api call failed' % (e, e))

        ipv6_value = ipv6['value']
        if update_ipv6 and ipv6_value == 'off':
            print(zone_id, ipv6_value, zone_name, '(now updating... off -> on)')
            try:
                ipv6 = cf.zones.settings.ipv6.patch(zone_id, data={'value':'on'})
            except CloudFlare.exceptions.CloudFlareAPIError as e:
                exit('/zones.settings.ipv6.patch %d %s - api call failed' % (e, e))
            ipv6_value = ipv6['value']
            if ipv6_value == 'on':
                print('\t', '... updated!')
        else:
            print(zone_id, ipv6_value, zone_name)

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_are_zones_ipv6_simple.py000077500000000000000000000010551461736615400254600ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    cf = CloudFlare.CloudFlare()
    zones = cf.zones.get(params={'per_page':50})
    for zone in zones:
        zone_name = zone['name']
        zone_id = zone['id']
        settings_ipv6 = cf.zones.settings.ipv6.get(zone_id)
        ipv6_on = settings_ipv6['value']
        print(zone_id, ipv6_on, zone_name)
    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_bot_management.py000077500000000000000000000033041461736615400241350ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import json
sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))

import CloudFlare

def main():
    """Cloudflare API code - example"""

    try:
        zone_name = sys.argv[1]
    except IndexError:
        exit('usage: example_bot_management.py zone_name True/False')

    try:
        enable_value = sys.argv[2]
    except IndexError:
        exit('usage: example_bot_management.py zone_name True/False')

    enable_value = True if enable_value in ['true','True','1'] else False

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name': zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zone %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zone.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zones not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone_id = zones[0]['id']

    settings_bot = cf.zones.bot_management.get(zone_id)
    print(json.dumps(settings_bot, indent=4))

    try:
        settings_bot = cf.zones.bot_management.put(zone_id, data={'enable_js': enable_value})
    except Exception as e:
        if int(e) == 99998:
            print('Exception: 99998 ignored!', file=sys.stderr)
            pass
        else:
            exit('Exception: %d %s' % (int(e), str(e)))

    settings_bot = cf.zones.bot_management.get(zone_id)
    print(json.dumps(settings_bot, indent=4))

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_certificates.py000077500000000000000000000051601461736615400236240ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Grab the first argument, if there is one
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        params = {'per_page':50}

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones - %s - api call failed' % (e))

    # there should only be one zone
    for zone in sorted(zones, key=lambda v: v['name']):
        zone_name = zone['name']
        zone_id = zone['id']
        try:
            certificates = cf.zones.ssl.certificate_packs.get(zone_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.ssl.certificate_packs %d %s - api call failed' % (e, e))

        for certificate in certificates:
            certificate_type = certificate['type']
            primary_certificate = certificate['primary_certificate']
            certificate_hosts = certificate['hosts']
            certificate_sig = certificate['certificates'][0]['signature']
            certificate_sig_count = len(certificate['certificates'])
            if certificate_sig_count > 1:
                c = certificate['certificates'][0]
                print('%-40s %-10s %-32s    %-15s [ %s ]' % (
                    zone_name,
                    certificate_type,
                    primary_certificate,
                    c['signature'],
                    ','.join(certificate_hosts)
                ))
                nn = 0
                for c in certificate['certificates']:
                    nn += 1
                    if nn == 1:
                        next
                    print('%-40s %-10s %-32s %2d:%-15s [ %s ]' % (
                        '',
                        '',
                        '',
                        nn,
                        c['signature'],
                        ''
                    ))
            else:
                for c in certificate['certificates']:
                    print('%-40s %-10s %-32s    %-15s [ %s ]' % (
                        zone_name,
                        certificate_type,
                        primary_certificate,
                        c['signature'],
                        ','.join(certificate_hosts)
                    ))

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_create_zone_and_populate.py000077500000000000000000000075361461736615400262210ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    try:
        zone_name = sys.argv[1]
    except IndexError:
        exit('usage: provide a zone name as an argument on the command line')

    cf = CloudFlare.CloudFlare()

    # Create zone - which will only work if ...
    # 1) The zone is not on Cloudflare.
    # 2) The zone passes a whois test
    print('Create zone %s ...' % (zone_name))
    try:
        zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name})
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.post %s - %d %s' % (zone_name, e, e))
    except Exception as e:
        exit('/zones.post %s - %s' % (zone_name, e))

    zone_id = zone_info['id']
    if 'email' in zone_info['owner']:
        zone_owner = zone_info['owner']['email']
    else:
        zone_owner = '"' + zone_info['owner']['name'] + '"'
    zone_plan = zone_info['plan']['name']
    zone_status = zone_info['status']
    print('\t%s name=%s owner=%s plan=%s status=%s\n' % (
        zone_id,
        zone_name,
        zone_owner,
        zone_plan,
        zone_status
    ))

    # DNS records to create
    dns_records = [
        {'name':'ding', 'type':'A', 'content':'216.58.194.206'},
        {'name':'foo', 'type':'AAAA', 'content':'2001:d8b::1'},
        {'name':'foo', 'type':'A', 'content':'192.168.0.1'},
        {'name':'duh', 'type':'A', 'content':'10.0.0.1', 'ttl':120},
        {'name':'bar', 'type':'CNAME', 'content':'foo.%s' % (zone_name)}, # CNAME requires FQDN at content
        {'name':'shakespeare', 'type':'TXT', 'content':"What's in a name? That which we call a rose by any other name would smell as sweet."}
    ]

    print('Create DNS records ...')
    for dns_record in dns_records:
        # Create DNS record
        try:
            r = cf.zones.dns_records.post(zone_id, data=dns_record)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.dns_records.post %s %s - %d %s' % (zone_name, dns_record['name'], e, e))
        # Print respose info - they should be the same
        dns_record = r
        print('\t%s %30s %6d %-5s %s ; proxied=%s proxiable=%s' % (
            dns_record['id'],
            dns_record['name'],
            dns_record['ttl'],
            dns_record['type'],
            dns_record['content'],
            dns_record['proxied'],
            dns_record['proxiable']
        ))

        # set proxied flag to false - for example
        dns_record_id = dns_record['id']

        new_dns_record = {
            # Must have type/name/content (even if they don't change)
            'type':dns_record['type'],
            'name':dns_record['name'],
            'content':dns_record['content'],
            # now add new values you want to change
            'proxied':False
        }

        try:
            dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=new_dns_record)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones/dns_records.put %d %s - api call failed' % (e, e))

    print('')

    # Now read back all the DNS records
    print('Read back DNS records ...')
    try:
        dns_records = cf.zones.dns_records.get(zone_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.dns_records.get %s - %d %s' % (zone_name, e, e))

    for dns_record in sorted(dns_records, key=lambda v: v['name']):
        print('\t%s %30s %6d %-5s %s ; proxied=%s proxiable=%s' % (
            dns_record['id'],
            dns_record['name'],
            dns_record['ttl'],
            dns_record['type'],
            dns_record['content'],
            dns_record['proxied'],
            dns_record['proxiable']
        ))

    print('')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_custom_hostnames.py000077500000000000000000000043471461736615400245600ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Grab the first argument, if there is one
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        params = {'per_page':500}

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    # there should only be one zone - but handle more if needed
    for zone in sorted(zones, key=lambda v: v['name']):
        zone_name = zone['name']
        zone_id = zone['id']
        zone_plan = zone['plan']['name']
        if zone_plan != 'Enterprise Website':
            print('%s %s %s - not Enterprise' % (zone_id, zone_name, zone_plan))
            continue

        print('%s %-40s %s' % (zone_id, zone_name, zone_plan))
        try:
            custom_info = cf.zones.custom_hostnames.get(zone_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.stderr.write('/zones.custom_hostnames.get %d %s - api call failed\n' % (e, e))
            continue

        for hostname_info in custom_info:
            print('\t%s %-30s %s' % (hostname_info['id'], hostname_info['hostname'], hostname_info['created_at']))

            for s in sorted(hostname_info.keys()):
                if s in ['id', 'hostname', 'created_at'] or hostname_info[s] is None:
                    continue
                print('\t%-15s = %s' % (s, hostname_info[s]))

        try:
            fallback_origin = cf.zones.custom_hostnames.fallback_origin.get(zone_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.stderr.write('/zones.custom_hostnames.fallback_origin.get %d %s - api call failed\n' % (e, e))
            continue

        print('\t%s %-30s %s %s' % ('', fallback_origin['origin'], fallback_origin['created_at'], fallback_origin['status']))
        print('')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_delete_zone_entry.py000077500000000000000000000037611461736615400247020ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    try:
        zone_name = sys.argv[1]
        dns_name = sys.argv[2]
    except IndexError:
        exit('usage: example_delete_zone_entry.py zone dns_record')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name':zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zone not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone = zones[0]

    zone_id = zone['id']
    zone_name = zone['name']

    print('ZONE:', zone_id, zone_name)

    try:
        params = {'name':dns_name + '.' + zone_name}
        dns_records = cf.zones.dns_records.get(zone_id, params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones/dns_records %s - %d %s - api call failed' % (dns_name, e, e))

    found = False
    for dns_record in dns_records:
        dns_record_id = dns_record['id']
        dns_record_name = dns_record['name']
        dns_record_type = dns_record['type']
        dns_record_value = dns_record['content']
        print('DNS RECORD:', dns_record_id, dns_record_name, dns_record_type, dns_record_value)

        try:
            dns_record = cf.zones.dns_records.delete(zone_id, dns_record_id)
            print('DELETED')
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.dns_records.delete %s - %d %s - api call failed' % (dns_name, e, e))
        found = True

    if not found:
        print('RECORD NOT FOUND')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_dns_export.py000077500000000000000000000025421461736615400233450ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import CloudFlare

def main():
    """Cloudflare API code - example"""

    try:
        zone_name = sys.argv[1]
    except IndexError:
        exit('usage: example_dns_export.py zone')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name': zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zone not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone_id = zones[0]['id']

    try:
        dns_records = cf.zones.dns_records.export.get(zone_id)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones/dns_records/export %s - %d %s - api call failed' % (zone_name, e, e))

    for line in dns_records.splitlines():
        if len(line) == 0 or line[0] == ';':
            # blank line or comment line are skipped - to make example easy to see
            continue
        print(line)

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_dns_import.py000077500000000000000000000026671461736615400233460ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import json

import CloudFlare

def main():
    """Cloudflare API code - example"""

    try:
        zone_name = sys.argv[1]
        file_name = sys.argv[2]
    except IndexError:
        exit('usage: example_dns_import.py zone zone-file')

    try:
        fd = open(file_name, 'rb')
    except IOError as e:
        exit('file open - %s' % (e))

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name': zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zone not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone_id = zones[0]['id']

    try:
        #
        # "import" is a reserved word and hence we add '_' to the end of verb.
        #
        r = cf.zones.dns_records.import_.post(zone_id, files={'file':fd})
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones/dns_records/import %s - %d %s - api call failed' % (dns_name, e, e))

    print(json.dumps(r))

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_dnssec_settings.py000077500000000000000000000026621461736615400243620ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Grab the first argument, if there is one
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        params = {'per_page':1}

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    # there should only be one zone
    for zone in sorted(zones, key=lambda v: v['name']):
        zone_name = zone['name']
        zone_id = zone['id']
        # grab the DNSSEC settings
        try:
            settings = cf.zones.dnssec.get(zone_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.dnssec.get %d %s - api call failed' % (e, e))

        print(zone_id, zone_name)
        # display every setting value
        for setting in sorted(settings):
            print('\t%-30s %10s = %s' % (
                setting,
                '(editable)' if setting == 'status' else '',
                settings[setting]
            ))

        print('')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_firewall_rules.py000066400000000000000000000066701461736615400242020ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import re
import json
import uuid

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    cf = CloudFlare.CloudFlare()

    try:
        zone_name = sys.argv[1]
    except IndexError:
        exit('usage: example_firewall_rules.py zone_name')

    # grab the zone identifier
    try:
        params = {'name': zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zone %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zone.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zones not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone_id = zones[0]['id']

    # SHOW EXISTING FIREWALL RULES
    r = cf.zones.firewall.rules.get(zone_id)
    print('existing filewall rules =\n' + json.dumps(r, indent=4, sort_keys=False) + '\n')

    # SHOW EXISTING FILTERS
    r = cf.zones.filters.get(zone_id)
    print('existing filters =\n' + json.dumps(r, indent=4, sort_keys=False) + '\n')

    # CREATE A FILTER & FIREWALL RULES

    reference_name = 'FILTER-' + str(uuid.uuid1())

    my_filter = {
        'expression': 'http.request.uri.path == "/private.html$"',
        'paused': True,
        'description': 'stop access to /private.html',
        'ref': reference_name,
    }

    my_rule = [
        {
            'action': 'block',
            'filter': my_filter,
            'paused': True,
        }
    ]

    try:
        r = cf.zones.firewall.rules.post(zone_id, data=my_rule)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('create zones.filewall.rules: %d %s' % (int(e), str(e)))
        exit(1)

    print('firewall rule created =\n' + json.dumps(r, indent=4, sort_keys=False) + '\n')

    firewall_id = r[0]['id']
    filter_id = r[0]['filter']['id']

    print('filewall_id = %s filter_id = %s' % (firewall_id, filter_id))

    # SHOW PRESENT FIREWALL RULES
    r = cf.zones.firewall.rules.get(zone_id)
    print('present filewall rules =\n' + json.dumps(r, indent=4, sort_keys=False) + '\n')

    # DELETE NEW FIREWALL RULES
    for f in r:
        print('id = ' + f['id'])
        try:
            r2 = cf.zones.firewall.rules.delete(zone_id, f['id'])
            print('deleted id = ' + r2['id'])
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            print('zones.filewall.rules.delete: %d %s' % (int(e), str(e)))

    # SHOW PRESENT FILTERS
    r = cf.zones.filters.get(zone_id)
    print('present filters =\n' + json.dumps(r, indent=4, sort_keys=False) + '\n')

    # DELETE NEW FILTERS
    for f in r:
        print('id = ' + f['id'])
        try:
            r2 = cf.zones.filters.delete(zone_id, f['id'])
            print('deleted id = ' + r2['id'])
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            print('zones.filters.delete: %d %s' % (int(e), str(e)))

    # SHOW FINAL FIREWALL RULES
    r = cf.zones.firewall.rules.get(zone_id)
    print('final filewall rules =\n' + json.dumps(r, indent=4, sort_keys=False) + '\n')

    # SHOW FINAL FILTERS
    r = cf.zones.filters.get(zone_id)
    print('final filters =\n' + json.dumps(r, indent=4, sort_keys=False) + '\n')

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_graphql.py000077500000000000000000000045141461736615400226170ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import datetime

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def rfc3339_iso8601_time(hour_delta=0, with_hms=False):
    """ rfc3339_iso8601_time """
    # format time (with an hour offset in RFC3339 ISO8601 format (and do it UTC time)
    dt = (datetime.datetime.now(datetime.UTC).replace(microsecond=0) + datetime.timedelta(hours=hour_delta))
    if with_hms:
        return dt.isoformat().replace('+00:00', 'Z')
    return dt.strftime('%Y-%m-%d')

def main():
    """Cloudflare API code - example"""

    # Grab the zone name
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        sys.exit('usage: example_graphql zone')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        sys.exit('/zones.get %d %s - api call failed' % (int(e), str(e)))

    date_before = rfc3339_iso8601_time(0) # now
    date_after = rfc3339_iso8601_time(-7 * 24) # 7 previous days worth

    zone_id = zones[0]['id']
    query = """
      query {
        viewer {
            zones(filter: {zoneTag: "%s"} ) {
            httpRequests1dGroups(limit:40, filter:{date_lt: "%s", date_gt: "%s"}) {
              sum { countryMap { bytes, requests, clientCountryName } }
              dimensions { date }
            }
          }
        }
      }
    """ % (zone_id, date_before, date_after)

    # query - always a post
    try:
        r = cf.graphql.post(data={'query':query})
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        sys.exit('/graphql.post %d %s - api call failed' % (int(e), str(e)))

    # only one zone, so use zero'th element!
    zone_info = r['data']['viewer']['zones'][0]

    http_requests1d_groups = zone_info['httpRequests1dGroups']

    for h in sorted(http_requests1d_groups, key=lambda v: v['dimensions']['date']):
        result_date = h['dimensions']['date']
        result_info = h['sum']['countryMap']
        print(result_date)
        for element in sorted(result_info, key=lambda v: -v['bytes']):
            print("    %7d %7d %2s" % (element['bytes'], element['requests'], element['clientCountryName']))

if __name__ == '__main__':
    main()
    sys.exit(0)
python-cloudflare-2.20.0/examples/example_graphql.sh000077500000000000000000000017251461736615400226020ustar00rootroot00000000000000:

#
# Show usage of GraphQL - see https://developers.cloudflare.com/analytics/graphql-api for all info
#

# pass one argument - the zone
ZONEID=`cli4 name="$1"  /zones | jq -r '.[].id'`
if [ "${ZONEID}" = "" ]
then
	echo "$1: zone not found" 1>&2
	exit 1
fi

# Just query the last 24 hours
DATE_BEFORE=`date -u +%Y-%m-%dT%H:%M:%SZ`
DATE_AFTER=`date -u -v -24H +%Y-%m-%dT%H:%M:%SZ`

# build the GraphQL query - this is just a simple example
QUERY='
  query {
    viewer {
      zones(filter: {zoneTag: "'${ZONEID}'"} ) {
        httpRequests1hGroups(limit:100, orderBy:[datetime_ASC], filter:{datetime_gt:"'${DATE_AFTER}'", datetime_lt:"'${DATE_BEFORE}'"}) {
          dimensions { datetime }
          sum { bytes }
        }
      }
    }
  }
'

# this not only does the query; but also drills down into the results to print the final data
cli4 --post query="${QUERY}" /graphql | jq -cr '.data.viewer.zones[]|.httpRequests1hGroups[]|.dimensions.datetime,.sum.bytes' | paste - - 
python-cloudflare-2.20.0/examples/example_images_v2_direct_upload.py000066400000000000000000000176341461736615400257370ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import json
import datetime
import requests

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

#
# Warning: You need to enable image storage on your account for this code to work.
# You'll get 403 errors if you don't have it enabled (go pay for it)
#
# If you need to delete images after running this code you can do this:
#
# cli4 --delete /accounts/:"${ACCOUNT}"/images/v1/::${image_id}
#
# Or if you want to live dangerously - do this and delete every image you have
# You should not run this unless you really really really know what you're doing! (like rm -rf /)
#
# cli4 /accounts/:"${ACCOUNT}"/images/v1 | jq -r '.images[]|.id' | while read image_id ; do cli4 --delete /accounts/:"${ACCOUNT}"/images/v1/::$image_id ; done
#

#
# A note about version numbers.
# this code works with 2.14.2 in a simple way
# this code works with 2.18.0 is a simple way
# released between then require at-least one paramater send via files= in order to not trigger a backend API bug
#

def rfc3339_iso8601_time(hour_delta=0, with_hms=False):
    """ rfc3339_iso8601_time """
    # format time (with an hour offset in RFC3339 ISO8601 format (and do it UTC time)
    dt = (datetime.datetime.now(datetime.UTC).replace(microsecond=0) + datetime.timedelta(hours=hour_delta))
    if with_hms:
        return dt.isoformat().replace('+00:00', 'Z')
    return dt.strftime('%Y-%m-%d')

def method_from_library_version():
    """ method_from_library_version """
    if CloudFlare.__version__ <= '2.14.2':
        print('Using %s version of Cloudflare python library - hence do not need data= or files=; but use files= if passing anything' % (CloudFlare.__version__))
        return ''
    if CloudFlare.__version__ <= '2.17.0':
        print('Using %s version of Cloudflare python library - hence must use files=' % (CloudFlare.__version__))
        return 'USE-FILES'
    # with newer library than 2.17.0 (i.e 2.18.0 and above) you should be able to pass just the data version
    print('Using %s version of Cloudflare python library - hence use data= as it is simpler' % (CloudFlare.__version__))
    return 'USE-DATA'

def doit(account_name, image_filename):
    """ doit """

    # https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/
    # https://developers.cloudflare.com/api/operations/cloudflare-images-create-authenticated-direct-upload-url-v-2

    with CloudFlare.CloudFlare(debug=False) as cf:
        try:
            params = {'name': account_name, 'per_page': 1}
            accounts = cf.accounts.get(params=params)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.exit('%s: %d %s - api call failed' % ('/accounts', int(e), str(e)))
        try:
            account_id = accounts[0]['id']
        except IndexError:
            sys.exit('%s: account name not found' % (account_name))

        try:
           image_fp = open(image_filename, 'rb')
        except Exception as e:
            sys.exit('%s: %s - file read failed' % (image_filename, e))

        image_filesize = os.fstat(image_fp.fileno()).st_size
        if image_filesize > 1024*1024*1024:
                print('%s: filesize = %0.1f GBytes' % (image_filename, float(image_filesize)/1024*1024*1024))
        elif image_filesize > 1024*1024:
                print('%s: filesize = %0.1f MBytes' % (image_filename, float(image_filesize)/1024*1024))
        elif image_filesize > 1024:
                print('%s: filesize = %0.1f KBytes' % (image_filename, float(image_filesize)/1024))
        else:
                print('%s: filesize = %d Bytes' % (image_filename, image_filesize))

        # format future time in RFC3339 format (and do it UTC time)
        time_plus_one_hour_in_iso = rfc3339_iso8601_time(1, True)

        # direct_upload uses multipart/form-data and hence this info is passed as files (but None for filename)
        # these are the four form values
        # presently you need to upload at-least one of these until the library version is greater than 2.17.0
        # --form expiry= \
        # --form id= \
        # --form metadata= \
        # --form requireSignedURLs=

        # here's examples using metadata and expiry.

        # this is just simple metadata created to show it working - your code will be different
        metadata_values = {
            'source': image_filename,
            'size': image_filesize,
        }

        data = None
        files = None

        lib_method = method_from_library_version()

        if lib_method == 'USE-FILES':
            files = {
                ('metadata', (None, json.dumps(metadata_values))),
                ('expiry', (None, time_plus_one_hour_in_iso))
            }
        elif lib_method == 'USE-DATA':
            data = {
                'metadata': json.dumps(metadata_values),
                'expiry': time_plus_one_hour_in_iso,
            }
        elif lib_method == '':
            # optionally do nothing or send via files=
            pass

        try:
            r = cf.accounts.images.v2.direct_upload.post(account_id, data=data, files=files)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.exit('%s: %d %s - api call failed' % ('/accounts/images/v2/direct_upload', int(e), str(e)))
        print('v2 new image post results')
        print(json.dumps(r, indent=4))

        image_id = r['id']
        image_url = r['uploadURL']

        # https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/
        # curl -X POST \
        #      -F file=@/Users/mickie/Downloads/example_video.mp4 \
        #      https://upload.videodelivery.net/f65014bc6ff5419ea86e7972a047ba22

        try:
            r = requests.post(image_url, files={('file', image_fp)}, timeout=5)
        except Exception as e:
            sys.exit('%s: %s - api call failed' % (image_url, e))

        image_fp.close()

        if r.status_code != 200:
           if r.status_code == 403:
               print('403 means you need to enable images in your account')
           if r.status_code == 403:
               print('415 means the file is a bad image format')
           sys.exit('%s: HTTP Error %s' % (image_url, r.status_code))

        j = r.json()
        if j['success'] is True:
            print('Image upload results')
            print(json.dumps(j['result'], indent=4))
        else:
            sys.exit('Error:\n    errors: %s\n    messages: %s' % (j['errors'], j['messages']))

        # list all images
        try:
            r = cf.accounts.images.v2(account_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.exit('%s: %d %s - api call failed' % ('/accounts/images/v1', int(e), str(e)))

        print('All account images:')
        for img in r['images']:
            print('%s   %s: %s %s %s' % ('>' if img['id'] == image_id else ' ', img['id'], img['uploaded'], img['filename'], img['variants'][0]))
            if 'meta' in img:
                for k,v in img['meta'].items():
                    print('        %s = %s' % (k, v))
            else:
                print('        - no meta data')

        # delete the image - this was just a test (comment this out if you end up using this code for uploads)
        try:
            r = cf.accounts.images.v1.delete(account_id, image_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.exit('%s: %d %s - api call failed' % ('/accounts/images/v1', int(e), str(e)))
        print('Image delete')
        print(json.dumps(r, indent=4))

def main():
    """ main """
    try:
        account_name = sys.argv[1]
    except IndexError:
        sys.exit('usage: example_images_v2_direct_upload.py account_name image_filename')
    try:
        image_filename = sys.argv[2]
    except IndexError:
        sys.exit('usage: example_images_v2_direct_upload.py account_name image_filename')
    doit(account_name, image_filename)
    sys.exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_ips.py000077500000000000000000000013721461736615400217530ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    cf = CloudFlare.CloudFlare()
    try:
        ips = cf.ips.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/ips - %d %s' % (e, e))
    except Exception as e:
        exit('/ips - %s - api call connection failed' % (e))

    print('ipv4_cidrs count = ', len(ips['ipv4_cidrs']))
    for cidr in sorted(set(ips['ipv4_cidrs'])):
        print('\t', cidr)
    print('ipv6_cidrs count = ', len(ips['ipv6_cidrs']))
    for cidr in sorted(set(ips['ipv6_cidrs'])):
        print('\t', cidr)
    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_list_api_from_web.py000077500000000000000000000035731461736615400246510ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import json

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    cf = CloudFlare.CloudFlare()
    try:
        found_comands = cf.api_from_openapi()
    except Exception as e:
        exit('api_from_web: - %s - api call connection failed' % (e))

    # {
    #   "action": "GET",
    #   "cmd": "/accounts",
    #   "deprecated": false,
    #   "deprecated_date": "",
    #   "deprecated_already": false
    # }

    # {
    #  "action": "DELETE",
    #  "cmd": "/accounts/:id/addressing/prefixes/:id/delegations",
    #  "deprecated": false,
    #  "deprecated_date": "",
    #  "deprecated_already": false,
    #  "content_type": "application/json"
    # }

    print('# cloudflare-python')
    print('')
    print('## Table of commands')
    print('')
    print('|`GET`   |`PUT`   |`POST`  |`PATCH` |`DELETE`|API call|')
    print('|--------|--------|--------|--------|--------|:-------|')

    cmds = {}
    for r in found_comands:
        if r['deprecated'] or r['deprecated_already']:
            continue
        cmd = r['cmd']
        action = r['action']
        if cmd not in cmds:
            cmds[cmd] = {}
        cmds[cmd][action] = action

    # This produces something like this ...
    # GET    -      -      PATCH  -       /zones/:zone_identifier/settings/always_online
    # GET    -      PUT    PATCH  DELETE  /zones/:zone_identifier/waiting_rooms/:waiting_room_id

    for cmd in cmds.keys():
        p = '|'
        for method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']:
            if method in cmds[cmd]:
                p += '%-8s|' % ('`' + method + '`')
            else:
                p += '%-8s|' % ('-')
        print("%s %s |" % (p, cmd))

    print('')

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_page_rules.py000077500000000000000000000042361461736615400233100ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import CloudFlare

def main():
    """Cloudflare API code - example"""

    try:
        zone_name = sys.argv[1]
    except IndexError:
        exit('usage: example_page_rules.py zone')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name': zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zone not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone_id = zones[0]['id']

    url_match = "*.%s/url1*" % (zone_name)
    url_forwarded = "http://%s/url2" % (zone_name)

    targets = [{"target":"url","constraint":{"operator":"matches","value":url_match}}]
    actions = [{"id":"forwarding_url","value":{"status_code":302,"url":url_forwarded}}]
    pagerule_for_redirection = {"status": "active","priority": 1,"actions": actions,"targets": targets}

    try:
        r = cf.zones.pagerules.get(zone_id, data=pagerule_for_redirection)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.pagerules.get %d %s - api call failed' % (e, e))

    create = True

    for rule in r:
        if (rule['actions'] == pagerule_for_redirection["actions"] and rule["targets"] == pagerule_for_redirection["targets"]):
            print('\t', '... rule already present!')
            create = False
            break

    if (create):
        try:
            r = cf.zones.pagerules.post(zone_id, data=pagerule_for_redirection)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.pagerules.post %d %s - api call failed' % (e, e))
        if (r['actions'] == pagerule_for_redirection["actions"] and r["targets"] == pagerule_for_redirection["targets"]):
            print('\t', '... created!')
    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_page_rules.sh000077500000000000000000000006701461736615400232700ustar00rootroot00000000000000:

ZONE=${1-example.com}

URL_MATCH="*.${ZONE}/url1*"
URL_FORWARDED="http://${ZONE}/url2"

cli4 --post \
	targets='[ { "target": "url", "constraint": { "operator": "matches", "value": "'${URL_MATCH}'" } } ]' \
	actions='[ { "id": "forwarding_url", "value": { "status_code": 302, "url": "'${URL_FORWARDED}'" } } ]' \
	status=active \
	priority=1 \
		/zones/:${ZONE}/pagerules | jq '{"status":.status,"priority":.priority,"id":.id}'

exit 0

python-cloudflare-2.20.0/examples/example_paging_thru_zones.py000077500000000000000000000022621461736615400247040ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    cf = CloudFlare.CloudFlare(raw=True)

    page_number = 0
    while True:
        page_number += 1
        try:
            raw_results = cf.zones.get(params={'per_page':5,'page':page_number})
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.get %d %s - api call failed' % (e, e))

        zones = raw_results['result']
        domains = []
        for zone in zones:
            zone_id = zone['id']
            zone_name = zone['name']
            domains.append(zone_name)

        count = raw_results['result_info']['count']
        page = raw_results['result_info']['page']
        per_page = raw_results['result_info']['per_page']
        total_count = raw_results['result_info']['total_count']
        total_pages = raw_results['result_info']['total_pages']

        print("COUNT=%d PAGE=%d PER_PAGE=%d TOTAL_COUNT=%d TOTAL_PAGES=%d -- %s" % (count, page, per_page, total_count, total_pages, domains))

        if page_number == total_pages:
            break

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_paging_thru_zones.sh000077500000000000000000000014161461736615400246660ustar00rootroot00000000000000:

tmp=/tmp/$$_
trap "rm ${tmp}; exit 0" 0 1 2 15

PAGE_NUMBER=0

while true
do
	# grab the next page
	PAGE_NUMBER=`expr ${PAGE_NUMBER} + 1`
	cli4 --raw per_page==5 page==${PAGE_NUMBER} /zones > ${tmp}

	domains=`jq -c '.|.result|.[]|.name' < ${tmp} | tr -d '"'`
	result_info=`jq -c '.|.result_info' < ${tmp}`

	COUNT=`      echo "${result_info}" | jq .count`
	PAGE=`       echo "${result_info}" | jq .page`
	PER_PAGE=`   echo "${result_info}" | jq .per_page`
	TOTAL_COUNT=`echo "${result_info}" | jq .total_count`
	TOTAL_PAGES=`echo "${result_info}" | jq .total_pages`

	echo COUNT=${COUNT} PAGE=${PAGE} PER_PAGE=${PER_PAGE} TOTAL_COUNT=${TOTAL_COUNT} TOTAL_PAGES=${TOTAL_PAGES} -- ${domains}

	if [ "${PAGE_NUMBER}" == "${TOTAL_PAGES}" ]
	then
		## last section
		break
	fi
done

python-cloudflare-2.20.0/examples/example_proxied.py000077500000000000000000000064731461736615400226410ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Change the proxied value on a FQDN"""

    try:
        zone_name = sys.argv[1]
        dns_name = sys.argv[2]
        if sys.argv[3] == 'false':
            new_r_proxied_flag = False
        elif sys.argv[3] == 'true':
            new_r_proxied_flag = True
        else:
            raise ValueError('bad arg')
    except IndexError:
        exit('usage: ./example-make-zone-proxied.py zone dns_record [true|false]')
    except ValueError:
        exit('usage: ./example-make-zone-proxied.py zone dns_record [true|false]')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name':zone_name, 'per_page':1}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    # there should only be one zone
    zone = zones[0]

    zone_name = zone['name']
    zone_id = zone['id']

    print("Zone:\t%s %s" % (zone_id, zone_name))

    try:
        params = {'name': dns_name}
        dns_records = cf.zones.dns_records.get(zone_id, params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones/dns_records.get %d %s - api call failed' % (e, e))

    if len(dns_records) == 0:
        exit('/zones.dns_records.get - %s - no records found' % (dns_name))

    for dns_record in dns_records:
        r_zone_id = dns_record['zone_id']
        r_id = dns_record['id']
        r_name = dns_record['name']
        r_type = dns_record['type']
        r_content = dns_record['content']
        r_ttl = dns_record['ttl']
        r_proxied = dns_record['proxied']
        r_proxiable = dns_record['proxiable']
        print('Record:\t%s %s %s %6d %-5s %s ; proxied=%s proxiable=%s' % (
            r_zone_id, r_id, r_name, r_ttl, r_type, r_content, r_proxied, r_proxiable
        ))

        if r_proxied == new_r_proxied_flag:
            # Nothing to do
            continue

        dns_record_id = dns_record['id']

        new_dns_record = {
            'zone_id': r_zone_id,
            'id': r_id,
            'type': r_type,
            'name': r_name,
            'content': r_content,
            'ttl': r_ttl,
            'proxied': new_r_proxied_flag
        }

        try:
            dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=new_dns_record)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones/dns_records.put %d %s - api call failed' % (e, e))

        r_zone_id = dns_record['zone_id']
        r_id = dns_record['id']
        r_name = dns_record['name']
        r_type = dns_record['type']
        r_content = dns_record['content']
        r_ttl = dns_record['ttl']
        r_proxied = dns_record['proxied']
        r_proxiable = dns_record['proxiable']
        print('Record:\t%s %s %s %6d %-5s %s ; proxied=%s proxiable=%s <<-- after' % (
            r_zone_id, r_id, r_name, r_ttl, r_type, r_content, r_proxied, r_proxiable
        ))

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_settings.py000077500000000000000000000034611461736615400230210ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Grab the first argument, if there is one
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        params = {'per_page':1}

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    # there should only be one zone
    for zone in sorted(zones, key=lambda v: v['name']):
        zone_name = zone['name']
        zone_id = zone['id']
        try:
            settings = cf.zones.settings.get(zone_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.settings.get %d %s - api call failed' % (e, e))

        print(zone_id, zone_name)
        for setting in sorted(settings, key=lambda v: v['id']):
            r_name = setting['id']
            r_value = setting['value']
            r_editable = setting['editable']
            try:
                k = sorted(r_value.keys())
                print('\t%-30s %10s = %s' % (r_name, '(editable)' if r_editable else '', '{'))
                for k in sorted(r_value.keys()):
                    print('\t%-30s %10s    %s = %s' % ('', '', r_name+'/'+k, r_value[k]))
                print('\t%-30s %10s = %s' % ('', '', '}'))
            except AttributeError:
                print('\t%-30s %10s = %s' % (r_name, '(editable)' if r_editable else '', r_value))

        print('')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_show_zones_email.py000066400000000000000000000026311461736615400245210ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import json
sys.path.insert(0, os.path.abspath('..'))

import CloudFlare

def main():
    """Cloudflare API code - example"""

    try:
        zone_name = sys.argv[1]
    except IndexError:
        exit('usage: example_page_rules.py zone')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name': zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zone not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone_id = zones[0]['id']

    routing = cf.zones.email.routing(zone_id)
    print('%s: %s enabled=%s synced=%s status=%s' % (
        routing['tag'],
        routing['name'],
        routing['enabled'],
        routing['synced'],
        routing['status']
    ))

    rules = cf.zones.email.routing.rules(zone_id)
    for r in rules:
        print('%s: matches=%s actions=%s enabled=%s' % (
            r['tag'],
            r['matchers'],
            r['actions'],
            r['enabled']
        ))

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_time_calls.py000066400000000000000000000021051461736615400232640ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import time

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Simple timing of calls

    print('Create')
    tic = time.process_time_ns()
    try:
        cf = CloudFlare.CloudFlare()
    except Exception as e:
        print('\tError: %s' % (e))
        cf = None
    toc = time.process_time_ns()
    print('\t%7.3f ms' % ((toc-tic)/1000000.0))
    print('')

    if not cf:
        return

    print('Call')
    for ii in range(0,10):
        tic = time.process_time_ns()
        try:
            r = cf.ips()
        except Exception as e:
            print('\tError: %s' % (e))
            break
        toc = time.process_time_ns()
        print('\t%7.3f ms' % ((toc-tic)/1000000.0))
    print('')

    print('Close')
    tic = time.process_time_ns()
    del cf
    toc = time.process_time_ns()
    print('\t%7.3f ms' % ((toc-tic)/1000000.0))
    print('')

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_update_dynamic_dns.py000077500000000000000000000103401461736615400250050ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import requests

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def my_ip_address():
    """Cloudflare API code - example"""

    # This list is adjustable - plus some v6 enabled services are needed
    # url = 'http://myip.dnsomatic.com'
    # url = 'http://www.trackip.net/ip'
    # url = 'http://myexternalip.com/raw'
    url = 'https://api.ipify.org'
    try:
        ip_address = requests.get(url).text
    except requests.exceptions.ConnectionError as e:
        exit('%s: failed - %s' % (url, e))
    if ip_address == '':
        exit('%s: failed' % (url))

    if ':' in ip_address:
        ip_address_type = 'AAAA'
    else:
        ip_address_type = 'A'

    return ip_address, ip_address_type

def do_dns_update(cf, zone_name, zone_id, dns_name, ip_address, ip_address_type):
    """Cloudflare API code - example"""

    try:
        params = {'name':dns_name, 'match':'all', 'type':ip_address_type}
        dns_records = cf.zones.dns_records.get(zone_id, params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones/dns_records %s - %d %s - api call failed' % (dns_name, e, e))

    updated = False

    # update the record - unless it's already correct
    for dns_record in dns_records:
        old_ip_address = dns_record['content']
        old_ip_address_type = dns_record['type']

        if ip_address_type not in ['A', 'AAAA']:
            # we only deal with A / AAAA records
            continue

        if ip_address_type != old_ip_address_type:
            # only update the correct address type (A or AAAA)
            # we don't see this becuase of the search params above
            print('IGNORED: %s %s ; wrong address family' % (dns_name, old_ip_address))
            continue

        if ip_address == old_ip_address:
            print('UNCHANGED: %s %s' % (dns_name, ip_address))
            updated = True
            continue

        proxied_state = dns_record['proxied']

        # Yes, we need to update this record - we know it's the same address type

        dns_record_id = dns_record['id']
        dns_record = {
            'name':dns_name,
            'type':ip_address_type,
            'content':ip_address,
            'proxied':proxied_state
        }
        try:
            dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=dns_record)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            exit('/zones.dns_records.put %s - %d %s - api call failed' % (dns_name, e, e))
        print('UPDATED: %s %s -> %s' % (dns_name, old_ip_address, ip_address))
        updated = True

    if updated:
        return

    # no exsiting dns record to update - so create dns record
    dns_record = {
        'name':dns_name,
        'type':ip_address_type,
        'content':ip_address
    }
    try:
        dns_record = cf.zones.dns_records.post(zone_id, data=dns_record)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones.dns_records.post %s - %d %s - api call failed' % (dns_name, e, e))
    print('CREATED: %s %s' % (dns_name, ip_address))

def main():
    """Cloudflare API code - example"""

    try:
        dns_name = sys.argv[1]
    except IndexError:
        exit('usage: example-update-dynamic-dns.py fqdn-hostname')

    host_name, zone_name = '.'.join(dns_name.split('.')[:2]), '.'.join(dns_name.split('.')[-2:])

    ip_address, ip_address_type = my_ip_address()

    print('MY IP: %s %s' % (dns_name, ip_address))

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name':zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zone not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone = zones[0]

    zone_name = zone['name']
    zone_id = zone['id']

    do_dns_update(cf, zone_name, zone_id, dns_name, ip_address, ip_address_type)
    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_user.py000077500000000000000000000127021461736615400221350ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    cf = CloudFlare.CloudFlare()

    print('USER:')
    # grab the user info
    try:
        user = cf.user.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/user.get %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/user.get - %s - api call failed' % (e))
    for kk in sorted(user.keys()):
        if isinstance(user[kk], list):
            if isinstance(user[kk][0], dict):
                print('\t%-40s =' % (kk))
                for ll in user[kk]:
                    for jj in sorted(ll.keys()):
                        if isinstance(ll[jj], list):
                            print('\t%-40s   %s = [ %s ]' % ('', jj, ', '.join(ll[jj])))
                        else:
                            print('\t%-40s   %s = %s' % ('', jj, ll[jj]))
            else:
                print('\t%-40s = [ %s ]' % (kk, ', '.join(user[kk])))
        elif isinstance(user[kk], dict):
            print('\t%-40s =' % (kk))
            for jj in sorted(user[kk].keys()):
                print('\t%-40s   %s = %s' % ('', jj, user[kk][jj]))
        else:
            print('\t%-40s = %s' % (kk, user[kk]))
    print('')

    print('ORGANIZATIONS:')
    # grab the user organizations info
    try:
        organizations = cf.user.organizations.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/user.organizations.get %d %s - api call failed' % (e, e))
    if len(organizations) == 0:
        print('\tNo organization')
    for organization in organizations:
        organization_name = organization['name']
        organization_id = organization['id']
        organization_status = organization['status']
        print('\t%-40s %-10s %s' % (organization_id, organization_status, organization_name))
    print('')

    print('INVITES:')
    # grab the user invites info
    try:
        invites = cf.user.invites.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/user.invites.get %d %s - api call failed' % (e, e))
    if len(invites) == 0:
        print('\tNo user invites')
    for invite in invites:
        invited_member_id = invite['invited_member_id']
        invited_member_email = invite['invited_member_email']
        organization_id = invite['organization_id']
        organization_name = invite['organization_name']
        invited_by = invite['invited_by']
        invited_on = invite['invited_on']
        expires_on = invite['expires_on']
        status = invite['status']
        print('\t %s %s %s %s %s %s %s %s' % (
            organization_id,
            status,
            invited_member_id,
            invited_member_email,
            organization_name,
            invited_by,
            invited_on,
            expires_on
        ))
    print('')

    print('BILLING:')
    # grab the user billing profile info
    try:
        profile = cf.user.billing.profile.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/user.billing.profile.get %d %s - api call failed' % (e, e))
    profile_id = profile['id']
    profile_first = profile['first_name']
    profile_last = profile['last_name']
    profile_company = profile['company'] if 'company' in profile else ''
    if profile_company is None:
        profile_company = ''

    if profile['payment_email'] != '':
        payment_email = profile['payment_email']
        card_number = None
        card_expiry_year = None
        card_expiry_month = None
    else:
        payment_email = None
        card_number = profile['card_number']
        card_expiry_year = profile['card_expiry_year']
        card_expiry_month = profile['card_expiry_month']

    if payment_email is not None:
        print('\t %s %s %s %s PayPal: %s' % (
            profile_id,
            profile_first,
            profile_last,
            profile_company,
            payment_email
        ))
    else:
        if card_number is None:
            card_number = '---- ---- ----- ----'
        if card_expiry_year is not None and card_expiry_month is not None:
            card_expiry = card_expiry_month + '/' + card_expiry_year
        else:
            card_expiry = '--/--'
        print('\t %s %s %s %s CC: %s %s' % (
            profile_id,
            profile_first,
            profile_last,
            profile_company,
            card_number,
            card_expiry
        ))

    print('')

    print('BILLING HISTORY:')
    # grab the user billing history info
    try:
        history = cf.user.billing.history.get()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/user.billing.history.get %d %s - api call failed' % (e, e))
    if len(history) == 0:
        print('\tNo billing history')
    for h in sorted(history, key=lambda v: v['occurred_at']):
        history_id = h['id']
        history_type = h['type']
        history_action = h['action']
        history_occurred_at = h['occurred_at']
        history_amount = h['amount']
        history_currency = h['currency']
        history_description = h['description']
        print('\t %s %s %s %s %s %s %s' % (
            history_id,
            history_type,
            history_action,
            history_occurred_at,
            history_amount,
            history_currency,
            history_description
        ))

    print('')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_user_tokens.py000077500000000000000000000042141461736615400235170ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    #
    # Usage: examples/example_user_tokens.py [config file profile name]
    #
    # Store your access token in the config file as-per the README ("Cloudflare" is the default).
    #
    # $ cat ~/.cloudflare/cloudflare.cfg
    # [Work]
    # token = 00000000000000000000000000000000
    # [Home]
    # email = home@example.com
    # token = 00000000000000000000000000000000
    # $
    #

    try:
        profile_id = sys.argv[1]
    except IndexError:
        profile_id = None

    cf = CloudFlare.CloudFlare(profile=profile_id)

    # display all the users tokens
    try:
        v = cf.user.tokens()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        print('/user.tokens.get %d %s - api call failed' % (e, e))
        v = None
    except Exception as e:
        exit('/user.tokens.get - %s - api call failed' % (e))

    if v:
        print('TOKENS:')
        for t in v:
            print('  %s %s [%-20s %-20s %-20s] %d %s' % (
                t['id'],
                t['status'],
                t['issued_on'],
                t['modified_on'],
                t['last_used_on'],
                len(t['policies']),
                t['name']
            ))
        print('')

    # verify the user token being used (vs. email/key - this will throw an exception if it's not valid
    try:
        v = cf.user.tokens.verify()
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        # exit('/user.tokens.verify.get %d %s - api call failed' % (e, e))
        v = None
    except Exception as e:
        exit('/user.tokens.verify.get - %s - api call failed' % (e))

    if v:
        print('VERIFYED TOKENS')
        print(' %s %-10s [%-20s %-20s]' % (
            v['id'],
            v['status'],
            v['not_before'] if 'not_before' in v else '',
            v['expires_on'] if 'expires_on' in v else ''
        ))
    else:
        print('User token not verified - i.e invalid (or not used)')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_with_usage.py000077500000000000000000000012441461736615400233150ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Grab the first argument, if there is one
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        params = {'per_page':50}

    #
    # Show how 'with' statement works
    #
    with CloudFlare.CloudFlare() as cf:
        zones = cf.zones(params=params)
        for zone in sorted(zones, key=lambda v: v['name']):
            print(zone['id'], zone['name'])

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_zone_purge_cache.py000066400000000000000000000041571461736615400244610ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import CloudFlare

def main():
    """Cloudflare API code - example"""

    method = 'POST'

    try:
        if sys.argv[1] == '--delete':
            del sys.argv[1]
            method = 'DELETE'
    except IndexError:
        pass

    try:
        zone_name = sys.argv[1]
    except IndexError:
        exit('usage: example_zone_purge_cache.py zone')

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        params = {'name': zone_name}
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    if len(zones) == 0:
        exit('/zones.get - %s - zone not found' % (zone_name))

    if len(zones) != 1:
        exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones)))

    zone_id = zones[0]['id']
    zone_type = zones[0]['plan']['name']

    if 'Enterprise' in zone_type:
        # Enterprise accounts can do all things ...
        data = {
            # 'purge_everything': True,
            'hosts': [zone_name],
            'tags': ['random-tag'],
            'prefixes': [zone_name + '/' + 'index.html'],
        }
    else:
        # Free, Pro, Business accounts can only do this ...
        data = {'purge_everything': True}

    print('%s: zone type="%s" and hence using data="%s" method="%s"' % (zone_name, zone_type, data, method))

    try:
        if method == 'DELETE':
            # delete method is not in documents; however, it works
            r = cf.zones.purge_cache.delete(zone_id, data=data)
        else:
            r = cf.zones.purge_cache.post(zone_id, data=data)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones/purge_cache %s - %d %s - api call failed' % (zone_name, e, e))

    if 'id' not in r or r['id'] != zone_id:
        print('%s: weird response: result="%s"' % (zone_name, r))

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/examples/example_zone_search.sh000077500000000000000000000006451461736615400234440ustar00rootroot00000000000000:

ZONE=${1-example.com}
EXTRA=${2}

SEARCH_TYPES="
	equal
	not_equal
	greater_than
	less_than
	starts_with
	ends_with
	contains
	starts_with_case_sensitive
	ends_with_case_sensitive
	contains_case_sensitive
	list_contains
"

for search_type in ${SEARCH_TYPES}
do
	echo TRY: "name=${search_type}:${ZONE}"
	cli4 per_page==50 name="${search_type}:${ZONE}" ${EXTRA} /zones/ | jq -r '.[]|.id,.name' | paste - -
done

exit 0

python-cloudflare-2.20.0/examples/example_zones.py000077500000000000000000000045001461736615400223120ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - example"""

import os
import sys
import re

sys.path.insert(0, os.path.abspath('..'))
import CloudFlare

def main():
    """Cloudflare API code - example"""

    # Grab the first argument, if there is one
    try:
        zone_name = sys.argv[1]
        params = {'name':zone_name, 'per_page':1}
    except IndexError:
        params = {'per_page':50}

    cf = CloudFlare.CloudFlare()

    # grab the zone identifier
    try:
        zones = cf.zones.get(params=params)
    except CloudFlare.exceptions.CloudFlareAPIError as e:
        exit('/zones %d %s - api call failed' % (e, e))
    except Exception as e:
        exit('/zones.get - %s - api call failed' % (e))

    # there should only be one zone
    for zone in sorted(zones, key=lambda v: v['name']):
        zone_name = zone['name']
        zone_id = zone['id']
        zone_type = zone['type']
        if 'email' in zone['owner']:
            zone_owner = zone['owner']['email']
        else:
            zone_owner = '"' + zone['owner']['name'] + '"'
        zone_plan = zone['plan']['name']

        print('%s %-35s %-30s %-20s %s' % (zone_id, zone_name, zone_type, zone_owner, zone_plan))

        try:
            dns_records = cf.zones.dns_records.get(zone_id)
        except CloudFlare.exceptions.CloudFlareAPIError as e:
            sys.stderr.write('/zones/dns_records %d %s - api call failed\n' % (e, e))
            continue

        prog = re.compile(r'\.*'+zone_name+'$')
        dns_records = sorted(dns_records, key=lambda v: prog.sub('', v['name']) + '_' + v['type'])
        for dns_record in dns_records:
            r_name = dns_record['name']
            r_type = dns_record['type']
            if 'content' in dns_record:
                r_value = dns_record['content']
            else:
                # should not happen
                r_value = ''
            if 'priority' in dns_record:
                r_priority = dns_record['priority']
            else:
                r_priority = ''
            r_ttl = dns_record['ttl']
            if zone_type == 'secondary':
                r_id = 'secondary'
            else:
                r_id = dns_record['id']
            print('\t%s %60s %6d %-5s %4s %s' % (r_id, r_name, r_ttl, r_type, r_priority, r_value))

        print('')

    exit(0)

if __name__ == '__main__':
    main()
python-cloudflare-2.20.0/pylintrc000066400000000000000000000020711461736615400170360ustar00rootroot00000000000000[MASTER]
; load-plugins=pylint_mccabe
load-plugins=
    pylint.extensions.no_self_use,
    pylint.extensions.bad_builtin

[REPORTS]
output-format=colorized

[MESSAGES CONTROL]
disable=
    attribute-defined-outside-init,
    duplicate-code,
    fixme,
    locally-disabled,
    too-few-public-methods,
    too-many-ancestors,
    too-many-lines,
    unused-argument,
    consider-using-f-string,
    RP0001,
    RP0002,
    RP0003,
    RP0101,
    RP0401,
    RP0402,
    RP0701,
    RP0801,
    W0311

[DESIGN]
max-attributes=12
max-args=12
max-statements=160
max-branches=70
max-locals=40
max-nested-blocks=10

[FORMAT]
max-line-length=200

[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$|test_[a-zA-Z_][a-zA-Z0-9_]{2,100}$|setUp$|tearDown$
method-rgx=[a-z_][a-z0-9_]{2,30}$|test_[a-zA-Z_][a-zA-Z0-9_]{2,100}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$|test_[a-zA-Z_][a-zA-Z0-9_]{2,100}$
argument-rgx=[a-z_][a-z0-9_]{0,30}$|test_[a-zA-Z_][a-zA-Z0-9_]{2,100}$
attr-rgx=[a-z_][a-z0-9_]{2,30}$|maxDiff$
exclude-protected=_asdict,_fields,_replace,_source,_make,_meta
no-docstring-rgx=^Meta$|^_
python-cloudflare-2.20.0/requirements.txt000066400000000000000000000000631461736615400205320ustar00rootroot00000000000000requests>=2.4.2
pyyaml
jsonlines
pytest
pytest-cov
python-cloudflare-2.20.0/setup.cfg000066400000000000000000000002611461736615400170670ustar00rootroot00000000000000#
# setup.cfg for setup.py - not used presently
#
#[build]
#
#[install]
#
#[sdist]
#
#[upload]
#
[options.extras_require]
test =
    pytest

[tool:pytest]
testpaths =
    tests
python-cloudflare-2.20.0/setup.py000077500000000000000000000045021461736615400167650ustar00rootroot00000000000000#!/usr/bin/env python
"""Cloudflare API code - setup.py file"""
import re
from setuptools import setup

_version_re = re.compile(r"__version__\s=\s'(.*)'")


def main():
    """Cloudflare API code - setup.py file"""

    with open('README.md', encoding="utf-8") as read_me:
        long_description = read_me.read()

    with open('CloudFlare/__init__.py', 'r') as f:
        version = _version_re.search(f.read()).group(1)

    setup(
        name='cloudflare',
        version=version,
        description='Python wrapper for the Cloudflare v4 API',
        long_description=long_description,
        long_description_content_type='text/markdown',
        author='Martin J. Levy',
        author_email='mahtin@mahtin.com',
        url='https://github.com/cloudflare/python-cloudflare',
        project_urls={
            "Documentation": "https://python-cloudflare.readthedocs.io/",
            "API Documentaton": "https://developers.cloudflare.com/api/",
            "Source Code": "https://github.com/cloudflare/python-cloudflare",
        },
        license='MIT',
        options={"bdist_wheel": {"universal": True}},
        packages=['CloudFlare', 'CloudFlare/tests', 'cli4', 'examples'],
        test_suite="CloudFlare.tests",
        include_package_data=True,
        data_files=[('share/man/man1', ['cli4/cli4.1'])],
        install_requires=['requests', 'pyyaml', 'jsonlines'],
        keywords='cloudflare',
        entry_points={
            'console_scripts': [
                'cli4=cli4.__main__:main'
            ]
        },
        python_requires='>3.6.0',
        classifiers=[
            'Development Status :: 5 - Production/Stable',
            'Intended Audience :: Developers',
            'Topic :: Software Development :: Libraries :: Python Modules',
            'License :: OSI Approved :: MIT License',
            'Programming Language :: Python',
            'Programming Language :: Python :: 3',
            'Programming Language :: Python :: 3.6',
            'Programming Language :: Python :: 3.7',
            'Programming Language :: Python :: 3.8',
            'Programming Language :: Python :: 3.9',
            'Programming Language :: Python :: 3.10',
            'Programming Language :: Python :: 3.11',
            'Programming Language :: Python :: 3 :: Only',
        ]
    )


if __name__ == '__main__':
    main()