From 4ef06df423821a10890e27fd05c3fdc07cc6971d Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Mon, 12 Aug 2019 19:08:24 +0100 Subject: [PATCH 1/6] Add request_kwargs --- kong/__init__.py | 2 +- kong/client.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kong/__init__.py b/kong/__init__.py index d14ec87..a45d2dd 100644 --- a/kong/__init__.py +++ b/kong/__init__.py @@ -1,3 +1,3 @@ """Asynchronous Kong client""" -__version__ = '0.6.0' +__version__ = '0.6.1' diff --git a/kong/client.py b/kong/client.py index adacc50..aeb0015 100644 --- a/kong/client.py +++ b/kong/client.py @@ -24,9 +24,10 @@ class Kong: """ url = os.environ.get('KONG_URL', 'http://127.0.0.1:8001') - def __init__(self, url: str = None, session: typing.Any = None) -> None: + def __init__(self, url: str = None, session: typing.Any = None, request_kwargs: typing.Dict = None) -> None: self.url = url or self.url self.session = session + self.request_kwargs self.services = Services(self) self.plugins = Plugins(self) self.consumers = Consumers(self) From 99c036d970d289511ae84e938e6af17bb05c8553 Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Mon, 12 Aug 2019 21:40:22 +0100 Subject: [PATCH 2/6] Bump kong and black --- .circleci/config.yml | 28 ++++++----- .vscode/settings.json | 7 +-- Makefile | 3 ++ dev/requirements-dev.txt | 5 ++ kong/__init__.py | 2 +- kong/auths.py | 39 +++++++------- kong/certificates.py | 2 +- kong/cli.py | 33 +++--------- kong/client.py | 56 ++++++++++++--------- kong/components.py | 51 +++++++++---------- kong/consumers.py | 34 ++++++------- kong/plugins.py | 37 ++++++-------- kong/routes.py | 15 +++--- kong/services.py | 33 ++++++------ kong/snis.py | 2 +- kong/utils.py | 3 +- readme.md | 2 +- setup.cfg | 8 +++ tests/__init__.py | 5 +- tests/conftest.py | 17 +++---- tests/test_apply.py | 55 ++++++++++---------- tests/test_cli.py | 23 ++++----- tests/test_kong.py | 106 ++++++++++++++++----------------------- tests/test_plugins.py | 37 +++++++------- 24 files changed, 288 insertions(+), 315 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f700e3..b8171cd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,19 +8,19 @@ jobs: - checkout - run: docker run --name postgres -d postgres:10 - run: docker run --link postgres:postgres - -e KONG_DATABASE=postgres - -e KONG_PG_HOST=postgres - -e KONG_PG_USER=postgres - -e KONG_PG_DATABASE=postgres - kong:1.0.3 kong migrations bootstrap + -e KONG_DATABASE=postgres + -e KONG_PG_HOST=postgres + -e KONG_PG_USER=postgres + -e KONG_PG_DATABASE=postgres + kong:1.0.3 kong migrations bootstrap - run: docker run --name kong - --link postgres:postgres - -e KONG_DATABASE=postgres - -e KONG_PG_HOST=postgres - -e KONG_PG_USER=postgres - -e KONG_PG_DATABASE=postgres - -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" - -p 8001:8001 -d kong:1.0.3 + --link postgres:postgres + -e KONG_DATABASE=postgres + -e KONG_PG_HOST=postgres + -e KONG_PG_USER=postgres + -e KONG_PG_DATABASE=postgres + -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" + -p 8001:8001 -d kong:1.2.1 # - run: git clone git://github.com/pyenv/pyenv-update.git $(pyenv root)/plugins/pyenv-update # - run: pyenv update && pyenv install -l # - run: pyenv install 3.7.2 && pyenv global 3.7.2 @@ -31,6 +31,9 @@ jobs: - run: name: flake8 command: flake8 + - run: + name: black + command: make black - run: name: test command: pytest --cov @@ -58,7 +61,6 @@ jobs: name: create tag command: agilekit git release --yes - workflows: version: 2 build-deploy: diff --git a/.vscode/settings.json b/.vscode/settings.json index 8959390..f9807a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,3 @@ { - "python.pythonPath": "${workspaceFolder}/venv/bin/python", - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "[yaml]": { - "editor.tabSize": 2 - } + "python.pythonPath": "${workspaceFolder}/venv/bin/python" } diff --git a/Makefile b/Makefile index cb513b6..4d0e99b 100644 --- a/Makefile +++ b/Makefile @@ -15,3 +15,6 @@ clean: ## remove python cache files version: ## dipsplay software version @python3 -c "import kong; print(kong.__version__)" + +black: ## check black formatting + black --check kong tests diff --git a/dev/requirements-dev.txt b/dev/requirements-dev.txt index 032505f..14a9aec 100644 --- a/dev/requirements-dev.txt +++ b/dev/requirements-dev.txt @@ -1,4 +1,6 @@ # testing and linting +black +isort[requirements] mypy pytest pytest-cov @@ -6,3 +8,6 @@ pytest-aiohttp flake8 twine agile-toolkit +flake8-blind-except +# flake8-builtins +flake8-commas diff --git a/kong/__init__.py b/kong/__init__.py index a45d2dd..f3eccb0 100644 --- a/kong/__init__.py +++ b/kong/__init__.py @@ -1,3 +1,3 @@ """Asynchronous Kong client""" -__version__ = '0.6.1' +__version__ = "0.7.0" diff --git a/kong/auths.py b/kong/auths.py index 86566b6..92199da 100644 --- a/kong/auths.py +++ b/kong/auths.py @@ -2,8 +2,7 @@ def auth_factory(consumer, auth_type): - known_types = {'basic-auth': BasicAuth, - 'key-auth': KeyAuth} + known_types = {"basic-auth": BasicAuth, "key-auth": KeyAuth} constructor = known_types.get(auth_type, ConsumerAuth) return constructor(consumer, auth_type) @@ -14,17 +13,21 @@ class ConsumerAuth(CrudComponent): @property def url(self) -> str: - return f'{self.root.url}/{self.name}' + return f"{self.root.url}/{self.name}" async def get_existing_id(self, creds_config): if not self.unique_field: - raise NotImplementedError('Existence check not implemented for this type of\ - authentication') + raise NotImplementedError( + "Existence check not implemented for this type of\ + authentication" + ) cur_unique = creds_config[self.unique_field] try: - return next(cred for cred - in await self.get_list() - if cred[self.unique_field] == cur_unique)['id'] + return next( + cred + for cred in await self.get_list() + if cred[self.unique_field] == cur_unique + )["id"] except StopIteration: return None @@ -36,21 +39,23 @@ async def create_or_update_credentials(self, creds_config): await self.create_credentials(data=creds_config) async def update_credentials(self, id_, **kw): - url = f'{self.url}/{id_}' + url = f"{self.url}/{id_}" return await self.cli.execute( - url, 'patch', - headers={'Content-Type': 'application/x-www-form-urlencoded'}, + url, + "patch", + headers={"Content-Type": "application/x-www-form-urlencoded"}, wrap=self.wrap, - **kw + **kw, ) async def create_credentials(self, **kw): return await self.cli.execute( - self.url, 'post', - headers={'Content-Type': 'application/x-www-form-urlencoded'}, + self.url, + "post", + headers={"Content-Type": "application/x-www-form-urlencoded"}, wrap=self.wrap, - **kw + **kw, ) async def get_or_create(self): @@ -59,8 +64,8 @@ async def get_or_create(self): class BasicAuth(ConsumerAuth): - unique_field = 'username' + unique_field = "username" class KeyAuth(ConsumerAuth): - unique_field = 'key' + unique_field = "key" diff --git a/kong/certificates.py b/kong/certificates.py index 00059c8..cc0456c 100644 --- a/kong/certificates.py +++ b/kong/certificates.py @@ -3,7 +3,6 @@ class Certificate(KongEntity): - @property def snis(self): return Snis(self) @@ -11,4 +10,5 @@ def snis(self): class Certificates(CrudComponent): """Kong TLS certificate component""" + Entity = Certificate diff --git a/kong/cli.py b/kong/cli.py index d82fc52..d24eb75 100644 --- a/kong/cli.py +++ b/kong/cli.py @@ -1,12 +1,7 @@ - -import json import asyncio +import json import click -import dotenv - -dotenv.load_dotenv() # noqa - import yaml as _yaml from . import __version__ @@ -15,26 +10,12 @@ @click.command() +@click.option("--version", is_flag=True, default=False, help="Display version and exit") +@click.option("--ip", is_flag=True, default=False, help="Show local IP address") @click.option( - '--version', - is_flag=True, - default=False, - help='Display version and exit' -) -@click.option( - '--ip', - is_flag=True, - default=False, - help='Show local IP address' -) -@click.option( - '--key-auth', - help='Create or display an authentication key for a consumer' -) -@click.option( - '--yaml', type=click.File('r'), - help='Yaml configuration to upload' + "--key-auth", help="Create or display an authentication key for a consumer" ) +@click.option("--yaml", type=click.File("r"), help="Yaml configuration to upload") @click.pass_context def kong(ctx, version, ip, key_auth, yaml): if version: @@ -56,7 +37,7 @@ def _run(coro): async def _yml(ctx, yaml): async with Kong() as cli: try: - result = await cli.apply_json(_yaml.load(yaml)) + result = await cli.apply_json(_yaml.load(yaml, Loader=_yaml.FullLoader)) click.echo(json.dumps(result, indent=4)) except KongError as exc: raise click.ClickException(str(exc)) @@ -76,5 +57,5 @@ async def _auth_key(ctx, consumer): raise click.ClickException(str(exc)) -def main(): # pragma nocover +def main(): # pragma nocover kong() diff --git a/kong/client.py b/kong/client.py index aeb0015..69dd2d3 100644 --- a/kong/client.py +++ b/kong/client.py @@ -4,39 +4,41 @@ import aiohttp -from .components import KongError, KongResponseError, CrudComponent -from .services import Services -from .plugins import Plugins -from .consumers import Consumers from .certificates import Certificates +from .components import CrudComponent, KongError, KongResponseError +from .consumers import Consumers +from .plugins import Plugins +from .services import Services from .snis import Snis - -__all__ = [ - 'Kong', - 'KongError', - 'KongResponseError' -] +__all__ = ["Kong", "KongError", "KongResponseError"] class Kong: """Kong client """ - url = os.environ.get('KONG_URL', 'http://127.0.0.1:8001') - def __init__(self, url: str = None, session: typing.Any = None, request_kwargs: typing.Dict = None) -> None: + url = os.environ.get("KONG_URL", "http://127.0.0.1:8001") + + def __init__( + self, + url: str = None, + session: typing.Any = None, + request_kwargs: typing.Dict = None, + ) -> None: self.url = url or self.url self.session = session - self.request_kwargs + self.request_kwargs = request_kwargs or {} self.services = Services(self) self.plugins = Plugins(self) self.consumers = Consumers(self) self.certificates = Certificates(self) - self.acls = CrudComponent(self, 'acls') + self.acls = CrudComponent(self, "acls") self.snis = Snis(self) def __repr__(self) -> str: return self.url + __str__ = __repr__ @property @@ -53,17 +55,23 @@ async def __aenter__(self) -> object: async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: await self.close() - async def execute(self, url, method=None, headers=None, - callback=None, wrap=None, timeout=None, - **kw): + async def execute( + self, + url, + method=None, + headers=None, + callback=None, + wrap=None, + timeout=None, + **kw, + ): if not self.session: self.session = aiohttp.ClientSession() - method = method or 'GET' + method = method or "GET" headers = headers or {} - headers['Accept'] = 'application/json, text/*; q=0.5' - response = await self.session.request( - method, url, headers=headers, **kw - ) + headers["Accept"] = "application/json, text/*; q=0.5" + kw.update(self.request_kwargs) + response = await self.session.request(method, url, headers=headers, **kw) if callback: return await callback(response) if response.status == 204: @@ -81,14 +89,14 @@ async def execute(self, url, method=None, headers=None, async def apply_json(self, config): config = copy.deepcopy(config) if not isinstance(config, dict): - raise KongError('Expected a dict got %s' % type(config).__name__) + raise KongError("Expected a dict got %s" % type(config).__name__) result = {} for name, data in config.items(): if not isinstance(data, list): data = [data] o = getattr(self, name) if not o: - raise KongError('Kong object %s not available' % name) + raise KongError("Kong object %s not available" % name) result[name] = await o.apply_json(data) return result diff --git a/kong/components.py b/kong/components.py index dd18636..2c56496 100644 --- a/kong/components.py +++ b/kong/components.py @@ -1,6 +1,6 @@ import json -from .utils import as_params, as_dict, uid +from .utils import as_dict, as_params, uid class KongError(Exception): @@ -8,12 +8,12 @@ class KongError(Exception): class KongResponseError(KongError): - def __init__(self, response, message=''): + def __init__(self, response, message=""): self.response = response - self.message = as_dict(message, 'message') - self.message['request_url'] = str(response.url) - self.message['request_method'] = response.method - self.message['response_status'] = response.status + self.message = as_dict(message, "message") + self.message["request_url"] = str(response.url) + self.message["request_method"] = response.method + self.message["response_status"] = response.status @property def status(self): @@ -33,6 +33,7 @@ class KongEntity: - Certificate - SNI """ + def __init__(self, root, data): self.root = root self.data = data @@ -55,15 +56,15 @@ def cli(self): @property def id(self): - return self.data['id'] + return self.data["id"] @property def name(self): - return self.data.get('name', '') + return self.data.get("name", "") @property def url(self): - return '%s/%s' % (self.root.url, self.id) + return "%s/%s" % (self.root.url, self.id) def get(self, item, default=None): return self.data.get(item, default) @@ -91,7 +92,7 @@ def cli(self): @property def url(self) -> str: - return f'{self.cli.url}/{self.name}' + return f"{self.cli.url}/{self.name}" @property def is_entity(self): @@ -111,38 +112,36 @@ async def paginate(self, **params): if not next_.startswith(url): next_ = f'{url}?{next_.split("?")[1]}' data = await self.execute(next_, params=params) - next_ = data.get('next') - for d in data['data']: + next_ = data.get("next") + for d in data["data"]: yield self.wrap(d) def get_list(self, **params): url = self.list_create_url() - return self.execute( - url, params=as_params(**params), wrap=self.wrap_list - ) + return self.execute(url, params=as_params(**params), wrap=self.wrap_list) async def get_full_list(self, **params): return [d async for d in self.paginate(**params)] def get(self, id_): - url = f'{self.url}/{uid(id_)}' + url = f"{self.url}/{uid(id_)}" return self.execute(url, wrap=self.wrap) def has(self, id_): - url = f'{self.url}/{uid(id_)}' - return self.execute(url, 'get', callback=self.head) + url = f"{self.url}/{uid(id_)}" + return self.execute(url, "get", callback=self.head) def create(self, **params): url = self.list_create_url() - return self.execute(url, 'post', json=params, wrap=self.wrap) + return self.execute(url, "post", json=params, wrap=self.wrap) def update(self, id_, **params): - url = f'{self.url}/{uid(id_)}' - return self.execute(url, 'patch', json=params, wrap=self.wrap) + url = f"{self.url}/{uid(id_)}" + return self.execute(url, "patch", json=params, wrap=self.wrap) def delete(self, id_): - url = f'{self.url}/{uid(id_)}' - return self.execute(url, 'delete') + url = f"{self.url}/{uid(id_)}" + return self.execute(url, "delete") async def delete_all(self) -> int: n = 0 @@ -156,17 +155,17 @@ async def head(self, response): return False elif response.status == 200: return True - else: # pragma: no cover + else: # pragma: no cover raise KongResponseError(response) def wrap(self, data): return self.Entity(self, data) def wrap_list(self, data): - return [self.wrap(d) for d in data['data']] + return [self.wrap(d) for d in data["data"]] def list_create_url(self) -> str: if self.is_entity: - return f'{self.root.url}/{self.name}' + return f"{self.root.url}/{self.name}" else: return self.url diff --git a/kong/consumers.py b/kong/consumers.py index cc9776b..c748fa6 100644 --- a/kong/consumers.py +++ b/kong/consumers.py @@ -1,17 +1,16 @@ +from .auths import auth_factory from .components import CrudComponent, KongError from .plugins import KongEntityWithPlugins -from .auths import auth_factory class Consumers(CrudComponent): - def wrap(self, data): return Consumer(self, data) async def apply_credentials(self, auths, consumer): for auth_data in auths: - auth = auth_factory(consumer, auth_data['type']) - await auth.create_or_update_credentials(auth_data['config']) + auth = auth_factory(consumer, auth_data["type"]) + await auth.create_or_update_credentials(auth_data["config"]) async def apply_json(self, data): if not isinstance(data, list): @@ -19,16 +18,16 @@ async def apply_json(self, data): result = [] for entry in data: if not isinstance(entry, dict): - raise KongError('dictionary required') - groups = entry.pop('groups', []) - auths = entry.pop('auths', []) + raise KongError("dictionary required") + groups = entry.pop("groups", []) + auths = entry.pop("auths", []) udata = entry.copy() - id_ = udata.pop('id', None) + id_ = udata.pop("id", None) username = None if not id_: - username = udata.pop('username', None) + username = udata.pop("username", None) if not username: - raise KongError('Consumer username or id is required') + raise KongError("Consumer username or id is required") uid = id_ or username try: consumer = await self.get(uid) @@ -41,7 +40,7 @@ async def apply_json(self, data): if entry: consumer = await self.update(uid, **udata) acls = await consumer.acls.get_list() - current_groups = dict(((a['group'], a) for a in acls)) + current_groups = dict(((a["group"], a) for a in acls)) for group in groups: if group not in current_groups: await consumer.acls.create(group=group) @@ -49,7 +48,7 @@ async def apply_json(self, data): current_groups.pop(group) for acl in current_groups.values(): - await consumer.acls.delete(acl['id']) + await consumer.acls.delete(acl["id"]) await self.apply_credentials(auths, consumer) @@ -59,23 +58,22 @@ async def apply_json(self, data): class Consumer(KongEntityWithPlugins): - @property def username(self): - return self.data.get('username') + return self.data.get("username") @property def acls(self): - return CrudComponent(self, 'acls') + return CrudComponent(self, "acls") @property def jwts(self): - return auth_factory(self, 'jwt') + return auth_factory(self, "jwt") @property def keyauths(self): - return auth_factory(self, 'key-auth') + return auth_factory(self, "key-auth") @property def basicauths(self): - return auth_factory(self, 'basic-auth') + return auth_factory(self, "basic-auth") diff --git a/kong/plugins.py b/kong/plugins.py index 91f1515..8683c81 100644 --- a/kong/plugins.py +++ b/kong/plugins.py @@ -1,8 +1,7 @@ -from .components import CrudComponent, KongError, KongEntity +from .components import CrudComponent, KongEntity, KongError class Plugins(CrudComponent): - async def create(self, **params): params = await self.preprocess_parameters(params) return await super().create(**params) @@ -13,34 +12,31 @@ async def apply_json(self, data): plugins = await self.get_full_list() if not self.is_entity: plugins = [p for p in plugins if self.root_plugin(p)] - plugins = dict(((p['name'], p) for p in plugins)) + plugins = dict(((p["name"], p) for p in plugins)) result = [] for entry in data: - name = entry.pop('name', None) + name = entry.pop("name", None) if not name: - raise KongError('Plugin name not specified') + raise KongError("Plugin name not specified") if name in plugins: plugin = plugins.pop(name) - plugin = await self.update( - plugin.id, name=name, **entry) + plugin = await self.update(plugin.id, name=name, **entry) else: plugin = await self.create(name=name, **entry) result.append(plugin.data) for entry in plugins.values(): - await self.delete(entry['id']) + await self.delete(entry["id"]) return result def root_plugin(self, plugin): return not ( - plugin.get('service') or - plugin.get('route') or - plugin.get('consumer') + plugin.get("service") or plugin.get("route") or plugin.get("consumer") ) async def preprocess_parameters(self, params): await anonymous(self.cli, params) - preprocessor = PLUGIN_PREPROCESSORS.get(params.get('name')) + preprocessor = PLUGIN_PREPROCESSORS.get(params.get("name")) if preprocessor: params = await preprocessor(self.cli, params) return params @@ -51,26 +47,23 @@ async def update(self, id, **params): class KongEntityWithPlugins(KongEntity): - @property def plugins(self): return Plugins(self) async def consumer_id_from_username(cli, params): - if 'id' in (params.get('consumer') or {}): - c = await cli.consumers.get(params['consumer']['id']) - params['consumer']['id'] = c['id'] + if "id" in (params.get("consumer") or {}): + c = await cli.consumers.get(params["consumer"]["id"]) + params["consumer"]["id"] = c["id"] return params async def anonymous(cli, params): - if 'config' in params and 'anonymous' in params['config']: - c = await cli.consumers.get(params['config']['anonymous']) - params['config']['anonymous'] = c['id'] + if "config" in params and "anonymous" in params["config"]: + c = await cli.consumers.get(params["config"]["anonymous"]) + params["config"]["anonymous"] = c["id"] return params -PLUGIN_PREPROCESSORS = { - 'request-termination': consumer_id_from_username -} +PLUGIN_PREPROCESSORS = {"request-termination": consumer_id_from_username} diff --git a/kong/routes.py b/kong/routes.py index a6af41f..35c67dc 100644 --- a/kong/routes.py +++ b/kong/routes.py @@ -1,8 +1,8 @@ from itertools import zip_longest from .components import CrudComponent -from .utils import as_list from .plugins import KongEntityWithPlugins +from .utils import as_list class Routes(CrudComponent): @@ -10,10 +10,11 @@ class Routes(CrudComponent): Routes are always associated with a Service """ + Entity = KongEntityWithPlugins async def delete(self, id_): - route = self.wrap({'id': id_}) + route = self.wrap({"id": id_}) await route.plugins.delete_all() return await super().delete(id_) @@ -27,14 +28,14 @@ async def apply_json(self, data): if route: await self.delete(route.id) continue - plugins = d.pop('plugins', []) - as_list('hosts', d) - as_list('paths', d) - as_list('methods', d) + plugins = d.pop("plugins", []) + as_list("hosts", d) + as_list("paths", d) + as_list("methods", d) if not route: route = await self.create(**d) else: route = await self.update(route.id, **d) - route.data['plugins'] = await route.plugins.apply_json(plugins) + route.data["plugins"] = await route.plugins.apply_json(plugins) result.append(route.data) return result diff --git a/kong/services.py b/kong/services.py index cce9413..95674a0 100644 --- a/kong/services.py +++ b/kong/services.py @@ -1,32 +1,33 @@ from .components import CrudComponent, KongError -from .routes import Routes from .plugins import KongEntityWithPlugins +from .routes import Routes from .utils import local_ip - -REMOVE = frozenset(('absent', 'remove')) -LOCAL_HOST = frozenset(('localhost', '127.0.0.1')) +REMOVE = frozenset(("absent", "remove")) +LOCAL_HOST = frozenset(("localhost", "127.0.0.1")) class Service(KongEntityWithPlugins): """Object representing a Kong service """ + @property def routes(self): return Routes(self) @property def host(self): - return self.data.get('host') + return self.data.get("host") class Services(CrudComponent): """Kong Services """ + Entity = Service async def delete(self, id_): - srv = self.wrap({'id': id_}) + srv = self.wrap({"id": id_}) await srv.routes.delete_all() await srv.plugins.delete_all() return await super().delete(id_) @@ -39,29 +40,29 @@ async def apply_json(self, data): result = [] for entry in data: if not isinstance(entry, dict): - raise KongError('dictionary required') - ensure = entry.pop('ensure', None) - name = entry.pop('name', None) - routes = entry.pop('routes', []) - plugins = entry.pop('plugins', []) - host = entry.pop('host', None) + raise KongError("dictionary required") + ensure = entry.pop("ensure", None) + name = entry.pop("name", None) + routes = entry.pop("routes", []) + plugins = entry.pop("plugins", []) + host = entry.pop("host", None) if host in LOCAL_HOST: host = local_ip() if not name: - raise KongError('Service name is required') + raise KongError("Service name is required") if ensure in REMOVE: if await self.has(name): await self.delete(name) continue # backward compatible with config entry - config = entry.pop('config', None) + config = entry.pop("config", None) if isinstance(config, dict): entry.update(config) if await self.has(name): srv = await self.update(name, host=host, **entry) else: srv = await self.create(name=name, host=host, **entry) - srv.data['routes'] = await srv.routes.apply_json(routes) - srv.data['plugins'] = await srv.plugins.apply_json(plugins) + srv.data["routes"] = await srv.routes.apply_json(routes) + srv.data["plugins"] = await srv.plugins.apply_json(plugins) result.append(srv.data) return result diff --git a/kong/snis.py b/kong/snis.py index c4719ac..a742e73 100644 --- a/kong/snis.py +++ b/kong/snis.py @@ -11,7 +11,7 @@ async def apply_json(self, data): data = [data] result = [] for entry in data: - name = entry.pop('name') + name = entry.pop("name") if await self.has(name): sni = await self.update(name, **entry) else: diff --git a/kong/utils.py b/kong/utils.py index cb8a3ec..ddaad38 100644 --- a/kong/utils.py +++ b/kong/utils.py @@ -1,5 +1,6 @@ import socket from uuid import UUID + from multidict import MultiDict @@ -13,7 +14,7 @@ def as_list(key, data): return data -def as_dict(data, key='data'): +def as_dict(data, key="data"): return {key: data} if not isinstance(data, dict) else data diff --git a/readme.md b/readme.md index 6d90424..69afb28 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,7 @@ [![CircleCI](https://circleci.com/gh/quantmind/aio-kong.svg?style=svg)](https://circleci.com/gh/quantmind/aio-kong) [![codecov](https://codecov.io/gh/quantmind/aio-kong/branch/master/graph/badge.svg)](https://codecov.io/gh/quantmind/aio-kong) -Tested with [kong][] v1.0.x +Tested with [kong][] v1.2.x ## Installation & Testing diff --git a/setup.cfg b/setup.cfg index 7d26d96..dba49fe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,13 @@ [flake8] exclude = __pycache__,.eggs,venv,build,dist +max-line-length = 88 +ignore = C815,C812,W503 + +[isort] +line-length = 88 +skip=venv +known_standard_library=dataclasses +multi_line_output=3 [tool:pytest] norecursedirs = src dist build diff --git a/tests/__init__.py b/tests/__init__.py index 4278258..faaa91b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,9 +2,8 @@ import dotenv - dotenv.load_dotenv() -if not os.environ.get('PYTHON_ENV'): - os.environ['PYTHON_ENV'] = 'test' +if not os.environ.get("PYTHON_ENV"): + os.environ["PYTHON_ENV"] = "test" diff --git a/tests/conftest.py b/tests/conftest.py index 3265aed..e526c01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,11 @@ import asyncio -import pytest - import aiohttp - +import pytest from kong.client import Kong - -TESTS = ('test', 'foo', 'pippo') -CONSUMERS = ('an-xxxx-test', 'test-xx', 'test-yy') +TESTS = ("test", "foo", "pippo") +CONSUMERS = ("an-xxxx-test", "test-xx", "test-yy") @pytest.fixture(autouse=True) @@ -30,13 +27,11 @@ async def cli(loop): @pytest.fixture() async def service(cli): - return await cli.services.create( - name='test', host='example.upstream', port=8080 - ) + return await cli.services.create(name="test", host="example.upstream", port=8080) @pytest.fixture() async def consumer(cli, service): - await service.plugins.create(name='jwt') - consumer = await cli.consumers.create(username='test-xx') + await service.plugins.create(name="jwt") + consumer = await cli.consumers.create(username="test-xx") return consumer diff --git a/tests/test_apply.py b/tests/test_apply.py index 8d0041f..f555d6b 100644 --- a/tests/test_apply.py +++ b/tests/test_apply.py @@ -1,18 +1,17 @@ import os + import pytest import yaml - from kong.client import KongError - PATH = os.path.dirname(__file__) async def test_json(cli): - with open(os.path.join(PATH, 'test.yaml')) as fp: + with open(os.path.join(PATH, "test.yaml")) as fp: manifest = yaml.load(fp) await cli.apply_json(manifest) - srv = await cli.services.get('foo') + srv = await cli.services.get("foo") routes = await srv.routes.get_list() assert len(routes) == 2 # @@ -22,10 +21,10 @@ async def test_json(cli): async def test_json2(cli): - with open(os.path.join(PATH, 'test2.yaml')) as fp: + with open(os.path.join(PATH, "test2.yaml")) as fp: manifest = yaml.load(fp) await cli.apply_json(manifest) - srv = await cli.services.get('foo') + srv = await cli.services.get("foo") routes = await srv.routes.get_list() assert len(routes) == 1 # @@ -39,82 +38,82 @@ async def test_hedge_cases(cli): await cli.apply_json([]) with pytest.raises(KongError): - with open(os.path.join(PATH, 'test3.yaml')) as fp: + with open(os.path.join(PATH, "test3.yaml")) as fp: await cli.apply_json(yaml.load(fp)) assert str(cli) == cli.url async def test_json_plugins(cli): - with open(os.path.join(PATH, 'test4.yaml')) as fp: + with open(os.path.join(PATH, "test4.yaml")) as fp: await cli.apply_json(yaml.load(fp)) async def test_json_route_plugins(cli): - with open(os.path.join(PATH, 'test6.yaml')) as fp: + with open(os.path.join(PATH, "test6.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - with open(os.path.join(PATH, 'test6.yaml')) as fp: + with open(os.path.join(PATH, "test6.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - srv = await cli.services.get('pippo') + srv = await cli.services.get("pippo") plugins = await srv.plugins.get_list() assert len(plugins) == 1 routes = await srv.routes.get_list() assert len(routes) == 1 plugins = await routes[0].plugins.get_list() assert len(plugins) == 3 - cs = await cli.consumers.get('an-xxxx-test') + cs = await cli.consumers.get("an-xxxx-test") acls = await cs.acls.get_list() assert len(acls) == 2 - with open(os.path.join(PATH, 'test61.yaml')) as fp: + with open(os.path.join(PATH, "test61.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - srv = await cli.services.get('pippo') + srv = await cli.services.get("pippo") plugins = await srv.plugins.get_list() assert len(plugins) == 0 routes = await srv.routes.get_list() assert len(routes) == 1 plugins = await routes[0].plugins.get_list() assert len(plugins) == 1 - cs = await cli.consumers.get('an-xxxx-test') + cs = await cli.consumers.get("an-xxxx-test") acls = await cs.acls.get_list() assert len(acls) == 1 async def test_auth_handling(cli): - with open(os.path.join(PATH, 'test_auth.yaml')) as fp: + with open(os.path.join(PATH, "test_auth.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - consumer = await cli.consumers.get('admin') + consumer = await cli.consumers.get("admin") basic_auths = await consumer.basicauths.get_list() assert len(basic_auths) == 1 - assert basic_auths[0]['username'] == 'admin_creds' + assert basic_auths[0]["username"] == "admin_creds" key_auths = await consumer.keyauths.get_list() assert len(key_auths) == 1 async def test_auth_overwrite(cli): - with open(os.path.join(PATH, 'test_auth.yaml')) as fp: + with open(os.path.join(PATH, "test_auth.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - with open(os.path.join(PATH, 'test_auth_overwrite.yaml')) as fp: + with open(os.path.join(PATH, "test_auth_overwrite.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - consumer = await cli.consumers.get('admin') + consumer = await cli.consumers.get("admin") basic_auths = await consumer.basicauths.get_list() assert len(basic_auths) == 1 - assert basic_auths[0]['username'] == 'admin_creds' + assert basic_auths[0]["username"] == "admin_creds" key_auths = await consumer.keyauths.get_list() assert len(key_auths) == 1 async def test_ensure_remove(cli): - with open(os.path.join(PATH, 'test6.yaml')) as fp: + with open(os.path.join(PATH, "test6.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - assert await cli.services.has('pippo') is True - with open(os.path.join(PATH, 'test7.yaml')) as fp: + assert await cli.services.has("pippo") is True + with open(os.path.join(PATH, "test7.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - assert await cli.services.has('pippo') is False - with open(os.path.join(PATH, 'test7.yaml')) as fp: + assert await cli.services.has("pippo") is False + with open(os.path.join(PATH, "test7.yaml")) as fp: await cli.apply_json(yaml.load(fp)) - assert await cli.services.has('pippo') is False + assert await cli.services.has("pippo") is False diff --git a/tests/test_cli.py b/tests/test_cli.py index de1533c..e381a82 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,6 @@ import json from click.testing import CliRunner - from kong import __version__ from kong.cli import kong from kong.utils import local_ip @@ -11,46 +10,46 @@ def test_empty(): runner = CliRunner() result = runner.invoke(kong, []) assert result.exit_code == 0 - assert result.output.startswith('Usage: kong [OPTIONS]') + assert result.output.startswith("Usage: kong [OPTIONS]") def test_version(): runner = CliRunner() - result = runner.invoke(kong, ['--version']) + result = runner.invoke(kong, ["--version"]) assert result.exit_code == 0 assert result.output.rstrip() == __version__ def test_ip(): runner = CliRunner() - result = runner.invoke(kong, ['--ip']) + result = runner.invoke(kong, ["--ip"]) assert result.exit_code == 0 assert result.output.rstrip() == local_ip() def test_plugins(): runner = CliRunner() - result = runner.invoke(kong, ['--yaml', 'tests/test4.yaml']) + result = runner.invoke(kong, ["--yaml", "tests/test4.yaml"]) assert result.exit_code == 0 def test_bad_config(): runner = CliRunner() - result = runner.invoke(kong, ['--yaml', 'tests/test5.yaml']) + result = runner.invoke(kong, ["--yaml", "tests/test5.yaml"]) assert result.exit_code == 1 - assert result.output.strip() == 'Error: Plugin name not specified' + assert result.output.strip() == "Error: Plugin name not specified" def test_key_auth(): runner = CliRunner() - result = runner.invoke(kong, ['--key-auth', 'foo']) + result = runner.invoke(kong, ["--key-auth", "foo"]) assert result.exit_code == 1 - result = runner.invoke(kong, ['--yaml', 'tests/test8.yaml']) + result = runner.invoke(kong, ["--yaml", "tests/test8.yaml"]) assert result.exit_code == 0 - result = runner.invoke(kong, ['--key-auth', 'an-xxxx-test']) + result = runner.invoke(kong, ["--key-auth", "an-xxxx-test"]) assert result.exit_code == 0 data = json.loads(result.output.rstrip()) - result = runner.invoke(kong, ['--key-auth', 'an-xxxx-test']) + result = runner.invoke(kong, ["--key-auth", "an-xxxx-test"]) assert result.exit_code == 0 data2 = json.loads(result.output.rstrip()) - assert data['key'] == data2['key'] + assert data["key"] == data2["key"] diff --git a/tests/test_kong.py b/tests/test_kong.py index 613fda7..308a8b9 100644 --- a/tests/test_kong.py +++ b/tests/test_kong.py @@ -1,7 +1,6 @@ import os - -PATH = os.path.join(os.path.dirname(__file__), 'certificates') +PATH = os.path.join(os.path.dirname(__file__), "certificates") def read(name): @@ -15,113 +14,96 @@ def test_client(cli): async def test_create_service(cli): - srv = await cli.services.create( - name='test', host='example.upstream', port=8080 - ) - assert srv.name == 'test' - assert srv.host == 'example.upstream' + srv = await cli.services.create(name="test", host="example.upstream", port=8080) + assert srv.name == "test" + assert srv.host == "example.upstream" assert srv.id assert srv.routes.root == srv assert srv.plugins.root == srv assert str(srv) - assert 'id' in srv + assert "id" in srv async def test_update_service(cli): - await cli.services.create(name='test', host='example.upstream', port=8080) - c = await cli.services.update('test', host='test.upstream') - assert c.name == 'test' - assert c.host == 'test.upstream' + await cli.services.create(name="test", host="example.upstream", port=8080) + c = await cli.services.update("test", host="test.upstream") + assert c.name == "test" + assert c.host == "test.upstream" async def test_routes(cli): - await cli.services.create(name='test', host='example.upstream', port=8080) - c = await cli.services.get('test') + await cli.services.create(name="test", host="example.upstream", port=8080) + c = await cli.services.get("test") routes = await c.routes.get_list() assert len(routes) == 0 - route = await c.routes.create(hosts=['example.com']) - assert route['service']['id'] == c.id + route = await c.routes.create(hosts=["example.com"]) + assert route["service"]["id"] == c.id async def test_add_certificate(cli): - c = await cli.certificates.create( - cert=read('cert1.pem'), - key=read('key1.pem') - ) + c = await cli.certificates.create(cert=read("cert1.pem"), key=read("key1.pem")) assert c.id - assert len(c.data['snis']) == 0 + assert len(c.data["snis"]) == 0 snis = await c.snis.get_list() assert snis == [] await cli.certificates.delete(c.id) async def test_snis(cli): - c1 = await cli.certificates.create( - cert=read('cert1.pem'), - key=read('key1.pem') - ) - c2 = await cli.certificates.create( - cert=read('cert2.pem'), - key=read('key2.pem') - ) + c1 = await cli.certificates.create(cert=read("cert1.pem"), key=read("key1.pem")) + c2 = await cli.certificates.create(cert=read("cert2.pem"), key=read("key2.pem")) config = { - 'snis': [ + "snis": [ { - 'name': 'a1.example.com', - 'certificate': { - 'id': c1['id'] - } + "name": "a1.example.com", + "certificate": {"id": c1["id"]}, + "tags": ["foo"], }, { - 'name': 'a2.example.com', - 'certificate': { - 'id': c2['id'] - } + "name": "a2.example.com", + "certificate": {"id": c2["id"]}, + "tags": ["one", "two"], }, ] } resp = await cli.apply_json(config) - snis = resp['snis'] + snis = resp["snis"] # CREATE for sni in snis: - sni.pop('created_at') - sni.pop('id') - assert snis == config['snis'] + sni.pop("created_at") + sni.pop("id") + print(config["snis"]) + print(snis) + assert snis == config["snis"] # UPDATE - config['snis'][0]['certificate'] = {'id': c2['id']} - config['snis'][1]['certificate'] = {'id': c1['id']} + config["snis"][0]["certificate"] = {"id": c2["id"]} + config["snis"][1]["certificate"] = {"id": c1["id"]} resp = await cli.apply_json(config) - snis = resp['snis'] + snis = resp["snis"] for sni in snis: - sni.pop('created_at') - sni.pop('id') - assert snis == config['snis'] + sni.pop("created_at") + sni.pop("id") + assert snis == config["snis"] # GET snis = await cli.snis.get_list() assert len(snis) == 2 - snis = { - sni.data['name']: sni.data['certificate']['id'] - for sni in snis - } - expected = { - sni['name']: sni['certificate']['id'] - for sni in config['snis'] - } + snis = {sni.data["name"]: sni.data["certificate"]["id"] for sni in snis} + expected = {sni["name"]: sni["certificate"]["id"] for sni in config["snis"]} assert snis == expected async def test_paginate_params(cli, consumer): - await consumer.acls.create(group='test1') - await consumer.acls.create(group='test2') - consumer2 = await cli.consumers.create(username='an-xxxx-test') - consumer3 = await cli.consumers.create(username='test-yy') - await consumer2.acls.create(group='test2') - await consumer3.acls.create(group='test3') + await consumer.acls.create(group="test1") + await consumer.acls.create(group="test2") + consumer2 = await cli.consumers.create(username="an-xxxx-test") + consumer3 = await cli.consumers.create(username="test-yy") + await consumer2.acls.create(group="test2") + await consumer3.acls.create(group="test3") acls = [u async for u in cli.acls.paginate(size=1)] assert len(acls) == 4 acls = [u async for u in consumer.acls.paginate(size=1)] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 1fe6d45..c9ec30e 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,30 +1,29 @@ import pytest - from kong.client import KongResponseError async def test_consumer(cli, consumer): - assert consumer.username == 'test-xx' - assert consumer.get('custom_id') is None + assert consumer.username == "test-xx" + assert consumer.get("custom_id") is None async def test_jwt_create(cli, consumer): jwt = await consumer.jwts.create() - assert jwt['consumer']['id'] == consumer.id + assert jwt["consumer"]["id"] == consumer.id data = await consumer.jwts.get_list() assert data - data = [d for d in data if d['id'] == jwt['id']] + data = [d for d in data if d["id"] == jwt["id"]] assert data - jwt2 = await consumer.jwts.get(jwt['id']) + jwt2 = await consumer.jwts.get(jwt["id"]) assert jwt.data == jwt2.data async def test_jwt_delete(cli, consumer): jwt = await consumer.jwts.create() - assert jwt['consumer']['id'] == consumer.id - await consumer.jwts.delete(jwt['id']) + assert jwt["consumer"]["id"] == consumer.id + await consumer.jwts.delete(jwt["id"]) with pytest.raises(KongResponseError) as e: - await consumer.jwts.delete(jwt['id']) + await consumer.jwts.delete(jwt["id"]) assert e.value.response.status == 404 @@ -36,22 +35,22 @@ async def test_get_or_create_jwt(cli, consumer): async def test_key_auth_create(cli, consumer): auth = await consumer.keyauths.create() - assert auth['consumer']['id'] == consumer.id + assert auth["consumer"]["id"] == consumer.id async def test_key_auth_delete(cli, consumer): auth = await consumer.keyauths.create() - assert auth['consumer']['id'] == consumer.id - await consumer.keyauths.delete(auth['id']) + assert auth["consumer"]["id"] == consumer.id + await consumer.keyauths.delete(auth["id"]) with pytest.raises(KongResponseError) as e: - await consumer.keyauths.delete(auth['id']) + await consumer.keyauths.delete(auth["id"]) assert e.value.response.status == 404 async def test_group(cli, consumer): - r = await consumer.acls.create(group='a') - assert r['consumer']['id'] == consumer.id - assert r['group'] == 'a' - r = await consumer.acls.create(group='b') - assert r['consumer']['id'] == consumer.id - assert r['group'] == 'b' + r = await consumer.acls.create(group="a") + assert r["consumer"]["id"] == consumer.id + assert r["group"] == "a" + r = await consumer.acls.create(group="b") + assert r["consumer"]["id"] == consumer.id + assert r["group"] == "b" From 08cc111c3fc8e00a0d52a71f551a94befbaeab95 Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Mon, 12 Aug 2019 21:46:32 +0100 Subject: [PATCH 3/6] bump to py 3.7.3 --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b8171cd..73f9b32 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: -e KONG_PG_HOST=postgres -e KONG_PG_USER=postgres -e KONG_PG_DATABASE=postgres - kong:1.0.3 kong migrations bootstrap + kong:1.2.1 kong migrations bootstrap - run: docker run --name kong --link postgres:postgres -e KONG_DATABASE=postgres @@ -23,8 +23,8 @@ jobs: -p 8001:8001 -d kong:1.2.1 # - run: git clone git://github.com/pyenv/pyenv-update.git $(pyenv root)/plugins/pyenv-update # - run: pyenv update && pyenv install -l - # - run: pyenv install 3.7.2 && pyenv global 3.7.2 - - run: pyenv install 3.6.3 && pyenv global 3.6.3 + - run: pyenv install 3.7.3 && pyenv global 3.7.3 + # - run: pyenv install 3.6.3 && pyenv global 3.6.3 - run: name: install command: ./dev/install.sh From b5a497afccd2bb1adee0af4bc03abc3edb8cadfa Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Mon, 12 Aug 2019 21:50:37 +0100 Subject: [PATCH 4/6] Revert to 3.6.3 --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 73f9b32..dec2e74 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,8 +23,8 @@ jobs: -p 8001:8001 -d kong:1.2.1 # - run: git clone git://github.com/pyenv/pyenv-update.git $(pyenv root)/plugins/pyenv-update # - run: pyenv update && pyenv install -l - - run: pyenv install 3.7.3 && pyenv global 3.7.3 - # - run: pyenv install 3.6.3 && pyenv global 3.6.3 + # - run: pyenv install 3.7.3 && pyenv global 3.7.3 + - run: pyenv install 3.6.3 && pyenv global 3.6.3 - run: name: install command: ./dev/install.sh From dc5770a74f94e9abce19976b4000cbfce43ada86 Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Mon, 12 Aug 2019 22:14:19 +0100 Subject: [PATCH 5/6] Add test for request_kwargs --- tests/test_mocks.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/test_mocks.py diff --git a/tests/test_mocks.py b/tests/test_mocks.py new file mode 100644 index 0000000..73b1943 --- /dev/null +++ b/tests/test_mocks.py @@ -0,0 +1,21 @@ +from typing import Dict, Tuple + +import aiohttp +from kong.client import Kong + + +async def async_mock(*args, **kwargs) -> Tuple: + return (args, kwargs) + + +async def async_passthrough(response) -> Dict: + return response[1] + + +async def test_request_kwargs(loop): + session = aiohttp.ClientSession(loop=loop) + cli = Kong(session=session, request_kwargs=dict(ssl=False)) + cli.session.request = async_mock + kwargs = await cli.execute("...", callback=async_passthrough, bla="foo") + kwargs.pop('headers') + assert kwargs == dict(ssl=False, bla="foo") From 36550fd72638399c735fc022db87f92be782d529 Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Mon, 12 Aug 2019 22:23:32 +0100 Subject: [PATCH 6/6] Black regression --- tests/test_mocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mocks.py b/tests/test_mocks.py index 73b1943..4809053 100644 --- a/tests/test_mocks.py +++ b/tests/test_mocks.py @@ -17,5 +17,5 @@ async def test_request_kwargs(loop): cli = Kong(session=session, request_kwargs=dict(ssl=False)) cli.session.request = async_mock kwargs = await cli.execute("...", callback=async_passthrough, bla="foo") - kwargs.pop('headers') + kwargs.pop("headers") assert kwargs == dict(ssl=False, bla="foo")