diff -Nru python-cloudflare-2.1.0/cli4/cli4.py python-cloudflare-2.6.5/cli4/cli4.py --- python-cloudflare-2.1.0/cli4/cli4.py 2018-02-25 11:29:49.000000000 +0000 +++ python-cloudflare-2.6.5/cli4/cli4.py 2020-02-10 19:05:49.000000000 +0000 @@ -14,9 +14,8 @@ except ImportError: jsonlines = None -from . import converters - import CloudFlare +from . import converters def dump_commands(): """dump a tree of all the known API commands""" @@ -56,23 +55,30 @@ elif element[0] == ':': # raw string - used for workers script_name - use ::script_name identifier1 = element[1:] - elif 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] == '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: - exit("/%s/%s :NOT CODED YET 1" % ('/'.join(cmd), element)) + 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 Exception("/%s/%s :NOT CODED YET" % ('/'.join(cmd), element)) + except Exception 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): @@ -81,12 +87,17 @@ elif element[0] == ':': # raw string - used for workers script_names identifier2 = element[1:] - elif (cmd[0] and cmd[0] == 'zones') and (cmd[2] and cmd[2] == 'dns_records'): - identifier2 = converters.convert_dns_record_to_identifier(cf, - identifier1, - element) else: - exit("/%s/%s :NOT CODED YET 2" % ('/'.join(cmd), element)) + 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) + else: + raise Exception("/%s/%s :NOT CODED YET" % ('/'.join(cmd), element)) + except Exception 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) + ']') @@ -100,7 +111,8 @@ elif waf_rules.match(element): identifier3 = element else: - exit("/%s/%s :NOT CODED YET 3" % ('/'.join(cmd), element)) + sys.stderr.write('/%s/%s :NOT CODED YET 3\n' % ('/'.join(cmd), element)) + raise e else: try: m = getattr(m, element) @@ -108,12 +120,14 @@ except AttributeError: # the verb/element was not found if len(cmd) == 0: - exit('cli4: /%s - not found' % (element)) + sys.stderr.write('cli4: /%s - not found\n' % (element)) else: - exit('cli4: /%s/%s - not found' % ('/'.join(cmd), element)) + sys.stderr.write('cli4: /%s/%s - not found\n' % ('/'.join(cmd), element)) + raise e if content and params: - exit('cli4: /%s - content and params not allowed together' % (command)) + sys.stderr.write('cli4: /%s - content and params not allowed together\n' % (command)) + raise Exception if content: params = content @@ -122,27 +136,27 @@ identifier2 = [None] for i2 in identifier2: try: - if method is 'GET': + if method == 'GET': r = m.get(identifier1=identifier1, identifier2=i2, identifier3=identifier3, params=params) - elif method is 'PATCH': + elif method == 'PATCH': r = m.patch(identifier1=identifier1, identifier2=i2, identifier3=identifier3, data=params) - elif method is 'POST': + elif method == 'POST': r = m.post(identifier1=identifier1, identifier2=i2, identifier3=identifier3, data=params, files=files) - elif method is 'PUT': + elif method == 'PUT': r = m.put(identifier1=identifier1, identifier2=i2, identifier3=identifier3, data=params) - elif method is 'DELETE': + elif method == 'DELETE': r = m.delete(identifier1=identifier1, identifier2=i2, identifier3=identifier3, @@ -154,11 +168,14 @@ # more than one error returned by the API for x in e: sys.stderr.write('cli4: /%s - %d %s\n' % (command, x, x)) - exit('cli4: /%s - %d %s' % (command, e, e)) + sys.stderr.write('cli4: /%s - %d %s\n' % (command, e, e)) + raise e except CloudFlare.exceptions.CloudFlareInternalError as e: - exit('cli4: InternalError: /%s - %d %s' % (command, e, e)) + sys.stderr.write('cli4: InternalError: /%s - %d %s\n' % (command, e, e)) + raise e except Exception as e: - exit('cli4: /%s - %s - api error' % (command, e)) + sys.stderr.write('cli4: /%s - %s - api error\n' % (command, e)) + raise e results.append(r) return results @@ -166,6 +183,9 @@ def write_results(results, output): """dump the results""" + if output is None: + return + if len(results) == 1: results = results[0] @@ -175,6 +195,8 @@ pass else: # anything more complex (dict, list, etc) should be dumped as JSON/YAML + if output is None: + results = None if output == 'json': try: results = json.dumps(results, @@ -191,14 +213,21 @@ results = yaml.safe_dump(results) if output == 'ndjson': # NDJSON support seems like a hack. There has to be a better way - writer = jsonlines.Writer(sys.stdout) - writer.write_all(results) - writer.close() + try: + writer = jsonlines.Writer(sys.stdout) + writer.write_all(results) + writer.close() + except (BrokenPipeError, IOError): + pass return - sys.stdout.write(results) - if not results.endswith('\n'): - sys.stdout.write('\n') + if results: + try: + 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""" @@ -207,6 +236,7 @@ output = 'json' raw = False dump = False + profile = None method = 'GET' usage = ('usage: cli4 ' @@ -214,27 +244,29 @@ + '[-j|--json] [-y|--yaml] [-n|ndjson]' + '[-r|--raw] ' + '[-d|--dump] ' + + '[-p|--profile profile-name] ' + '[--get|--patch|--post|--put|--delete] ' + '[item=value|item=@filename|@filename ...] ' + '/command...') try: opts, args = getopt.getopt(args, - 'VhvqjyrdGPOUD', + 'Vhvqjyrdp:GPOUD', [ 'version', 'help', 'verbose', 'quiet', 'json', 'yaml', 'ndjson', 'raw', 'dump', + 'profile=', 'get', 'patch', 'post', 'put', 'delete' ]) except getopt.GetoptError: - exit(usage) + sys.exit(usage) for opt, arg in opts: if opt in ('-V', '--version'): - exit('Cloudflare library version: %s' % (CloudFlare.__version__)) + sys.exit('Cloudflare library version: %s' % (CloudFlare.__version__)) if opt in ('-h', '--help'): - exit(usage) + sys.exit(usage) elif opt in ('-v', '--verbose'): verbose = True elif opt in ('-q', '--quiet'): @@ -243,14 +275,16 @@ output = 'json' elif opt in ('-y', '--yaml'): if yaml is None: - exit('cli4: install yaml support') + sys.exit('cli4: install yaml support') output = 'yaml' elif opt in ('-n', '--ndjson'): if jsonlines is None: - exit('cli4: install jsonlines support') + sys.exit('cli4: install jsonlines support') output = 'ndjson' elif opt in ('-r', '--raw'): raw = True + elif opt in ('-p', '--profile'): + profile = arg elif opt in ('-d', '--dump'): dump = True elif opt in ('-G', '--get'): @@ -266,7 +300,7 @@ if dump: dump_commands() - exit(0) + sys.exit(0) digits_only = re.compile('^-?[0-9]+$') floats_only = re.compile('^-?[0-9.]+$') @@ -281,7 +315,7 @@ # a file to be uploaded - used in workers/script - only via PUT filename = arg[1:] if method != 'PUT': - exit('cli4: %s - raw file upload only with PUT' % (filename)) + sys.exit('cli4: %s - raw file upload only with PUT' % (filename)) try: if filename == '-': content = sys.stdin.read() @@ -289,7 +323,7 @@ with open(filename, 'r') as f: content = f.read() except IOError: - exit('cli4: %s - file open failure' % (filename)) + sys.exit('cli4: %s - file open failure' % (filename)) continue tag_string, value_string = arg.split('=', 1) if value_string.lower() == 'true': @@ -298,28 +332,28 @@ value = False elif value_string == '' or value_string.lower() == 'none': value = None - elif value_string[0] is '=' and value_string[1:] == '': - exit('cli4: %s== - no number value passed' % (tag_string)) - elif value_string[0] is '=' and digits_only.match(value_string[1:]): + 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] is '=' and floats_only.match(value_string[1:]): + elif value_string[0] == '=' and floats_only.match(value_string[1:]): value = float(value_string[1:]) - elif value_string[0] is '=': - exit('cli4: %s== - invalid number value passed' % (tag_string)) + 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 if yaml is None: - exit('cli4: install yaml support') + sys.exit('cli4: install yaml support') value = yaml.safe_load(value_string) except ValueError: - exit('cli4: %s="%s" - can\'t parse json value' % (tag_string, value_string)) - elif value_string[0] is '@': + 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 - only via POST filename = value_string[1:] if method != 'POST': - exit('cli4: %s=%s - file upload only with POST' % (tag_string, filename)) + sys.exit('cli4: %s=%s - file upload only with POST' % (tag_string, filename)) files = {} try: if filename == '-': @@ -327,7 +361,7 @@ else: files[tag_string] = open(filename, 'rb') except IOError: - exit('cli4: %s=%s - file open failure' % (tag_string, filename)) + sys.exit('cli4: %s=%s - file open failure' % (tag_string, filename)) # no need for param code below continue else: @@ -340,8 +374,8 @@ try: params.append(value) except AttributeError: - exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' % - (tag_string, value_string)) + sys.exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' % + (tag_string, value_string)) else: if params is None: params = {} @@ -349,21 +383,29 @@ try: params[tag] = value except TypeError: - exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' % - (tag_string, value_string)) + sys.exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' % + (tag_string, value_string)) # what's left is the command itself - if len(args) != 1: - exit(usage) - command = args[0] - - cf = CloudFlare.CloudFlare(debug=verbose, raw=raw) - results = run_command(cf, method, command, params, content, files) - write_results(results, output) + if len(args) < 1: + sys.exit(usage) + commands = args + + try: + cf = CloudFlare.CloudFlare(debug=verbose, raw=raw, profile=profile) + except Exception as e: + sys.exit(e) + + for command in commands: + try: + results = run_command(cf, method, command, params, content, files) + write_results(results, output) + except Exception as e: + if len(commands) > 1: + continue def cli4(args): """Cloudflare API via command line""" do_it(args) - exit(0) - + sys.exit(0) diff -Nru python-cloudflare-2.1.0/cli4/converters.py python-cloudflare-2.6.5/cli4/converters.py --- python-cloudflare-2.1.0/cli4/converters.py 2018-02-16 01:47:46.000000000 +0000 +++ python-cloudflare-2.6.5/cli4/converters.py 2020-01-16 18:14:19.000000000 +0000 @@ -3,20 +3,38 @@ 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: - exit('cli4: %s - %d %s' % (zone_name, e, e)) + raise ConverterError(int(e), '%s - %d %s' % (zone_name, e, e)) except Exception as e: - exit('cli4: %s - %s' % (zone_name, e)) + raise ConverterError(0, '%s - %s' % (zone_name, e)) if len(zones) == 1: return zones[0]['id'] - exit('cli4: %s - zone not found' % (zone_name)) + raise ConverterError('%s: not found' % (zone_name)) + +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, e, e)) + except Exception as e: + raise ConverterError(0, '%s - %s' % (account_name, e)) + + if len(accounts) == 1: + return accounts[0]['id'] + + raise ConverterError('%s: not found' % (account_name)) def convert_dns_record_to_identifier(cf, zone_id, dns_name): """dns record names to numbers""" @@ -25,9 +43,9 @@ try: dns_records = cf.zones.dns_records.get(zone_id, params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: - exit('cli4: %s - %d %s' % (dns_name, e, e)) + raise ConverterError(int(e), '%s - %d %s' % (dns_name, e, e)) except Exception as e: - exit('cli4: %s - %s' % (dns_name, e)) + raise ConverterError(0, '%s - %s' % (dns_name, e)) r = [] for dns_record in dns_records: @@ -36,80 +54,79 @@ if len(r) > 0: return r - exit('cli4: %s - dns name not found' % (dns_name)) + raise ConverterError('%s: not found' % (dns_name)) def convert_certificates_to_identifier(cf, certificate_name): """certificate names to numbers""" try: certificates = cf.certificates.get() except CloudFlare.exceptions.CloudFlareAPIError as e: - exit('cli4: %s - %d %s' % (certificate_name, e, e)) + raise ConverterError(int(e), '%s - %d %s' % (certificate_name, e, e)) except Exception as e: - exit('cli4: %s - %s' % (certificate_name, e)) + raise ConverterError(0, '%s - %s' % (certificate_name, e)) for certificate in certificates: if certificate_name in certificate['hostnames']: return certificate['id'] - exit('cli4: %s - no zone certificates found' % (certificate_name)) + raise ConverterError('%s: not found' % (certificate_name)) def convert_organizations_to_identifier(cf, organization_name): """organizations names to numbers""" try: organizations = cf.user.organizations.get() except CloudFlare.exceptions.CloudFlareAPIError as e: - exit('cli4: %s - %d %s' % (organization_name, e, e)) + raise ConverterError(int(e), '%s - %d %s' % (organization_name, e, e)) except Exception as e: - exit('cli4: %s - %s' % (organization_name, e)) + raise ConverterError(0, '%s - %s' % (organization_name, e)) for organization in organizations: if organization_name == organization['name']: return organization['id'] - exit('cli4: %s - no organizations found' % (organization_name)) + raise ConverterError('%s not found' % (organization_name)) def convert_invites_to_identifier(cf, invite_name): """invite names to numbers""" try: invites = cf.user.invites.get() except CloudFlare.exceptions.CloudFlareAPIError as e: - exit('cli4: %s - %d %s' % (invite_name, e, e)) + raise ConverterError(int(e), '%s - %d %s' % (invite_name, e, e)) except Exception as e: - exit('cli4: %s - %s' % (invite_name, e)) + raise ConverterError(0, '%s - %s' % (invite_name, e)) for invite in invites: if invite_name == invite['organization_name']: return invite['id'] - exit('cli4: %s - no invites found' % (invite_name)) + raise ConverterError('%s: not found' % (invite_name)) 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: - exit('cli4: %s - %d %s\n' % (virtual_dns_name, e, e)) + raise ConverterError(int(e), '%s - %d %s' % (virtual_dns_name, e, e)) except Exception as e: - exit('cli4: %s - %s\n' % (virtual_dns_name, e)) + raise ConverterError(0, '%s - %s' % (virtual_dns_name, e)) for virtual_dns in virtual_dnss: if virtual_dns_name == virtual_dns['name']: return virtual_dns['id'] - exit('cli4: %s - no virtual_dns found' % (virtual_dns_name)) + raise ConverterError('%s: not found' % (virtual_dns_name)) 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: - exit('cli4: %s - %d %s' % (pool_name, e, e)) + raise ConverterError(int(e), '%s - %d %s' % (pool_name, e, e)) except Exception as e: - exit('cli4: %s - %s' % (pool_name, e)) + raise ConverterError(0, '%s - %s' % (pool_name, e)) for p in pools: if pool_name == p['description']: return p['id'] - exit('cli4: %s - no pools found' % (pool_name)) - + raise ConverterError('%s: not found' % (pool_name)) diff -Nru python-cloudflare-2.1.0/cli4/__main__.py python-cloudflare-2.6.5/cli4/__main__.py --- python-cloudflare-2.1.0/cli4/__main__.py 2017-08-22 10:13:23.000000000 +0000 +++ python-cloudflare-2.6.5/cli4/__main__.py 2020-01-15 21:29:49.000000000 +0000 @@ -14,4 +14,3 @@ if __name__ == '__main__': main() - diff -Nru python-cloudflare-2.1.0/CloudFlare/api_extras.py python-cloudflare-2.6.5/CloudFlare/api_extras.py --- python-cloudflare-2.1.0/CloudFlare/api_extras.py 2017-08-22 10:13:23.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/api_extras.py 2020-02-06 02:13:26.000000000 +0000 @@ -2,56 +2,61 @@ import re +from .exceptions import CloudFlareAPIError + def api_extras(self, extras=None): """ API extras for Cloudflare API""" + count = 0; for extra in extras: - if extra == '': - continue 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 = [] - nn = 0 + part = None for element in extra.split('/'): if element[0] == ':': - nn += 1 - continue - try: - parts[nn] - except IndexError: - parts.append([]) - parts[nn].append(element) - - # insert extra command into class - element_path = [] - current = self - for element in parts[0]: - element_path.append(element) - try: - m = getattr(current, element) - # exists - but still add it there's a second part - if element == parts[0][-1] and len(parts) > 1: - api_call_part1 = '/'.join(element_path) - api_call_part2 = '/'.join(parts[1]) - setattr(m, parts[1][0], - self._add_with_auth(self._base, api_call_part1, api_call_part2)) - current = m + parts.append(part) + part = None continue - except: - pass - # does not exist - if element == parts[0][-1] and len(parts) > 1: - # last element - api_call_part1 = '/'.join(element_path) - api_call_part2 = '/'.join(parts[1]) - setattr(current, element, - self._add_with_auth(self._base, api_call_part1, api_call_part2)) + if part: + part += '/' + element else: - api_call_part1 = '/'.join(element_path) - setattr(current, element, - self._add_with_auth(self._base, api_call_part1)) - current = getattr(current, element) + 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 diff -Nru python-cloudflare-2.1.0/CloudFlare/api_v4.py python-cloudflare-2.6.5/CloudFlare/api_v4.py --- python-cloudflare-2.1.0/CloudFlare/api_v4.py 2018-02-25 07:05:02.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/api_v4.py 2020-04-09 01:12:03.000000000 +0000 @@ -8,6 +8,7 @@ user_audit_logs(self) user_load_balancers(self) user_load_balancing_analytics(self) + user_tokens_verify(self) user_virtual_dns(self) user_workers(self) @@ -20,10 +21,13 @@ zones_dnssec(self) zones_firewall(self) zones_load_balancers(self) + zones_logpush(self) zones_logs(self) zones_media(self) zones_rate_limits(self) + zones_secondary_dns(self) zones_settings(self) + zones_spectrum(self) zones_ssl(self) zones_workers(self) @@ -34,7 +38,6 @@ organizations(self) organizations_audit_logs(self) organizations_virtual_dns(self) - organizations_workers(self) # The API commands for /certificates/ certificates(self) @@ -42,9 +45,17 @@ # The API commands for /ips/ ips(self) - # The API commands for /account/ - account(self) - account_load_balancing_analytics(self) + # The API commands for /accounts/ + accounts(self) + accounts_addressing(self) + accounts_audit_logs(self) + accounts_firewall(self) + accounts_load_balancers(self) + accounts_secondary_dns(self) + accounts_stream(self) + + # The API commands for /memberships/ + memberships(self) def user(self): """ API core commands for Cloudflare API""" @@ -67,46 +78,70 @@ """ 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") self.add('AUTH', "zones", "custom_certificates") self.add('AUTH', "zones", "custom_certificates/prioritize") self.add('AUTH', "zones", "custom_hostnames") + self.add('AUTH', "zones", "custom_hostnames/fallback_origin") 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") + self.add('AUTH', "zones", "filters") + self.add('AUTH', "zones", "healthchecks") + self.add('AUTH', "zones", "healthchecks/preview") self.add('AUTH', "zones", "keyless_certificates") 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('VOID', "zones", "security") + self.add('AUTH', "zones", "security/events") self.add('AUTH', "zones", "subscription") - self.add('AUTH', "zones", "subscriptions") def zones_settings(self): """ API core commands for Cloudflare API""" 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/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/email_obfuscation") + 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/opportunistic_encryption") + self.add('AUTH', "zones", "settings/opportunistic_onion") self.add('AUTH', "zones", "settings/origin_error_page_pass_thru") 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/pseudo_ipv4") self.add('AUTH', "zones", "settings/response_buffering") self.add('AUTH', "zones", "settings/rocket_loader") self.add('AUTH', "zones", "settings/security_header") @@ -114,19 +149,12 @@ 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/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/tls_1_2_only") - self.add('AUTH', "zones", "settings/tls_1_3") - self.add('AUTH', "zones", "settings/websockets") self.add('AUTH', "zones", "settings/waf") self.add('AUTH', "zones", "settings/webp") - self.add('AUTH', "zones", "settings/http2") - self.add('AUTH', "zones", "settings/pseudo_ipv4") - self.add('AUTH', "zones", "settings/opportunistic_encryption") - self.add('AUTH', "zones", "settings/automatic_https_rewrites") - self.add('AUTH', "zones", "settings/brotli") - self.add('AUTH', "zones", "settings/privacy_pass") + self.add('AUTH', "zones", "settings/websockets") def zones_analytics(self): """ API core commands for Cloudflare API""" @@ -134,6 +162,8 @@ self.add('VOID', "zones", "analytics") self.add('AUTH', "zones", "analytics/colos") self.add('AUTH', "zones", "analytics/dashboard") + self.add('AUTH', "zones", "analytics/latency") + self.add('AUTH', "zones", "analytics/latency/colos") def zones_firewall(self): """ API core commands for Cloudflare API""" @@ -142,8 +172,10 @@ self.add('VOID', "zones", "firewall/access_rules") 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('VOID', "zones", "firewall/waf") + 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") @@ -166,12 +198,31 @@ self.add('VOID', "zones", "amp") self.add('AUTH', "zones", "amp/viewer") +def zones_logpush(self): + """ API core commands for Cloudflare API""" + + self.add('VOID', "zones", "logpush") + self.add('VOID', "zones", "logpush/datasets") + self.add('AUTH', "zones", "logpush/datasets", "fields") + self.add('AUTH', "zones", "logpush/datasets", "jobs") + self.add('AUTH', "zones", "logpush/jobs") + self.add('AUTH', "zones", "logpush/ownership") + self.add('AUTH', "zones", "logpush/ownership/validate") + self.add('VOID', "zones", "logpush/validate") + self.add('VOID', "zones", "logpush/validate/destination") + self.add('AUTH', "zones", "logpush/validate/destination/exists") + self.add('AUTH', "zones", "logpush/validate/origin") + def zones_logs(self): """ API core commands for Cloudflare API""" self.add('VOID', "zones", "logs") + self.add('AUTH', "zones", "logs/control") + self.add('VOID', "zones", "logs/control/retention") + self.add('AUTH', "zones", "logs/control/retention/flag") self.add('AUTH_UNWRAPPED', "zones", "logs/received") self.add('AUTH_UNWRAPPED', "zones", "logs/received/fields") + self.add('AUTH_UNWRAPPED', "zones", "logs/rayids") def railguns(self): """ API core commands for Cloudflare API""" @@ -184,7 +235,6 @@ self.add('AUTH', "organizations") self.add('AUTH', "organizations", "members") - self.add('AUTH', "organizations", "invite") self.add('AUTH', "organizations", "invites") self.add('AUTH', "organizations", "railguns") self.add('AUTH', "organizations", "railguns", "zones") @@ -195,6 +245,7 @@ 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""" @@ -218,6 +269,18 @@ self.add('AUTH', "zones", "dnssec") +def zones_spectrum(self): + """ API core commands for Cloudflare API""" + + self.add('VOID', "zones", "spectrum") + self.add('VOID', "zones", "spectrum/analytics") + self.add('VOID', "zones", "spectrum/analytics/aggregate") + self.add('AUTH', "zones", "spectrum/analytics/aggregate/current") + self.add('VOID', "zones", "spectrum/analytics/events") + 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): """ API core commands for Cloudflare API""" @@ -235,18 +298,29 @@ 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): """ API core commands for Cloudflare API""" self.add('AUTH', "zones", "load_balancers") +def zones_secondary_dns(self): + """ API core commands for Cloudflare API""" + + self.add('AUTH', "zones", "secondary_dns") + self.add('AUTH', "zones", "secondary_dns/force_axfr") + def user_load_balancers(self): """ API core commands for Cloudflare API""" 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/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""" @@ -255,7 +329,6 @@ 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") - return def user_workers(self): """ API core commands for Cloudflare API""" @@ -270,7 +343,6 @@ 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") - return def user_audit_logs(self): """ API core commands for Cloudflare API""" @@ -284,28 +356,103 @@ self.add('AUTH', "user", "load_balancing_analytics/events") self.add('AUTH', "user", "load_balancing_analytics/entities") -def organizations_audit_logs(self): +def user_tokens_verify(self): """ API core commands for Cloudflare API""" - self.add('AUTH', "organizations", "audit_logs") - -def organizations_workers(self): - """ API core commands for Cloudflare API""" - - self.add('VOID', "organizations", "workers") - self.add('AUTH', "organizations", "workers/scripts") + 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 account(self): +def organizations_audit_logs(self): """ API core commands for Cloudflare API""" - self.add('VOID', "account") + self.add('AUTH', "organizations", "audit_logs") -def account_load_balancing_analytics(self): +def accounts(self): """ API core commands for Cloudflare API""" - self.add('VOID', "account", "load_balancing_analytics") - self.add('AUTH', "account", "load_balancing_analytics/events") - self.add('AUTH', "account", "load_balancing_analytics/entities") + 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('VOID', "accounts", "registrar") + self.add('AUTH', "accounts", "registrar/domains") + self.add('AUTH', "accounts", "roles") + self.add('VOID', "accounts", "storage") + self.add('AUTH', "accounts", "storage/analytics") + self.add('AUTH', "accounts", "storage/analytics/stored") + self.add('VOID', "accounts", "storage/kv") + 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") + self.add('AUTH', "accounts", "subscriptions") + 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", "workers") + self.add('AUTH', "accounts", "workers/scripts") + +def accounts_addressing(self): + """ API core commands for Cloudflare API""" + + self.add('VOID', "accounts", "addressing") + self.add('AUTH', "accounts", "addressing/prefixes") + self.add('VOID', "accounts", "addressing/prefixes", "bgp") + self.add('AUTH', "accounts", "addressing/prefixes", "bgp/status") + +def accounts_audit_logs(self): + """ API core commands for Cloudflare API""" + + self.add('AUTH', "accounts", "audit_logs") + +def accounts_load_balancers(self): + """ API core commands for Cloudflare API""" + + self.add('VOID', "accounts", "load_balancers") + 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/pools") + self.add('AUTH', "accounts", "load_balancers/pools", "health") + self.add('AUTH', 'accounts', 'load_balancers/pools', 'preview') + self.add('AUTH', 'accounts', 'load_balancers/search') + +def accounts_firewall(self): + """ API core commands for Cloudflare API""" + + self.add('VOID', "accounts", "firewall") + self.add('VOID', "accounts", "firewall/access_rules") + self.add('AUTH', "accounts", "firewall/access_rules/rules") + +def accounts_secondary_dns(self): + """ API core commands for Cloudflare API""" + + self.add('VOID', "accounts", "secondary_dns") + self.add('AUTH', "accounts", "secondary_dns/masters") + 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/copy") + self.add('AUTH', "accounts", "stream/direct_upload") + self.add('AUTH', "accounts", "stream/embed") + self.add('AUTH', "accounts", "stream/keys") + self.add('AUTH', "accounts", "stream/preview") + self.add('AUTH', "accounts", "stream", "captions") def zones_media(self): """ API core commands for Cloudflare API""" @@ -314,3 +461,7 @@ self.add('AUTH', "zones", "media", "embed") self.add('AUTH', "zones", "media", "preview") +def memberships(self): + """ API core commands for Cloudflare API""" + + self.add('AUTH', "memberships") diff -Nru python-cloudflare-2.1.0/CloudFlare/cloudflare.py python-cloudflare-2.6.5/CloudFlare/cloudflare.py --- python-cloudflare-2.1.0/CloudFlare/cloudflare.py 2018-02-25 11:33:23.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/cloudflare.py 2020-02-06 01:31:46.000000000 +0000 @@ -19,20 +19,32 @@ class _v4base(object): """ Cloudflare v4 API""" - def __init__(self, email, token, certtoken, base_url, debug, raw, use_sessions): + def __init__(self, config): """ Cloudflare v4 API""" - self.email = email - self.token = token - self.certtoken = certtoken - self.base_url = base_url - self.raw = raw - self.use_sessions = use_sessions + self.config = config + if 'email' in config: + self.email = config['email'] + else: + self.email = None + if 'token' in config: + self.token = config['token'] + else: + self.token = None + if 'certtoken' in config: + self.certtoken = config['certtoken'] + else: + self.certtoken = None + + self.base_url = config['base_url'] + self.raw = config['raw'] + self.use_sessions = config['use_sessions'] + self.profile = config['profile'] self.session = None self.user_agent = user_agent() - if debug: - self.logger = CFlogger(debug).getLogger() + if 'debug' in config and config['debug']: + self.logger = CFlogger(config['debug']).getLogger() else: self.logger = None @@ -41,28 +53,64 @@ params=None, data=None, files=None): """ Cloudflare v4 API""" - headers = { - 'User-Agent': self.user_agent, - 'Content-Type': 'application/json' - } + headers = {} + self._AddHeaders(headers) return self._call(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) + def _AddHeaders(self, headers): + """ Add default headers """ + headers['User-Agent'] = self.user_agent + headers['Content-Type'] = 'application/json' + + def _AddAuthHeaders(self, headers, method): + """ Add authentication headers """ + + v = 'email' + '.' + method.lower() + if v in self.config: + email = self.config[v] # use specific value for this method + else: + email = self.email # use generic value for all methods + + v = 'token' + '.' + method.lower() + if v in self.config: + token = self.config[v] # use specific value for this method + else: + token = self.token # use generic value for all methods + + if email is None and token is None: + raise CloudFlareAPIError(0, 'no email and no token defined') + if token is None: + raise CloudFlareAPIError(0, 'no token defined') + if email is None: + headers['Authorization'] = 'Bearer %s' % (token) + else: + headers['X-Auth-Email'] = email + headers['X-Auth-Key'] = token + + def _AddCerttokenHeaders(self, headers, method): + """ Add authentication headers """ + + v = 'certtoken' + '.' + method.lower() + if v in self.config: + certtoken = self.config[v] # use specific value for this method + else: + certtoken = self.certtoken # use generic value for all methods + + if certtoken is None: + raise CloudFlareAPIError(0, 'no cert token defined') + headers['X-Auth-User-Service-Key'] = certtoken + def call_with_auth(self, method, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" - if self.email is '' or self.token is '': - raise CloudFlareAPIError(0, 'no email and/or token defined') - headers = { - 'User-Agent': self.user_agent, - 'X-Auth-Email': self.email, - 'X-Auth-Key': self.token, - 'Content-Type': 'application/json' - } - if type(data) == str: + headers = {} + self._AddHeaders(headers) + self._AddAuthHeaders(headers, method) + if isinstance(data, str): # passing javascript vs JSON headers['Content-Type'] = 'application/javascript' if files: @@ -79,15 +127,10 @@ params=None, data=None, files=None): """ Cloudflare v4 API""" - if self.email is '' or self.token is '': - raise CloudFlareAPIError(0, 'no email and/or token defined') - headers = { - 'User-Agent': self.user_agent, - 'X-Auth-Email': self.email, - 'X-Auth-Key': self.token, - 'Content-Type': 'application/json' - } - if type(data) == str: + headers = {} + self._AddHeaders(headers) + self._AddAuthHeaders(headers, method) + if isinstance(data, str): # passing javascript vs JSON headers['Content-Type'] = 'application/javascript' if files: @@ -104,13 +147,9 @@ params=None, data=None, files=None): """ Cloudflare v4 API""" - if self.certtoken is '' or self.certtoken is None: - raise CloudFlareAPIError(0, 'no cert token defined') - headers = { - 'User-Agent': self.user_agent, - 'X-Auth-User-Service-Key': self.certtoken, - 'Content-Type': 'application/json' - } + headers = {} + self._AddHeaders(headers) + self._AddCerttokenHeaders(headers, method) return self._call(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) @@ -187,7 +226,7 @@ params=params, data=data) elif method == 'POST': - if type(data) == str: + if isinstance(data, str): response = self.session.post(url, headers=headers, params=params, @@ -200,7 +239,7 @@ json=data, files=files) elif method == 'PUT': - if type(data) == str: + if isinstance(data, str): response = self.session.put(url, headers=headers, params=params, @@ -211,7 +250,7 @@ params=params, json=data) elif method == 'DELETE': - if type(data) == str: + if isinstance(data, str): response = self.session.delete(url, headers=headers, params=params, @@ -222,7 +261,7 @@ params=params, json=data) elif method == 'PATCH': - if type(data) == str: + if isinstance(data, str): response = self.session.request('PATCH', url, headers=headers, params=params, @@ -239,7 +278,7 @@ self.logger.debug('Call: done!') except Exception as e: if self.logger: - self.logger.debug('Call: exception!') + self.logger.debug('Call: exception! "%s"' % (e)) raise CloudFlareAPIError(0, 'connection failed.') if self.logger: @@ -257,7 +296,7 @@ response_type = 'application/octet-stream' response_code = response.status_code response_data = response.content - if type(response_data) != str: + if not isinstance(response_data, str): response_data = response_data.decode("utf-8") if self.logger: @@ -453,8 +492,8 @@ message = errors['error'] else: message = '' - if 'messages' in response_data: - errors['error_chain'] = response_data['messages'] + ##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: @@ -498,7 +537,7 @@ result = response_data return result - class _add_unused(object): + class _AddUnused(object): """ Cloudflare v4 API""" def __init__(self, base, p1, p2=None, p3=None): @@ -543,7 +582,7 @@ raise CloudFlareAPIError(0, 'delete() call not available for this endpoint') - class _add_noauth(object): + class _AddNoAuth(object): """ Cloudflare v4 API""" def __init__(self, base, p1, p2=None, p3=None): @@ -590,7 +629,7 @@ raise CloudFlareAPIError(0, 'delete() call not available for this endpoint') - class _add_with_auth(object): + class _AddWithAuth(object): """ Cloudflare v4 API""" def __init__(self, base, p1, p2=None, p3=None): @@ -645,7 +684,7 @@ identifier1, identifier2, identifier3, params, data) - class _add_with_auth_unwrapped(object): + class _AddWithAuthUnwrapped(object): """ Cloudflare v4 API""" def __init__(self, base, p1, p2=None, p3=None): @@ -700,7 +739,7 @@ identifier1, identifier2, identifier3, params, data) - class _add_with_cert_auth(object): + class _AddWithCertAuth(object): """ Cloudflare v4 API""" def __init__(self, base, p1, p2=None, p3=None): @@ -775,16 +814,24 @@ raise CloudFlareAPIError(0, 'api load name failed') name = a[-1] + try: + 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: + # this is the required behavior - i.e. it's a new node to create + pass + if t == 'VOID': - f = self._add_unused(self._base, p1, p2, p3) + f = self._AddUnused(self._base, p1, p2, p3) elif t == 'OPEN': - f = self._add_noauth(self._base, p1, p2, p3) + f = self._AddNoAuth(self._base, p1, p2, p3) elif t == 'AUTH': - f = self._add_with_auth(self._base, p1, p2, p3) + f = self._AddWithAuth(self._base, p1, p2, p3) elif t == 'CERT': - f = self._add_with_cert_auth(self._base, p1, p2, p3) + f = self._AddWithCertAuth(self._base, p1, p2, p3) elif t == 'AUTH_UNWRAPPED': - f = self._add_with_auth_unwrapped(self._base, p1, p2, p3) + f = self._AddWithAuthUnwrapped(self._base, p1, p2, p3) else: # should never happen raise CloudFlareAPIError(0, 'api load type mismatch') @@ -814,27 +861,46 @@ w = w + self.api_list(a, s + '/' + n) return w - def __init__(self, email=None, token=None, certtoken=None, debug=False, raw=False, use_sessions=True): + def __init__(self, email=None, token=None, certtoken=None, debug=False, raw=False, use_sessions=True, profile=None): """ Cloudflare v4 API""" base_url = BASE_URL - # class creation values override configuration values - [conf_email, conf_token, conf_certtoken, extras] = read_configs() - - if email is None: - email = conf_email - if token is None: - token = conf_token - if certtoken is None: - certtoken = conf_certtoken + try: + config = read_configs(profile) + except: + raise CloudFlareAPIError(0, 'profile/configuration read error') + + # class creation values override all configuration values + if email is not None: + config['email'] = email + if token is not None: + config['token'] = token + if certtoken is not None: + config['certtoken'] = certtoken + if base_url is not None: + config['base_url'] = base_url + 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 + + # we do not need to handle item.call values - they pass straight thru + + for x in config: + if config[x] == '': + config[x] = None - self._base = self._v4base(email, token, certtoken, base_url, debug, raw, use_sessions) + self._base = self._v4base(config) # add the API calls api_v4(self) - if extras: - api_extras(self, extras) + if 'extras' in config and config['extras']: + api_extras(self, config['extras']) def __call__(self): """ Cloudflare v4 API""" @@ -855,13 +921,25 @@ def __str__(self): """ Cloudflare v4 API""" - return '["%s","%s"]' % (self._base.email, "REDACTED") + if self._base.email is None: + s = '["%s","%s"]' % (self._base.profile, 'REDACTED') + else: + s = '["%s","%s","%s"]' % (self._base.profile, self._base.email, 'REDACTED') + return s def __repr__(self): """ Cloudflare v4 API""" - return '%s,%s(%s,"%s","%s","%s",%s,"%s")' % ( - self.__module__, type(self).__name__, - self._base.email, 'REDACTED', 'REDACTED', - self._base.base_url, self._base.raw, self._base.user_agent - ) + if self._base.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.email, 'REDACTED', 'REDACTED', + self._base.base_url, self._base.raw, self._base.user_agent + ) + return s diff -Nru python-cloudflare-2.1.0/CloudFlare/exceptions.py python-cloudflare-2.6.5/CloudFlare/exceptions.py --- python-cloudflare-2.1.0/CloudFlare/exceptions.py 2017-08-22 10:13:23.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/exceptions.py 2020-01-16 20:01:18.000000000 +0000 @@ -19,7 +19,7 @@ self.evalue = self.CodeMessage(int(code), str(message)) self.error_chain = None - if error_chain != None: + if error_chain is not None: self.error_chain = [] for evalue in error_chain: self.error_chain.append( @@ -41,8 +41,7 @@ if self.error_chain is None: return 0 - else: - return len(self.error_chain) + return len(self.error_chain) def __getitem__(self, ii): """ Cloudflare API errors can contain a chain of errors""" @@ -53,7 +52,7 @@ """ Cloudflare API errors can contain a chain of errors""" if self.error_chain is None: - raise StopIteration + return for evalue in self.error_chain: yield evalue @@ -61,15 +60,10 @@ """ Cloudflare API errors can contain a chain of errors""" if self.error_chain is None: - raise StopIteration() + raise StopIteration class CloudFlareAPIError(CloudFlareError): """ errors for Cloudflare API""" - pass - class CloudFlareInternalError(CloudFlareError): """ errors for Cloudflare API""" - - pass - diff -Nru python-cloudflare-2.1.0/CloudFlare/__init__.py python-cloudflare-2.6.5/CloudFlare/__init__.py --- python-cloudflare-2.1.0/CloudFlare/__init__.py 2018-02-25 11:41:29.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/__init__.py 2020-04-09 01:13:05.000000000 +0000 @@ -1,7 +1,7 @@ """ Cloudflare v4 API""" from __future__ import absolute_import -__version__ = '2.1.0' +__version__ = '2.6.5' from .cloudflare import CloudFlare diff -Nru python-cloudflare-2.1.0/CloudFlare/logging_helper.py python-cloudflare-2.6.5/CloudFlare/logging_helper.py --- python-cloudflare-2.1.0/CloudFlare/logging_helper.py 2018-02-15 01:15:35.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/logging_helper.py 2020-01-19 00:13:37.000000000 +0000 @@ -47,5 +47,4 @@ """ Logging for Cloudflare API""" if level is True: return logging.DEBUG - else: - return logging.INFO + return logging.INFO diff -Nru python-cloudflare-2.1.0/CloudFlare/read_configs.py python-cloudflare-2.6.5/CloudFlare/read_configs.py --- python-cloudflare-2.1.0/CloudFlare/read_configs.py 2017-08-22 10:13:23.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/read_configs.py 2020-02-05 00:18:44.000000000 +0000 @@ -3,53 +3,80 @@ import os import re try: - import ConfigParser # py2 + import configparser # py3 except ImportError: - import configparser as ConfigParser # py3 + import ConfigParser as configparser # py2 -def read_configs(): +def read_configs(profile=None): """ reading the config file for Cloudflare API""" - # envioronment variables override config files - email = os.getenv('CF_API_EMAIL') - token = os.getenv('CF_API_KEY') - certtoken = os.getenv('CF_API_CERTKEY') - extras = os.getenv('CF_API_EXTRAS') + # We return all these values + config = {'email': None, 'token': None, 'certtoken': None, 'extras': None, 'profile': None} - # grab values from config files - config = ConfigParser.RawConfigParser() - config.read([ - '.cloudflare.cfg', - os.path.expanduser('~/.cloudflare.cfg'), - os.path.expanduser('~/.cloudflare/cloudflare.cfg') - ]) - - if email is None: - try: - email = re.sub(r"\s+", '', config.get('CloudFlare', 'email')) - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - email = None - - if token is None: - try: - token = re.sub(r"\s+", '', config.get('CloudFlare', 'token')) - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - token = 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') + if profile is None: + profile = 'CloudFlare' + config['profile'] = profile - if certtoken is None: - try: - certtoken = re.sub(r"\s+", '', config.get('CloudFlare', 'certtoken')) - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - certtoken = None + # 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 Exception as e: + raise Exception("%s: configuration file error" % (profile)) - if extras is None: + if len(cp.sections()) > 0: + # we have a configuration file - lets use it try: - extras = re.sub(r"\s+", ' ', config.get('CloudFlare', 'extras')) - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - extras = None - - if extras: - extras = extras.split(' ') - - return [email, token, certtoken, extras] + # grab the section - as we will use it for all values + section = cp[profile] + except Exception as e: + # however section name is missing - this is an error + raise Exception("%s: configuration section missing" % (profile)) + + for option in ['email', 'token', 'certtoken', 'extras']: + if option not in config or config[option] is None: + try: + if option == 'extras': + config[option] = re.sub(r"\s+", ' ', section.get(option)) + else: + config[option] = re.sub(r"\s+", '', section.get(option)) + if config[option] == '': + config.pop(option) + except (configparser.NoOptionError, configparser.NoSectionError): + pass + except Exception as e: + 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[option_for_method] = re.sub(r"\s+", '', section.get(option_for_method)) + if config[option_for_method] == '': + config.pop(option_for_method) + except (configparser.NoOptionError, configparser.NoSectionError) as e: + pass + except Exception as e: + pass + + # do any final cleanup - only needed for extras (which are multiline) + if 'extras' in config and config['extras'] is not None: + config['extras'] = config['extras'].strip().split(' ') + + # remove blank entries + for x in sorted(config.keys()): + if config[x] is None or config[x] == '': + try: + config.pop(x) + except: + pass + return config diff -Nru python-cloudflare-2.1.0/CloudFlare/utils.py python-cloudflare-2.6.5/CloudFlare/utils.py --- python-cloudflare-2.1.0/CloudFlare/utils.py 2017-08-22 10:13:23.000000000 +0000 +++ python-cloudflare-2.6.5/CloudFlare/utils.py 2020-01-08 22:41:32.000000000 +0000 @@ -29,5 +29,7 @@ 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 diff -Nru python-cloudflare-2.1.0/cloudflare.egg-info/PKG-INFO python-cloudflare-2.6.5/cloudflare.egg-info/PKG-INFO --- python-cloudflare-2.1.0/cloudflare.egg-info/PKG-INFO 2018-02-25 11:44:43.000000000 +0000 +++ python-cloudflare-2.6.5/cloudflare.egg-info/PKG-INFO 2020-04-09 01:16:24.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: cloudflare -Version: 2.1.0 +Version: 2.6.5 Summary: Python wrapper for the Cloudflare v4 API Home-page: https://github.com/cloudflare/python-cloudflare Author: Martin J. Levy @@ -61,7 +61,7 @@ All example code is available on GitHub (see `package `__ in the `examples `__ - folder. + folder). Blog ---- @@ -120,8 +120,8 @@ main() In order to query more than a single page of zones, we would have to use - the raw mode (decribed more below). We can loop over many get calls and - pass the page paramater to facilitate the paging. + 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. @@ -202,10 +202,10 @@ ----------------------------------------- When you create a **CloudFlare** class you can pass up to four - paramaters. + parameters. - - Account email - - Account API key + - API Token or API Key + - Account email (only if an API Key is being used) - Optional Origin-CA Certificate Token - Optional Debug flag (True/False) @@ -219,17 +219,29 @@ # A minimal call with debug enabled cf = CloudFlare.CloudFlare(debug=True)) - # A full blown call with passed basic account information + # An authenticated call using an API Token (note the missing email) + cf = CloudFlare.CloudFlare(token='00000000000000000000000000000000') + + # An authenticated call using an API Key cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000') - # A full blown call with passed basic account information and CA-Origin info + # An authenticated call using an API Key and CA-Origin info cf = CloudFlare.CloudFlare(email='user@example.com', token='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 retreived from either the users exported shell + 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`` attribute or be a zero length string and the + ``CF_API_EMAIL`` environment variable must be unset or be a zero length + string, otherwise the token will be treated as a key and will throw an + error. + 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. @@ -239,7 +251,7 @@ .. code:: bash - $ export CF_API_EMAIL='user@example.com' + $ export CF_API_EMAIL='user@example.com' # Do not set if using an API Token $ export CF_API_KEY='00000000000000000000000000000000' $ export CF_API_CERTKEY='v1.0-...' $ @@ -254,12 +266,86 @@ $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] - email = user@example.com + email = user@example.com # Do not set if using an API Token token = 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/token). + + .. code:: bash + + $ cat ~/.cloudflare/cloudflare.cfg + [Work] + token = 00000000000000000000000000000000 + [Home] + email = home@example.com + token = 00000000000000000000000000000000 + $ + + To select a profile, use the ``--profile profile-name`` option for + ``cli4`` command or use ``profile="profile-name"`` in the library call. + + .. code:: 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. + + .. code:: python + + #!/usr/bin/env python + + import CloudFlare + + def main(): + cf = CloudFlare.CloudFlare(profile="Work") + ... + + 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. + + .. code:: 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: + + .. code:: 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 *CF\_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). @@ -362,8 +448,8 @@ 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 itterate over that array to see - the additional error. + value **e** is also an array. You can iterate over that array to see the + additional error. .. code:: python @@ -405,6 +491,18 @@ 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. + + :: + + $ 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. @@ -466,7 +564,7 @@ def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() - zone_info = cf.zones.get(param={'name': zone_name}) + zone_info = cf.zones.get(params={'name': zone_name}) zone_id = zone_info['id'] dns_name = sys.argv[2] @@ -483,13 +581,15 @@ --- All API calls can be called from the command line. The command will - convert domain names on-the-fly into zone\_identifier's. + convert domain names prefixed with a colon (``:``) into + zone\_identifiers: e.g. to view ``example.com`` you must use + ``cli4 /zones/:example.com`` (the zone ID cannot be used). .. code:: bash $ cli4 [-V|--version] [-h|--help] [-v|--verbose] [-q|--quiet] [-j|--json] [-y|--yaml] [-r|--raw] [-d|--dump] [--get|--patch|--post|--put|--delete] [item=value ...] /command... - CLI paramaters for POST/PUT/PATCH + CLI parameters for POST/PUT/PATCH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For API calls that need to pass data or parameters there is various @@ -622,7 +722,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~ Here's some Cloudflare CA examples. Note the need of the zone\_id= - paramater with the basic **/certificates** call. + parameter with the basic **/certificates** call. .. code:: bash @@ -912,7 +1012,7 @@ :: - $ python3 -m cli4 /user/workers/scripts + $ cli4 /user/workers/scripts [ { "created_on": "2018-02-15T00:00:00.000000Z", @@ -944,7 +1044,7 @@ $ With that script added to the zone and the route added, we can now see - the the website has been modified because of the Cloudflare Worker. + the website has been modified because of the Cloudflare Worker. :: @@ -1177,9 +1277,19 @@ for this came from `Danielle Madeley (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. + [STRIKEOUT:While the codebase has been edited to run on Python 3.x, + there's not been enough Python 3.x testing performed.] [STRIKEOUT: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. Credit ------ @@ -1201,12 +1311,11 @@ Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 diff -Nru python-cloudflare-2.1.0/cloudflare.egg-info/SOURCES.txt python-cloudflare-2.6.5/cloudflare.egg-info/SOURCES.txt --- python-cloudflare-2.1.0/cloudflare.egg-info/SOURCES.txt 2018-02-25 11:44:43.000000000 +0000 +++ python-cloudflare-2.6.5/cloudflare.egg-info/SOURCES.txt 2020-04-09 01:16:24.000000000 +0000 @@ -39,4 +39,5 @@ examples/example_update_dynamic_dns.py examples/example_user.py examples/example_with_usage.py +examples/example_zone_search.sh examples/example_zones.py \ No newline at end of file diff -Nru python-cloudflare-2.1.0/debian/changelog python-cloudflare-2.6.5/debian/changelog --- python-cloudflare-2.1.0/debian/changelog 2018-06-24 18:06:18.000000000 +0000 +++ python-cloudflare-2.6.5/debian/changelog 2020-04-14 04:51:30.000000000 +0000 @@ -1,3 +1,18 @@ +python-cloudflare (2.6.5-1) unstable; urgency=medium + + [ Ondřej Nový ] + * Use debhelper-compat instead of debian/compat. + + [ Harlan Lieberman-Berg ] + * New upstream version 2.6.5 + * Switch to debhelper 12 + * Update S-V to 4.5, add R-R-R + * Update emails from lists.alioth.debian.org to tracker.debian.org + (Closes: #899295) + * Add manual smoke test. + + -- Harlan Lieberman-Berg Tue, 14 Apr 2020 00:51:30 -0400 + python-cloudflare (2.1.0-1) unstable; urgency=medium * New upstream release. diff -Nru python-cloudflare-2.1.0/debian/compat python-cloudflare-2.6.5/debian/compat --- python-cloudflare-2.1.0/debian/compat 2018-06-24 18:00:31.000000000 +0000 +++ python-cloudflare-2.6.5/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -11 diff -Nru python-cloudflare-2.1.0/debian/control python-cloudflare-2.6.5/debian/control --- python-cloudflare-2.1.0/debian/control 2018-06-24 18:06:18.000000000 +0000 +++ python-cloudflare-2.6.5/debian/control 2020-04-14 04:42:38.000000000 +0000 @@ -1,20 +1,21 @@ Source: python-cloudflare -Maintainer: Debian Python Modules Team +Maintainer: Debian Python Modules Team Uploaders: Harlan Lieberman-Berg , - Debian Let's Encrypt Team + Debian Let's Encrypt Team Section: python Priority: optional -Build-Depends: debhelper (>= 11~), +Build-Depends: debhelper-compat (= 12), dh-python, python3, python3-requests, python3-future, python3-setuptools, python3-yaml -Standards-Version: 4.1.4 +Standards-Version: 4.5.0 Vcs-Browser: https://salsa.debian.org/python-team/modules/cloudflare Vcs-Git: https://salsa.debian.org/python-team/modules/cloudflare.git Homepage: https://github.com/cloudflare/python-cloudflare +Rules-Requires-Root: no Package: python3-cloudflare Architecture: all @@ -24,4 +25,4 @@ python3-cloudflare is a wrapper around the Cloudflare v4 API that lets you perform any action that you would be able to through the Cloudflare control panel itself. For example, you can add and remove - zones, change DNS records, switch billing information, etc. \ No newline at end of file + zones, change DNS records, switch billing information, etc. diff -Nru python-cloudflare-2.1.0/debian/tests/control python-cloudflare-2.6.5/debian/tests/control --- python-cloudflare-2.1.0/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ python-cloudflare-2.6.5/debian/tests/control 2020-04-14 04:51:18.000000000 +0000 @@ -0,0 +1,5 @@ +# Smoke test by hand, since the autotest gets confused by the package +# having capitals in it +Test-Command: python3 -c 'import CloudFlare; print(CloudFlare)' +Restrictions: superficial +Features: test-name=smoke \ No newline at end of file diff -Nru python-cloudflare-2.1.0/examples/example_update_dynamic_dns.py python-cloudflare-2.6.5/examples/example_update_dynamic_dns.py --- python-cloudflare-2.1.0/examples/example_update_dynamic_dns.py 2018-02-15 01:25:22.000000000 +0000 +++ python-cloudflare-2.6.5/examples/example_update_dynamic_dns.py 2020-02-09 20:28:33.000000000 +0000 @@ -65,13 +65,16 @@ 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 + 'content':ip_address, + 'proxied':proxied_state } try: dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=dns_record) @@ -103,7 +106,7 @@ except IndexError: exit('usage: example-update-dynamic-dns.py fqdn-hostname') - host_name, zone_name = dns_name.split('.', 1) + host_name, zone_name = '.'.join(dns_name.split('.')[:2]), '.'.join(dns_name.split('.')[-2:]) ip_address, ip_address_type = my_ip_address() diff -Nru python-cloudflare-2.1.0/examples/example_zone_search.sh python-cloudflare-2.6.5/examples/example_zone_search.sh --- python-cloudflare-2.1.0/examples/example_zone_search.sh 1970-01-01 00:00:00.000000000 +0000 +++ python-cloudflare-2.6.5/examples/example_zone_search.sh 2020-04-09 00:38:19.000000000 +0000 @@ -0,0 +1,27 @@ +: + +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 + diff -Nru python-cloudflare-2.1.0/PKG-INFO python-cloudflare-2.6.5/PKG-INFO --- python-cloudflare-2.1.0/PKG-INFO 2018-02-25 11:44:43.000000000 +0000 +++ python-cloudflare-2.6.5/PKG-INFO 2020-04-09 01:16:24.306920800 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: cloudflare -Version: 2.1.0 +Version: 2.6.5 Summary: Python wrapper for the Cloudflare v4 API Home-page: https://github.com/cloudflare/python-cloudflare Author: Martin J. Levy @@ -61,7 +61,7 @@ All example code is available on GitHub (see `package `__ in the `examples `__ - folder. + folder). Blog ---- @@ -120,8 +120,8 @@ main() In order to query more than a single page of zones, we would have to use - the raw mode (decribed more below). We can loop over many get calls and - pass the page paramater to facilitate the paging. + 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. @@ -202,10 +202,10 @@ ----------------------------------------- When you create a **CloudFlare** class you can pass up to four - paramaters. + parameters. - - Account email - - Account API key + - API Token or API Key + - Account email (only if an API Key is being used) - Optional Origin-CA Certificate Token - Optional Debug flag (True/False) @@ -219,17 +219,29 @@ # A minimal call with debug enabled cf = CloudFlare.CloudFlare(debug=True)) - # A full blown call with passed basic account information + # An authenticated call using an API Token (note the missing email) + cf = CloudFlare.CloudFlare(token='00000000000000000000000000000000') + + # An authenticated call using an API Key cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000') - # A full blown call with passed basic account information and CA-Origin info + # An authenticated call using an API Key and CA-Origin info cf = CloudFlare.CloudFlare(email='user@example.com', token='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 retreived from either the users exported shell + 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`` attribute or be a zero length string and the + ``CF_API_EMAIL`` environment variable must be unset or be a zero length + string, otherwise the token will be treated as a key and will throw an + error. + 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. @@ -239,7 +251,7 @@ .. code:: bash - $ export CF_API_EMAIL='user@example.com' + $ export CF_API_EMAIL='user@example.com' # Do not set if using an API Token $ export CF_API_KEY='00000000000000000000000000000000' $ export CF_API_CERTKEY='v1.0-...' $ @@ -254,12 +266,86 @@ $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] - email = user@example.com + email = user@example.com # Do not set if using an API Token token = 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/token). + + .. code:: bash + + $ cat ~/.cloudflare/cloudflare.cfg + [Work] + token = 00000000000000000000000000000000 + [Home] + email = home@example.com + token = 00000000000000000000000000000000 + $ + + To select a profile, use the ``--profile profile-name`` option for + ``cli4`` command or use ``profile="profile-name"`` in the library call. + + .. code:: 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. + + .. code:: python + + #!/usr/bin/env python + + import CloudFlare + + def main(): + cf = CloudFlare.CloudFlare(profile="Work") + ... + + 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. + + .. code:: 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: + + .. code:: 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 *CF\_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). @@ -362,8 +448,8 @@ 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 itterate over that array to see - the additional error. + value **e** is also an array. You can iterate over that array to see the + additional error. .. code:: python @@ -405,6 +491,18 @@ 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. + + :: + + $ 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. @@ -466,7 +564,7 @@ def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() - zone_info = cf.zones.get(param={'name': zone_name}) + zone_info = cf.zones.get(params={'name': zone_name}) zone_id = zone_info['id'] dns_name = sys.argv[2] @@ -483,13 +581,15 @@ --- All API calls can be called from the command line. The command will - convert domain names on-the-fly into zone\_identifier's. + convert domain names prefixed with a colon (``:``) into + zone\_identifiers: e.g. to view ``example.com`` you must use + ``cli4 /zones/:example.com`` (the zone ID cannot be used). .. code:: bash $ cli4 [-V|--version] [-h|--help] [-v|--verbose] [-q|--quiet] [-j|--json] [-y|--yaml] [-r|--raw] [-d|--dump] [--get|--patch|--post|--put|--delete] [item=value ...] /command... - CLI paramaters for POST/PUT/PATCH + CLI parameters for POST/PUT/PATCH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For API calls that need to pass data or parameters there is various @@ -622,7 +722,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~ Here's some Cloudflare CA examples. Note the need of the zone\_id= - paramater with the basic **/certificates** call. + parameter with the basic **/certificates** call. .. code:: bash @@ -912,7 +1012,7 @@ :: - $ python3 -m cli4 /user/workers/scripts + $ cli4 /user/workers/scripts [ { "created_on": "2018-02-15T00:00:00.000000Z", @@ -944,7 +1044,7 @@ $ With that script added to the zone and the route added, we can now see - the the website has been modified because of the Cloudflare Worker. + the website has been modified because of the Cloudflare Worker. :: @@ -1177,9 +1277,19 @@ for this came from `Danielle Madeley (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. + [STRIKEOUT:While the codebase has been edited to run on Python 3.x, + there's not been enough Python 3.x testing performed.] [STRIKEOUT: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. Credit ------ @@ -1201,12 +1311,11 @@ Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 diff -Nru python-cloudflare-2.1.0/README.rst python-cloudflare-2.6.5/README.rst --- python-cloudflare-2.1.0/README.rst 2018-02-23 07:20:17.000000000 +0000 +++ python-cloudflare-2.6.5/README.rst 2020-04-09 00:55:12.000000000 +0000 @@ -53,7 +53,7 @@ All example code is available on GitHub (see `package `__ in the `examples `__ -folder. +folder). Blog ---- @@ -112,8 +112,8 @@ main() In order to query more than a single page of zones, we would have to use -the raw mode (decribed more below). We can loop over many get calls and -pass the page paramater to facilitate the paging. +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. @@ -194,10 +194,10 @@ ----------------------------------------- When you create a **CloudFlare** class you can pass up to four -paramaters. +parameters. -- Account email -- Account API key +- API Token or API Key +- Account email (only if an API Key is being used) - Optional Origin-CA Certificate Token - Optional Debug flag (True/False) @@ -211,17 +211,29 @@ # A minimal call with debug enabled cf = CloudFlare.CloudFlare(debug=True)) - # A full blown call with passed basic account information + # An authenticated call using an API Token (note the missing email) + cf = CloudFlare.CloudFlare(token='00000000000000000000000000000000') + + # An authenticated call using an API Key cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000') - # A full blown call with passed basic account information and CA-Origin info + # An authenticated call using an API Key and CA-Origin info cf = CloudFlare.CloudFlare(email='user@example.com', token='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 retreived from either the users exported shell +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`` attribute or be a zero length string and the +``CF_API_EMAIL`` environment variable must be unset or be a zero length +string, otherwise the token will be treated as a key and will throw an +error. + 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. @@ -231,7 +243,7 @@ .. code:: bash - $ export CF_API_EMAIL='user@example.com' + $ export CF_API_EMAIL='user@example.com' # Do not set if using an API Token $ export CF_API_KEY='00000000000000000000000000000000' $ export CF_API_CERTKEY='v1.0-...' $ @@ -246,12 +258,86 @@ $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] - email = user@example.com + email = user@example.com # Do not set if using an API Token token = 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/token). + +.. code:: bash + + $ cat ~/.cloudflare/cloudflare.cfg + [Work] + token = 00000000000000000000000000000000 + [Home] + email = home@example.com + token = 00000000000000000000000000000000 + $ + +To select a profile, use the ``--profile profile-name`` option for +``cli4`` command or use ``profile="profile-name"`` in the library call. + +.. code:: 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. + +.. code:: python + + #!/usr/bin/env python + + import CloudFlare + + def main(): + cf = CloudFlare.CloudFlare(profile="Work") + ... + +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. + +.. code:: 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: + +.. code:: 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 *CF\_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). @@ -354,8 +440,8 @@ 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 itterate over that array to see -the additional error. +value **e** is also an array. You can iterate over that array to see the +additional error. .. code:: python @@ -397,6 +483,18 @@ 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. + +:: + + $ 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. @@ -458,7 +556,7 @@ def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() - zone_info = cf.zones.get(param={'name': zone_name}) + zone_info = cf.zones.get(params={'name': zone_name}) zone_id = zone_info['id'] dns_name = sys.argv[2] @@ -475,13 +573,15 @@ --- All API calls can be called from the command line. The command will -convert domain names on-the-fly into zone\_identifier's. +convert domain names prefixed with a colon (``:``) into +zone\_identifiers: e.g. to view ``example.com`` you must use +``cli4 /zones/:example.com`` (the zone ID cannot be used). .. code:: bash $ cli4 [-V|--version] [-h|--help] [-v|--verbose] [-q|--quiet] [-j|--json] [-y|--yaml] [-r|--raw] [-d|--dump] [--get|--patch|--post|--put|--delete] [item=value ...] /command... -CLI paramaters for POST/PUT/PATCH +CLI parameters for POST/PUT/PATCH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For API calls that need to pass data or parameters there is various @@ -614,7 +714,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~ Here's some Cloudflare CA examples. Note the need of the zone\_id= -paramater with the basic **/certificates** call. +parameter with the basic **/certificates** call. .. code:: bash @@ -904,7 +1004,7 @@ :: - $ python3 -m cli4 /user/workers/scripts + $ cli4 /user/workers/scripts [ { "created_on": "2018-02-15T00:00:00.000000Z", @@ -936,7 +1036,7 @@ $ With that script added to the zone and the route added, we can now see -the the website has been modified because of the Cloudflare Worker. +the website has been modified because of the Cloudflare Worker. :: @@ -1169,9 +1269,19 @@ for this came from `Danielle Madeley (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. +[STRIKEOUT:While the codebase has been edited to run on Python 3.x, +there's not been enough Python 3.x testing performed.] [STRIKEOUT: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. Credit ------ diff -Nru python-cloudflare-2.1.0/setup.cfg python-cloudflare-2.6.5/setup.cfg --- python-cloudflare-2.1.0/setup.cfg 2018-02-25 11:44:43.000000000 +0000 +++ python-cloudflare-2.6.5/setup.cfg 2020-04-09 01:16:24.307399000 +0000 @@ -1,5 +1,4 @@ [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 diff -Nru python-cloudflare-2.1.0/setup.py python-cloudflare-2.6.5/setup.py --- python-cloudflare-2.1.0/setup.py 2018-02-25 09:03:31.000000000 +0000 +++ python-cloudflare-2.6.5/setup.py 2020-01-14 04:22:19.000000000 +0000 @@ -44,15 +44,14 @@ 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6' + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8' ] )