diff --git a/.travis.yml b/.travis.yml index b59365c..531f507 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - "3.7-dev" # 3.7 development branch # command to install dependencies install: - - pip install -r requirements.txt + - pip install .[tests] - npm install script: - ./scripts/tests.sh diff --git a/kong/exceptions.py b/kong/exceptions.py index b1ce99b..834e746 100644 --- a/kong/exceptions.py +++ b/kong/exceptions.py @@ -1,2 +1,6 @@ class SchemaViolation(Exception): pass + + +class ObjectNotFound(Exception): + pass diff --git a/kong/kong_clients.py b/kong/kong_clients.py index 90a38c7..24bd504 100644 --- a/kong/kong_clients.py +++ b/kong/kong_clients.py @@ -1,9 +1,13 @@ +import logging from abc import abstractmethod + from urllib3.util.url import Url, parse_url from requests import session from kong.structures import ApiData, ServiceData, ConsumerData, \ PluginData, RouteData, TargetData, UpstreamData -from kong.exceptions import SchemaViolation +from kong.exceptions import SchemaViolation, ObjectNotFound + +logger = logging.getLogger(__name__) class RestClient: # pylint:disable=too-few-public-methods @@ -68,6 +72,19 @@ def _to_object_data(self, data_dict): def _to_list_object_data(self, list_data_dict): return map(self._to_object_data, list_data_dict) + def update_or_create(self, **kwargs): + pk_or_id = kwargs.pop(self._object_data_class.pk_identifier, None) or kwargs.get("name") + + try: + data_dict = self._perform_retrieve(pk_or_id) + pk_or_id = data_dict.get(self._object_data_class.pk_identifier) + except ObjectNotFound: + return self.create(**kwargs), True + + self.update(pk_or_id, **kwargs) + + return self._to_object_data(data_dict), False + def create(self, **kwargs): data_dict = self._perform_create(**kwargs) return self._to_object_data(data_dict) @@ -177,6 +194,9 @@ def _send_retrieve(self, name_or_id, endpoint=None): response = self.session.get(url) if response.status_code == 404: + if response.json().get('message') == "Not found": + raise ObjectNotFound(response.content) + raise NameError(response.content) if response.status_code != 200: @@ -188,7 +208,7 @@ def _validate_params(self, query_params, allowed_params): validated_params = {} for k, val in query_params.items(): if k in allowed_params: - validated_params[k] = self._stringify_if_list(val) + validated_params[k] = val # self._stringify_if_list(val) else: raise KeyError('invalid query parameter: %s' % k) return validated_params @@ -409,11 +429,11 @@ def _object_data_class(self): def _allowed_update_params(self): return 'name', 'protocol', 'host', 'port', 'path', \ 'retries', 'connect_timeout', 'send_timeout', \ - 'read_timeout', 'url' + 'read_timeout', 'url', "tags" @property def _allowed_query_params(self): - return [] + return ["tags"] @property def _path(self): @@ -432,9 +452,9 @@ def _object_data_class(self): @property def _allowed_update_params(self): - return 'protocols', 'methods', 'hosts',\ + return 'name', 'protocols', 'methods', 'hosts',\ 'paths', 'strip_path', 'preserve_host',\ - 'service', + 'service', 'tags' @property def _allowed_query_params(self): @@ -444,6 +464,13 @@ def _allowed_query_params(self): def _path(self): return 'routes/' + # pylint: disable=arguments-differ + def _perform_update(self, pk_or_id, **kwargs): + if 'service' in kwargs.keys(): + kwargs['service'] = {"id": self.get_service_id(kwargs['service'])} + + return super(RouteAdminClient, self)._perform_update(pk_or_id, **kwargs) + # pylint: disable=arguments-differ def _perform_create(self, service, **kwargs): diff --git a/kong/structures.py b/kong/structures.py index 6ff3d7b..e5ef9ab 100644 --- a/kong/structures.py +++ b/kong/structures.py @@ -89,6 +89,7 @@ def __normalize_uri(uri): class ServiceData(ObjectData): + pk_identifier = 'id' def validate_semi_optional_parameters(self, **kwargs): pass @@ -128,7 +129,7 @@ def allowed_parameters(self): return 'name', 'protocol', 'host', 'port', 'path',\ 'retries', 'connect_timeout', 'send_timeout',\ 'read_timeout', 'url', 'id', \ - 'created_at', 'updated_at', 'write_timeout' + 'created_at', 'updated_at', 'write_timeout', 'tags' @property def url(self): @@ -156,7 +157,7 @@ def validate_obligatory_parameters(self, **kwargs): @property def allowed_parameters(self): - return "id", "username", "custom_id", "created_at" + return "id", "username", "custom_id", "created_at", "tags" def validate_semi_optional_parameters(self, **kwargs): if ("username" not in kwargs) and ("custom_id" not in kwargs): @@ -166,6 +167,7 @@ def validate_semi_optional_parameters(self, **kwargs): class RouteData(ObjectData): + pk_identifier = 'id' def validate_semi_optional_parameters(self, **kwargs): if not ('hosts' in kwargs @@ -178,10 +180,11 @@ def validate_obligatory_parameters(self, **kwargs): @property def allowed_parameters(self): - return "id", "created_at", "updated_at", \ + return "name", "id", "created_at", "updated_at", \ "protocols", "methods", "hosts", \ "paths", "regex_priority", "strip_path", \ - "preserve_host", "service" + "preserve_host", "service", "snis", "sources", \ + "destinations", "tags" class TargetData(ObjectData): diff --git a/pytest.ini b/pytest.ini index a118a6e..91a5cca 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,2 @@ [pytest] -DJANGO_SETTINGS_MODULE = conf.settings.testing - python_files = tests.py test_*.py *_tests.py diff --git a/readme.md b/readme.md index 317c0ca..c63e967 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,7 @@ Supported Information Routes - node_information Supported operations for Services, Routes, Apis, Consumers, Plugins and Upstreams +- update_or_create - create - delete - retrieve @@ -33,7 +34,6 @@ Additional supported operations for Targets - set_healthy Not supported -- update_or_create - count (dropped in 0.16/kong 0.13.0) - Certificates object routes (yet) - SNI object routes (yet) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4ade7ca..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --r ./requirements/testing.txt diff --git a/requirements/testing.in b/requirements/testing.in new file mode 100644 index 0000000..4b4bf05 --- /dev/null +++ b/requirements/testing.in @@ -0,0 +1,4 @@ +flake8 # This covers pycodestyle +pylint +pytest +Faker diff --git a/requirements/testing.txt b/requirements/testing.txt index 0bb045e..8a92d1d 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,5 +1,23 @@ --r base.txt -flake8==3.5.0 # This covers pycodestyle +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile ./testing.in +# +astroid==1.6.6 # via pylint +attrs==19.1.0 # via pytest +faker==1.0.5 +flake8==3.5.0 +isort==4.3.17 # via pylint +lazy-object-proxy==1.3.1 # via astroid +mccabe==0.6.1 # via flake8, pylint +pluggy==0.6.0 # via pytest +py==1.8.0 # via pytest +pycodestyle==2.3.1 # via flake8 +pyflakes==1.6.0 # via flake8 pylint==1.8.2 pytest==3.4.0 -Faker==0.8.10 +python-dateutil==2.8.0 # via faker +six==1.12.0 # via astroid, faker, pylint, pytest, python-dateutil +text-unidecode==1.2 # via faker +wrapt==1.11.1 # via astroid diff --git a/setup.py b/setup.py index e06215e..cdb67c5 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,7 @@ from os.path import dirname from os.path import join -from pip.req import parse_requirements - +from pip._internal.req import parse_requirements from setuptools import setup __version__ = '0.1.7' @@ -19,9 +18,12 @@ def read(*names, **kwargs): ).read() -# parse_requirements() returns generator of pip.req.InstallRequirement objects -requirements = [str(ir.req) for ir in parse_requirements(os.path.join(BASE_DIR, './requirements/base.txt'), session=False)] -# requirements_test = [str(ir.req) for ir in parse_requirements('./requirements-test.txt', session=False)] +with open(os.path.join(BASE_DIR, 'requirements', 'base.txt')) as f: + requirements = f.read().splitlines() + +with open(os.path.join(BASE_DIR, 'requirements', 'testing.txt')) as f: + requirements_test = f.read().splitlines() + setup( name='python-kong-client', @@ -43,5 +45,7 @@ def read(*names, **kwargs): ], keywords=[], install_requires=requirements, - extras_require={}, + extras_require={ + "tests": requirements_test + }, )