Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-cloudflare for 
openSUSE:Factory checked in at 2022-01-23 18:38:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cloudflare (Old)
 and      /work/SRC/openSUSE:Factory/.python-cloudflare.new.1938 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-cloudflare"

Sun Jan 23 18:38:44 2022 rev:8 rq:948246 version:2.8.15

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cloudflare/python-cloudflare.changes      
2020-07-14 08:00:59.122119080 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-cloudflare.new.1938/python-cloudflare.changes
    2022-01-23 18:38:47.989901928 +0100
@@ -1,0 +2,37 @@
+Sun Jan 23 16:44:10 UTC 2022 - Dirk M??ller <[email protected]>
+
+- update to 2.8.15:
+  * added cursor example
+  * zones/rulesets added
+  * Removing excess trailing parenthesis
+  * first pass at adding travis CI
+  * zones/waiting_rooms, accounts/diagnostics, and more
+  * configparser added it haste, but not needed - oops!
+  * /zones/:id/access/...
+  * configparser missing - oops!
+  * cleaner and easier way to find missing api calls - this changes 
--dump/--api for the better
+  * /zones/:id/access/apps/policies - fixed along with a bunch more similar 
typos
+  * added more profile info
+  * revoke-tokens -> revoke_tokens
+  * updated and included AMP RealURL/Signed Exchange API
+  * now with curl style debug - i.e. matches api information page
+  * now with curl style debug - i.e. matches api information page
+  * now with access_requests as underscore
+  * make sure verbose, etc is always used
+  * dashes vs underscores - finally tamed!
+  * /zones/:id/access/apps/:id/revoke-tokens - added
+  * improve deprecated code - add dates, check expire, improve parse of api 
webpage
+  * added support for dashes/underscores in commands and python calls - kinda 
overdue
+  * cleanup of logic around uuid match. added support for dashes/underscores 
in commands
+  * /zones/:id/access/apps/revoke-tokens - removed as depricated
+  * removed deprecated /organizations and /user/virtual_dns api
+  * Added base_url to config and env variables
+  * accounts/:id/rules/lists/bulk_operations/:operation_id - syntax fixed
+  * Added working GraphQL examples
+  * improved debug for jSON based data/params
+  * rules,access/logs,access/apps,etc added
+  * api decode and Makefile now consistent - no leading slash
+  * moved network functions - but forgot one call - now fixed
+  * moved network functions into their own file - part of splitting up a large 
file 
+ 
+-------------------------------------------------------------------

Old:
----
  cloudflare-2.8.3.tar.gz

New:
----
  cloudflare-2.8.15.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-cloudflare.spec ++++++
--- /var/tmp/diff_new_pack.TjGy11/_old  2022-01-23 18:38:49.277893262 +0100
+++ /var/tmp/diff_new_pack.TjGy11/_new  2022-01-23 18:38:49.281893235 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-cloudflare
 #
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2022 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-cloudflare
-Version:        2.8.3
+Version:        2.8.15
 Release:        0
 Summary:        Python wrapper for the Cloudflare v4 API
 License:        MIT
@@ -39,7 +39,7 @@
 Requires:       python-jsonlines
 Requires:       python-requests >= 2.4.2
 Requires(post): update-alternatives
-Requires(postun): update-alternatives
+Requires(postun):update-alternatives
 BuildArch:      noarch
 %python_subpackages
 

++++++ cloudflare-2.8.3.tar.gz -> cloudflare-2.8.15.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/CloudFlare/__init__.py 
new/cloudflare-2.8.15/CloudFlare/__init__.py
--- old/cloudflare-2.8.3/CloudFlare/__init__.py 2020-06-23 03:44:49.000000000 
+0200
+++ new/cloudflare-2.8.15/CloudFlare/__init__.py        2021-01-01 
01:17:57.000000000 +0100
@@ -1,7 +1,7 @@
 """ Cloudflare v4 API"""
 from __future__ import absolute_import
 
-__version__ = '2.8.3'
+__version__ = '2.8.15'
 
 from .cloudflare import CloudFlare
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/CloudFlare/api_decode_from_web.py 
new/cloudflare-2.8.15/CloudFlare/api_decode_from_web.py
--- old/cloudflare-2.8.3/CloudFlare/api_decode_from_web.py      2020-06-19 
23:59:12.000000000 +0200
+++ new/cloudflare-2.8.15/CloudFlare/api_decode_from_web.py     2020-08-04 
23:06:19.000000000 +0200
@@ -1,5 +1,7 @@
 """ API extras for Cloudflare API"""
 
+import datetime
+
 from bs4 import BeautifulSoup, Comment
 
 API_TYPES = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE']
@@ -10,9 +12,31 @@
     cmds = []
     # look for deprecated first in section
     deprecated = False
+    deprecated_date = ''
+    deprecated_already = False
     for tag2 in section.find_all('h3'):
+        # <h3 class="text-warning" data-reactid="13490">Deprecation 
Warning</h3>
         if 'Deprecation Warning' in str(tag2):
             deprecated = True
+            break
+    for tag2 in section.find_all('p'):
+        # <p class="deprecation-date" data-reactid="13491">End of life Date: 
November 2, 2020</p>
+        if 'End of life Date:' in str(tag2):
+            for child in tag2.children:
+                deprecated_date = str(child).replace('End of life 
Date:','').strip()
+                try:
+                    # clean up date
+                    d = datetime.datetime.strptime(deprecated_date, '%B %d, 
%Y')
+                    if d <= datetime.datetime.now():
+                        # already done!
+                        deprecated_already = True
+                    deprecated_date = d.strftime('%Y-%m-%d')
+                except ValueError:
+                    # Lets not worry about all the date formats that could 
show-up. Leave as a string
+                    pass
+                break
+        if deprecated_date != '':
+            break
     # look for all API calls in section
     for tag2 in section.find_all('pre'):
         cmd = []
@@ -24,10 +48,12 @@
         if len(cmd) == 0:
             continue
         action = cmd[0]
-        cmd = '/' + ''.join(cmd[1:])
         if action == '' or action not in API_TYPES:
             continue
-        v = {'deprecated': deprecated, 'action': action, 'cmd': cmd}
+        cmd = ''.join(cmd[1:])
+        if cmd[0] != '/':
+            cmd = '/' + cmd
+        v = {'action': action, 'cmd': cmd, 'deprecated': deprecated, 
'deprecated_date': deprecated_date, 'deprecated_already': deprecated_already}
         cmds.append(v)
     return cmds
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/CloudFlare/api_v4.py 
new/cloudflare-2.8.15/CloudFlare/api_v4.py
--- old/cloudflare-2.8.3/CloudFlare/api_v4.py   2020-06-17 21:27:12.000000000 
+0200
+++ new/cloudflare-2.8.15/CloudFlare/api_v4.py  2020-12-31 23:16:37.000000000 
+0100
@@ -9,11 +9,11 @@
     user_load_balancers(self)
     user_load_balancing_analytics(self)
     user_tokens_verify(self)
-    user_virtual_dns(self)
     user_workers(self)
 
     # The API commands for /zones/
     zones(self)
+    zones_access(self)
     zones_amp(self)
     zones_analytics(self)
     zones_argo(self)
@@ -30,16 +30,12 @@
     zones_settings(self)
     zones_spectrum(self)
     zones_ssl(self)
+    zones_waiting_rooms(self)
     zones_workers(self)
 
     # The API commands for /railguns/
     railguns(self)
 
-    # The API commands for /organizations/
-    organizations(self)
-    organizations_audit_logs(self)
-    organizations_virtual_dns(self)
-
     # The API commands for /certificates/
     certificates(self)
 
@@ -48,8 +44,10 @@
 
     # 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)
@@ -82,11 +80,6 @@
     """ API core commands for Cloudflare API"""
 
     self.add('AUTH', "zones")
-    self.add('VOID', "zones", "access")
-    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/certificates")
     self.add('AUTH', "zones", "activation_check")
     self.add('AUTH', "zones", "available_plans")
     self.add('AUTH', "zones", "available_rate_plans")
@@ -107,6 +100,8 @@
     self.add('AUTH', "zones", "purge_cache")
     self.add('AUTH', "zones", "railguns")
     self.add('AUTH', "zones", "railguns", "diagnose")
+    self.add('AUTH', 'zones', 'rulesets')
+    self.add('AUTH', 'zones', 'rulesets', 'versions')
     self.add('VOID', "zones", "security")
     self.add('AUTH', "zones", "security/events")
     self.add('AUTH', "zones", "subscription")
@@ -200,7 +195,7 @@
     """ API core commands for Cloudflare API"""
 
     self.add('VOID', "zones", "amp")
-    self.add('AUTH', "zones", "amp/viewer")
+    self.add('AUTH', "zones", "amp/sxg")
 
 def zones_logpush(self):
     """ API core commands for Cloudflare API"""
@@ -234,23 +229,6 @@
     self.add('AUTH', "railguns")
     self.add('AUTH', "railguns", "zones")
 
-def organizations(self):
-    """ API core commands for Cloudflare API"""
-
-    self.add('AUTH', "organizations")
-    self.add('AUTH', "organizations", "members")
-    self.add('AUTH', "organizations", "invites")
-    self.add('AUTH', "organizations", "railguns")
-    self.add('AUTH', "organizations", "railguns", "zones")
-    self.add('AUTH', "organizations", "roles")
-    self.add('VOID', "organizations", "firewall")
-    self.add('VOID', "organizations", "firewall/access_rules")
-    self.add('AUTH', "organizations", "firewall/access_rules/rules")
-    self.add('VOID', "organizations", "load_balancers")
-    self.add('AUTH', "organizations", "load_balancers/monitors")
-    self.add('AUTH', "organizations", "load_balancers/pools")
-    self.add('AUTH', "organizations", "load_balancers/pools", "health")
-
 def certificates(self):
     """ API core commands for Cloudflare API"""
 
@@ -331,18 +309,12 @@
     self.add('VOID', "user/load_balancers")
     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")
-
-def user_virtual_dns(self):
-    """ API core commands for Cloudflare API"""
-
-    self.add('AUTH', "user/virtual_dns")
-    self.add('VOID', "user/virtual_dns", "dns_analytics")
-    self.add('AUTH', "user/virtual_dns", "dns_analytics/report")
-    self.add('AUTH', "user/virtual_dns", "dns_analytics/report/bytime")
+    self.add('AUTH', 'user/load_balancers/pools', 'references')
 
 def user_workers(self):
     """ API core commands for Cloudflare API"""
@@ -350,14 +322,6 @@
     self.add('VOID', "user/workers")
     self.add('AUTH', "user/workers/scripts")
 
-def organizations_virtual_dns(self):
-    """ API core commands for Cloudflare API"""
-
-    self.add('AUTH', "organizations", "virtual_dns")
-    self.add('VOID', "organizations", "virtual_dns", "dns_analytics")
-    self.add('AUTH', "organizations", "virtual_dns", "dns_analytics/report")
-    self.add('AUTH', "organizations", "virtual_dns", 
"dns_analytics/report/bytime")
-
 def user_audit_logs(self):
     """ API core commands for Cloudflare API"""
 
@@ -366,9 +330,8 @@
 def user_load_balancing_analytics(self):
     """ API core commands for Cloudflare API"""
 
-    self.add('VOID', "user", "load_balancing_analytics")
-    self.add('AUTH', "user", "load_balancing_analytics/events")
-    self.add('AUTH', "user", "load_balancing_analytics/entities")
+    self.add('VOID', "user/load_balancing_analytics")
+    self.add('AUTH', "user/load_balancing_analytics/events")
 
 def user_tokens_verify(self):
     """ API core commands for Cloudflare API"""
@@ -378,30 +341,26 @@
     self.add('AUTH', "user/tokens/verify")
     self.add('AUTH', "user/tokens", "value")
 
-def organizations_audit_logs(self):
-    """ API core commands for Cloudflare API"""
-
-    self.add('AUTH', "organizations", "audit_logs")
-
 def accounts(self):
     """ API core commands for Cloudflare API"""
 
     self.add('AUTH', "accounts")
-    self.add('VOID', "accounts", "access")
-    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/revoke_user")
-    self.add('AUTH', "accounts", "access/service_tokens")
     self.add('VOID', "accounts", "billing")
     self.add('AUTH', "accounts", "billing/profile")
     self.add('AUTH', "accounts", "custom_pages")
     self.add('AUTH', "accounts", "members")
     self.add('AUTH', "accounts", "railguns")
-    self.add('AUTH', "accounts", "railguns/connections")
+    self.add('AUTH', "accounts", "railguns", "connections")
     self.add('VOID', "accounts", "registrar")
     self.add('AUTH', "accounts", "registrar/domains")
     self.add('AUTH', "accounts", "roles")
+    self.add('VOID', 'accounts', 'rules')
+    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/import')
     self.add('VOID', "accounts", "storage")
     self.add('AUTH', "accounts", "storage/analytics")
     self.add('AUTH', "accounts", "storage/analytics/stored")
@@ -411,12 +370,15 @@
     self.add('AUTH', "accounts", "storage/kv/namespaces", "keys")
     self.add('AUTH', "accounts", "storage/kv/namespaces", "values")
     self.add('AUTH', "accounts", "subscriptions")
+    self.add('AUTH', 'accounts', 'tunnels')
+    self.add('AUTH', 'accounts', 'tunnels', 'connections')
     self.add('AUTH', "accounts", "virtual_dns")
-    self.add('VOID', "accounts", "virtual_dns/dns_analytics")
-    self.add('AUTH', "accounts", "virtual_dns/dns_analytics/report")
-    self.add('AUTH', "accounts", "virtual_dns/dns_analytics/report/bytime")
+    self.add('VOID', "accounts", "virtual_dns", "dns_analytics")
+    self.add('AUTH', "accounts", "virtual_dns", "dns_analytics/report")
+    self.add('AUTH', "accounts", "virtual_dns", "dns_analytics/report/bytime")
     self.add('VOID', "accounts", "workers")
     self.add('AUTH', "accounts", "workers/scripts")
+    self.add('AUTH', 'accounts', 'workers/scripts', 'schedules')
 
 def accounts_addressing(self):
     """ API core commands for Cloudflare API"""
@@ -439,9 +401,12 @@
     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):
@@ -456,18 +421,21 @@
 
     self.add('VOID', "accounts", "secondary_dns")
     self.add('AUTH', "accounts", "secondary_dns/masters")
+    self.add('AUTH', 'accounts', 'secondary_dns/primaries')
     self.add('AUTH', "accounts", "secondary_dns/tsigs")
 
 def accounts_stream(self):
     """ API core commands for Cloudflare API"""
 
     self.add('AUTH', "accounts", "stream")
+    self.add('AUTH', "accounts", "stream", "captions")
     self.add('AUTH', "accounts", "stream/copy")
     self.add('AUTH', "accounts", "stream/direct_upload")
-    self.add('AUTH', "accounts", "stream/embed")
+    self.add('AUTH', "accounts", "stream", "embed")
     self.add('AUTH', "accounts", "stream/keys")
     self.add('AUTH', "accounts", "stream/preview")
-    self.add('AUTH', "accounts", "stream", "captions")
+    self.add('AUTH', "accounts", "stream/watermarks")
+    self.add('AUTH', "accounts", "stream/webhook")
 
 def zones_media(self):
     """ API core commands for Cloudflare API"""
@@ -485,3 +453,50 @@
     """ API core commands for Cloudflare API"""
 
     self.add('AUTH', "graphql")
+
+def zones_access(self):
+    """ API core commands for Cloudflare API"""
+
+    self.add('VOID', "zones", "access")
+    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/certificates")
+    #self.add('AUTH', "zones", "access/apps/ca")
+    self.add('AUTH', "zones", "access/apps", "ca")
+    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):
+    """ API core commands for Cloudflare API"""
+
+    self.add('VOID', "accounts", "access")
+    self.add('AUTH', 'accounts', 'access/certificates')
+    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/revoke_user")
+    self.add('AUTH', "accounts", "access/service_tokens")
+    self.add('VOID', "accounts", "access/logs")
+    self.add('AUTH', 'accounts', 'access/logs/access_requests')
+    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')
+
+def accounts_diagnostics(self):
+    """ API core commands for Cloudflare API"""
+
+    self.add('VOID', 'accounts', 'diagnostics')
+    self.add('AUTH', 'accounts', 'diagnostics/traceroute')
+
+def zones_waiting_rooms(self):
+    """ API core commands for Cloudflare API"""
+
+    self.add('AUTH', 'zones', 'waiting_rooms')
+    self.add('AUTH', 'zones', 'waiting_rooms', 'status')
+    self.add('AUTH', 'zones', 'waiting_rooms/preview')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/CloudFlare/cloudflare.py 
new/cloudflare-2.8.15/CloudFlare/cloudflare.py
--- old/cloudflare-2.8.3/CloudFlare/cloudflare.py       2020-06-23 
01:09:21.000000000 +0200
+++ new/cloudflare-2.8.15/CloudFlare/cloudflare.py      2020-08-04 
22:21:31.000000000 +0200
@@ -4,8 +4,9 @@
 import json
 import requests
 
+from .network import CFnetwork
 from .logging_helper import CFlogger
-from .utils import user_agent, sanitize_secrets
+from .utils import user_agent, build_curl
 from .read_configs import read_configs
 from .api_v4 import api_v4
 from .api_extras import api_extras
@@ -36,12 +37,15 @@
                 self.certtoken = config['certtoken']
             else:
                 self.certtoken = None
-
-            self.base_url = config['base_url']
+            if 'base_url' in config:
+                self.base_url = config['base_url']
+            else:
+                # We must have a base_url value
+                self.base_url = BASE_URL
             self.raw = config['raw']
             self.use_sessions = config['use_sessions']
             self.profile = config['profile']
-            self.session = None
+            self.network = CFnetwork(use_sessions=self.use_sessions)
             self.user_agent = user_agent()
 
             if 'debug' in config and config['debug']:
@@ -155,71 +159,11 @@
                               identifier1, identifier2, identifier3,
                               params, data, files)
 
-        def _connection(self, method, url, headers=None, params=None, 
data=None, files=None):
-            """ Cloudflare v4 API"""
-
-            if self.use_sessions:
-                if self.session is None:
-                    self.session = requests.Session()
-            else:
-                self.session = requests
-
-            method = method.upper()
-
-            if method == 'GET':
-                return self.session.get(url,
-                                        headers=headers, params=params, 
data=data)
-            if method == 'POST':
-                if isinstance(data, str):
-                    return self.session.post(url,
-                                             headers=headers, params=params, 
data=data, files=files)
-                else:
-                    return self.session.post(url,
-                                             headers=headers, params=params, 
json=data, files=files)
-            if method == 'PUT':
-                if isinstance(data, str):
-                    return self.session.put(url,
-                                            headers=headers, params=params, 
data=data)
-                else:
-                    return self.session.put(url,
-                                            headers=headers, params=params, 
json=data)
-            if method == 'DELETE':
-                if isinstance(data, str):
-                    return self.session.delete(url,
-                                               headers=headers, params=params, 
data=data)
-                else:
-                    return self.session.delete(url,
-                                               headers=headers, params=params, 
json=data)
-            if method == 'PATCH':
-                if isinstance(data, str):
-                    return self.session.request('PATCH', url,
-                                                headers=headers, 
params=params, data=data)
-                else:
-                    return self.session.request('PATCH', url,
-                                                headers=headers, 
params=params, json=data)
-
-            # should never happen
-            raise CloudFlareAPIError(0, 'method not supported')
-
-        def _network(self, method, headers, parts,
+        def _call_network(self, method, headers, parts,
                      identifier1=None, identifier2=None, identifier3=None,
                      params=None, data=None, files=None):
             """ Cloudflare v4 API"""
 
-            if self.logger:
-                self.logger.debug('Call: %s,%s,%s,%s,%s,%s',
-                                  str(parts[0]),
-                                  str(identifier1),
-                                  str(parts[1]),
-                                  str(identifier2),
-                                  str(parts[2]),
-                                  str(identifier3))
-                self.logger.debug('Call: optional params and data %s %s',
-                                  str(params),
-                                  str(data))
-                if files:
-                    self.logger.debug('Call: upload file %r', files)
-
             if (method is None) or (parts[0] is None):
                 # should never happen
                 raise CloudFlareInternalError(0, 'You must specify a method 
and endpoint')
@@ -252,23 +196,16 @@
                 url += '/' + identifier3
 
             if self.logger:
-                self.logger.debug('Call: method and url %s %s', str(method), 
str(url))
-                self.logger.debug('Call: headers %s', 
str(sanitize_secrets(headers)))
+                msg = build_curl(method, url, headers, params, data, files)
+                self.logger.debug('Call: emulated curl command ...\n' + msg)
 
             try:
-                if self.logger:
-                    self.logger.debug('Call: doit!')
-                response = self._connection(method, url, headers, params, 
data, files)
-                if self.logger:
-                    self.logger.debug('Call: done!')
+                response = self.network(method, url, headers, params, data, 
files)
             except Exception as e:
                 if self.logger:
                     self.logger.debug('Call: exception! "%s"' % (e))
                 raise CloudFlareAPIError(0, 'connection failed.')
 
-            if self.logger:
-                self.logger.debug('Response: url %s', response.url)
-
             # Create response_{type|code|data}
             try:
                 response_type = response.headers['Content-Type']
@@ -335,13 +272,13 @@
                  params=None, data=None, files=None):
             """ Cloudflare v4 API"""
 
-            [response_type, response_code, response_data] = 
self._network(method,
-                                                                          
headers, parts,
-                                                                          
identifier1,
-                                                                          
identifier2,
-                                                                          
identifier3,
-                                                                          
params, data, files)
-
+            [response_type, response_code, response_data] = 
self._call_network(method,
+                                                                               
headers, parts,
+                                                                               
identifier1,
+                                                                               
identifier2,
+                                                                               
identifier3,
+                                                                               
params, data, files)
+     
             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
@@ -607,7 +544,7 @@
             try:
                 if self.logger:
                     self.logger.debug('Call: doit!')
-                response = self._connection("GET", url)
+                response = self.network('GET', url)
                 if self.logger:
                     self.logger.debug('Call: done!')
             except Exception as e:
@@ -888,14 +825,20 @@
         branch = self
         for element in a[0:-1]:
             try:
-                branch = getattr(branch, element)
+                if '-' in element:
+                    branch = getattr(element, element.replace('-','_'))
+                else:
+                    branch = getattr(branch, element)
             except:
                 # should never happen
                 raise CloudFlareAPIError(0, 'api load name failed')
         name = a[-1]
 
         try:
-            f = getattr(branch, name)
+            if '-' in name:
+                f = getattr(element, name.replace('-','_'))
+            else:
+                f = getattr(branch, name)
             # already exists - don't let it overwrite
             raise CloudFlareAPIError(0, 'api duplicate name found: %s/**%s**' 
% ('/'.join(a[0:-1]), name))
         except AttributeError:
@@ -916,7 +859,11 @@
             # should never happen
             raise CloudFlareAPIError(0, 'api load type mismatch')
 
-        setattr(branch, name, f)
+        if '-' in name:
+            # dashes (vs underscores) cause issues in Python and other 
languages
+            setattr(branch, name.replace('-','_'), f)
+        else:
+            setattr(branch, name, f)
 
     def api_list(self, m=None, s=''):
         """recursive walk of the api tree returning a list of api calls"""
@@ -937,7 +884,9 @@
                 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 '_parts' in d:
-                        w.append(s + '/' + n)
+                        # handle underscores by returning the actual API call 
vs the method name
+                        w.append(str(a)[1:-1])
+                        ## w.append(str(a)[1:-1].replace('/:id/','/'))
                 w = w + self.api_list(a, s + '/' + n)
         return w
 
@@ -946,15 +895,13 @@
 
         return api_decode_from_web(self._base._api_from_web())
 
-    def __init__(self, email=None, token=None, certtoken=None, debug=False, 
raw=False, use_sessions=True, profile=None):
+    def __init__(self, email=None, token=None, certtoken=None, debug=False, 
raw=False, use_sessions=True, profile=None, base_url=None):
         """ Cloudflare v4 API"""
 
-        base_url = BASE_URL
-
         try:
             config = read_configs(profile)
-        except:
-            raise CloudFlareAPIError(0, 'profile/configuration read error')
+        except Exception as e:
+            raise CloudFlareAPIError(0, str(e))
 
         # class creation values override all configuration values
         if email is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/CloudFlare/network.py 
new/cloudflare-2.8.15/CloudFlare/network.py
--- old/cloudflare-2.8.3/CloudFlare/network.py  1970-01-01 01:00:00.000000000 
+0100
+++ new/cloudflare-2.8.15/CloudFlare/network.py 2020-06-24 02:38:21.000000000 
+0200
@@ -0,0 +1,68 @@
+""" Network for Cloudflare API"""
+from __future__ import absolute_import
+
+import requests
+
+from .exceptions import CloudFlareAPIError
+
+class CFnetwork(object):
+    """ Network for Cloudflare API"""
+
+    def __init__(self, use_sessions=True):
+        """ Network for Cloudflare API"""
+
+        self.use_sessions = use_sessions
+        self.session = None
+
+    def __call__(self, method, url, headers=None, params=None, data=None, 
files=None):
+        """ Network for Cloudflare API"""
+
+        if self.use_sessions:
+            if self.session is None:
+                self.session = requests.Session()
+        else:
+            self.session = requests
+
+        method = method.upper()
+
+        if method == 'GET':
+            return self.session.get(url,
+                                    headers=headers, params=params, data=data)
+        if method == 'POST':
+            if isinstance(data, str):
+                return self.session.post(url,
+                                         headers=headers, params=params, 
data=data, files=files)
+            else:
+                return self.session.post(url,
+                                         headers=headers, params=params, 
json=data, files=files)
+        if method == 'PUT':
+            if isinstance(data, str):
+                return self.session.put(url,
+                                        headers=headers, params=params, 
data=data)
+            else:
+                return self.session.put(url,
+                                        headers=headers, params=params, 
json=data)
+        if method == 'DELETE':
+            if isinstance(data, str):
+                return self.session.delete(url,
+                                           headers=headers, params=params, 
data=data)
+            else:
+                return self.session.delete(url,
+                                           headers=headers, params=params, 
json=data)
+        if method == 'PATCH':
+            if isinstance(data, str):
+                return self.session.request('PATCH', url,
+                                            headers=headers, params=params, 
data=data)
+            else:
+                return self.session.request('PATCH', url,
+                                            headers=headers, params=params, 
json=data)
+
+        # should never happen
+        raise CloudFlareAPIError(0, 'method not supported')
+
+    def __del__(self):
+        """ Network for Cloudflare API"""
+
+        if self.use_sessions and self.session:
+            self.session.close()
+            self.session = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/CloudFlare/read_configs.py 
new/cloudflare-2.8.15/CloudFlare/read_configs.py
--- old/cloudflare-2.8.3/CloudFlare/read_configs.py     2020-02-05 
01:18:44.000000000 +0100
+++ new/cloudflare-2.8.15/CloudFlare/read_configs.py    2020-07-18 
01:24:02.000000000 +0200
@@ -11,13 +11,14 @@
     """ reading the config file for Cloudflare API"""
 
     # We return all these values
-    config = {'email': None, 'token': None, 'certtoken': None, 'extras': None, 
'profile': None}
+    config = {'email': None, 'token': None, 'certtoken': None, 'extras': None, 
'base_url': None, 'profile': None}
 
     # envioronment variables override config files - so setup first
     config['email'] = os.getenv('CF_API_EMAIL')
     config['token'] = os.getenv('CF_API_KEY')
     config['certtoken'] = os.getenv('CF_API_CERTKEY')
     config['extras'] = os.getenv('CF_API_EXTRAS')
+    config['base_url'] = os.getenv('CF_API_URL')
     if profile is None:
         profile = 'CloudFlare'
     config['profile'] = profile
@@ -42,7 +43,7 @@
             # however section name is missing - this is an error
             raise Exception("%s: configuration section missing" % (profile))
 
-        for option in ['email', 'token', 'certtoken', 'extras']:
+        for option in ['email', 'token', 'certtoken', 'extras', 'base_url']:
             if option not in config or config[option] is None:
                 try:
                     if option == 'extras':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/CloudFlare/utils.py 
new/cloudflare-2.8.15/CloudFlare/utils.py
--- old/cloudflare-2.8.3/CloudFlare/utils.py    2020-01-08 23:41:32.000000000 
+0100
+++ new/cloudflare-2.8.15/CloudFlare/utils.py   2020-07-22 02:18:18.000000000 
+0200
@@ -33,3 +33,38 @@
         secrets_copy['Authorization'] = redacted_phrase
 
     return secrets_copy
+
+def build_curl(method, url, headers, params, data, 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 -X %s "%s" \\' % (str(method), str(url_full)))
+    # headers
+    h = sanitize_secrets(headers)
+    for k in h:
+        if k is None:
+            continue
+        msg.append('            -H "%s: %s" \\' % (k, h[k]))
+    # data
+    if data is not None:
+        try:
+            str_data = json.dumps(data)
+        except:
+            str_data = str(data)
+        msg.append('            --data \'%s\' \\' % (str_data.replace('\n', ' 
')))
+    # files
+    if files is not None:
+        msg.append('            --form "file=@%s" \\' % (files))
+
+    # remove the last \ from the last line.
+    msg[-1] = msg[-1][:-1]
+
+    return '\n'.join(msg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/PKG-INFO 
new/cloudflare-2.8.15/PKG-INFO
--- old/cloudflare-2.8.3/PKG-INFO       2020-06-23 03:46:31.000000000 +0200
+++ new/cloudflare-2.8.15/PKG-INFO      2021-01-01 01:20:46.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: cloudflare
-Version: 2.8.3
+Version: 2.8.15
 Summary: Python wrapper for the Cloudflare v4 API
 Home-page: https://github.com/cloudflare/python-cloudflare
 Author: Martin J. Levy
@@ -208,6 +208,7 @@
         -  Account email (only if an API Key is being used)
         -  Optional Origin-CA Certificate Token
         -  Optional Debug flag (True/False)
+        -  Optional Profile name (the default is ``Cloudflare``)
         
         .. code:: python
         
@@ -217,7 +218,7 @@
                 cf = CloudFlare.CloudFlare()
         
                 # A minimal call with debug enabled
-                cf = CloudFlare.CloudFlare(debug=True))
+                cf = CloudFlare.CloudFlare(debug=True)
         
                 # An authenticated call using an API Token (note the missing 
email)
                 cf = 
CloudFlare.CloudFlare(token='00000000000000000000000000000000')
@@ -832,6 +833,85 @@
             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.
+        
+        ::
+        
+            $ 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).
+        
+        ::
+        
+            $ 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.
+        
+        ::
+        
+            $ 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 examples
         ~~~~~~~~~~~~~~~~~~~
         
@@ -1069,6 +1149,64 @@
         
         Refer to the Cloudflare Workers API documentation for more information.
         
+        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 <examples/example_graphql.py>`__ 
files
+        for working examples. Here is the working example of the shell version:
+        
+        ::
+        
+            $ 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.
+        
         Implemented API calls
         ---------------------
         
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/README.rst 
new/cloudflare-2.8.15/README.rst
--- old/cloudflare-2.8.3/README.rst     2020-06-20 07:53:59.000000000 +0200
+++ new/cloudflare-2.8.15/README.rst    2020-12-31 23:06:35.000000000 +0100
@@ -200,6 +200,7 @@
 -  Account email (only if an API Key is being used)
 -  Optional Origin-CA Certificate Token
 -  Optional Debug flag (True/False)
+-  Optional Profile name (the default is ``Cloudflare``)
 
 .. code:: python
 
@@ -209,7 +210,7 @@
         cf = CloudFlare.CloudFlare()
 
         # A minimal call with debug enabled
-        cf = CloudFlare.CloudFlare(debug=True))
+        cf = CloudFlare.CloudFlare(debug=True)
 
         # An authenticated call using an API Token (note the missing email)
         cf = CloudFlare.CloudFlare(token='00000000000000000000000000000000')
@@ -824,6 +825,85 @@
     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.
+
+::
+
+    $ 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).
+
+::
+
+    $ 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.
+
+::
+
+    $ 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 examples
 ~~~~~~~~~~~~~~~~~~~
 
@@ -1061,6 +1141,64 @@
 
 Refer to the Cloudflare Workers API documentation for more information.
 
+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 <examples/example_graphql.py>`__ files
+for working examples. Here is the working example of the shell version:
+
+::
+
+    $ 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.
+
 Implemented API calls
 ---------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/cli4/cli4.py 
new/cloudflare-2.8.15/cli4/cli4.py
--- old/cloudflare-2.8.3/cli4/cli4.py   2020-06-23 03:23:50.000000000 +0200
+++ new/cloudflare-2.8.15/cli4/cli4.py  2020-07-24 20:07:15.000000000 +0200
@@ -17,20 +17,22 @@
 import CloudFlare
 from . import converters
 
-def dump_commands():
+def dump_commands(cf):
     """dump a tree of all the known API commands"""
-    cf = CloudFlare.CloudFlare()
     w = cf.api_list()
     sys.stdout.write('\n'.join(w) + '\n')
 
-def dump_commands_from_web():
+def dump_commands_from_web(cf):
     """dump a tree of all the known API commands - from web"""
-    cf = CloudFlare.CloudFlare()
     w = cf.api_from_web()
     for r in w:
         if r['deprecated']:
-            continue
-        sys.stdout.write('%-6s %s\n' % (r['action'], r['cmd']))
+            if r['deprecated_already']:
+                sys.stdout.write('%-6s %s ; deprecated %s - expired!\n' % 
(r['action'], r['cmd'], r['deprecated_date']))
+            else:
+                sys.stdout.write('%-6s %s ; deprecated %s\n' % (r['action'], 
r['cmd'], r['deprecated_date']))
+        else:
+            sys.stdout.write('%-6s %s\n' % (r['action'], r['cmd']))
 
 def run_command(cf, method, command, params=None, content=None, files=None):
     """run the command line"""
@@ -62,7 +64,7 @@
                 if len(element) in [32, 40, 48] and hex_only.match(element):
                     # raw identifier - lets just use it as-is
                     identifier1 = element
-                if len(element) == 36  and uuid_value.match(element):
+                elif len(element) == 36  and uuid_value.match(element):
                     # uuid identifier - lets just use it as-is
                     identifier1 = element
                 elif element[0] == ':':
@@ -97,7 +99,7 @@
                 if len(element) in [32, 40, 48] and hex_only.match(element):
                     # raw identifier - lets just use it as-is
                     identifier2 = element
-                if len(element) == 36  and uuid_value.match(element):
+                elif len(element) == 36  and uuid_value.match(element):
                     # uuid identifier - lets just use it as-is
                     identifier2 = element
                 elif element[0] == ':':
@@ -128,11 +130,14 @@
                 if len(element) in [32, 40, 48] and hex_only.match(element):
                     # raw identifier - lets just use it as-is
                     identifier3 = element
-                if len(element) == 36  and uuid_value.match(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:
                     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
@@ -141,7 +146,11 @@
                         raise e
         else:
             try:
-                m = getattr(m, element)
+                # dashes (vs underscores) cause issues in Python and other 
languages
+                if '-' in element:
+                    m = getattr(m, element.replace('-','_'))
+                else:
+                    m = getattr(m, element)
                 cmd.append(element)
             except AttributeError:
                 # the verb/element was not found
@@ -329,11 +338,13 @@
             method = 'DELETE'
 
     if dump:
-        dump_commands()
+        cf = CloudFlare.CloudFlare(debug=verbose, raw=raw, profile=profile)
+        dump_commands(cf)
         sys.exit(0)
 
     if dump_from_web:
-        dump_commands_from_web()
+        cf = CloudFlare.CloudFlare(debug=verbose, raw=raw, profile=profile)
+        dump_commands_from_web(cf)
         sys.exit(0)
 
     digits_only = re.compile('^-?[0-9]+$')
@@ -380,7 +391,10 @@
                 #value = json.loads(value) - changed to yaml code to remove 
unicode string issues
                 if yaml is None:
                     sys.exit('cli4: install yaml support')
-                value = yaml.safe_load(value_string)
+                try:
+                    value = yaml.safe_load(value_string)
+                except yaml.parser.ParserError as e:
+                    raise ValueError
             except ValueError:
                 sys.exit('cli4: %s="%s" - can\'t parse json value' % 
(tag_string, value_string))
         elif value_string[0] == '@':
@@ -404,10 +418,8 @@
         if tag_string == '':
             # There's no tag; it's just an unnamed list
             if params is None:
-                params = []
-            try:
-                params.append(value)
-            except AttributeError:
+                params = value
+            else:
                 sys.exit('cli4: %s=%s - param error. Can\'t mix unnamed and 
named list' %
                          (tag_string, value_string))
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/cloudflare.egg-info/PKG-INFO 
new/cloudflare-2.8.15/cloudflare.egg-info/PKG-INFO
--- old/cloudflare-2.8.3/cloudflare.egg-info/PKG-INFO   2020-06-23 
03:46:31.000000000 +0200
+++ new/cloudflare-2.8.15/cloudflare.egg-info/PKG-INFO  2021-01-01 
01:20:46.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: cloudflare
-Version: 2.8.3
+Version: 2.8.15
 Summary: Python wrapper for the Cloudflare v4 API
 Home-page: https://github.com/cloudflare/python-cloudflare
 Author: Martin J. Levy
@@ -208,6 +208,7 @@
         -  Account email (only if an API Key is being used)
         -  Optional Origin-CA Certificate Token
         -  Optional Debug flag (True/False)
+        -  Optional Profile name (the default is ``Cloudflare``)
         
         .. code:: python
         
@@ -217,7 +218,7 @@
                 cf = CloudFlare.CloudFlare()
         
                 # A minimal call with debug enabled
-                cf = CloudFlare.CloudFlare(debug=True))
+                cf = CloudFlare.CloudFlare(debug=True)
         
                 # An authenticated call using an API Token (note the missing 
email)
                 cf = 
CloudFlare.CloudFlare(token='00000000000000000000000000000000')
@@ -832,6 +833,85 @@
             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.
+        
+        ::
+        
+            $ 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).
+        
+        ::
+        
+            $ 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.
+        
+        ::
+        
+            $ 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 examples
         ~~~~~~~~~~~~~~~~~~~
         
@@ -1069,6 +1149,64 @@
         
         Refer to the Cloudflare Workers API documentation for more information.
         
+        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 <examples/example_graphql.py>`__ 
files
+        for working examples. Here is the working example of the shell version:
+        
+        ::
+        
+            $ 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.
+        
         Implemented API calls
         ---------------------
         
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/cloudflare.egg-info/SOURCES.txt 
new/cloudflare-2.8.15/cloudflare.egg-info/SOURCES.txt
--- old/cloudflare-2.8.3/cloudflare.egg-info/SOURCES.txt        2020-06-23 
03:46:31.000000000 +0200
+++ new/cloudflare-2.8.15/cloudflare.egg-info/SOURCES.txt       2021-01-01 
01:20:46.000000000 +0100
@@ -10,6 +10,7 @@
 CloudFlare/cloudflare.py
 CloudFlare/exceptions.py
 CloudFlare/logging_helper.py
+CloudFlare/network.py
 CloudFlare/read_configs.py
 CloudFlare/utils.py
 cli4/__init__.py
@@ -33,6 +34,8 @@
 examples/example_delete_zone_entry.py
 examples/example_dns_export.py
 examples/example_dnssec_settings.py
+examples/example_graphql.py
+examples/example_graphql.sh
 examples/example_ips.py
 examples/example_list_api_from_web.py
 examples/example_page_rules.sh
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/examples/example_graphql.py 
new/cloudflare-2.8.15/examples/example_graphql.py
--- old/cloudflare-2.8.3/examples/example_graphql.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/cloudflare-2.8.15/examples/example_graphql.py   2020-07-15 
03:45:23.000000000 +0200
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+"""Cloudflare API code - example"""
+
+import os
+import sys
+import time
+import datetime
+import pytz
+
+sys.path.insert(0, os.path.abspath('..'))
+import CloudFlare
+
+def now_iso8601_time(h_delta):
+    """Cloudflare API code - example"""
+
+    t = time.time() - (h_delta * 3600)
+    r = datetime.datetime.fromtimestamp(int(t), 
tz=pytz.timezone("UTC")).strftime('%Y-%m-%dT%H:%M:%SZ')
+    return r
+
+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:
+        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:
+        exit('/zones.get %d %s - api call failed' % (e, e))
+    except Exception as e:
+        exit('/zones - %s - api call failed' % (e))
+
+    date_before = now_iso8601_time(0) # now
+    date_after = now_iso8601_time(7 * 24) # 7 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[0:10], date_after[0:10]) # only use yyyy-mm-dd 
part for httpRequests1dGroups
+
+    # query - always a post
+    try:
+        r = cf.graphql.post(data={'query':query})
+    except CloudFlare.exceptions.CloudFlareAPIError as e:
+        exit('/graphql.post %d %s - api call failed' % (e, e))
+
+    ## only one zone, so use zero'th element!
+    zone_info = r['data']['viewer']['zones'][0]
+
+    httpRequests1dGroups = zone_info['httpRequests1dGroups']
+
+    for h in sorted(httpRequests1dGroups, 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()
+    exit(0)
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cloudflare-2.8.3/examples/example_graphql.sh 
new/cloudflare-2.8.15/examples/example_graphql.sh
--- old/cloudflare-2.8.3/examples/example_graphql.sh    1970-01-01 
01:00:00.000000000 +0100
+++ new/cloudflare-2.8.15/examples/example_graphql.sh   2020-07-15 
03:43:04.000000000 +0200
@@ -0,0 +1,34 @@
+:
+
+#
+# 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 - - 

Reply via email to