From 1c6430bf3fb643d0c89dbe31d8e65f6cecaa140b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 Mar 2015 09:57:25 -0400 Subject: [PATCH] Allow noderange pagination of all nodes When a noderange starts with '<' or '>', use the set of all nodes as basis for pagination. Additionally, provide better feedback to client on noderange parsing issues. Also implement natural sort in various places after doing it for the pagination. --- confluent_server/confluent/core.py | 23 +++++++++++++--- confluent_server/confluent/httpapi.py | 4 +-- confluent_server/confluent/noderange.py | 36 ++++++++++++++++++++----- confluent_server/confluent/sockapi.py | 4 +-- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 360ec4ad..ea1a7fcd 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -262,7 +262,12 @@ def enumerate_nodegroup_collection(collectionpath, configmanager): def enumerate_node_collection(collectionpath, configmanager): if collectionpath == ['nodes']: # it is just '/node/', need to list nodes - return iterate_collections(configmanager.list_nodes()) + allnodes = list(configmanager.list_nodes()) + try: + allnodes.sort(key=noderange.humanify_nodename) + except TypeError: + allnodes.sort() + return iterate_collections(allnodes) nodeorrange = collectionpath[1] if collectionpath[0] == 'nodes' and not configmanager.is_node(nodeorrange): raise exc.NotFoundException("Invalid element requested") @@ -368,8 +373,8 @@ def handle_node_request(configmanager, inputdata, operation, if isnoderange: try: nodes = noderange.NodeRange(nodeorrange, configmanager).nodes - except Exception: - raise exc.NotFoundException("Invalid Noderange") + except Exception as e: + raise exc.NotFoundException("Invalid Noderange: " + str(e)) else: nodes = (nodeorrange,) except IndexError: # doesn't actually have a long enough path @@ -381,9 +386,19 @@ def handle_node_request(configmanager, inputdata, operation, if operation == "create": inputdata = msg.InputAttributes(pathcomponents, inputdata) create_node(inputdata.attribs, configmanager) - return iterate_collections(configmanager.list_nodes()) + allnodes = list(configmanager.list_nodes()) + try: + allnodes.sort(key=noderange.humanify_nodename) + except TypeError: + allnodes.sort() + return iterate_collections(allnodes) if isnoderange and len(pathcomponents) == 3 and pathcomponents[2] == 'nodes': # this means that it's a list of relevant nodes + nodes = list(nodes) + try: + nodes.sort(key=noderange.humanify_nodename) + except TypeError: + nodes.sort() return iterate_collections(nodes) if len(pathcomponents) == 2: iscollection = True diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 7e103c27..b1c70b21 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -393,9 +393,9 @@ def resourcehandler_backend(env, start_response): pagecontent += datum start_response('200 OK', headers) yield pagecontent - except exc.NotFoundException: + except exc.NotFoundException as ne: start_response('404 Not found', headers) - yield "404 - Request path not recognized" + yield "404 - Request path not recognized - " + str(ne) except exc.InvalidArgumentException as e: start_response('400 Bad Request - ' + str(e), headers) yield '400 - Bad Request - ' + str(e) diff --git a/confluent_server/confluent/noderange.py b/confluent_server/confluent/noderange.py index bb58574b..c533cfd4 100644 --- a/confluent_server/confluent/noderange.py +++ b/confluent_server/confluent/noderange.py @@ -22,19 +22,30 @@ import itertools import pyparsing as pp +import re # construct custom grammar with pyparsing _nodeword = pp.Word(pp.alphanums + '~^$/=-:.*+!') _nodebracket = pp.QuotedString(quoteChar='[', endQuoteChar=']', unquoteResults=False) _nodeatom = pp.Group(pp.OneOrMore(_nodeword | _nodebracket)) -_paginationstart = pp.Word('<', pp.nums) -_paginationend = pp.Word('>', pp.nums) +_paginationstart = pp.Group(pp.Word('<', pp.nums)) +_paginationend = pp.Group(pp.Word('>', pp.nums)) _grammar = _nodeatom | ',-' | ',' | '@' | _paginationstart | _paginationend _parser = pp.nestedExpr(content=_grammar) _numextractor = pp.OneOrMore(pp.Word(pp.alphas + '-') | pp.Word(pp.nums)) +numregex = re.compile('([0-9]+)') +def humanify_nodename(nodename): + """Analyzes nodename in a human way to enable natural sort + + :param nodename: The node name to analyze + :returns: A structure that can be consumed by 'sorted' + """ + return [int(text) if text.isdigit() else text.lower() + for text in re.split(numregex, nodename)] + # TODO: pagination operators pp.nums for begin and end respective class NodeRange(object): @@ -48,15 +59,27 @@ class NodeRange(object): self.beginpage = None self.endpage = None self.cfm = config - elements = _parser.parseString("(" + noderange + ")").asList()[0] - self._noderange = self._evaluate(elements) + try: + elements = _parser.parseString("(" + noderange + ")").asList()[0] + except pp.ParseException as pe: + raise Exception("Invalid syntax") + if noderange[0] in ('<', '>'): + # pagination across all nodes + self._evaluate(elements) + self._noderange = set(self.cfm.list_nodes()) + else: + self._noderange = self._evaluate(elements) @property def nodes(self): if self.beginpage is None and self.endpage is None: return self._noderange sortedlist = list(self._noderange) - sortedlist.sort() + try: + sortedlist.sort(key=humanify_nodename) + except TypeError: + # The natural sort attempt failed, fallback to ascii sort + sortedlist.sort() if self.beginpage is not None: sortedlist = sortedlist[self.beginpage:] if self.endpage is not None: @@ -135,8 +158,6 @@ class NodeRange(object): def expand_entity(self, entname): if self.cfm is None or self.cfm.is_node(entname): return set([entname]) - if self.cfm.is_node(entname): - return set([entname]) if self.cfm.is_nodegroup(entname): grpcfg = self.cfm.get_nodegroup_attributes(entname) nodes = grpcfg['nodes'] @@ -144,6 +165,7 @@ class NodeRange(object): nodes |= NodeRange( grpcfg['noderange']['value'], self.cfm).nodes return nodes + raise Exception('Unknown node ' + entname) def _expandstring(self, element, filternodes=None): prefix = '' diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index ae9a9713..ead744aa 100644 --- a/confluent_server/confluent/sockapi.py +++ b/confluent_server/confluent/sockapi.py @@ -214,9 +214,9 @@ def process_request(connection, request, cfm, authdata, authname, skipauth): configmanager.ConfigManager.shutdown() else: hdlr = pluginapi.handle_path(path, operation, cfm, params) - except exc.NotFoundException: + except exc.NotFoundException as e: tlvdata.send(connection, {"errorcode": 404, - "error": "Target not found"}) + "error": "Target not found - " + str(e)}) tlvdata.send(connection, {"_requestdone": 1}) except exc.InvalidArgumentException as e: tlvdata.send(connection, {"errorcode": 400,