diff --git a/README.md b/README.md index adb9c14..bd1799a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![Build Status](https://travis-ci.org/Tanganelli/CoAPthon.svg?branch=master)](https://travis-ci.org/Tanganelli/CoAPthon) [![Coverage Status](https://coveralls.io/repos/Tanganelli/CoAPthon/badge.svg?branch=master&service=github)](https://coveralls.io/github/Tanganelli/CoAPthon?branch=master) [![Documentation Status](https://readthedocs.org/projects/coapthon/badge/?version=latest)](http://coapthon.readthedocs.org/en/latest/?badge=latest) -[![BuyMeACoffe](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/jeo) CoAPthon ======== @@ -408,4 +407,3 @@ $ make html ``` The documentation will be build in CoAPthon/docs/build/html. Let's start from index.html to have an overview of the library. - diff --git a/coapclient.py b/coapclient.py index b725ae5..3bc693a 100644 --- a/coapclient.py +++ b/coapclient.py @@ -104,6 +104,15 @@ def main(): # pragma: no cover sys.exit(2) host, port, path = parse_uri(path) + + """ Idee sul controllo finale dell'url + + if path and not path.endswith("coap://"): + print "errore nel passaggio" + usage() + sys.exit(2) + """ + try: tmp = socket.gethostbyname(host) host = tmp @@ -124,7 +133,7 @@ def main(): # pragma: no cover usage() sys.exit(2) client.observe(path, client_callback_observe) - + elif op == "DELETE": if path is None: print "Path cannot be empty for a DELETE request" @@ -161,6 +170,30 @@ def main(): # pragma: no cover response = client.discover() print response.pretty_print() client.stop() + """test con implementazione in request layer""" + elif op == "FETCH": + print "FETCH CALLBACK OK" + if path is None: + print "Path cannot be empty for FETCH" + usage() + sys.exit(2) + ##AGGIUNTA DEL PAYLOAD DA DOVE ARRIVA LA RICHIESTA + if payload is None: + print "Payload cannot be empty for a FETCH request" + usage() + sys.exit(2) + response = client.fetch(path, payload, proxy_uri=proxy_uri) + print response.pretty_print() + client.stop() + elif op == "PATCH": + print "PATCH CALLBACK OK" + if path is None: + print "Path cannot be empty for FETCH" + usage() + sys.exit(2) + response = client.patch(path, payload, proxy_uri=proxy_uri) + print response.pretty_print() + client.stop() else: print "Operation not recognized" usage() diff --git a/coapserver.py b/coapserver.py index 79139f5..c4fcdb6 100644 --- a/coapserver.py +++ b/coapserver.py @@ -1,68 +1,69 @@ -#!/usr/bin/env python - -import getopt -import sys -from coapthon.server.coap import CoAP -from exampleresources import BasicResource, Long, Separate, Storage, Big, voidResource, XMLResource, ETAGResource, \ - Child, \ - MultipleEncodingResource, AdvancedResource, AdvancedResourceSeparate, DynamicResource - -__author__ = 'Giacomo Tanganelli' - - -class CoAPServer(CoAP): - def __init__(self, host, port, multicast=False): - CoAP.__init__(self, (host, port), multicast) - self.add_resource('basic/', BasicResource()) - self.add_resource('storage/', Storage()) - self.add_resource('separate/', Separate()) - self.add_resource('long/', Long()) - self.add_resource('big/', Big()) - self.add_resource('void/', voidResource()) - self.add_resource('xml/', XMLResource()) - self.add_resource('encoding/', MultipleEncodingResource()) - self.add_resource('etag/', ETAGResource()) - self.add_resource('child/', Child()) - self.add_resource('advanced/', AdvancedResource()) - self.add_resource('advancedSeparate/', AdvancedResourceSeparate()) - self.add_resource('dynamic/', DynamicResource()) - - print "CoAP Server start on " + host + ":" + str(port) - print self.root.dump() - - -def usage(): # pragma: no cover - print "coapserver.py -i -p " - - -def main(argv): # pragma: no cover - ip = "0.0.0.0" - port = 5683 - multicast = False - try: - opts, args = getopt.getopt(argv, "hi:p:m", ["ip=", "port=", "multicast"]) - except getopt.GetoptError: - usage() - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - usage() - sys.exit() - elif opt in ("-i", "--ip"): - ip = arg - elif opt in ("-p", "--port"): - port = int(arg) - elif opt in ("-m", "--multicast"): - multicast = True - - server = CoAPServer(ip, port, multicast) - try: - server.listen(10) - except KeyboardInterrupt: - print "Server Shutdown" - server.close() - print "Exiting..." - - -if __name__ == "__main__": # pragma: no cover - main(sys.argv[1:]) +#!/usr/bin/env python + +import getopt +import sys +from coapthon.server.coap import CoAP +from exampleresources import BasicResource, Long, Separate, Storage, Big, voidResource, XMLResource, ETAGResource, \ + Child, \ + MultipleEncodingResource, AdvancedResource, AdvancedResourceSeparate, FetchResource + +__author__ = 'Giacomo Tanganelli' + + +class CoAPServer(CoAP): + def __init__(self, host, port, multicast=False): + CoAP.__init__(self, (host, port), multicast) + self.add_resource('basic/', BasicResource()) + self.add_resource('storage/', Storage()) + self.add_resource('separate/', Separate()) + self.add_resource('long/', Long()) + self.add_resource('big/', Big()) + self.add_resource('void/', voidResource()) + self.add_resource('xml/', XMLResource()) + self.add_resource('encoding/', MultipleEncodingResource()) + self.add_resource('etag/', ETAGResource()) + self.add_resource('child/', Child()) + self.add_resource('advanced/', AdvancedResource()) + self.add_resource('advancedSeparate/', AdvancedResourceSeparate()) + ##Resource to test FETCH + self.add_resource('fetchTest/', FetchResource()) + + print "CoAP Server start on " + host + ":" + str(port) + print self.root.dump() + + +def usage(): # pragma: no cover + print "coapserver.py -i -p " + + +def main(argv): # pragma: no cover + ip = "0.0.0.0" + port = 5683 + multicast = False + try: + opts, args = getopt.getopt(argv, "hi:p:m", ["ip=", "port=", "multicast"]) + except getopt.GetoptError: + usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + usage() + sys.exit() + elif opt in ("-i", "--ip"): + ip = arg + elif opt in ("-p", "--port"): + port = int(arg) + elif opt in ("-m", "--multicast"): + multicast = True + + server = CoAPServer(ip, port, multicast) + try: + server.listen(10) + except KeyboardInterrupt: + print "Server Shutdown" + server.close() + print "Exiting..." + + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) diff --git a/coapthon/client/helperclient.py b/coapthon/client/helperclient.py index e109b1c..7680e36 100644 --- a/coapthon/client/helperclient.py +++ b/coapthon/client/helperclient.py @@ -143,6 +143,54 @@ def get(self, path, proxy_uri=None, callback=None, timeout=None, **kwargs): # p return self.send_request(request, callback, timeout) + def fetch(self, path, payload, proxy_uri=None, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a FETCH on a certain path. + + :param path: the path + :param payload: the request payload + :param proxy_uri: Proxy-Uri option of a request + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.FETCH, path) + request.token = generate_random_token(2) + if proxy_uri: + request.proxy_uri = proxy_uri + request.payload = payload + request.content_type = defines.Content_types["application/map-keys+json"] + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def patch(self, path, payload, proxy_uri=None, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a PATCH on a certain path. + + :param path: the path + :param payload: the request payload + :param proxy_uri: Proxy-Uri option of a request + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.PATCH, path) + request.token = generate_random_token(2) + request.payload = payload + request.content_type = defines.Content_types["application/json-patch+json"] + if proxy_uri: + request.proxy_uri = proxy_uri + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + def observe(self, path, callback, timeout=None, **kwargs): # pragma: no cover """ Perform a GET with observe on a certain path. diff --git a/coapthon/defines.py b/coapthon/defines.py index a01bdce..32aefb8 100644 --- a/coapthon/defines.py +++ b/coapthon/defines.py @@ -222,6 +222,7 @@ class Codes(object): """ CoAP codes. Every code is represented as (NUMBER, NAME) """ + ERROR_LOWER_BOUND = 128 EMPTY = CodeItem(0, 'EMPTY') @@ -229,6 +230,8 @@ class Codes(object): POST = CodeItem(2, 'POST') PUT = CodeItem(3, 'PUT') DELETE = CodeItem(4, 'DELETE') + FETCH = CodeItem(5, 'FETCH') + PATCH = CodeItem(6, 'PATCH') CREATED = CodeItem(65, 'CREATED') DELETED = CodeItem(66, 'DELETED') @@ -253,6 +256,8 @@ class Codes(object): SERVICE_UNAVAILABLE = CodeItem(163, 'SERVICE_UNAVAILABLE') GATEWAY_TIMEOUT = CodeItem(164, 'GATEWAY_TIMEOUT') PROXY_NOT_SUPPORTED = CodeItem(165, 'PROXY_NOT_SUPPORTED') + UNPROCESSABLE_ENTITY = CodeItem(166, 'UNPROCESSABLE_ENTITY') + INCONSISTENT_STATE = CodeItem(167, 'INCOSISTENT_STATE') LIST = { 0: EMPTY, @@ -260,6 +265,8 @@ class Codes(object): 2: POST, 3: PUT, 4: DELETE, + 5: FETCH, + 6: PATCH, 65: CREATED, 66: DELETED, @@ -283,7 +290,9 @@ class Codes(object): 162: BAD_GATEWAY, 163: SERVICE_UNAVAILABLE, 164: GATEWAY_TIMEOUT, - 165: PROXY_NOT_SUPPORTED + 165: PROXY_NOT_SUPPORTED, + 166: UNPROCESSABLE_ENTITY, + 167: INCONSISTENT_STATE } @@ -295,7 +304,9 @@ class Codes(object): "application/octet-stream": 42, "application/exi": 47, "application/json": 50, - "application/cbor": 60 + "application/cbor": 60, + "application/map-keys+json": 51, + "application/json-patch+json": 52 } COAP_PREFACE = "coap://" @@ -328,6 +339,8 @@ class Codes(object): "BAD_GATEWAY": "502", "SERVICE_UNAVAILABLE": "503", "GATEWAY_TIMEOUT": "504", - "PROXY_NOT_SUPPORTED": "502" + "PROXY_NOT_SUPPORTED": "502", + "INCONSISTENT_STATE": "409", + "UNPROCESSABLE_ENTITY": "422" } diff --git a/coapthon/layers/requestlayer.py b/coapthon/layers/requestlayer.py index 0286b4c..e416e4a 100644 --- a/coapthon/layers/requestlayer.py +++ b/coapthon/layers/requestlayer.py @@ -29,6 +29,10 @@ def receive_request(self, transaction): transaction = self._handle_put(transaction) elif method == defines.Codes.DELETE.number: transaction = self._handle_delete(transaction) + elif method == defines.Codes.FETCH.number: + transaction = self._handle_fetch(transaction) + elif method == defines.Codes.PATCH.number: + transaction = self._handle_patch(transaction) else: transaction.response = None return transaction @@ -140,3 +144,57 @@ def _handle_delete(self, transaction): transaction = self._server.resourceLayer.delete_resource(transaction, path) return transaction + def _handle_fetch(self, transaction): + """ + Handle FETCH requests + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction with the response to the request + """ + path = str("/" + transaction.request.uri_path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + if path == defines.DISCOVERY_URL: + transaction = self._server.resourceLayer.discover(transaction) + else: + try: + resource = self._server.root[path] + except KeyError: + resource = None + if resource is None or path == '/': + # Not Found + transaction.response.code = defines.Codes.NOT_FOUND.number + else: + transaction.resource = resource + transaction = self._server.resourceLayer.fetch_resource(transaction) + + return transaction + + def _handle_patch(self, transaction): + """ + Handle PATCH requests + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction with the response to the request + """ + path = str("/" + transaction.request.uri_path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + try: + resource = self._server.root[path] + except KeyError: + resource = None + if resource is None: + transaction.response.code = defines.Codes.NOT_FOUND.number + else: + transaction.resource = resource + # Update request + transaction = self._server.resourceLayer.patch_resource(transaction) + + return transaction diff --git a/coapthon/layers/resourcelayer.py b/coapthon/layers/resourcelayer.py index 2609678..ad904d3 100644 --- a/coapthon/layers/resourcelayer.py +++ b/coapthon/layers/resourcelayer.py @@ -501,6 +501,186 @@ def get_resource(self, transaction): return transaction + def fetch_resource(self, transaction): + """ + Render a FETCH request. + + :param transaction: the transaction + :return: the transaction + """ + method = getattr(transaction.resource, 'render_FETCH', None) + + transaction.resource.actual_content_type = None + # Accept + if transaction.request.accept is not None: + transaction.resource.actual_content_type = transaction.request.accept + + # Render_GET + try: + resource = method(request=transaction.request) + except NotImplementedError: + try: + method = getattr(transaction.resource, "render_FETCH_advanced", None) + ret = method(request=transaction.request, response=transaction.response) + if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler + resource, response = ret + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CONTENT.number + return transaction + elif isinstance(ret, tuple) and len(ret) == 3 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler separate + resource, response, callback = ret + ret = self._handle_separate_advanced(transaction, callback) + if not isinstance(ret, tuple) or \ + not (isinstance(ret[0], Resource) and isinstance(ret[1], Response)): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + resource, response = ret + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CONTENT.number + return transaction + else: + raise NotImplementedError + except NotImplementedError: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + + if isinstance(resource, Resource): + pass + elif isinstance(resource, tuple) and len(resource) == 2: + resource, callback = resource + resource = self._handle_separate(transaction, callback) + if not isinstance(resource, Resource): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + else: # pragma: no cover + # Handle error + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction.response + + if resource.etag in transaction.request.etag: + transaction.response.code = defines.Codes.VALID.number + else: + transaction.response.code = defines.Codes.CONTENT.number + + try: + transaction.response.payload = resource.payload + if resource.actual_content_type is not None \ + and resource.actual_content_type != defines.Content_types["text/plain"]: + transaction.response.content_type = resource.actual_content_type + except KeyError: + transaction.response.code = defines.Codes.NOT_ACCEPTABLE.number + return transaction.response + + assert(isinstance(resource, Resource)) + if resource.etag is not None: + transaction.response.etag = resource.etag + if resource.max_age is not None: + transaction.response.max_age = resource.max_age + + transaction.resource = resource + + return transaction + + def patch_resource(self, transaction): + """ + Render a PATCH request. + + :param transaction: the transaction + :return: the response + """ + # If-Match + if transaction.request.if_match: + if None not in transaction.request.if_match and str(transaction.resource.etag) \ + not in transaction.request.if_match: + transaction.response.code = defines.Codes.PRECONDITION_FAILED.number + return transaction + # If-None-Match + if transaction.request.if_none_match: + transaction.response.code = defines.Codes.PRECONDITION_FAILED.number + return transaction + + method = getattr(transaction.resource, "render_PATCH", None) + try: + resource = method(request=transaction.request) + except NotImplementedError: + try: + method = getattr(transaction.resource, "render_PATCH_advanced", None) + ret = method(request=transaction.request, response=transaction.response) + if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler + resource, response = ret + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CHANGED.number + return transaction + elif isinstance(ret, tuple) and len(ret) == 3 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler separate + resource, response, callback = ret + ret = self._handle_separate_advanced(transaction, callback) + if not isinstance(ret, tuple) or \ + not (isinstance(ret[0], Resource) and isinstance(ret[1], Response)): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + resource, response = ret + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CHANGED.number + return transaction + else: + raise NotImplementedError + except NotImplementedError: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + + if isinstance(resource, Resource): + pass + elif isinstance(resource, tuple) and len(resource) == 2: + resource, callback = resource + resource = self._handle_separate(transaction, callback) + if not isinstance(resource, Resource): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + else: # pragma: no cover + # Handle error + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + + if resource.etag is not None: + transaction.response.etag = resource.etag + + transaction.response.code = defines.Codes.CHANGED.number + + transaction.response.payload = None + + assert (isinstance(resource, Resource)) + if resource.etag is not None: + transaction.response.etag = resource.etag + if resource.max_age is not None: + transaction.response.max_age = resource.max_age + + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + + return transaction + + def discover(self, transaction): """ Render a GET request to the .well-know/core link. diff --git a/coapthon/resources/resource.py b/coapthon/resources/resource.py index d09a432..f1ff902 100644 --- a/coapthon/resources/resource.py +++ b/coapthon/resources/resource.py @@ -442,6 +442,14 @@ def render_GET(self, request): """ raise NotImplementedError + def render_FETCH(self, request): + + raise NotImplementedError + + def render_PATCH(self, request): + + raise NotImplementedError + def render_GET_advanced(self, request, response): """ Method to be redefined to render a GET request on the resource. diff --git a/docs/logging.conf b/docs/logging.conf new file mode 100644 index 0000000..86a74a0 --- /dev/null +++ b/docs/logging.conf @@ -0,0 +1,22 @@ +[loggers] +keys=root + +[handlers] +keys=consoleHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s - %(threadName)-10s - %(name)s - %(levelname)s - %(message)s +datefmt= \ No newline at end of file diff --git a/exampleresources.py b/exampleresources.py index 455839e..f11aed2 100644 --- a/exampleresources.py +++ b/exampleresources.py @@ -1,9 +1,15 @@ import time - +import ast from coapthon import defines from coapthon.resources.resource import Resource +##VAR DUMP +import pprint + +#Per JSON +import json + __author__ = 'Giacomo Tanganelli' @@ -179,7 +185,8 @@ def render_GET(self, request): elif request.accept == defines.Content_types["text/plain"]: self.payload = (defines.Content_types["text/plain"], str(self.value)) return self - + def render_FETCH(self, request): + return self def render_PUT(self, request): self.edit_resource(request) return self @@ -281,3 +288,96 @@ def render_DELETE_separate(self, request, response): response.payload = "Response deleted" return True, response +class FetchResource(Resource): + def __init__(self, name="FetchResource", coap_server=None): + super(FetchResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=True) + self.x_coord = 256 + self.y_coord = 45 + self.foo = ["bar", "baz"] + self.allValues = { + 'x-coord': self.x_coord, + 'y-coord':self.y_coord, + 'foo': self.foo + } + + self.resource_type = "rt1" + #self.content_type = "application/json" + self.interface_type = "if1" + + def render_GET(self, request): + return self + + def render_FETCH(self, request): + if not request.payload: + tempPayload = '{' + for i, q in enumerate(self.allValues): + if i > 0: + tempPayload += ',' + tempPayload += "'" + str(q) + "': '" + str(self.allValues[q]) + "'" + tempPayload += '}' + self.payload = (defines.Content_types["application/json"], tempPayload) + return self + else: + query = request.payload + query = json.loads(query) + values = {} + for q in query["keys"]: + if q in self.allValues: + values[q] = self.allValues[q] + if (len(values.keys()) == 0): + self.payload = (defines.Codes.UNPROCESSABLE_ENTITY) + else: + self.payload = json.dumps(values) + return self + + def render_PATCH(self, request): + if not request.payload: + return self + else: + query = request.payload + query = json.loads(query) + for keys,values in query.iteritems(): + splitPath = values['path'].split('/'); + if (len(splitPath) == 2): + AVpath = splitPath[0] + AVindex = int(splitPath[1]) + else: + AVpath = splitPath[0] + AVindex = -1 + try: + int(values['value']) + valueToAdd = int(values['value']) + except ValueError: + valueToAdd = values['value'] + if(values['op'] == 'replace'): + if(AVpath in self.allValues): + self.allValues[AVpath] = valueToAdd + else: + self.payload = (defines.Codes.INCONSISTENT_STATE) + return self + else: + if(AVpath in self.allValues): + if (type(self.allValues[AVpath]) == list ): + if (AVindex > len(self.allValues[AVpath])): + self.payload = (defines.Codes.UNPROCESSABLE_ENTITY) + else: + self.allValues[AVpath].insert(AVindex, valueToAdd) + else: + tempValue = [self.allValues[AVpath]] + tempValue.insert(AVindex, valueToAdd) + self.allValues[AVpath] = tempValue + else: + self.payload = (defines.Codes.UNPROCESSABLE_ENTITY) + return self + + def render_PUT(self, request): + self.edit_resource(request) + return self + + def render_POST(self, request): + res = self.init_resource(request, FetchResource()) + return res + + def render_DELETE(self, request): + return True diff --git a/payload_fetch.json b/payload_fetch.json new file mode 100644 index 0000000..b11455e --- /dev/null +++ b/payload_fetch.json @@ -0,0 +1,2 @@ +{"keys": ["x-coord", "y-coord", "foo"]} + diff --git a/payload_patch.json b/payload_patch.json new file mode 100644 index 0000000..24595f0 --- /dev/null +++ b/payload_patch.json @@ -0,0 +1,2 @@ +{"keys": {"op":"add", "path":"foo/0", "value":"Test"}} + diff --git a/tests.py b/tests.py index ff50f72..ae8f33f 100644 --- a/tests.py +++ b/tests.py @@ -55,6 +55,7 @@ def _test_plugtest(self, message_list): def test_retrasnmissions(self): print "Retransmissions" + print "PROVA IN CORSO" path = "/separate" req = Request() req.code = defines.Codes.GET.number