From b33695ef0e73581dd73135c41d2143800c0d865d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 27 Jan 2016 13:45:31 +0000 Subject: [PATCH 1/3] Include websockets by default --- coreapi/commandline.py | 26 +++++++++++++- coreapi/transports/__init__.py | 5 +-- coreapi/transports/websockets.py | 60 ++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 coreapi/transports/websockets.py diff --git a/coreapi/commandline.py b/coreapi/commandline.py index bd3674a..fcf59fe 100644 --- a/coreapi/commandline.py +++ b/coreapi/commandline.py @@ -65,7 +65,8 @@ def get_client(): credentials = get_credentials() headers = get_headers() http_transport = coreapi.transports.HTTPTransport(credentials, headers) - return coreapi.Client(transports=[http_transport]) + websocket_transport = coreapi.transports.WebSocketsTransport() + return coreapi.Client(transports=[http_transport, websocket_transport]) def get_document(): @@ -134,6 +135,28 @@ def get(url): set_history(history) +@click.command(help='Watch a document at the given URL.') +@click.argument('url') +def watch(url): + client = get_client() + history = get_history() + heading = click.style('Watching %s' % url, bold=True) + watched = client.get(url) + try: + for doc in watched: + click.clear() + click.echo(heading) + click.echo(display(doc)) + if isinstance(doc, coreapi.Document): + history = history.add(doc) + set_document(doc) + set_history(history) + except coreapi.exceptions.ErrorMessage as exc: + click.echo(display(exc.error)) + sys.exit(1) + + + @click.command(help='Load a document from disk.') @click.argument('input_file', type=click.File('rb')) @click.option('--format', default='corejson', type=click.Choice(['corejson', 'hal', 'hyperschema'])) @@ -546,6 +569,7 @@ def history_forward(): client.add_command(get) +client.add_command(watch) client.add_command(show) client.add_command(action) client.add_command(reload_document, name='reload') diff --git a/coreapi/transports/__init__.py b/coreapi/transports/__init__.py index b61b817..d6d5f68 100644 --- a/coreapi/transports/__init__.py +++ b/coreapi/transports/__init__.py @@ -3,14 +3,15 @@ from coreapi.exceptions import TransportError from coreapi.transports.base import BaseTransport from coreapi.transports.http import HTTPTransport +from coreapi.transports.websockets import WebSocketsTransport import itypes __all__ = [ - 'BaseTransport', 'HTTPTransport' + 'BaseTransport', 'HTTPTransport', 'WebSocketsTransport' ] -default_transports = itypes.List([HTTPTransport()]) +default_transports = itypes.List([HTTPTransport(), WebSocketsTransport()]) def determine_transport(url, transports=default_transports): diff --git a/coreapi/transports/websockets.py b/coreapi/transports/websockets.py new file mode 100644 index 0000000..0fd6547 --- /dev/null +++ b/coreapi/transports/websockets.py @@ -0,0 +1,60 @@ +from coreapi.codecs import negotiate_decoder, default_decoders +from coreapi.compat import force_bytes +from coreapi.transports.base import BaseTransport +from websocket import create_connection +from websocket._exceptions import WebSocketConnectionClosedException +import json +import jsonpatch + + +def _get_headers_and_body(content): + head, body = content.split('\n\n', 1) + key_value_pairs = [line.split(':', 1) for line in head.splitlines()] + headers = dict([ + (key.strip().lower(), value.strip()) + for key, value in key_value_pairs + ]) + return (headers, body) + + +def _decode_content(headers, content, decoders=None, base_url=None): + content_type = headers.get('content-type') + codec = negotiate_decoder(content_type, decoders=decoders) + return codec.load(content, base_url=base_url) + + +def _apply_diff(content, diff): + # TODO: Negotiate diff scheme. + previous = json.loads(content.decode('utf-8')) + new = jsonpatch.apply_patch(previous, diff) + return force_bytes(json.dumps(new)) + + +def _get_request(decoders=None): + # TODO: Include User-Agent, X-Accept-Diff + if decoders is None: + decoders = default_decoders + + accept = ', '.join([decoder.media_type for decoder in decoders]) + return 'Accept: %s\n\n' % accept + + +class WebSocketsTransport(BaseTransport): + schemes = ['ws', 'wss'] + + def transition(self, link, params=None, decoders=None, link_ancestors=None): + url = link.url + ws = create_connection(url) + request = _get_request(decoders) + ws.send(request) + content = ws.recv() + headers, body = _get_headers_and_body(content) + yield _decode_content(headers, body, decoders=decoders, base_url=url) + while True: + try: + diff = ws.recv() + except WebSocketConnectionClosedException: + return + patch = jsonpatch.JsonPatch.from_string(diff) + body = json.dumps(jsonpatch.apply_patch(json.loads(body), patch)) + yield _decode_content(headers, body, decoders=decoders, base_url=url) From 0c0ccce8f57911dd9883c85fab8d80a97b30b955 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 27 Jan 2016 17:04:09 +0000 Subject: [PATCH 2/3] Minor tweaks --- coreapi/transports/websockets.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/coreapi/transports/websockets.py b/coreapi/transports/websockets.py index 0fd6547..01969ad 100644 --- a/coreapi/transports/websockets.py +++ b/coreapi/transports/websockets.py @@ -23,14 +23,14 @@ def _decode_content(headers, content, decoders=None, base_url=None): return codec.load(content, base_url=base_url) -def _apply_diff(content, diff): - # TODO: Negotiate diff scheme. - previous = json.loads(content.decode('utf-8')) - new = jsonpatch.apply_patch(previous, diff) - return force_bytes(json.dumps(new)) +def _diff_content(heaaders, body, diff): + patch = jsonpatch.JsonPatch.from_string(diff) + previous_data = json.loads(body) + next_data = jsonpatch.apply_patch(previous_data, patch) + return json.dumps(next_data) -def _get_request(decoders=None): +def _generate_request(decoders=None): # TODO: Include User-Agent, X-Accept-Diff if decoders is None: decoders = default_decoders @@ -44,17 +44,18 @@ class WebSocketsTransport(BaseTransport): def transition(self, link, params=None, decoders=None, link_ancestors=None): url = link.url - ws = create_connection(url) - request = _get_request(decoders) - ws.send(request) - content = ws.recv() + connection = create_connection(url) + request = _generate_request(decoders) + connection.send(request) + content = connection.recv() headers, body = _get_headers_and_body(content) yield _decode_content(headers, body, decoders=decoders, base_url=url) while True: try: - diff = ws.recv() + diff = connection.recv() except WebSocketConnectionClosedException: return + body = _diff_content(headers, body, diff) patch = jsonpatch.JsonPatch.from_string(diff) body = json.dumps(jsonpatch.apply_patch(json.loads(body), patch)) yield _decode_content(headers, body, decoders=decoders, base_url=url) From 6bfbf927c7d2cef50a5e2c1ab061e9556941224f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Jan 2016 15:12:58 +0000 Subject: [PATCH 3/3] Minor fixes --- coreapi/transports/http.py | 2 +- coreapi/transports/websockets.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/coreapi/transports/http.py b/coreapi/transports/http.py index adf5883..bafebcb 100644 --- a/coreapi/transports/http.py +++ b/coreapi/transports/http.py @@ -209,7 +209,7 @@ def transition(self, link, params=None, decoders=None, link_ancestors=None): response = _make_http_request(url, method, headers, query_params, form_params) result = _decode_result(response, decoders) - if isinstance(result, Document) and link_ancestors: + if (isinstance(result, Document) or result is None) and link_ancestors: result = _handle_inplace_replacements(result, link, link_ancestors) if isinstance(result, Error): diff --git a/coreapi/transports/websockets.py b/coreapi/transports/websockets.py index 01969ad..c1e34f2 100644 --- a/coreapi/transports/websockets.py +++ b/coreapi/transports/websockets.py @@ -23,7 +23,7 @@ def _decode_content(headers, content, decoders=None, base_url=None): return codec.load(content, base_url=base_url) -def _diff_content(heaaders, body, diff): +def _diff_content(headers, body, diff): patch = jsonpatch.JsonPatch.from_string(diff) previous_data = json.loads(body) next_data = jsonpatch.apply_patch(previous_data, patch) @@ -44,18 +44,17 @@ class WebSocketsTransport(BaseTransport): def transition(self, link, params=None, decoders=None, link_ancestors=None): url = link.url + base_url = url.replace('wss:', 'https:').replace('ws:', 'http:') connection = create_connection(url) request = _generate_request(decoders) connection.send(request) content = connection.recv() headers, body = _get_headers_and_body(content) - yield _decode_content(headers, body, decoders=decoders, base_url=url) + yield _decode_content(headers, body, decoders=decoders, base_url=base_url) while True: try: diff = connection.recv() except WebSocketConnectionClosedException: return body = _diff_content(headers, body, diff) - patch = jsonpatch.JsonPatch.from_string(diff) - body = json.dumps(jsonpatch.apply_patch(json.loads(body), patch)) - yield _decode_content(headers, body, decoders=decoders, base_url=url) + yield _decode_content(headers, body, decoders=decoders, base_url=base_url)