From 5ec9f29f6ce7028fa72adf3d3446e372de89e2d9 Mon Sep 17 00:00:00 2001 From: Sriram Velamur Date: Mon, 10 Dec 2018 12:08:21 +0530 Subject: [PATCH 1/2] Fixes default value for droplets in get_neighbour method TODO: Find all similar usages and fix them. --- doclient/droplet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doclient/droplet.py b/doclient/droplet.py index 7b5020a..4414573 100644 --- a/doclient/droplet.py +++ b/doclient/droplet.py @@ -77,7 +77,7 @@ def get_neighbours(self): """ url = self.droplet_neighbours_url.format(self.id) response = self.client.api_request(url=url) - droplets = response.get("droplets") + droplets = response.get("droplets", []) return [Droplet(**droplet) for droplet in droplets] def delete(self): From da18b3f5915dcf87669ab937c0524e76877cc6a1 Mon Sep 17 00:00:00 2001 From: Sriram Velamur Date: Sat, 27 Jul 2019 22:29:58 +0530 Subject: [PATCH 2/2] Restructured entire codebase for Python3 support --- doclient/base.py | 7 +-- doclient/client.py | 130 +++++++++++++++++++++++--------------------- doclient/droplet.py | 30 +++++----- doclient/errors.py | 1 + doclient/meta.py | 25 ++++----- setup.py | 3 +- tests.py | 6 +- 7 files changed, 104 insertions(+), 98 deletions(-) diff --git a/doclient/base.py b/doclient/base.py index bc24734..a621db0 100644 --- a/doclient/base.py +++ b/doclient/base.py @@ -8,7 +8,7 @@ from json import dumps -class BaseObject(object): +class BaseObject: """ BaseObject class for use with doclient objects. @@ -22,7 +22,7 @@ def __init__(self, **kwargs): """BaseObject class init""" if not self.props: self.props = [] - for name, value in kwargs.iteritems(): + for name, value in kwargs.items(): if name in ("id", "token"): name = "_{}".format(name) setattr(self, name, value) @@ -54,7 +54,6 @@ def __getattr__(self, key): try: if key == "id": return self._id - else: - return self.key + return self.key except RuntimeError: return None diff --git a/doclient/client.py b/doclient/client.py index 7ca4d63..d6f7d8d 100644 --- a/doclient/client.py +++ b/doclient/client.py @@ -1,23 +1,23 @@ #! coding=utf-8 +#pylint: disable=R0904,R0913,W0142,C0413 """DigitalOcean APIv2 client module""" __author__ = "Sriram Velamur" __all__ = ("DOClient",) -#pylint: disable=R0904,R0913,W0142 import sys sys.dont_write_bytecode = True from json import dumps as json_dumps from re import compile as re_compile, match as re_match from ast import literal_eval -from datetime import datetime as dt -from time import mktime, gmtime +# from datetime import datetime as dt +# from time import mktime, gmtime import requests from .base import BaseObject from .droplet import Droplet, Image, DropletSize -from .meta import Domain, Kernel, Snapshot, \ +from .meta import Domain, Kernel, \ Region, SSHKey, DropletNetwork from .errors import APIAuthError, InvalidArgumentError, \ APIError, NetworkError @@ -90,7 +90,7 @@ def __init__(self, token): r""" DigitalOcean APIv2 client init :param token: DigitalOcean API authentication token - :type token: basestring + :type token: str """ super(DOClient, self).__init__(**{"token": token}) self.droplets = None @@ -192,9 +192,9 @@ def api_request(self, url, method="GET", DigitalOcean API request helper method. :param url: REST API url to place a HTTP request to. - :type url: basestring + :type url: str :param method: HTTP method - :type method: basestring + :type method: str :param data: HTTP payload (JSON dumpable) :type data: dict :param return_json: Specifies return data format. @@ -238,20 +238,24 @@ def api_request(self, url, method="GET", if response.status_code == 400: raise APIError("Invalid request data. Please check data") - elif response.status_code in (401, 403): + if response.status_code in (401, 403): raise APIAuthError( - "Invalid authorization bearer. Please check token") - elif response.status_code == 500: + "Invalid authorization bearer. Please check token" + ) + if response.status_code == 500: raise APIError("DigitalOcean API error. Please try later") - reset_timestamp = response.headers.get("ratelimit-reset") - reset_timestamp = float(reset_timestamp) - reset_timestamp = dt.fromtimestamp( - mktime(gmtime(reset_timestamp))) + # Tuesday 23 July 2019 11:44:38 AM IST + # Breaking since the ratelimit-reset key is missing in the + # headers! + # reset_timestamp = response.headers.get("ratelimit-reset") + # reset_timestamp = float(reset_timestamp) + # reset_timestamp = dt.fromtimestamp( + # mktime(gmtime(reset_timestamp))) - self.api_calls_left = \ - response.headers.get("ratelimit-remaining") - self.api_quota_reset_at = reset_timestamp + # self.api_calls_left = \ + # response.headers.get("ratelimit-remaining") + # self.api_quota_reset_at = reset_timestamp return response.json() if return_json else response @@ -262,7 +266,7 @@ def get_domain(name): DigitalOcean's DNS interface. :param name: Domain name - :type name: basestring + :type name: str :rtype: dict """ return Domain.get(name) @@ -274,7 +278,7 @@ def delete_domain(name): interface. :param name: Domain name - :type name: basestring + :type name: str :rtype: dict """ return Domain.delete(name) @@ -286,22 +290,22 @@ def create_domain(name, ip_address): managed through DigitalOcean's DNS interface. :param name: Domain name - :type name: basestring + :type name: str :param ip_address: IP address to map domain name to. - :type ip_address: basestring + :type ip_address: str :rtype: dict """ try: - assert isinstance(name, basestring), \ + assert isinstance(name, str), \ "name needs to be a valid domain name string" - assert isinstance(ip_address, basestring), \ + assert isinstance(ip_address, str), \ "ip_address needs to be a valid IPV4/IPV6 address" domain = Domain.create(name, ip_address) return { "message": "Domain mapping created successfully", "data": domain.as_json() } - except AssertionError, error: + except AssertionError as error: raise InvalidArgumentError(error) def get_domains(self): @@ -403,7 +407,7 @@ def poweroff_droplet(self, instance_id): Instance power off helper method. :param instance_id: ID of the instance to turn off. - :type instance_id: int, basestring + :type instance_id: int, str :rtype: dict """ url = self.power_onoff_url % instance_id @@ -412,7 +416,7 @@ def poweroff_droplet(self, instance_id): method="post", data=self.poweroff_data) return {"message": "Initiated droplet poweroff"} - except APIAuthError, error: + except APIAuthError as error: return {"message": error.message} def poweron_droplet(self, instance_id): @@ -420,7 +424,7 @@ def poweron_droplet(self, instance_id): Instance power on helper method. :param instance_id: ID of the instance to turn on. - :type instance_id: int, basestring + :type instance_id: int, str :rtype: dict """ url = self.power_onoff_url % instance_id @@ -429,7 +433,7 @@ def poweron_droplet(self, instance_id): method="post", data=self.poweron_data) return {"message": "Initiated droplet poweron"} - except APIAuthError, error: + except APIAuthError as error: return {"message": error.message} def powercycle_droplet(self, instance_id): @@ -437,7 +441,7 @@ def powercycle_droplet(self, instance_id): Instance power cycle helper method. :param instance_id: ID of the instance to powercycle. - :type instance_id: int, basestring + :type instance_id: int, str :rtype: dict """ url = self.power_onoff_url % instance_id @@ -446,7 +450,7 @@ def powercycle_droplet(self, instance_id): method="post", data=self.powercycle_data) return {"message": "Initiated droplet power cycle"} - except APIAuthError, error: + except APIAuthError as error: return {"message": error.message} def get_droplet(self, droplet_id): @@ -456,7 +460,7 @@ def get_droplet(self, droplet_id): [Essentially one droplet]. :param droplet_id: ID to match droplets against. - :type droplet_id: int, basestring + :type droplet_id: int, str :rtype: list """ @@ -482,13 +486,13 @@ def filter_droplets(self, matcher=None): Matcher defaults to empty string and returns all instances :param matcher: Token to match droplet names against. - :type matcher: basestring + :type matcher: str :rtype: list """ if matcher is None: return self.droplets - if not isinstance(matcher, (int, basestring)): + if not isinstance(matcher, (int, str)): raise InvalidArgumentError( "Method requires a string filter token or droplet ID") @@ -551,7 +555,7 @@ def delete_droplet(self, droplet_id): return_json=False) message = "Successfully initiated droplet delete for " \ "droplet {0}".format(droplet) - except APIAuthError, auth_error: + except APIAuthError as auth_error: message = auth_error.message except APIError: message = "DigitalOcean API error. Please try later" @@ -568,43 +572,43 @@ def create_droplet(self, name, region, size, image, Creates a droplet with requested payload features. :param name: Identifier for createddroplet. - :type name: basestring + :type name: str :param region: Region identifier to spawn droplet - :type region: basestring + :type region: str :param size: Size of droplet to create. [512mb, 1gb, 2gb, 4gb, 8gb, 16gb, 32gb, 48gb, 64gb] - :type size: basestring + :type size: str :param image: Name or slug identifier of base image to use. - :type image: int, basestring + :type image: int, str :param ssh_keys: SSH keys to add to created droplet - :type ssh_keys: list, list + :type ssh_keys: list, list :param backups: Droplet backups enable state parameter :type backups: bool :param ipv6: Droplet IPV6 enable state parameter :type ipv6: bool :param user_data: User data to be added to droplet's metadata - :type user_data: basestring + :type user_data: str :param private_networking: Droplet private networking enable parameter :type private_networking: bool :rtype: :class:`Droplet ` """ try: - assert isinstance(name, basestring), \ + assert isinstance(name, str), \ "Invalid droplet name. Requires a string name" - assert isinstance(region, basestring), \ + assert isinstance(region, str), \ "Invalid droplet region. Requires a string region id" - assert isinstance(size, basestring), \ + assert isinstance(size, str), \ "Invalid droplet size. Requires a string size" - assert isinstance(image, (int, long, basestring)), \ + assert isinstance(image, (int, str)), \ "Invalid base image id. Requires a numeric ID or slug" backups = backups if isinstance(backups, bool) else False private_networking = private_networking if \ isinstance(private_networking, bool) else False ipv6 = ipv6 if isinstance(ipv6, bool) else False user_data = user_data if \ - isinstance(user_data, basestring) else None + isinstance(user_data, str) else None ssh_keys = ssh_keys if isinstance(ssh_keys, list) and \ - all((isinstance(x, (int, long, basestring)) + all((isinstance(x, (int, str)) for x in ssh_keys)) else False payload = json_dumps({ "name": name, @@ -636,7 +640,7 @@ def create_droplet(self, name, region, size, image, self.get_droplets() return Droplet(**droplet) - except AssertionError, err: + except AssertionError as err: raise InvalidArgumentError(err) def create_droplets(self, names, region, size, image, @@ -648,21 +652,21 @@ def create_droplets(self, names, region, size, image, payload features. :param names: Identifiers for the droplets to be created. - :type names: list + :type names: list :param region: Region identifier to spawn droplet - :type region: basestring + :type region: str :param size: Size of droplet to create. [512mb, 1gb, 2gb, 4gb, 8gb, 16gb, 32gb, 48gb, 64gb] - :type size: basestring + :type size: str :param image: Name or slug identifier of base image to use. - :type image: int, basestring + :type image: int, str :param ssh_keys: SSH keys to add to created droplets - :type ssh_keys: list, list + :type ssh_keys: list, list :param backups: Droplet backups enable state parameter :type backups: bool :param ipv6: Droplet IPV6 enable state parameter :type ipv6: bool :param user_data: User data to be added to droplet's metadata - :type user_data: basestring + :type user_data: str :param private_networking: Droplet private networking enable parameter :type private_networking: bool :raises: :class:`InvalidArgumentError ` @@ -671,23 +675,25 @@ def create_droplets(self, names, region, size, image, try: assert isinstance(names, list), \ "Invalid droplet name. Requires a list of strings" - assert all((isinstance(x, basestring) for x in names)), \ - "".join(["One or more invalid droplet names." - "Requires a string name"]) - assert isinstance(region, basestring), \ + assert all((isinstance(x, str) for x in names)), \ + "".join([ + "One or more invalid droplet names.", + "Requires a string name" + ]) + assert isinstance(region, str), \ "Invalid droplet region. Requires a string region id" - assert isinstance(size, basestring), \ + assert isinstance(size, str), \ "Invalid droplet size. Requires a string size" - assert isinstance(image, (int, long, basestring)), \ + assert isinstance(image, (int, str)), \ "Invalid base image id. Requires a numeric ID or slug" backups = backups if isinstance(backups, bool) else False private_networking = private_networking if \ isinstance(private_networking, bool) else False ipv6 = ipv6 if isinstance(ipv6, bool) else False user_data = user_data if \ - isinstance(user_data, basestring) else None + isinstance(user_data, str) else None ssh_keys = ssh_keys if isinstance(ssh_keys, list) and \ - all((isinstance(x, (int, long, basestring)) + all((isinstance(x, (int, str)) for x in ssh_keys)) else False payload = json_dumps({ "names": names, @@ -723,7 +729,7 @@ def create_droplets(self, names, region, size, image, self.get_droplets() return _droplets - except AssertionError, err: + except AssertionError as err: raise InvalidArgumentError(err) def get_regions(self): diff --git a/doclient/droplet.py b/doclient/droplet.py index 4414573..e17e6dc 100644 --- a/doclient/droplet.py +++ b/doclient/droplet.py @@ -20,32 +20,29 @@ class Droplet(BaseObject): r"""DigitalOcean droplet object""" - __slots__ = ("client", "name", "ipv4_ip", "ipv6_ip", "networks") - client, name, ipv4_ip, ipv6_ip = (None,) * 4 networks = [] droplet_base_url = 'https://api.digitalocean.com/v2/droplets/' - droplet_snapshot_url = \ - '{droplet_base_url}/snapshots?page=1&per_page=100'.format( - **locals()) - droplet_neighbours_url = \ - '{droplet_base_url}/neighbors'.format(**locals()) + droplet_snapshot_url = '{}/snapshots?page=1&per_page=100'.format( + droplet_base_url + ) + droplet_neighbours_url = '{}/neighbors'.format(droplet_base_url) droplet_actions_url = "{0}{1}/actions" def power_off(self): """Droplet power off helper method""" - print "Powering off droplet {0}".format(self.name) + print("Powering off droplet {0}".format(self.name)) self.client.poweroff_droplet(self.id) def power_on(self): """Droplet power on helper method""" - print "Powering on droplet {0}".format(self.name) + print("Powering on droplet {0}".format(self.name)) self.client.poweron_droplet(self.id) def power_cycle(self): """Droplet power cycle helper method""" - print "Power cycling droplet {0}".format(self.name) + print("Power cycling droplet {0}".format(self.name)) self.client.powercycle_droplet(self.id) def __repr__(self): @@ -107,8 +104,9 @@ def reset_password(self): """ url = self.droplet_actions_url.format( self.droplet_base_url, self.id) - print "Attempting to reset password for droplet {0}".format( - self.id) + print("Attempting to reset password for droplet {0}".format( + self.id + )) payload = { "type": "password_reset" } @@ -119,7 +117,7 @@ def resize(self, new_size, disk_resize=False): Digitalocean droplet resize helper method :param new_size: New droplet size to be resized to. - :type new_size: basestring + :type new_size: str :param disk_resize: Boolean to indicate disk resizing. :type disk_resize: bool :return: Resized current droplet object. @@ -128,11 +126,11 @@ def resize(self, new_size, disk_resize=False): """ url = self.droplet_actions_url.format( self.droplet_base_url, self.id) - print "".join([ + print("".join([ "Droplet {0} needs to be powered off before resize.", " Caveat emptor: Please power on the droplet", " via the Digitalocean console or the API after " - "resizing."]).format(self.id) + "resizing."]).format(self.id)) self.power_off() # Wait for 30 seconds to allow for the droplet to power off # before inititalizing the resizing. This, and the sleep after @@ -141,7 +139,7 @@ def resize(self, new_size, disk_resize=False): # use event triggers/similar to initialize further changes. sleep(30) - if not isinstance(new_size, basestring): + if not isinstance(new_size, str): raise InvalidArgumentError( "Invalid size specified. Required a valid string " "size representation") diff --git a/doclient/errors.py b/doclient/errors.py index c267170..a40d7e1 100644 --- a/doclient/errors.py +++ b/doclient/errors.py @@ -24,6 +24,7 @@ def __init__(self, *args, **kwargs): prefix = getattr(self, "prefix", "GeneralError") message = "DOClient::{0} {1}: ".format(prefix, message) args = (message,) + self.message = message super(BaseError, self).__init__(*args, **kwargs) diff --git a/doclient/meta.py b/doclient/meta.py index faa40e7..ca3eed2 100644 --- a/doclient/meta.py +++ b/doclient/meta.py @@ -61,12 +61,12 @@ def create(cls, name, ip_address): status = response.status_code if status in (401, 403): raise APIAuthError("Invalid authentication bearer") - elif status == 400: + if status == 400: raise InvalidArgumentError("Invalid payload data") - elif status == 500: + if status == 500: raise APIError( "DigitalOcean API error. Please try later.") - elif status != 201: + if status != 201: message = response.json().get("message") raise InvalidArgumentError(message) @@ -81,12 +81,12 @@ def get(cls, name): status = response.status_code if status in (401, 403): raise APIAuthError("Invalid authentication bearer") - elif status == 400: + if status == 400: raise InvalidArgumentError("Invalid payload data") - elif status == 500: + if status == 500: raise APIError( "DigitalOcean API error. Please try later.") - elif status != 200: + if status != 200: message = response.json().get("message") raise InvalidArgumentError(message) @@ -104,12 +104,12 @@ def get_all(cls): status = response.status_code if status in (401, 403): raise APIAuthError("Invalid authentication bearer") - elif status == 400: + if status == 400: raise InvalidArgumentError("Invalid payload data") - elif status == 500: + if status == 500: raise APIError( "DigitalOcean API error. Please try later.") - elif status != 200: + if status != 200: message = response.json().get("message") raise InvalidArgumentError(message) domains = response.json().get("domains", []) @@ -130,12 +130,12 @@ def delete(cls, name): status = response.status_code if status in (401, 403): raise APIAuthError("Invalid authentication bearer") - elif status == 400: + if status == 400: raise InvalidArgumentError("Invalid payload data") - elif status == 500: + if status == 500: raise APIError( "DigitalOcean API error. Please try later.") - elif status != 204: + if status != 204: message = response.json().get("message") raise InvalidArgumentError(message) @@ -219,4 +219,3 @@ class DropletNetwork(BaseObject): network_type, netmask, ip_address,\ gateway, is_public = (None,) * 5 - diff --git a/setup.py b/setup.py index d61cf76..74ce6cf 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,8 @@ setup( name='do-client', - version='1.0.6', + version='1.0.7', + python_requires='>3.5.2', description='DigitalOcean REST API python client', author='Sriram Velamur', author_email='sriram.velamur@gmail.com', diff --git a/tests.py b/tests.py index c11ce89..39dd133 100644 --- a/tests.py +++ b/tests.py @@ -4,13 +4,15 @@ sys.dont_write_bytecode = True from os import environ import unittest -from types import NoneType from doclient import DOClient, Droplet from doclient.errors import InvalidArgumentError, APIAuthError, APIError from doclient.meta import Domain, Snapshot +NoneType = type(None) + + class DOClientTest(unittest.TestCase): """Tests for DigitalOcean client class""" @@ -59,7 +61,7 @@ def test_domain_methods(self): self.assertTrue(all_domains) del_response = self.client.delete_domain(name) self.assertIsInstance(del_response, dict) - except BaseException, error: + except BaseException as error: self.assertIsInstance(error, (APIAuthError, APIError, InvalidArgumentError))