├── .coveragerc ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── README.rst.original ├── VER ├── build.sh ├── objectpath ├── __init__.py ├── core │ ├── __init__.py │ ├── interpreter.py │ └── parser.py ├── shell.py └── utils │ ├── __init__.py │ ├── colorify.py │ ├── debugger.py │ ├── json_ext.py │ └── timeutils.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── shell.py ├── testObjectPath.py └── tests ├── __init__.py ├── test_ObjectPath.py └── test_utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */tests* 4 | */shell.py 5 | */debugger.py 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.8' 5 | - pypy 6 | install: 7 | - pip install -r requirements.txt 8 | - pip install coveralls 9 | script: 10 | - nosetests 11 | - coverage run --source=objectpath setup.py test 12 | deploy: 13 | provider: pypi 14 | user: adriankal 15 | password: 16 | secure: bWTP43XxjAIwC6PMmfjt4/dCBenky+zF7empMgzfmkUq5jCoQjCJm4IUdYIdjCKLYEIU1DaOTp7+rqmIZf2d8wWvGAc4+k3vinV9k8WzycpBM+YgnW2knQ5eko93H0lpNOIrat4J0wvc51JjHfj4uqib6SCTaXmBS/kRHmiRkx8= 17 | on: 18 | tags: true 19 | all_branches: true 20 | repo: adriank/ObjectPath 21 | distributions: "sdist bdist_wheel" 22 | after_success: 23 | - coveralls 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File (Integrated Terminal)", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "/Users/adrian/projects/ObjectPath/shell.py", 12 | "args": ["/Users/adrian/projects/nutrient-db/butter.json"], 13 | "console": "integratedTerminal" 14 | }, 15 | { 16 | "name": "Python: Attach", 17 | "type": "python", 18 | "request": "attach", 19 | "port": 5678, 20 | "host": "localhost" 21 | }, 22 | { 23 | "name": "Python: Module", 24 | "type": "python", 25 | "request": "launch", 26 | "module": "objectpath", 27 | "console": "integratedTerminal" 28 | }, 29 | { 30 | "name": "Python: Django", 31 | "type": "python", 32 | "request": "launch", 33 | "program": "${workspaceFolder}/manage.py", 34 | "console": "integratedTerminal", 35 | "args": [ 36 | "runserver", 37 | "--noreload", 38 | "--nothreading" 39 | ], 40 | "django": true 41 | }, 42 | { 43 | "name": "Python: Flask", 44 | "type": "python", 45 | "request": "launch", 46 | "module": "flask", 47 | "env": { 48 | "FLASK_APP": "app.py" 49 | }, 50 | "args": [ 51 | "run", 52 | "--no-debugger", 53 | "--no-reload" 54 | ], 55 | "jinja": true 56 | }, 57 | { 58 | "name": "Python: Current File (External Terminal)", 59 | "type": "python", 60 | "request": "launch", 61 | "program": "${file}", 62 | "console": "externalTerminal" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adrian Kalbarczyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include VER 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ObjectPath 2 | ========== 3 | 4 | [![Downloads](https://img.shields.io/pypi/dm/objectpath.svg)](https://pypi.python.org/pypi/objectpath/) 5 | 6 | [![Build Status](https://travis-ci.org/adriank/ObjectPath.svg?branch=master)](https://travis-ci.org/adriank/ObjectPath) 7 | [![Code Health](https://landscape.io/github/adriank/ObjectPath/master/landscape.png)](https://landscape.io/github/adriank/ObjectPath/master) 8 | [![Coverage Status](https://coveralls.io/repos/adriank/ObjectPath/badge.png?branch=master)](https://coveralls.io/r/adriank/ObjectPath?branch=master) 9 | 10 | The agile NoSQL query language for semi-structured data 11 | ----------------------------------------------- 12 | 13 | **#Python #NoSQL #Javascript #JSON #nested-array-object** 14 | 15 | ObjectPath is a query language similar to XPath or JSONPath, but much more powerful thanks to embedded arithmetic calculations, comparison mechanisms and built-in functions. This makes the language more like SQL in terms of expressiveness, but it works over JSON documents rather than relations. ObjectPath can be considered a full-featured expression language. Besides selector mechanism there is also boolean logic, type system and string concatenation available. On top of that, the language implementations (Python at the moment; Javascript is in beta!) are secure and relatively fast. 16 | 17 | More at [ObjectPath site](https://adriank.github.io/ObjectPath/) 18 | 19 | ![ObjectPath img](http://adriank.github.io/ObjectPath/img/op-colors.png) 20 | 21 | ObjectPath makes it easy to find data in big nested JSON documents. It borrows the best parts from E4X, JSONPath, XPath and SQL. ObjectPath is to JSON documents what XPath is to XML. Other examples to ilustrate this kind of relationship are: 22 | 23 | | Scope | Language | 24 | |---|---| 25 | | text documents | regular expression | 26 | | XML | XPath | 27 | | HTML | CSS selectors | 28 | | JSON documents | ObjectPath | 29 | 30 | Documentation 31 | ------------- 32 | 33 | [ObjectPath Reference](https://adriank.github.io/ObjectPath/reference.html) 34 | 35 | Command line usage 36 | ----- 37 | 38 | `````sh 39 | $ sudo pip install objectpath 40 | $ objectpath file.json 41 | ````` 42 | or 43 | `````sh 44 | $ git clone https://github.com/adriank/ObjectPath.git 45 | $ cd ObjectPath 46 | $ python shell.py file.json 47 | ````` 48 | 49 | Python usage 50 | ---------------- 51 | 52 | `````sh 53 | $ sudo pip install objectpath 54 | $ python 55 | >>> from objectpath import * 56 | >>> tree=Tree({"a":1}) 57 | >>> tree.execute("$.a") 58 | 1 59 | >>> 60 | ````` 61 | 62 | `````sh 63 | $ git clone https://github.com/adriank/ObjectPath.git 64 | $ cd ObjectPath 65 | $ python 66 | >>> from objectpath import * 67 | >>> tree=Tree({"a":1}) 68 | >>> tree.execute("$.a") 69 | 1 70 | >>> 71 | ````` 72 | 73 | Contributing & bugs 74 | ------------------- 75 | 76 | I appreciate all contributions and bugfix requests for ObjectPath, however since I don't code in Python anymore, this library is not maintained as of now. Since I can't fully assure that code contributed by others meets quality standards, I can't accept PRs. 77 | 78 | If you feel you could maintain this code, ping me. I'd be more than happy to transfer this repo to a dedicated ObjectPath organization on GitHub and give the ownership to someone with more time for this project than me. 79 | 80 | License 81 | ------- 82 | 83 | **MIT** 84 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ObjectPath 2 | ========== 3 | 4 | |Downloads| |Build Status| |Code Health| |Coverage Status| 5 | 6 | The agile NoSQL query language for semi-structured data 7 | ------------------------------------------------------- 8 | 9 | **#Python #NoSQL #Javascript #JSON #nested-array-object** 10 | 11 | ObjectPath is a query language similar to XPath or JSONPath, but much 12 | more powerful thanks to embedded arithmetic calculations, comparison 13 | mechanisms and built-in functions. This makes the language more like SQL 14 | in terms of expressiveness, but it works over JSON documents rather than 15 | relations. ObjectPath can be considered a full-featured expression 16 | language. Besides selector mechanism there is also boolean logic, type 17 | system and string concatenation available. On top of that, the language 18 | implementations (Python at the moment; Javascript is in beta!) are 19 | secure and relatively fast. 20 | 21 | More at `ObjectPath site `__ 22 | 23 | .. figure:: http://adriank.github.io/ObjectPath/img/op-colors.png 24 | :alt: ObjectPath img 25 | 26 | ObjectPath img 27 | 28 | ObjectPath makes it easy to find data in big nested JSON documents. It 29 | borrows the best parts from E4X, JSONPath, XPath and SQL. ObjectPath is 30 | to JSON documents what XPath is to XML. Other examples to ilustrate this 31 | kind of relationship are: 32 | 33 | ============== ================== 34 | Scope Language 35 | ============== ================== 36 | text documents regular expression 37 | XML XPath 38 | HTML CSS selectors 39 | JSON documents ObjectPath 40 | ============== ================== 41 | 42 | Documentation 43 | ------------- 44 | 45 | `ObjectPath Reference `__ 46 | 47 | Command line usage 48 | ------------------ 49 | 50 | .. code:: sh 51 | 52 | $ sudo pip install objectpath 53 | $ objectpath file.json 54 | 55 | or 56 | 57 | .. code:: sh 58 | 59 | $ git clone https://github.com/adriank/ObjectPath.git 60 | $ cd ObjectPath 61 | $ python shell.py file.json 62 | 63 | Python usage 64 | ------------ 65 | 66 | .. code:: sh 67 | 68 | $ sudo pip install objectpath 69 | $ python 70 | >>> from objectpath import * 71 | >>> tree=Tree({"a":1}) 72 | >>> tree.execute("$.a") 73 | 1 74 | >>> 75 | 76 | .. code:: sh 77 | 78 | $ git clone https://github.com/adriank/ObjectPath.git 79 | $ cd ObjectPath 80 | $ python 81 | >>> from objectpath import * 82 | >>> tree=Tree({"a":1}) 83 | >>> tree.execute("$.a") 84 | 1 85 | >>> 86 | 87 | Contributing & bugs 88 | ------------------- 89 | 90 | I appreciate all contributions and bugfix requests for ObjectPath, 91 | however since I don’t code in Python any more, this library is not 92 | maintained as of now. Since I can’t fully assure that code contributed 93 | by others meets quality standards, I can’t accept PRs. 94 | 95 | If you feel you could maintain this code, ping me. I’d be more than 96 | happy to transfer this repo to a dedicated ObjectPath organization on 97 | GitHub and give the ownership to someone with more time for this project 98 | than me. 99 | 100 | License 101 | ------- 102 | 103 | **MIT** 104 | 105 | .. |Downloads| image:: https://img.shields.io/pypi/dm/objectpath.svg 106 | :target: https://pypi.python.org/pypi/objectpath/ 107 | .. |Build Status| image:: https://travis-ci.org/adriank/ObjectPath.svg?branch=master 108 | :target: https://travis-ci.org/adriank/ObjectPath 109 | .. |Code Health| image:: https://landscape.io/github/adriank/ObjectPath/master/landscape.png 110 | :target: https://landscape.io/github/adriank/ObjectPath/master 111 | .. |Coverage Status| image:: https://coveralls.io/repos/adriank/ObjectPath/badge.png?branch=master 112 | :target: https://coveralls.io/r/adriank/ObjectPath?branch=master 113 | -------------------------------------------------------------------------------- /README.rst.original: -------------------------------------------------------------------------------- 1 | ObjectPath 2 | ========== 3 | 4 | |Downloads| |Build Status| |Code Health| |Coverage Status| 5 | 6 | The agile NoSQL query language for semi-structured data 7 | ------------------------------------------------------- 8 | 9 | **#Python #NoSQL #Javascript #JSON #nested-array-object** 10 | 11 | ObjectPath is a query language similar to XPath or JSONPath, but much 12 | more powerful thanks to embedded arithmetic calculations, comparison 13 | mechanisms and built-in functions. This makes the language more like SQL 14 | in terms of expressiveness, but it works over JSON documents rather than 15 | relations. ObjectPath can be considered a full-featured expression 16 | language. Besides selector mechanism there is also boolean logic, type 17 | system and string concatenation available. On top of that, the language 18 | implementations (Python at the moment; Javascript is in beta!) are 19 | secure and relatively fast. 20 | 21 | More at `ObjectPath site `__ 22 | 23 | .. figure:: http://adriank.github.io/ObjectPath/img/op-colors.png 24 | :alt: ObjectPath img 25 | 26 | ObjectPath img 27 | 28 | ObjectPath makes it easy to find data in big nested JSON documents. It 29 | borrows the best parts from E4X, JSONPath, XPath and SQL. ObjectPath is 30 | to JSON documents what XPath is to XML. Other examples to ilustrate this 31 | kind of relationship are: 32 | 33 | ============== ================== 34 | Scope Language 35 | ============== ================== 36 | text documents regular expression 37 | XML XPath 38 | HTML CSS selectors 39 | JSON documents ObjectPath 40 | ============== ================== 41 | 42 | Documentation 43 | ------------- 44 | 45 | `ObjectPath Reference `__ 46 | 47 | Command line usage 48 | ------------------ 49 | 50 | .. code:: sh 51 | 52 | $ sudo pip install objectpath 53 | $ objectpath file.json 54 | 55 | or 56 | 57 | .. code:: sh 58 | 59 | $ git clone https://github.com/adriank/ObjectPath.git 60 | $ cd ObjectPath 61 | $ python shell.py file.json 62 | 63 | Python usage 64 | ------------ 65 | 66 | .. code:: sh 67 | 68 | $ sudo pip install objectpath 69 | $ python 70 | >>> from objectpath import * 71 | >>> tree=Tree({"a":1}) 72 | >>> tree.execute("$.a") 73 | 1 74 | >>> 75 | 76 | .. code:: sh 77 | 78 | $ git clone https://github.com/adriank/ObjectPath.git 79 | $ cd ObjectPath 80 | $ python 81 | >>> from objectpath import * 82 | >>> tree=Tree({"a":1}) 83 | >>> tree.execute("$.a") 84 | 1 85 | >>> 86 | 87 | Contributing & bugs 88 | ------------------- 89 | 90 | I appreciate all contributions and bugfix requests for ObjectPath, 91 | however since I don’t code in Python any more, this library is not 92 | maintained as of now. Since I can’t fully assure that code contributed 93 | by others meets quality standards, I can’t accept PRs. 94 | 95 | If you feel you could maintain this code, ping me. I’d be more than 96 | happy to transfer this repo to a dedicated ObjectPath organization on 97 | GitHub and give the ownership to someone with more time for this project 98 | than me. 99 | 100 | License 101 | ------- 102 | 103 | **MIT** 104 | 105 | .. |Downloads| image:: https://img.shields.io/pypi/dm/objectpath.svg 106 | :target: https://pypi.python.org/pypi/objectpath/ 107 | .. |Build Status| image:: https://travis-ci.org/adriank/ObjectPath.svg?branch=master 108 | :target: https://travis-ci.org/adriank/ObjectPath 109 | .. |Code Health| image:: https://landscape.io/github/adriank/ObjectPath/master/landscape.png 110 | :target: https://landscape.io/github/adriank/ObjectPath/master 111 | .. |Coverage Status| image:: https://coveralls.io/repos/adriank/ObjectPath/badge.png?branch=master 112 | :target: https://coveralls.io/r/adriank/ObjectPath?branch=master 113 | -------------------------------------------------------------------------------- /VER: -------------------------------------------------------------------------------- 1 | 0.6.2 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pandoc -f markdown -t rst -o README.rst README.md 3 | mdTable="aaa Scope aaa Language aaa aaa---aaa---aaa aaa text documents aaa regular 4 | expression aaa aaa XML aaa XPath aaa aaa HTML aaa CSS selectors aaa aaa JSON 5 | documents aaa ObjectPath aaa" 6 | rstTable="============== ================== 7 | Scope Language 8 | ============== ================== 9 | text documents regular expression 10 | XML XPath 11 | HTML CSS selectors 12 | JSON documents ObjectPath 13 | ============== ==================" 14 | sed -i "s/\\\|/aaa/g" README.rst 15 | perl -0777 -i.original -pe "s/$mdTable/$rstTable/g" README.rst 16 | 17 | python setup.py build 18 | python setup.py sdist bdist_wheel upload 19 | -------------------------------------------------------------------------------- /objectpath/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .core.interpreter import * 5 | from .utils import * 6 | -------------------------------------------------------------------------------- /objectpath/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of ObjectPath released under MIT license. 5 | # Copyright (C) 2010-2014 Adrian Kalbarczyk 6 | 7 | from types import GeneratorType as generator 8 | from itertools import chain 9 | 10 | SELECTOR_OPS = [ 11 | "is", ">", "<", "is not", ">=", "<=", "in", "not in", ":", "and", "or", "matches", "fn" 12 | ] 13 | # it must be list because of further concatenations 14 | NUM_TYPES = [int, float] 15 | 16 | try: 17 | NUM_TYPES += [long] 18 | except NameError: 19 | pass 20 | 21 | STR_TYPES = [str] 22 | 23 | try: 24 | STR_TYPES += [unicode] 25 | except NameError: 26 | pass 27 | 28 | ITER_TYPES = [list, generator, chain] 29 | 30 | try: 31 | ITER_TYPES += [map, filter] 32 | except NameError: 33 | pass 34 | 35 | class ProgrammingError(Exception): 36 | pass 37 | 38 | class ExecutionError(Exception): 39 | pass 40 | 41 | PY_TYPES_MAP = { 42 | "int": "number", 43 | "float": "number", 44 | "str": "string", 45 | "dict": "object", 46 | "list": "array" 47 | } 48 | -------------------------------------------------------------------------------- /objectpath/core/interpreter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file is part of ObjectPath released under MIT license. 4 | # Copyright (C) 2010-2014 Adrian Kalbarczyk 5 | 6 | import sys, re 7 | from .parser import parse 8 | from objectpath.core import * 9 | import objectpath.utils.colorify as color # pylint: disable=W0614 10 | from objectpath.utils import flatten, filter_dict, timeutils, skip 11 | from objectpath.utils.json_ext import py2JSON 12 | from objectpath.core import ITER_TYPES, generator, chain 13 | from objectpath.utils.debugger import Debugger 14 | 15 | EPSILON = 0.0000000000000001 #this is used in float comparison 16 | EXPR_CACHE = {} 17 | RE_TYPE = type(re.compile('')) 18 | # setting external modules to 0, thus enabling lazy loading. 0 ensures that Pythonic types are never matched. 19 | # this way is efficient because if statement is fast and once loaded these variables are pointing to libraries. 20 | ObjectId = generateID = calendar = escape = escapeDict = unescape = unescapeDict = 0 21 | 22 | class Tree(Debugger): 23 | _REGISTERED_FUNCTIONS = {} 24 | 25 | @classmethod 26 | def register_function(cls, name, func): 27 | """ 28 | This method is used to add custom functions not catered for by default 29 | :param str name: The name by which the function will be referred to in the expression 30 | :param callable func: The function 31 | :return: 32 | """ 33 | cls._REGISTERED_FUNCTIONS[name] = func 34 | 35 | def __init__(self, obj, cfg=None): 36 | if not cfg: 37 | cfg = {} 38 | self.D = cfg.get("debug", False) 39 | self.setObjectGetter(cfg.get("object_getter", None)) 40 | self.setData(obj) 41 | self.current = self.node = None 42 | if self.D: super(Tree, self).__init__() 43 | 44 | def setData(self, obj): 45 | if type(obj) in ITER_TYPES + [dict]: 46 | self.data = obj 47 | 48 | def setObjectGetter(self, object_getter_cb): 49 | if callable(object_getter_cb): 50 | self.object_getter = object_getter_cb 51 | else: 52 | 53 | def default_getter(obj, attr): 54 | try: 55 | return obj.__getattribute__(attr) 56 | except AttributeError: 57 | if self.D: 58 | self.end(color.op(".") + " returning '%s'", color.bold(obj)) 59 | return obj 60 | 61 | self.object_getter = default_getter 62 | 63 | def compile(self, expr): 64 | if expr in EXPR_CACHE: 65 | return EXPR_CACHE[expr] 66 | ret = EXPR_CACHE[expr] = parse(expr, self.D) 67 | return ret 68 | 69 | def execute(self, expr): 70 | D = self.D 71 | if D: self.start("Tree.execute") 72 | TYPES = [str, int, float, bool, generator, chain] 73 | try: 74 | TYPES += [long] 75 | except NameError: 76 | pass 77 | 78 | # TODO change to yield? 79 | def exe(node): 80 | """ 81 | node[0] - operator name 82 | node[1:] - params 83 | """ 84 | types = [ 85 | str, timeutils.datetime.time, timeutils.datetime.date, 86 | timeutils.datetime.datetime 87 | ] 88 | try: 89 | types += [unicode] 90 | except: 91 | pass 92 | if D: self.start("executing node %s", color.bold(self.cleanOutput(node))) 93 | type_node = type(node) 94 | if node is None or type_node in TYPES: 95 | return node 96 | elif type_node in types: 97 | return node 98 | elif type_node is list: 99 | return (exe(n) for n in node) 100 | elif type_node is dict: 101 | ret = {} 102 | for i in node.items(): 103 | ret[exe(i[0])] = exe(i[1]) 104 | return ret 105 | op = node[0] 106 | if op == "or": 107 | if D: self.debug("%s or %s", node[1], node[2]) 108 | return exe(node[1]) or exe(node[2]) 109 | elif op == "and": 110 | if D: self.debug("%s and %s", node[1], node[2]) 111 | return exe(node[1]) and exe(node[2]) 112 | elif op == "+": 113 | if len(node) > 2: 114 | fst = exe(node[1]) 115 | snd = exe(node[2]) 116 | if None in (fst, snd): 117 | return fst or snd 118 | typefst = type(fst) 119 | typesnd = type(snd) 120 | if typefst is dict: 121 | try: 122 | fst.update(snd) 123 | except Exception: 124 | if type(snd) is not dict: 125 | raise ProgrammingError( 126 | "Can't add value of type %s to %s" % ( 127 | color.bold( 128 | PY_TYPES_MAP. 129 | get(type(snd).__name__, 130 | type(snd).__name__) 131 | ), color.bold("object") 132 | ) 133 | ) 134 | return fst 135 | if typefst is list and typesnd is list: 136 | if D: self.debug("both sides are lists, returning '%s'", fst + snd) 137 | return fst + snd 138 | if typefst in ITER_TYPES or typesnd in ITER_TYPES: 139 | if typefst not in ITER_TYPES: 140 | fst = [fst] 141 | elif typesnd not in ITER_TYPES: 142 | snd = [snd] 143 | if D: self.debug("at least one side is a generator and the other is an iterable, returning chain") 144 | return chain(fst, snd) 145 | if typefst in NUM_TYPES: 146 | try: 147 | return fst + snd 148 | except Exception: 149 | return fst + float(snd) 150 | if typefst in STR_TYPES or typesnd in STR_TYPES: 151 | if D: self.info("doing string comparison '%s' is '%s'", fst, snd) 152 | if sys.version_info[0] < 3: 153 | if typefst is unicode: 154 | fst = fst.encode("utf-8") 155 | if typesnd is unicode: 156 | snd = snd.encode("utf-8") 157 | return str(fst) + str(snd) 158 | try: 159 | timeType = timeutils.datetime.time 160 | if typefst is timeType and typesnd is timeType: 161 | return timeutils.addTimes(fst, snd) 162 | except Exception: 163 | pass 164 | if D: self.debug("standard addition, returning '%s'", fst + snd) 165 | return fst + snd 166 | else: 167 | return exe(node[1]) 168 | elif op == "-": 169 | if len(node) > 2: 170 | fst = exe(node[1]) 171 | snd = exe(node[2]) 172 | try: 173 | return fst - snd 174 | except Exception: 175 | typefst = type(fst) 176 | typesnd = type(snd) 177 | timeType = timeutils.datetime.time 178 | if typefst is timeType and typesnd is timeType: 179 | return timeutils.subTimes(fst, snd) 180 | else: 181 | return -exe(node[1]) 182 | elif op == "*": 183 | return exe(node[1])*exe(node[2]) 184 | elif op == "%": 185 | return exe(node[1]) % exe(node[2]) 186 | elif op == "/": 187 | return exe(node[1])/float(exe(node[2])) 188 | elif op == ">": 189 | if D: self.debug("%s > %s, %s", node[1], node[2], node[1] > node[2]) 190 | return exe(node[1]) > exe(node[2]) 191 | elif op == "<": 192 | return exe(node[1]) < exe(node[2]) 193 | elif op == ">=": 194 | return exe(node[1]) >= exe(node[2]) 195 | elif op == "<=": 196 | return exe(node[1]) <= exe(node[2]) 197 | # TODO this algorithm produces 3 for 1<2<3 and should be true 198 | # elif op in "<=>=": 199 | # fst=exe(node[1]) 200 | # snd=exe(node[2]) 201 | # if op==">": 202 | # return fst > snd and snd or False 203 | # elif op=="<": 204 | # return fst < snd and snd or False 205 | # elif op==">=": 206 | # return fst >= snd and snd or False 207 | # elif op=="<=": 208 | # return fst <= snd and snd or False 209 | elif op == "not": 210 | fst = exe(node[1]) 211 | if D: self.debug("doing not '%s'", fst) 212 | return not fst 213 | elif op == "in": 214 | fst = exe(node[1]) 215 | snd = exe(node[2]) 216 | if D: self.debug("doing '%s' in '%s'", node[1], node[2]) 217 | if type(fst) in ITER_TYPES and type(snd) in ITER_TYPES: 218 | return any( 219 | x in max(fst, snd, key=len) for x in min(fst, snd, key=len) 220 | ) 221 | return exe(node[1]) in exe(node[2]) 222 | elif op == "not in": 223 | fst = exe(node[1]) 224 | snd = exe(node[2]) 225 | if D: self.debug("doing '%s' not in '%s'", node[1], node[2]) 226 | if type(fst) in ITER_TYPES and type(snd) in ITER_TYPES: 227 | return not any( 228 | x in max(fst, snd, key=len) for x in min(fst, snd, key=len) 229 | ) 230 | return exe(node[1]) not in exe(node[2]) 231 | elif op in ("is", "is not"): 232 | if D: self.debug("found operator '%s'", op) 233 | # try: 234 | fst = exe(node[1]) 235 | # except Exception as e: 236 | # if D: self.debug("NOT ERROR! Can't execute node[1] '%s', error: '%s'. Falling back to orginal value.",node[1],str(e)) 237 | # fst=node[1] 238 | # try: 239 | snd = exe(node[2]) 240 | # except Exception as e: 241 | # if D: self.debug("NOT ERROR! Can't execute node[2] '%s', error: '%s'. Falling back to orginal value.",node[2],str(e)) 242 | # snd=node[2] 243 | if op == "is" and fst == snd: 244 | return True 245 | 246 | # this doesn't work for 3 is not '3' 247 | # if op == "is not" and fst != snd: 248 | # return True 249 | typefst = type(fst) 250 | typesnd = type(snd) 251 | if D: self.debug("type fst: '%s', type snd: '%s'", typefst, typesnd) 252 | if typefst in STR_TYPES: 253 | if D: self.info("doing string comparison '\"%s\" is \"%s\"'", fst, snd) 254 | ret = str(fst) == str(snd) 255 | elif typefst is float or typesnd is float: 256 | if D: self.info("doing float comparison '%s is %s'", fst, snd) 257 | try: 258 | ret = abs(float(fst) - float(snd)) < EPSILON 259 | except: 260 | ret = False 261 | elif typefst is int or typesnd is int: 262 | if D: self.info("doing integer comparison '%s is %s'", fst, snd) 263 | try: 264 | ret = int(fst) == int(snd) 265 | except: 266 | ret = False 267 | elif typefst is list and typesnd is list: 268 | if D: self.info("doing array comparison '%s' is '%s'", fst, snd) 269 | ret = fst == snd 270 | elif typefst is dict and typesnd is dict: 271 | if D: self.info("doing object comparison '%s' is '%s'", fst, snd) 272 | ret = fst == snd 273 | elif fst is None or snd is None: 274 | if fst is None and snd is None: 275 | # this executes only for "is not" 276 | ret = True 277 | else: 278 | ret = (fst or snd) is None 279 | if D: self.info( 280 | "doing None comparison %s is %s = %s", color.bold(fst), color.bold(snd), 281 | color.bold(not not (fst or snd)) 282 | ) 283 | else: 284 | if D: self.info("can't compare %s and %s. Returning False", self.cleanOutput(fst), self.cleanOutput(snd)) 285 | ret = False 286 | # else: 287 | # try: 288 | # global ObjectId 289 | # if not ObjectId: 290 | # from bson.objectid import ObjectId 291 | # if typefst is ObjectId or typesnd is ObjectId: 292 | # if D: self.info("doing MongoDB objectID comparison '%s' is '%s'",fst,snd) 293 | # ret=str(fst)==str(snd) 294 | # else: 295 | # if D: self.info("doing standard comparison '%s' is '%s'",fst,snd) 296 | # ret=fst is snd 297 | # except Exception: 298 | # pass 299 | if op == "is not": 300 | if D: self.info("'is not' found. Returning %s", not ret) 301 | return not ret 302 | else: 303 | if D: self.info("returning %s is %s => %s", color.bold(self.cleanOutput(fst)), color.bold(self.cleanOutput(snd)), color.bold(ret)) 304 | return ret 305 | elif op == "re": 306 | return re.compile(exe(node[1])) 307 | elif op == "matches": 308 | fst = exe(node[1]) 309 | snd = exe(node[2]) 310 | if type(fst) not in STR_TYPES+[RE_TYPE]: 311 | raise Exception("operator " + color.bold("matches") + " expects regexp on the left. Example: 'a.*d' matches 'abcd'") 312 | if type(snd) in ITER_TYPES: 313 | for i in snd: 314 | if not not re.match(fst, i): 315 | return True 316 | return False 317 | else: 318 | # regex matches string 319 | return not not re.match(fst, snd) 320 | # elif op=="(literal)": 321 | # fstLetter=node[1][0] 322 | # if fstLetter is "'": 323 | # return node[1][1:-1] 324 | # elif fstLetter.isdigit: 325 | # return int(node[1]) 326 | elif op == "(root)": # this is $ 327 | return self.data 328 | # elif op=="(node)":# this is ! 329 | # if D: self.debug("returning node %s",self.node) 330 | # return self.node 331 | elif op == "(current)": # this is @ 332 | if D: self.debug("returning current node: \n %s", color.bold(self.current)) 333 | return self.current 334 | elif op == "name": 335 | return node[1] 336 | elif op == ".": 337 | fst = node[1] 338 | if type(fst) is tuple: 339 | fst = exe(fst) 340 | typefst = type(fst) 341 | if D: self.debug(color.op(".") + " left is '%s'", color.bold(self.cleanOutput(fst))) 342 | # try: 343 | if node[2][0] == "*": 344 | if D: 345 | self.end( 346 | color.op(".") + " returning '%s'", 347 | color.bold(typefst in ITER_TYPES and fst or [fst]) 348 | ) 349 | return fst # typefst in ITER_TYPES and fst or [fst] 350 | # except: 351 | # pass 352 | snd = exe(node[2]) 353 | if D: self.debug(color.op(".") + " right is '%s'", color.bold(snd)) 354 | if typefst in ITER_TYPES: 355 | if D: self.debug( 356 | color.op(".") + " filtering %s by %s", color.bold(self.cleanOutput(fst)), 357 | color.bold(snd) 358 | ) 359 | if type(snd) in ITER_TYPES: 360 | return filter_dict(fst, list(snd)) 361 | else: 362 | # if D: self.debug(list(fst)) 363 | return (e[snd] for e in fst if type(e) is dict and snd in e) 364 | try: 365 | if D: self.end(color.op(".") + " returning '%s'", fst.get(snd)) 366 | return fst.get(snd) 367 | except Exception: 368 | if isinstance(fst, object): 369 | return self.object_getter(fst, snd) 370 | if D: self.end(color.op(".") + " returning '%s'", color.bold(fst)) 371 | return fst 372 | elif op == "..": 373 | fst = flatten(exe(node[1])) 374 | if node[2][0] == "*": 375 | if D: self.debug(color.op("..") + " returning '%s'", color.bold(fst)) 376 | return fst 377 | # reduce objects to selected attributes 378 | snd = exe(node[2]) 379 | if D: self.debug( 380 | color.op("..") + " finding all %s in %s", color.bold(snd), 381 | color.bold(self.cleanOutput(fst)) 382 | ) 383 | if type(snd) in ITER_TYPES: 384 | ret = filter_dict(fst, list(snd)) 385 | if D: self.debug(color.op("..") + " returning %s", color.bold(ret)) 386 | return ret 387 | else: 388 | ret = chain.from_iterable( 389 | type(x) in ITER_TYPES and x or [x] 390 | for x in (e[snd] for e in fst if snd in e) 391 | ) 392 | # print list(chain(*(type(x) in ITER_TYPES and x or [x] for x in (e[snd] for e in fst if snd in e)))) 393 | if D: self.debug(color.op("..") + " returning %s", color.bold(self.cleanOutput(ret))) 394 | return ret 395 | elif op == "[": 396 | len_node = len(node) 397 | # TODO move it to tree generation phase 398 | if len_node == 1: # empty list 399 | if D: self.debug("returning an empty list") 400 | return [] 401 | if len_node == 2: # list - preserved to catch possible event of leaving it as '[' operator 402 | if D: self.debug("doing list mapping") 403 | return [exe(x) for x in node[1]] 404 | if len_node == 3: # selector used [] 405 | fst = exe(node[1]) 406 | # check against None 407 | if not fst: 408 | return fst 409 | selector = node[2] 410 | if D: 411 | self.debug( 412 | "\n found selector '%s'.\n executing on %s", color.bold(selector), 413 | color.bold(fst) 414 | ) 415 | selectorIsTuple = type(selector) is tuple 416 | 417 | if selectorIsTuple and selector[0] == "[": 418 | nodeList = [] 419 | nodeList_append = nodeList.append 420 | for i in fst: 421 | if D: self.debug("setting self.current to %s", color.bold(i)) 422 | self.current = i 423 | nodeList_append( 424 | exe((selector[0], exe(selector[1]), exe(selector[2]))) 425 | ) 426 | if D: self.debug( 427 | "returning %s objects: %s", color.bold(len(nodeList)), 428 | color.bold(nodeList) 429 | ) 430 | return nodeList 431 | 432 | if selectorIsTuple and selector[0] == "(current)": 433 | if D: 434 | self.warning( 435 | color.bold("$.*[@]") + " is eqivalent to " + 436 | color.bold("$.*") + "!" 437 | ) 438 | return fst 439 | 440 | if selectorIsTuple and selector[0] in SELECTOR_OPS: 441 | if D: self.debug( 442 | "found %s operator in selector, %s", color.bold(selector[0]), 443 | color.bold(selector) 444 | ) 445 | if type(fst) is dict: 446 | fst = [fst] 447 | # TODO move it to tree building phase 448 | if type(selector[1]) is tuple and selector[1][0] == "name": 449 | selector = (selector[0], selector[1][1], selector[2]) 450 | selector0 = selector[0] 451 | selector1 = selector[1] 452 | selector2 = selector[2] 453 | 454 | def exeSelector(fst): 455 | for i in fst: 456 | if D: 457 | self.debug("setting self.current to %s", color.bold(i)) 458 | self.debug(" s0: %s\n s1: %s\n s2: %s\n Current: %s", selector0, selector1, selector2, i) 459 | self.current = i 460 | if selector0 == "fn": 461 | yield exe(selector) 462 | # elif type(selector1) in STR_TYPES and False: 463 | # if D: self.debug("found string %s", type(i)) 464 | # try: 465 | # if exe((selector0,i[selector1],selector2)): 466 | # yield i 467 | # if D: self.debug("appended") 468 | # if D: self.debug("discarded") 469 | # except Exception as e: 470 | # if D: self.debug("discarded, Exception: %s",color.bold(e)) 471 | else: 472 | try: 473 | # TODO optimize an event when @ is not used. exe(selector1) can be cached 474 | if exe((selector0, exe(selector1), exe(selector2))): 475 | yield i 476 | if D: self.debug("appended %s", i) 477 | elif D: self.debug("discarded") 478 | except Exception: 479 | if D: self.debug("discarded") 480 | 481 | # if D and nodeList: self.debug("returning '%s' objects: '%s'", color.bold(len(nodeList)), color.bold(nodeList)) 482 | return exeSelector(fst) 483 | self.current = fst 484 | snd = exe(node[2]) 485 | typefst = type(fst) 486 | if typefst in [tuple] + ITER_TYPES + STR_TYPES: 487 | typesnd = type(snd) 488 | # nodes[N] 489 | if typesnd in NUM_TYPES or typesnd is str and snd.isdigit(): 490 | n = int(snd) 491 | if D: 492 | self.info( 493 | "getting %sth element from '%s'", color.bold(n), 494 | color.bold(fst) 495 | ) 496 | if typefst in (generator, chain): 497 | if n > 0: 498 | return skip(fst, n) 499 | elif n == 0: 500 | return next(fst) 501 | else: 502 | fst = list(fst) 503 | else: 504 | try: 505 | return fst[n] 506 | except (IndexError, TypeError): 507 | return None 508 | # $.*['string']==$.string 509 | if type(snd) in STR_TYPES: 510 | return exe((".", fst, snd)) 511 | else: 512 | # $.*[@.string] - bad syntax, but allowed 513 | return snd 514 | else: 515 | try: 516 | if D: self.debug("returning %s", color.bold(fst[snd])) 517 | return fst[snd] 518 | except KeyError: 519 | # CHECK - is it ok to do that or should it be ProgrammingError? 520 | if D: self.debug("returning an empty list") 521 | return [] 522 | raise ProgrammingError( 523 | "Wrong usage of " + color.bold("[") + " operator" 524 | ) 525 | elif op == "fn": 526 | # Built-in functions 527 | fnName = node[1] 528 | args = None 529 | try: 530 | args = [exe(x) for x in node[2:]] 531 | except IndexError: 532 | if D: 533 | self.debug("NOT ERROR: can't map '%s' with '%s'", node[2:], exe) 534 | # arithmetic 535 | if fnName == "sum": 536 | args = args[0] 537 | if type(args) in NUM_TYPES: 538 | return args 539 | return sum((x for x in args if type(x) in NUM_TYPES)) 540 | elif fnName == "max": 541 | args = args[0] 542 | if type(args) in NUM_TYPES: 543 | return args 544 | return max((x for x in args if type(x) in NUM_TYPES)) 545 | elif fnName == "min": 546 | args = args[0] 547 | if type(args) in NUM_TYPES: 548 | return args 549 | return min((x for x in args if type(x) in NUM_TYPES)) 550 | elif fnName == "avg": 551 | args = args[0] 552 | if type(args) in NUM_TYPES: 553 | return args 554 | if type(args) not in ITER_TYPES: 555 | raise Exception("Argument for avg() is not an array") 556 | else: 557 | args = list(args) 558 | try: 559 | return sum(args)/float(len(args)) 560 | except TypeError: 561 | args = [x for x in args if type(x) in NUM_TYPES] 562 | self.warning("Some items in array were ommited") 563 | return sum(args)/float(len(args)) 564 | elif fnName == "round": 565 | return round(*args) 566 | # casting 567 | elif fnName == "int": 568 | return int(args[0]) 569 | elif fnName == "float": 570 | return float(args[0]) 571 | elif fnName == "str": 572 | return str(py2JSON(args[0])) 573 | elif fnName in ("list", "array"): 574 | try: 575 | a = args[0] 576 | except IndexError: 577 | return [] 578 | targs = type(a) 579 | if targs is timeutils.datetime.datetime: 580 | return timeutils.date2list(a) + timeutils.time2list(a) 581 | if targs is timeutils.datetime.date: 582 | return timeutils.date2list(a) 583 | if targs is timeutils.datetime.time: 584 | return timeutils.time2list(a) 585 | return list(a) 586 | # string 587 | elif fnName == "upper": 588 | return args[0].upper() 589 | elif fnName == "lower": 590 | return args[0].lower() 591 | elif fnName == "capitalize": 592 | return args[0].capitalize() 593 | elif fnName == "title": 594 | return args[0].title() 595 | elif fnName == "split": 596 | return args[0].split(*args[1:]) 597 | elif fnName == "slice": 598 | if args and type(args[1]) not in ITER_TYPES: 599 | raise ExecutionError( 600 | "Wrong usage of slice(STRING, ARRAY). Second argument is not an array but %s." 601 | % color.bold(type(args[1]).__name__) 602 | ) 603 | try: 604 | pos = list(args[1]) 605 | if type(pos[0]) in ITER_TYPES: 606 | if D: self.debug("run slice() for a list of slicers") 607 | return (args[0][x[0]:x[1]] for x in pos) 608 | return args[0][pos[0]:pos[1]] 609 | except IndexError: 610 | if len(args) != 2: 611 | raise ProgrammingError( 612 | "Wrong usage of slice(STRING, ARRAY). Provided %s argument, should be exactly 2." 613 | % len(args) 614 | ) 615 | elif fnName == "escape": 616 | global escape, escapeDict 617 | if not escape: 618 | from objectpath.utils import escape, escapeDict 619 | return escape(args[0], escapeDict) 620 | elif fnName == "unescape": 621 | global unescape, unescapeDict 622 | if not unescape: 623 | from objectpath.utils import unescape, unescapeDict 624 | return unescape(args[0], unescapeDict) 625 | elif fnName == "replace": 626 | if sys.version_info[0] < 3 and type(args[0]) is unicode: 627 | args[0] = args[0].encode("utf8") 628 | return str.replace(args[0], args[1], args[2]) 629 | # TODO this should be supported by /regex/ 630 | # elif fnName=="REsub": 631 | # return re.sub(args[1],args[2],args[0]) 632 | elif fnName == "sort": 633 | if len(args) > 1: 634 | key = args[1] 635 | a = {"key": lambda x: x.get(key, 0)} 636 | else: 637 | a = {} 638 | args = args[0] 639 | if D: self.debug("doing sort on '%s'", args) 640 | try: 641 | return sorted(args, **a) 642 | except TypeError: 643 | return args 644 | elif fnName == "reverse": 645 | args = args[0] 646 | try: 647 | args.reverse() 648 | return args 649 | except TypeError: 650 | return args 651 | elif fnName == "unique": 652 | try: 653 | return list(set(args[0])) 654 | except TypeError: 655 | return args[0] 656 | elif fnName == "map": 657 | return chain.from_iterable(map(lambda x: exe(("fn", args[0], x)), args[1])) 658 | elif fnName in ("count", "len"): 659 | args = args[0] 660 | if args in (True, False, None): 661 | return args 662 | if type(args) in ITER_TYPES: 663 | return len(list(args)) 664 | return len(args) 665 | elif fnName == "join": 666 | try: 667 | joiner = args[1] 668 | except Exception: 669 | joiner = "" 670 | try: 671 | return joiner.join(args[0]) 672 | except TypeError: 673 | try: 674 | return joiner.join(map(str, args[0])) 675 | except Exception: 676 | return args[0] 677 | # time 678 | elif fnName in ("now", "age", "time", "date", "dateTime"): 679 | if fnName == "now": 680 | return timeutils.now() 681 | if fnName == "date": 682 | return timeutils.date(args) 683 | if fnName == "time": 684 | return timeutils.time(args) 685 | if fnName == "dateTime": 686 | return timeutils.dateTime(args) 687 | # TODO move lang to localize() entirely! 688 | if fnName == "age": 689 | a = {} 690 | if len(args) > 1: 691 | a["reference"] = args[1] 692 | if len(args) > 2: 693 | a["lang"] = args[2] 694 | return list(timeutils.age(args[0], **a)) 695 | elif fnName == "toMillis": 696 | args = args[0] 697 | if args.utcoffset() is not None: 698 | args = args - args.utcoffset() # pylint: disable=E1103 699 | global calendar 700 | if not calendar: 701 | import calendar 702 | return int( 703 | calendar.timegm(args.timetuple())*1000 + args.microsecond/1000 704 | ) 705 | elif fnName == "localize": 706 | if type(args[0]) is timeutils.datetime.datetime: 707 | return timeutils.UTC2local(*args) 708 | # polygons 709 | elif fnName == "area": 710 | 711 | def segments(p): 712 | p = list(map(lambda x: x[0:2], p)) 713 | return zip(p, p[1:] + [p[0]]) 714 | 715 | return 0.5*abs( 716 | sum(x0*y1 - x1*y0 for ((x0, y0), (x1, y1)) in segments(args[0])) 717 | ) 718 | # misc 719 | elif fnName == "keys": 720 | try: 721 | return list(args[0].keys()) 722 | except AttributeError: 723 | raise ExecutionError( 724 | "Argument is not " + color.bold("object") + 725 | " but %s in keys()" % color.bold(type(args[0]).__name__) 726 | ) 727 | elif fnName == "values": 728 | try: 729 | return list(args[0].values()) 730 | except AttributeError: 731 | raise ExecutionError( 732 | "Argument is not " + color.bold("object") + 733 | " but %s in values()" % color.bold(type(args[0]).__name__) 734 | ) 735 | elif fnName == "type": 736 | ret = type(args[0]) 737 | if ret in ITER_TYPES: 738 | return "array" 739 | if ret is dict: 740 | return "object" 741 | return ret.__name__ 742 | elif fnName in self._REGISTERED_FUNCTIONS: 743 | return self._REGISTERED_FUNCTIONS[fnName](*args) 744 | else: 745 | raise ProgrammingError( 746 | "Function " + color.bold(fnName) + " does not exist." 747 | ) 748 | else: 749 | return node 750 | 751 | D = self.D 752 | if type(expr) in STR_TYPES: 753 | tree = self.compile(expr) 754 | elif type(expr) not in (tuple, list, dict): 755 | return expr 756 | ret = exe(tree) 757 | if D: self.end("Tree.execute with: %s", color.bold(self.cleanOutput(ret))) 758 | return ret 759 | 760 | def __str__(self): 761 | return "TreeObject()" 762 | 763 | def __repr__(self): 764 | return self.__str__() 765 | -------------------------------------------------------------------------------- /objectpath/core/parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file is part of ObjectPath released under MIT license. 4 | # Copyright (C) 2010-2014 Adrian Kalbarczyk 5 | 6 | # Code from http://effbot.org/zone/simple-top-down-parsing.htm was used in this file. 7 | # Licence of the code is public domain. 8 | # Relicenced to MIT by Adrian Kalbarczyk and: 9 | # - specialized to work with ObjectPath, 10 | # - optimized 11 | 12 | import sys 13 | 14 | if sys.version_info[0] >= 3: 15 | from io import StringIO 16 | else: 17 | from cStringIO import StringIO 18 | 19 | from objectpath.core import SELECTOR_OPS, NUM_TYPES 20 | 21 | symbol_table = {} 22 | token = nextToken = None 23 | # TODO optimization ('-',1) -> -1 24 | # TODO optimization operators should be numbers 25 | 26 | TRUE = ["true", "t"] 27 | FALSE = ["false", "f"] 28 | NONE = ["none", "null", "n", "nil"] 29 | 30 | class symbol_base(object): 31 | id = None 32 | value = None 33 | fst = snd = third = None 34 | 35 | def nud(self): 36 | raise SyntaxError("Syntax error (%r)." % self.id) 37 | 38 | def led(self): 39 | raise SyntaxError("Unknown operator (%r)." % self.id) 40 | 41 | def getTree(self): 42 | if self.id == "(name)": 43 | val = self.value.lower() 44 | if val in TRUE: 45 | return True 46 | elif val in FALSE: 47 | return False 48 | elif val in NONE: 49 | return None 50 | return (self.id[1:-1], self.value) 51 | elif self.id == "(number)": 52 | return self.value 53 | elif self.id == "(literal)": 54 | fstLetter = self.value[0] 55 | if fstLetter in ["'", "\""]: 56 | return self.value[1:-1] 57 | # elif fstLetter.isdigit(): 58 | # try: 59 | # return int(self.value) 60 | # except: 61 | # return float(self.value) 62 | else: 63 | if self.value == "True": 64 | return True 65 | elif self.value == "False": 66 | return False 67 | elif self.value == "None": 68 | return None 69 | ret = [self.id] 70 | ret_append = ret.append 71 | L = (dict, tuple, list) 72 | for i in filter(None, [self.fst, self.snd, self.third]): 73 | if type(i) is str: 74 | ret_append(i) 75 | elif type(i) in L: 76 | t = [] 77 | t_append = t.append 78 | if self.id == "{": 79 | ret = {} 80 | for j in list(self.fst.items()): 81 | ret[j[0].getTree()] = j[1].getTree() 82 | return ret 83 | for j in i: 84 | try: 85 | t_append(j.getTree()) 86 | except Exception: 87 | t_append(j) 88 | if self.id in ("[", ".", ".."): 89 | ret.append(t) 90 | else: 91 | ret.extend(t) 92 | # ret_append(t) 93 | # return (self.id,ret[1:]) 94 | else: 95 | if type(self.fst.value) in NUM_TYPES and self.snd is None: 96 | if self.id == "-": 97 | return -self.fst.value 98 | if self.id == "+": 99 | return self.fst.value 100 | ret_append(i.getTree()) 101 | if self.id == "{": 102 | return {} 103 | # if self.id == "[" and self.fst == []: 104 | # return [] 105 | if self.id == "(": 106 | # this will produce ("fn","fnName",arg1,arg2,...argN) 107 | # try: 108 | return tuple(["fn", ret[1][1]] + ret[2:]) 109 | # except: 110 | # pass 111 | return tuple(ret) 112 | 113 | def __repr__(self): 114 | if self.id == "(name)" or self.id == "(literal)": 115 | return "(%s:%s)" % (self.id[1:-1], self.value) 116 | out = [self.id, self.fst, self.snd, self.third] 117 | # out=list(map(str, filter(None, out))) 118 | return "(" + " ".join(out) + ")" 119 | 120 | def symbol(ID, bp=0): 121 | try: 122 | s = symbol_table[ID] 123 | except KeyError: 124 | 125 | class s(symbol_base): 126 | pass 127 | 128 | s.__name__ = "symbol-" + ID # for debugging 129 | s.id = ID 130 | s.value = None 131 | s.lbp = bp 132 | symbol_table[ID] = s 133 | else: 134 | s.lbp = max(bp, s.lbp) 135 | return s 136 | 137 | # helpers 138 | 139 | def infix(ID, bp): 140 | def led(self, left): 141 | self.fst = left 142 | self.snd = expression(bp) 143 | return self 144 | 145 | symbol(ID, bp).led = led 146 | 147 | def infix_r(ID, bp): 148 | def led(self, left): 149 | self.fst = left 150 | self.snd = expression(bp - 1) 151 | return self 152 | 153 | symbol(ID, bp).led = led 154 | 155 | def prefix(ID, bp): 156 | def nud(self): 157 | self.fst = expression(bp) 158 | return self 159 | 160 | symbol(ID).nud = nud 161 | 162 | def advance(ID=None): 163 | global token 164 | if ID and token.id != ID: 165 | raise SyntaxError("Expected %r, got %s" % (ID, token.id)) 166 | token = nextToken() 167 | 168 | def method(s): 169 | # decorator 170 | assert issubclass(s, symbol_base) 171 | 172 | def bind(fn): 173 | setattr(s, fn.__name__, fn) 174 | 175 | return bind 176 | 177 | infix_r("or", 30) 178 | infix_r("and", 40) 179 | prefix("not", 50) 180 | infix("in", 60) 181 | infix("not", 60) # not in 182 | infix("is", 60) 183 | infix("matches", 60) 184 | infix("<", 60) 185 | infix("<=", 60) 186 | infix(">", 60) 187 | infix(">=", 60) 188 | # infix(" ", 60); infix("!=", 60); infix("==", 60) 189 | # infix("&", 90) 190 | # infix("<<", 100); infix(">>", 100) 191 | infix("+", 110) 192 | infix("-", 110) 193 | infix("*", 120) 194 | infix("/", 120) 195 | infix("//", 120) 196 | infix("%", 120) 197 | prefix("-", 130) 198 | prefix("+", 130) 199 | #prefix("~", 130) 200 | # infix_r("**", 140) 201 | symbol(".", 150) 202 | symbol("[", 150) 203 | symbol("{", 150) 204 | symbol("(", 150) 205 | # additional behavior 206 | symbol("(name)").nud = lambda self: self 207 | symbol("(literal)").nud = lambda self: self 208 | symbol("(number)").nud = lambda self: self 209 | symbol("(end)") 210 | symbol(")") 211 | 212 | # REGEX 213 | infix("|", 0) 214 | infix("^", 0) 215 | infix("?", 0) 216 | infix("\\", 0) 217 | 218 | symbol("@") 219 | 220 | @method(symbol("@")) 221 | def nud(self): # pylint: disable=E0102 222 | self.id = "(current)" 223 | return self 224 | 225 | symbol("!") 226 | 227 | @method(symbol("!")) 228 | def nud(self): # pylint: disable=E0102 229 | self.id = "(node)" 230 | return self 231 | 232 | # RegEx 233 | @method(symbol("/")) 234 | def nud(self): # pylint: disable=E0102 235 | self.id = "re" 236 | regex = [] 237 | if token.id != "/": 238 | self_fst_append = regex.append 239 | while 1: 240 | if token.id == "/": 241 | break 242 | if token.id in ["(name)", "(number)"]: 243 | self_fst_append(str(token.value)) 244 | else: 245 | self_fst_append(token.id) 246 | advance() 247 | self.fst = "".join(regex).replace("\\", "\\\\") 248 | advance("/") 249 | return self 250 | 251 | @method(symbol("(")) 252 | def nud(self): # pylint: disable=E0102,W0613 253 | expr = expression() 254 | advance(")") 255 | return expr 256 | 257 | symbol(",") 258 | 259 | @method(symbol(".")) 260 | def led(self, left): # pylint: disable=E0102 261 | attr = False 262 | if token.id == ".": 263 | self.id = ".." 264 | advance() 265 | if token.id == "@": 266 | attr = True 267 | advance() 268 | if token.id == "(": 269 | advance() 270 | self.fst = left 271 | self.snd = [] 272 | if token.id != ")": 273 | self_snd_append = self.snd.append 274 | while 1: 275 | self_snd_append(expression()) 276 | if token.id != ",": 277 | break 278 | advance(",") 279 | advance(")") 280 | return self 281 | if token.id not in ["(name)", "*", "(literal)", "("]: 282 | raise SyntaxError("Expected an attribute name.") 283 | self.fst = left 284 | if attr: 285 | token.value = "@" + token.value 286 | self.snd = token 287 | advance() 288 | return self 289 | 290 | # handling namespaces; e.g $.a.b.c or $ss.a.b.c 291 | # default storage is the request namespace 292 | symbol("$") 293 | 294 | @method(symbol("$")) 295 | def nud(self): # pylint: disable=E0102 296 | global token # pylint: disable=W0602 297 | self.id = "(root)" 298 | if token.id == ".": 299 | self.fst = "rs" 300 | else: 301 | self.fst = token.value 302 | advance() 303 | return self 304 | 305 | symbol("]") 306 | 307 | @method(symbol("[")) 308 | def led(self, left): # pylint: disable=E0102 309 | self.fst = left 310 | self.snd = expression() 311 | advance("]") 312 | return self 313 | 314 | symbol(",") 315 | 316 | # this is for built-in functions 317 | @method(symbol("(")) 318 | def led(self, left): # pylint: disable=E0102 319 | # self.id="fn" 320 | self.fst = left 321 | self.snd = [] 322 | if token.id != ")": 323 | self_snd_append = self.snd.append 324 | while 1: 325 | self_snd_append(expression()) 326 | if token.id != ",": 327 | break 328 | advance(",") 329 | advance(")") 330 | return self 331 | 332 | symbol(":") 333 | symbol("=") 334 | 335 | # constants 336 | 337 | def constant(ID): 338 | @method(symbol(ID)) 339 | def nud(self): # pylint: disable=W0612 340 | self.id = "(literal)" 341 | self.value = ID 342 | return self 343 | 344 | constant("None") 345 | constant("True") 346 | constant("False") 347 | 348 | # multitoken operators 349 | 350 | @method(symbol("not")) 351 | def led(self, left): # pylint: disable=E0102 352 | if token.id != "in": 353 | raise SyntaxError("Invalid syntax") 354 | advance() 355 | self.id = "not in" 356 | self.fst = left 357 | self.snd = expression(60) 358 | return self 359 | 360 | @method(symbol("is")) 361 | def led(self, left): # pylint: disable=E0102 362 | if token.id == "not": 363 | advance() 364 | self.id = "is not" 365 | self.fst = left 366 | self.snd = expression(60) 367 | return self 368 | 369 | symbol("]") 370 | 371 | @method(symbol("[")) 372 | def nud(self): # pylint: disable=E0102 373 | self.fst = [] 374 | if token.id != "]": 375 | while 1: 376 | if token.id == "]": 377 | break 378 | self.fst.append(expression()) 379 | if token.id not in SELECTOR_OPS + [","]: 380 | break 381 | advance(",") 382 | advance("]") 383 | return self 384 | 385 | symbol("}") 386 | 387 | @method(symbol("{")) 388 | def nud(self): # pylint: disable=E0102 389 | self.fst = {} 390 | if token.id != "}": 391 | while 1: 392 | if token.id == "}": 393 | break 394 | key = expression() 395 | advance(":") 396 | self.fst[key] = expression() 397 | if token.id != ",": 398 | break 399 | advance(",") 400 | advance("}") 401 | return self 402 | 403 | import tokenize as tokenizer 404 | type_map = { 405 | tokenizer.NUMBER: "(number)", 406 | tokenizer.STRING: "(literal)", 407 | tokenizer.OP: "(operator)", 408 | tokenizer.NAME: "(name)", 409 | tokenizer.ERRORTOKEN: 410 | "(operator)" #'$' is recognized in python tokenizer as error token! 411 | } 412 | 413 | # python tokenizer 414 | def tokenize_python(program): 415 | if sys.version_info[0] < 3: 416 | tokens = tokenizer.generate_tokens(StringIO(program).next) 417 | else: 418 | tokens = tokenizer.generate_tokens(StringIO(program).__next__) 419 | for t in tokens: 420 | # print type_map[t[0]], t[1] 421 | try: 422 | # change this to output python values in correct type 423 | yield type_map[t[0]], t[1] 424 | except KeyError: 425 | if t[0] in [tokenizer.NL, tokenizer.COMMENT, tokenizer.NEWLINE]: 426 | continue 427 | if t[0] == tokenizer.ENDMARKER: 428 | break 429 | else: 430 | raise SyntaxError("Syntax error") 431 | yield "(end)", "(end)" 432 | 433 | def tokenize(program): 434 | if isinstance(program, list): 435 | source = program 436 | else: 437 | source = tokenize_python(program) 438 | for ID, value in source: 439 | if ID == "(literal)": 440 | symbol = symbol_table[ID] 441 | s = symbol() 442 | s.value = value 443 | elif ID == "(number)": 444 | symbol = symbol_table[ID] 445 | s = symbol() 446 | try: 447 | s.value = int(value) 448 | except Exception: 449 | s.value = float(value) 450 | elif value == " ": 451 | continue 452 | else: 453 | # name or operator 454 | symbol = symbol_table.get(value) 455 | if symbol: 456 | s = symbol() 457 | elif ID == "(name)": 458 | symbol = symbol_table[ID] 459 | s = symbol() 460 | s.value = value 461 | else: 462 | raise SyntaxError("Unknown operator '%s', '%s'" % (ID, value)) 463 | yield s 464 | 465 | # parser engine 466 | def expression(rbp=0): 467 | global token 468 | t = token 469 | token = nextToken() 470 | left = t.nud() 471 | while rbp < token.lbp: 472 | t = token 473 | token = nextToken() 474 | left = t.led(left) 475 | return left 476 | 477 | def parse(expr, D=False): 478 | if sys.version_info[0] < 3 and type(expr) is unicode: 479 | expr = expr.encode("utf8") 480 | if type(expr) is not str: 481 | return expr 482 | expr = expr.strip() 483 | global token, nextToken 484 | if sys.version_info[0] >= 3: 485 | nextToken = tokenize(expr).__next__ 486 | else: 487 | nextToken = tokenize(expr).next 488 | token = nextToken() 489 | r = expression().getTree() 490 | if D: 491 | print("PARSE STAGE") 492 | print(r) 493 | return r 494 | -------------------------------------------------------------------------------- /objectpath/shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of ObjectPath released under MIT license. 5 | # Copyright (C) 2010-2014 Adrian Kalbarczyk 6 | 7 | import argparse 8 | import os 9 | import sys 10 | try: 11 | import readline 12 | # this is to prevent various tools from deleting import readline 13 | ___x = readline.__doc__ 14 | except ImportError: 15 | import pyreadline as readline 16 | 17 | from objectpath import Tree, ITER_TYPES 18 | from objectpath.utils.colorify import * # pylint: disable=W0614 19 | from objectpath.utils import json_ext as json 20 | from objectpath.utils.json_ext import printJSON 21 | 22 | try: 23 | import pytz 24 | except ImportError: 25 | if os.isatty(sys.stdin.fileno()) and sys.stdout.isatty(): 26 | print("WARNING! pytz is not installed. Localized times are not supported.") 27 | 28 | def main(): 29 | parser = argparse.ArgumentParser(description='Command line options') 30 | parser.add_argument( 31 | '-u', '--url', dest='URL', help='URL containing JSON document.' 32 | ) 33 | # parser.add_argument('-xml', dest='xml', help='[EXPERIMENTAL] Expect XML input.',action='store_true') 34 | parser.add_argument( 35 | '-d', 36 | '--debug', 37 | dest='debug', 38 | help='Debbuging on/off.', 39 | action='store_true' 40 | ) 41 | parser.add_argument( 42 | '-p', 43 | '--profile', 44 | dest='profile', 45 | help='Profiling on/off.', 46 | action='store_true' 47 | ) 48 | parser.add_argument( 49 | '-e', 50 | '--expr', 51 | dest='expr', 52 | help='Expression/query to execute on file, print on stdout and exit.' 53 | ) 54 | parser.add_argument('file', metavar='FILE', nargs="?", help='File name') 55 | 56 | args = parser.parse_args() 57 | a = {} 58 | expr = args.expr 59 | 60 | if not expr: 61 | print( 62 | bold("ObjectPath interactive shell") + "\n" + bold("ctrl+c") + 63 | " to exit, documentation at " + 64 | const("http://adriank.github.io/ObjectPath") + ".\n" 65 | ) 66 | 67 | if args.debug: 68 | a["debug"] = True 69 | if args.profile: 70 | try: 71 | from guppy import hpy 72 | except: 73 | pass 74 | File = args.file 75 | # if args.xml: 76 | # from utils.xmlextras import xml2tree 77 | src = False 78 | 79 | if args.URL: 80 | if sys.version_info[0] >= 3: 81 | from urllib.request import Request, build_opener # pylint: disable=E0611 82 | else: 83 | from urllib2 import Request, build_opener 84 | request = Request(args.URL) 85 | opener = build_opener() 86 | request.add_header('User-Agent', 'ObjectPath/1.0 +http://objectpath.org/') 87 | src = opener.open(request) 88 | elif File: 89 | src = open(File, "r") 90 | 91 | if not src: 92 | if not expr: 93 | print( 94 | "JSON document source not specified. Working with an empty object {}." 95 | ) 96 | tree = Tree({}, a) 97 | else: 98 | if not expr: 99 | sys.stdout.write( 100 | "Loading JSON document from " + str(args.URL or File) + "..." 101 | ) 102 | sys.stdout.flush() 103 | # if args.xml: 104 | # tree=Tree(json.loads(json.dumps(xml2tree(src))),a) 105 | # else: 106 | tree = Tree(json.load(src), a) 107 | if not expr: print(" " + bold("done") + ".") 108 | 109 | if expr: 110 | if args.profile: 111 | import cProfile, pstats, StringIO 112 | pr = cProfile.Profile() 113 | pr.enable() 114 | try: 115 | ret = tree.execute(expr) 116 | except Exception as e: 117 | print(e.__class__.__name__ + ": " + str(e)) 118 | exit(1) 119 | if type(ret) in ITER_TYPES: 120 | ret = list(ret) 121 | print(json.dumps(ret)) 122 | if args.profile: 123 | pr.disable() 124 | s = StringIO.StringIO() 125 | sortby = 'cumulative' 126 | ps = pstats.Stats(pr, stream=s).sort_stats(sortby) 127 | ps.print_stats() 128 | print(s.getvalue()) 129 | return 130 | 131 | try: 132 | while True: 133 | limitResult = 5 134 | try: 135 | if sys.version >= '3': 136 | r = input(">>> ") 137 | else: 138 | r = raw_input(">>> ") 139 | 140 | if r.startswith("all"): 141 | limitResult = -1 142 | r = tree.execute(r[3:].strip()) 143 | else: 144 | r = tree.execute(r) 145 | 146 | # python 3 raises error here - unicode is not a proper type there 147 | try: 148 | if type(r) is unicode: 149 | r = r.encode("utf8") 150 | except NameError: 151 | pass 152 | print(printJSON(r, length=limitResult)) 153 | if args.profile: 154 | h = hpy() 155 | print(h.heap()) 156 | except Exception as e: 157 | print(e) 158 | except KeyboardInterrupt: 159 | pass 160 | # new line at the end forces command prompt to apear at left 161 | print(bold("\nbye!")) 162 | 163 | if __name__ == "__main__": 164 | main() 165 | -------------------------------------------------------------------------------- /objectpath/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of ObjectPath released under MIT license. 5 | # Copyright (C) 2010-2014 Adrian Kalbarczyk 6 | 7 | from itertools import islice 8 | from xml.sax.saxutils import escape, unescape 9 | from objectpath.core import NUM_TYPES, generator, chain, ITER_TYPES 10 | 11 | escape = escape 12 | unescape = unescape 13 | unescapeDict = {"'": "'", """: "\""} 14 | escapeDict = {"'": "'", "\"": """} 15 | 16 | # islice=islice is an optimization 17 | def skip(iterable, n, islice=islice): 18 | try: 19 | return next(islice(iterable, n, None)) 20 | except StopIteration: 21 | return None 22 | # raise IndexError("generator index out of range") 23 | 24 | def filter_dict(iterable, keys): 25 | """ 26 | filters keys of each element of iterable 27 | $.(a,b) returns all objects from array that have at least one of the keys: 28 | [1,"aa",{"a":2,"c":3},{"c":3},{"a":1,"b":2}].(a,b) -> [{"a":2},{"a":1,"b":2}] 29 | """ 30 | if type(keys) is not list: 31 | keys = [keys] 32 | for i in iterable: 33 | try: 34 | d = {} 35 | for a in keys: 36 | try: 37 | d[a] = i[a] 38 | except KeyError: 39 | pass 40 | if d != {}: 41 | yield d 42 | except Exception: 43 | pass 44 | 45 | def flatten(fragment, skip=False): 46 | def rec(frg): 47 | typefrg = type(frg) 48 | if typefrg in ITER_TYPES: 49 | for i in frg: 50 | for j in rec(i): 51 | yield j 52 | elif typefrg is dict: 53 | yield frg 54 | for i in frg.items(): 55 | for j in rec(i[1]): 56 | yield j 57 | 58 | g = rec(fragment) 59 | if skip: 60 | for i in xrange(skip): 61 | g.next() 62 | for i in g: 63 | yield i 64 | -------------------------------------------------------------------------------- /objectpath/utils/colorify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | def color(c, s): 5 | return '\033[%sm%s\033[0m' % (c, s) 6 | 7 | def bold(s): 8 | return color(1, s) 9 | 10 | def op(s): 11 | return color(32, bold(s)) 12 | 13 | def const(s): 14 | return color(36, bold(s)) 15 | 16 | def string(s): 17 | return color(33, bold(s)) 18 | -------------------------------------------------------------------------------- /objectpath/utils/debugger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # move dbgMap outside 5 | import inspect 6 | from objectpath.core import ITER_TYPES 7 | 8 | class Debugger(object): 9 | dbg = None 10 | _debugStr = None 11 | dbgfn = None 12 | level = 40 13 | CUT_AFTER = 100 14 | CRITICAL = 50 15 | ERROR = 40 16 | WARNING = 30 17 | INFO = START = END = 20 18 | DEBUG = 10 19 | doDebug = False 20 | 21 | def __init__(self): 22 | self._debugStr = [] 23 | self.dbgfn = self.consolelog 24 | self.dbgMap = { 25 | "debug": self.DEBUG, 26 | "info": self.INFO, 27 | "start": self.START, 28 | "end": self.END, 29 | "warning": self.WARNING, 30 | "error": self.ERROR, 31 | "critical": self.CRITICAL 32 | } 33 | try: 34 | self.doDebug = True 35 | self.level = self.dbgMap["debug"] 36 | self.info("All strings will be cut to %s chatacters." % self.CUT_AFTER) 37 | except (KeyError, TypeError): 38 | pass 39 | 40 | def debug(self, *s): 41 | if self.dbgfn and self.level <= self.DEBUG: 42 | self.dbgfn("DEBUG", s) 43 | 44 | def info(self, *s): 45 | if self.dbgfn and self.level <= self.INFO: 46 | self.dbgfn("INFO", s) 47 | 48 | def start(self, *s): 49 | if self.dbgfn and self.level <= self.INFO: 50 | self.dbgfn("START", s) 51 | 52 | def end(self, *s): 53 | if self.dbgfn and self.level <= self.INFO: 54 | self.dbgfn("END", s) 55 | 56 | def warning(self, *s): 57 | if self.dbgfn and self.level <= self.WARNING: 58 | self.dbgfn("WARNING", s) 59 | 60 | def error(self, *s): 61 | if self.dbgfn and self.level <= self.ERROR: 62 | self.dbgfn("ERROR", s) 63 | 64 | def critical(self, *s): 65 | if self.dbgfn and self.level <= self.CRITICAL: 66 | self.dbgfn("CRITICAL", s) 67 | 68 | def lineno(self): 69 | """Returns the current line number in our program.""" 70 | return inspect.currentframe().f_back.f_back.f_back.f_lineno 71 | 72 | def cleanOutput(self, o): 73 | toOutput = o 74 | if type(toOutput) in ITER_TYPES: 75 | toOutput = list(toOutput) 76 | if type(toOutput) is tuple: 77 | return tuple(map(lambda x: type(x) in ITER_TYPES and list(x) or x, toOutput)) 78 | return toOutput 79 | 80 | def consolelog(self, lvl, s): 81 | def f(x): 82 | try: 83 | if type(x) is unicode: 84 | x = x.encode("utf8") 85 | except NameError: 86 | pass 87 | if self.CUT_AFTER and type(x) is dict: 88 | s = [] 89 | for i in x.items(): 90 | s.append("'%s': %s" % (i[0], repr(i[1])[:self.CUT_AFTER])) 91 | if len(s[-1]) > self.CUT_AFTER: 92 | s.append("...\033[0m") 93 | return "{\n\t" + ",\n\t".join(s) + "\n}" 94 | s = str(x).replace("\n", "").replace("\t", "").replace("u'", "'") 95 | if self.CUT_AFTER and len(s) > self.CUT_AFTER: 96 | return s[:self.CUT_AFTER] + "...\033[0m" 97 | else: 98 | return x 99 | 100 | if len(s) > 1: 101 | v = tuple(map(f, s[1:])) 102 | self._debugStr.append((lvl, s[0] % v)) 103 | print( 104 | lvl + "@" + str(self.lineno()) + " " + (s[0] % v).replace("u'", "'") 105 | ) 106 | else: 107 | self._debugStr.append((lvl, s[0])) 108 | print(lvl + "@" + str(self.lineno()) + " " + f(s[0])) 109 | -------------------------------------------------------------------------------- /objectpath/utils/json_ext.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | try: 3 | import simplejson as json 4 | except ImportError: 5 | try: 6 | import json 7 | except ImportError: 8 | raise Exception("JSONNotFound") 9 | try: 10 | import ujson as json_fast 11 | except ImportError: 12 | json_fast = json 13 | 14 | from types import GeneratorType as generator 15 | 16 | from objectpath.core import ITER_TYPES, STR_TYPES, NUM_TYPES 17 | from objectpath.utils.colorify import * # pylint: disable=W0614 18 | 19 | load = json_fast.load 20 | 21 | def loads(s): 22 | if s.find("u'") != -1: 23 | s = s.replace("u'", "'") 24 | s = s.replace("'", '"') 25 | try: 26 | return json_fast.loads(s) # ,object_hook=object_hook) 27 | except ValueError as e: 28 | raise Exception(str(e) + " " + s) 29 | 30 | def default(obj): 31 | if isinstance(obj, generator): 32 | return list(obj) 33 | 34 | def dumps(s, default=default, indent=None): 35 | return json.dumps(s, default=default, indent=indent, separators=(',', ':')) 36 | 37 | dump = json.dump 38 | 39 | def py2JSON(o): 40 | if o is True: 41 | return 'true' 42 | if o is False: 43 | return 'false' 44 | if o is None: 45 | return 'null' 46 | if type(o) in NUM_TYPES: 47 | return o 48 | # TODO - check if that is correct 49 | if type(o) is tuple: 50 | return list(o) 51 | elif type(o) in ITER_TYPES + [list, str]: 52 | return o 53 | try: 54 | return str(o) 55 | except UnicodeEncodeError: 56 | return o.encode("utf8") 57 | except Exception: 58 | return o 59 | 60 | LAST_LIST = None 61 | 62 | def printJSON(o, length=5, depth=5): 63 | spaces = 2 64 | 65 | def plus(): 66 | currIndent[0] += 1 67 | 68 | def minus(): 69 | currIndent[0] -= 1 70 | 71 | def out(s): 72 | try: 73 | s = str(s) 74 | except Exception: 75 | pass 76 | if not ret: 77 | ret.append(s) 78 | elif ret[-1][-1] == "\n": 79 | ret.append(currIndent[0]*spaces*" " + s) 80 | else: 81 | ret.append(s) 82 | 83 | def rec(o): 84 | if type(o) in ITER_TYPES: 85 | o = list(o) 86 | if currDepth[0] >= depth: 87 | out("") 88 | return 89 | out("[") 90 | if len(o) > 0: 91 | if len(o) > 1: 92 | out("\n") 93 | plus() 94 | for i in o[0:length]: 95 | rec(i) 96 | out(",\n") 97 | if length == -1: 98 | rec(o[-1]) 99 | out(",\n") 100 | 101 | if length != -1 and len(o) > length: 102 | out("... (" + str(len(o) - length) + " more items)\n") 103 | else: 104 | ret.pop() 105 | if len(o) > 1: 106 | out("\n") 107 | if len(o) > 1: 108 | minus() 109 | out("]") 110 | 111 | elif type(o) is dict: 112 | currDepth[0] += 1 113 | if currDepth[0] > depth: 114 | out("{...}") 115 | return 116 | keys = o.keys() 117 | out("{") 118 | if len(keys) > 0: 119 | if len(keys) > 1: 120 | plus() 121 | out("\n") 122 | for k in o.keys(): 123 | out(string('"' + str(k) + '"') + ": ") 124 | rec(o[k]) 125 | out(",\n") 126 | ret.pop() 127 | if len(keys) > 1: 128 | minus() 129 | out("\n") 130 | out("}") 131 | else: 132 | if type(o) in NUM_TYPES: 133 | out(const(o)) 134 | elif o in [None, False, True]: 135 | out(const(py2JSON(o))) 136 | elif type(o) in STR_TYPES: 137 | out(string('"' + o + '"')) 138 | else: 139 | out(string(o)) 140 | currDepth[0] -= 1 141 | 142 | currIndent = [0] 143 | currDepth = [0] 144 | ret = [] 145 | rec(o) 146 | currIndent[0] = 0 147 | currDepth[0] = 0 148 | return "".join(ret) 149 | -------------------------------------------------------------------------------- /objectpath/utils/timeutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of ObjectPath released under MIT license. 5 | # Copyright (C) 2008-2010 Adrian Kalbarczyk 6 | 7 | import datetime 8 | import sys, os 9 | try: 10 | import pytz 11 | TIMEZONE_CACHE = {"UTC": pytz.utc} 12 | except ImportError: 13 | pass 14 | 15 | from objectpath.core import STR_TYPES 16 | 17 | HOURS_IN_DAY = 24 18 | 19 | now = datetime.datetime.now 20 | 21 | def round9_10(n): 22 | i = int(n) 23 | if n - i > 0.9: 24 | return i + 1 25 | return i 26 | 27 | # TODO its 31 minuta, should be 31 minut - probably done 28 | 29 | def age(date, reference=None, lang="en"): 30 | if reference is None: 31 | reference = now() 32 | td = reference - date #TimeDelta 33 | 34 | days = float(td.days) 35 | langIsPL = lang == "pl" 36 | if days: 37 | years = round9_10(days/356) 38 | if years: 39 | if langIsPL: 40 | return (years, years == 1 and "rok" or years < 5 and "lata" or "lat") 41 | else: 42 | return (years, years == 1 and "year" or "years") 43 | 44 | months = round9_10(days/30) 45 | if months: 46 | if langIsPL: 47 | return ( 48 | months, months == 1 and "miesiąc" or 1 < months < 5 and "miesiące" 49 | or "miesięcy" 50 | ) 51 | else: 52 | return (months, months == 1 and "month" or "months") 53 | 54 | weeks = round9_10(days/7) 55 | if weeks: 56 | if langIsPL: 57 | return ( 58 | weeks, weeks == 1 and "tydzień" 59 | or weeks % 10 in [0, 1, 5, 6, 7, 8, 9] and "tygodni" or "tygodnie" 60 | ) 61 | else: 62 | return (weeks, weeks == 1 and "week" or "weeks") 63 | 64 | days = int(days) 65 | if langIsPL: 66 | return (days, days == 1 and "dzień" or "dni") 67 | else: 68 | return (days, days == 1 and "day" or "days") 69 | 70 | seconds = float(td.seconds) 71 | if seconds is not None: 72 | hours = round9_10(seconds/3600) 73 | if hours: 74 | if langIsPL: 75 | return ( 76 | hours, hours == 1 and "godzina" or 1 < hours < 5 and "godziny" 77 | or "godzin" 78 | ) 79 | else: 80 | return (hours, hours == 1 and "hour" or "hours") 81 | 82 | minutes = round9_10(seconds/60) 83 | if minutes: 84 | if langIsPL: 85 | return ( 86 | minutes, minutes == 1 and "minuta" or 1 < minutes < 5 and "minuty" 87 | or "minut" 88 | ) 89 | else: 90 | return (minutes, minutes == 1 and "minute" or "minutes") 91 | 92 | seconds = int(seconds) 93 | if langIsPL: 94 | return ( 95 | seconds, seconds == 1 and "sekunda" or 1 < seconds < 5 and "sekundy" 96 | or "sekund" 97 | ) 98 | else: 99 | return (seconds, seconds == 1 and "second" or "seconds") 100 | # return (0,"seconds") 101 | 102 | def date(d): 103 | if d: 104 | d = d[0] 105 | t = type(d) 106 | if t is datetime.datetime: 107 | return datetime.date(d.year, d.month, d.day) 108 | if t in (tuple, list): 109 | return datetime.date(*d) 110 | return datetime.date.today() 111 | 112 | def date2list(d): 113 | return [d.year, d.month, d.day] 114 | 115 | def time(d): 116 | if not d or not d[0]: 117 | d = now() 118 | else: 119 | d = d[0] 120 | t = type(d) 121 | if t in (tuple, list): 122 | return datetime.time(*d) 123 | return datetime.time(d.hour, d.minute, d.second, d.microsecond) 124 | 125 | def time2list(t): 126 | return [t.hour, t.minute, t.second, t.microsecond] 127 | 128 | def addTimes(fst, snd): 129 | l1 = time2list(fst) 130 | l2 = time2list(snd) 131 | t = [l1[0] + l2[0], l1[1] + l2[1], l1[2] + l2[2], l1[3] + l2[3]] 132 | t2 = [] 133 | one = 0 134 | ms = t[3] 135 | if ms >= 1000000: 136 | t2.append(ms - 1000000) 137 | one = 1 138 | else: 139 | t2.append(ms) 140 | for i in (t[2], t[1]): 141 | i = i + one 142 | one = 0 143 | if i >= 60: 144 | t2.append(i - 60) 145 | one = 1 146 | # elif i==60: 147 | # t2.append(0) 148 | # one=1 149 | else: 150 | t2.append(i) 151 | hour = t[0] + one 152 | if hour >= HOURS_IN_DAY: 153 | t2.append(hour - HOURS_IN_DAY) 154 | else: 155 | t2.append(hour) 156 | return datetime.time(*reversed(t2)) 157 | 158 | def subTimes(fst, snd): 159 | l1 = time2list(fst) 160 | l2 = time2list(snd) 161 | t = [l1[0] - l2[0], l1[1] - l2[1], l1[2] - l2[2], l1[3] - l2[3]] 162 | t2 = [] 163 | one = 0 164 | ms = t[3] 165 | if ms < 0: 166 | t2.append(1000000 + ms) 167 | one = 1 168 | else: 169 | t2.append(ms) 170 | for i in (t[2], t[1]): 171 | i = i - one 172 | one = 0 173 | if i >= 0: 174 | t2.append(i) 175 | else: 176 | t2.append(60 + i) 177 | one = 1 178 | hour = t[0] - one 179 | if hour < 0: 180 | t2.append(HOURS_IN_DAY + hour) 181 | else: 182 | t2.append(hour) 183 | return datetime.time(*reversed(t2)) 184 | 185 | def dateTime(arg): 186 | """ 187 | d may be: 188 | - datetime() 189 | - [y,m,d,h[,m[,ms]]] 190 | - [date(),time()] 191 | - [[y,m,d],[h,m,s,ms]] 192 | and permutations of above 193 | """ 194 | l = len(arg) 195 | if l == 1: 196 | dt = arg[0] 197 | typed = type(dt) 198 | if typed is datetime.datetime: 199 | return dt 200 | if typed in (tuple, list) and len(dt) in [5, 6, 7]: 201 | return datetime.datetime(*dt) 202 | if l == 2: 203 | date = time = None 204 | typeArg0 = type(arg[0]) 205 | typeArg1 = type(arg[1]) 206 | if typeArg0 in STR_TYPES: 207 | return datetime.datetime.strptime(arg[0], arg[1]) 208 | if typeArg0 is datetime.date: 209 | d = arg[0] 210 | date = [d.year, d.month, d.day] 211 | if typeArg0 in (tuple, list): 212 | date = arg[0] 213 | if typeArg1 is datetime.time: 214 | t = arg[1] 215 | time = [t.hour, t.minute, t.second, t.microsecond] 216 | if typeArg1 in (tuple, list): 217 | time = arg[1] 218 | return datetime.datetime(*date + time) 219 | 220 | # dt - dateTime, tzName is e.g. 'Europe/Warsaw' 221 | def UTC2local(dt, tzName="UTC"): 222 | try: 223 | if tzName in TIMEZONE_CACHE: 224 | tz = TIMEZONE_CACHE[tzName] 225 | else: 226 | tz = TIMEZONE_CACHE[tzName] = pytz.timezone(tzName) 227 | return TIMEZONE_CACHE["UTC"].localize(dt).astimezone(tz) 228 | except Exception: 229 | return dt 230 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytz 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | [yapf] 4 | # split_all_comma_separated_values=True 5 | no_spaces_around_selected_binary_operators="*,/" 6 | indent_width=2 7 | dedent_closing_brackets=True 8 | coalesce_brackets=True 9 | blank_lines_around_top_level_definition=1 10 | continuation_indent_width=2 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | def read(*rnames): 5 | return open(os.path.join(os.path.dirname(__file__), *rnames)).read() 6 | 7 | long_description = (read('README.rst') + '\n' + 'Download\n' '********\n') 8 | 9 | setup( 10 | name='objectpath', 11 | version=read('VER').strip(), 12 | description='The agile query language for semi-structured data. #JSON', 13 | long_description=long_description, 14 | url='http://adriank.github.io/ObjectPath', 15 | author='Adrian Kalbarczyk', 16 | author_email='adrian.kalbarczyk@gmail.com', 17 | license='MIT License', 18 | packages=['objectpath', 'objectpath.utils', 'objectpath.core'], 19 | # package_dir={'': 'objectpath'}, 20 | keywords="query, tree, JSON, nested structures", 21 | classifiers=[ 22 | "Development Status :: 6 - Mature", "Intended Audience :: Developers", 23 | "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", 24 | "Programming Language :: Python", 25 | "Topic :: Software Development :: Libraries :: Python Modules" 26 | ], 27 | install_requires=[ 28 | 'pytz', 29 | ], 30 | zip_safe=True, 31 | entry_points={'console_scripts': ['objectpath = objectpath.shell:main']}, 32 | test_suite="tests" 33 | ) 34 | -------------------------------------------------------------------------------- /shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from objectpath import shell 5 | 6 | shell.main() 7 | -------------------------------------------------------------------------------- /testObjectPath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from tests import doTests 5 | 6 | # starts all test suites 7 | doTests() 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | #unit tests for ACR functions 4 | 5 | from .test_ObjectPath import op_test 6 | from .test_utils import utils_test 7 | 8 | import unittest 9 | 10 | def doTests(): 11 | print('Started ObjectPath Python implementation testing.\n') 12 | unittest.TextTestRunner(verbosity=2).run(op_test) 13 | unittest.TextTestRunner(verbosity=2).run(utils_test) 14 | -------------------------------------------------------------------------------- /tests/test_ObjectPath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from objectpath.core.interpreter import * 5 | from objectpath.core import ProgrammingError, ExecutionError 6 | from random import randint, choice 7 | #from bson.objectid import ObjectId 8 | import sys, unittest, os 9 | 10 | sys.setrecursionlimit(20000) 11 | 12 | object1 = { 13 | "__lang__": "en", 14 | "test": { 15 | "_id": 1, 16 | "name": "aaa", 17 | "o": { 18 | "_id": 2 19 | }, 20 | "l": [{ 21 | "_id": 3, 22 | "aaa": "ddd", 23 | "false": 2 24 | }, { 25 | "_id": 4 26 | }] 27 | } 28 | } 29 | 30 | object2 = { 31 | "store": { 32 | "book": [{ 33 | "id":1, 34 | "category": "reference", 35 | "author": "Nigel Rees", 36 | "title": "Sayings of the Century", 37 | "price": 8.95 38 | }, 39 | { 40 | "category": "fiction", 41 | "author": "Evelyn Waugh", 42 | "title": "Sword of Honour", 43 | "price": 12.99 44 | }, 45 | { 46 | "category": "fiction", 47 | "author": "Herman Melville", 48 | "title": "Moby Dick", 49 | "isbn": "0-553-21311-3", 50 | "price": 8.99 51 | }, 52 | { 53 | "category": "fiction", 54 | "author": "J. R. R. Tolkien", 55 | "title": "The Lord of the Rings", 56 | "isbn": "0-395-19395-8", 57 | "price": 22.99 58 | }], 59 | "bicycle": { 60 | "color": "red", 61 | "price": 19.95 62 | }, 63 | "k": [{ 64 | "_id": 4 65 | }] 66 | } 67 | } 68 | 69 | object3 = { 70 | "item_1": { 71 | "value": "foo", 72 | "x": 5.6, 73 | "y": 9 74 | }, 75 | "item_2": { 76 | "value": "bar", 77 | "x": 5.6, 78 | "y": 9.891 79 | }, 80 | "item_3": { 81 | "value": "foobar", 82 | "x": 5.6, 83 | "y": 9.8 84 | } 85 | } 86 | 87 | object4 = { 88 | "root": { 89 | "response": { 90 | "200": { 91 | "value": 5, 92 | }, 93 | "201": { 94 | "value": 4, 95 | }, 96 | "404": { 97 | "value": 0, 98 | } 99 | } 100 | } 101 | } 102 | 103 | tree1 = Tree(object1) 104 | tree2 = Tree(object2) 105 | tree3 = Tree(object3) 106 | tree4 = Tree(object4) 107 | 108 | def execute_raw(expr): 109 | return tree1.execute(expr) 110 | 111 | TYPES = [generator, chain] 112 | if sys.version_info.major > 2: 113 | TYPES += [map] 114 | 115 | TYPES = tuple(TYPES) 116 | 117 | def execute(expr): 118 | r = tree1.execute(expr) 119 | if isinstance(r, TYPES): 120 | return list(r) 121 | else: 122 | return r 123 | 124 | def execute2(expr): 125 | r = tree2.execute(expr) 126 | if isinstance(r, TYPES): 127 | return list(r) 128 | else: 129 | return r 130 | 131 | def execute3(expr): 132 | r = tree3.execute(expr) 133 | if isinstance(r, TYPES): 134 | return list(r) 135 | else: 136 | return r 137 | 138 | def execute4(expr): 139 | r = tree4.execute(expr) 140 | if isinstance(r, TYPES): 141 | return list(r) 142 | else: 143 | return r 144 | 145 | class ObjectPath(unittest.TestCase): 146 | 147 | def test_simple_types(self): 148 | self.assertEqual(execute("null"), None) 149 | self.assertEqual(execute("true"), True) 150 | self.assertEqual(execute("false"), False) 151 | self.assertEqual(execute("''"), "") 152 | self.assertEqual(execute('""'), "") 153 | self.assertEqual(execute("2"), 2) 154 | self.assertEqual(execute("2.0"), 2.0) 155 | 156 | def test_arrays(self): 157 | self.assertEqual(execute("[]"), []) 158 | self.assertEqual(list(execute("[1,2,3]")), [1, 2, 3]) 159 | self.assertEqual( 160 | list(execute("[false,null,true,'',\"\",2,2.0,{}]")), 161 | [False, None, True, '', "", 2, 2.0, {}] 162 | ) 163 | 164 | def test_objects(self): 165 | self.assertEqual(execute("{}"), {}) 166 | self.assertEqual( 167 | execute("{a:1,b:false,c:'string'}"), { 168 | "a": 1, 169 | "b": False, 170 | "c": 'string' 171 | } 172 | ) 173 | self.assertEqual( 174 | execute("{'a':1,'b':false,'c':'string'}"), { 175 | "a": 1, 176 | "b": False, 177 | "c": 'string' 178 | } 179 | ) 180 | 181 | def test_arithm_add(self): 182 | self.assertEqual(execute("2+3"), 5) 183 | self.assertEqual(execute("2+3+4"), 9) 184 | self.assertEqual(execute("++3"), 3) 185 | # null is treated as neutral value 186 | self.assertEqual(execute("null+3"), 3) 187 | self.assertEqual(execute("3+null"), 3) 188 | 189 | def test_arithm_sub(self): 190 | self.assertEqual(execute("-1"), -1) 191 | self.assertEqual(execute("2-3"), 2 - 3) 192 | self.assertEqual(execute("2.2-3.4"), 2.2 - 3.4) 193 | self.assertEqual(execute("-+-3"), 3) 194 | self.assertEqual(execute("+-+3"), -3) 195 | 196 | def test_arithm_mul(self): 197 | self.assertEqual(execute("2*3*5*6"), 180) 198 | 199 | def test_arithm_mod(self): 200 | self.assertEqual(execute("2%3"), 2.0 % 3) 201 | self.assertEqual(execute("2.0%3"), 2.0 % 3) 202 | self.assertEqual(execute("float(2)%3"), float(2) % 3) 203 | 204 | def test_arithm_div(self): 205 | self.assertEqual(execute("2/3"), 2.0/3) 206 | self.assertEqual(execute("2.0/3"), 2.0/3) 207 | self.assertEqual(execute("float(2)/3"), float(2)/3) 208 | 209 | def test_arithm_group(self): 210 | self.assertEqual(execute("2-3+4+5-7"), 2 - 3 + 4 + 5 - 7) 211 | self.assertEqual(execute("33*2/5-2"), 33*2/5.0 - 2) 212 | self.assertEqual(execute("33-4*5+2/6"), 33 - 4*5 + 2/6.0) 213 | #self.assertEqual(execute("2//3//4//5"), ('//', ('//', ('//', 2, 3), 4), 5)) 214 | 215 | def test_arithm_parentheses(self): 216 | self.assertEqual(execute("+6"), 6) 217 | self.assertEqual(execute("2+2*2"), 6) 218 | self.assertEqual(execute("2+(2*2)"), 6) 219 | self.assertEqual(execute("(2+2)*2"), 8) 220 | self.assertEqual(execute("(33-4)*5+2/6"), (33 - 4)*5 + 2/6.0) 221 | self.assertEqual(execute("2/3/(4/5)*6"), 2/3.0/(4/5.0)*6) 222 | self.assertEqual(execute("((2+4))+6"), ((2 + 4)) + 6) 223 | 224 | def test_logic_negatives(self): 225 | self.assertEqual(execute("not false"), True) 226 | self.assertEqual(execute("not null"), True) 227 | self.assertEqual(execute("not 0"), True) 228 | self.assertEqual(execute("not 0.0"), True) 229 | self.assertEqual(execute("not ''"), True) 230 | self.assertEqual(execute("not []"), True) 231 | self.assertEqual(execute("not {}"), True) 232 | 233 | def test_logic_not(self): 234 | self.assertEqual(execute("not false"), True) 235 | self.assertEqual(execute("not not not false"), True) 236 | 237 | def test_logic_or(self): 238 | self.assertEqual(execute("1 or 2"), 1) 239 | self.assertEqual(execute("0 or 2"), 2) 240 | self.assertEqual(execute("'a' or 0 or 3"), 'a') 241 | self.assertEqual( 242 | execute("null or false or 0 or 0.0 or '' or [] or {}"), {} 243 | ) 244 | 245 | def test_logic_and(self): 246 | self.assertEqual(execute("1 and 2"), 2) 247 | self.assertEqual(execute("0 and 2"), 0) 248 | self.assertEqual(execute("'a' and false and 3"), False) 249 | self.assertEqual( 250 | execute("true and 1 and 1.0 and 'foo' and [1] and {a:1}"), {"a": 1} 251 | ) 252 | 253 | def test_comparison_regex(self): 254 | self.assertIsInstance(execute("/aaa/"), type(re.compile(""))) 255 | self.assertEqual(execute("/.*aaa/ matches 'xxxaaaadddd'"), True) 256 | self.assertEqual(execute("'.*aaa' matches 'xxxaaaadddd'"), True) 257 | self.assertEqual(execute("'.*aaa' matches ['xxxaaaadddd', 'xxx']"), True) 258 | 259 | def test_comparison_is(self): 260 | self.assertEqual(execute("2 is 2"), True) 261 | self.assertEqual(execute("'2' is 2"), True) 262 | self.assertEqual(execute("2 is '2'"), True) 263 | self.assertEqual(execute("2 is 2.0"), True) 264 | self.assertEqual(execute("0.1+0.2 is 0.3"), True) 265 | self.assertEqual(execute("[] is []"), True) 266 | self.assertEqual(execute("[1] is [1]"), True) 267 | self.assertEqual(execute("{} is {}"), True) 268 | self.assertEqual(execute("{} is []"), False) 269 | self.assertEqual(execute("None is 'aaa'"), False) 270 | self.assertEqual(execute("None is None"), True) 271 | self.assertEqual(execute("{'aaa':1} is {'aaa':1}"), True) 272 | #oid=ObjectId() 273 | #self.assertEqual(execute("ObjectID('"+str(oid)+"') is '"+str(oid)+"'"), True) 274 | 275 | def test_comparison_isnot(self): 276 | self.assertEqual(execute("None is not None"), False) 277 | self.assertEqual(execute("None is not 'aaa'"), True) 278 | self.assertEqual(execute("{} is not []"), True) 279 | self.assertEqual(execute("3 is not 6"), True) 280 | self.assertEqual(execute("3 is not '3'"), False) 281 | self.assertEqual(execute("[] is not [1]"), True) 282 | self.assertEqual(execute("[] is not []"), False) 283 | self.assertEqual(execute("{'aaa':2} is not {'bbb':2}"), True) 284 | self.assertEqual(execute("{} is not {}"), False) 285 | 286 | def test_membership_in(self): 287 | self.assertEqual(execute("4 in [6,4,3]"), True) 288 | self.assertEqual(execute("4 in {4:true}"), True) 289 | self.assertEqual(execute("[2,3] in [6,4,3]"), True) 290 | 291 | def test_membership_notin(self): 292 | self.assertEqual(execute("4 not in []"), True) 293 | self.assertEqual(execute("1 not in {'232':2}"), True) 294 | self.assertEqual(execute("[2,5] not in [6,4,3]"), True) 295 | 296 | def test_complex(self): 297 | self.assertEqual(execute("23 is not 56 or 25 is 57"), True) 298 | self.assertEqual(execute("2+3/4-6*7>0 or 10 is not 11 and 14"), 14) 299 | 300 | def test_comparison_lt(self): 301 | self.assertEqual(execute("2<3"), True) 302 | self.assertEqual(execute("3<3"), False) 303 | self.assertEqual(execute("2<=2"), True) 304 | self.assertEqual(execute("2<=1"), False) 305 | 306 | def test_comparison_gt(self): 307 | self.assertEqual(execute("5>4"), True) 308 | self.assertEqual(execute("5>5"), False) 309 | self.assertEqual(execute("5>=5"), True) 310 | 311 | def test_concatenation(self): 312 | self.assertEqual(execute("'a'+'b'+\"c\""), 'abc') 313 | self.assertEqual(execute("'5'+5"), '55') 314 | self.assertEqual(execute("5+'5'"), 10) 315 | self.assertEqual(list(execute("[1,2,4] + [3,5]")), [1, 2, 4, 3, 5]) 316 | self.assertEqual( 317 | execute('{"a":1,"b":2} + {"a":2,"c":3}'), { 318 | "a": 2, 319 | "b": 2, 320 | "c": 3 321 | } 322 | ) 323 | self.assertRaises( 324 | ProgrammingError, lambda: execute('{"a":1,"b":2} + "sss"') 325 | ) 326 | 327 | def test_builtin_casting(self): 328 | self.assertEqual(execute("str('foo')"), 'foo') 329 | self.assertEqual(execute("str(1)"), '1') 330 | self.assertEqual(execute("str(1.0)"), '1.0') 331 | self.assertEqual(execute("str(1 is 1)"), 'true') 332 | self.assertEqual(execute("int(1)"), 1) 333 | self.assertEqual(execute("int(1.0)"), 1) 334 | self.assertEqual(execute("int('1')"), 1) 335 | #Python can't handle that 336 | #self.assertEqual(execute("int('1.0')"), 1) 337 | self.assertEqual(execute("float(1.0)"), 1.0) 338 | self.assertEqual(execute("float(1)"), 1.0) 339 | self.assertEqual(execute("float('1')"), 1.0) 340 | self.assertEqual(execute("float('1.0')"), 1.0) 341 | self.assertEqual(execute("array()"), []) 342 | self.assertEqual(execute("array([])"), []) 343 | self.assertEqual(execute("array('abc')"), ['a', 'b', 'c']) 344 | self.assertEqual( 345 | execute("array(dateTime([2011,4,8,12,0]))"), [2011, 4, 8, 12, 0, 0, 0] 346 | ) 347 | self.assertEqual(execute("array(date([2011,4,8]))"), [2011, 4, 8]) 348 | self.assertEqual(execute("array(time([12,12,30]))"), [12, 12, 30, 0]) 349 | 350 | def test_builtin_arithmetic(self): 351 | self.assertEqual(execute("sum([1,2,3,4])"), sum([1, 2, 3, 4])) 352 | self.assertEqual(execute("sum([2,3,4,'333',[]])"), 9) 353 | self.assertEqual(execute("sum(1)"), 1) 354 | self.assertEqual(execute("min([1,2,3,4])"), min([1, 2, 3, 4])) 355 | self.assertEqual(execute("min([2,3,4,'333',[]])"), 2) 356 | self.assertEqual(execute("min(1)"), 1) 357 | self.assertEqual(execute("max([1,2,3,4])"), max([1, 2, 3, 4])) 358 | self.assertEqual(execute("max([2,3,4,'333',[]])"), 4) 359 | self.assertEqual(execute("max(1)"), 1) 360 | self.assertEqual(execute("avg([1,2,3,4])"), 2.5) 361 | self.assertEqual(execute("avg([1,3,3,1])"), 2.0) 362 | self.assertEqual(execute("avg([1.1,1.3,1.3,1.1])"), 1.2000000000000002) 363 | self.assertEqual(execute("avg([2,3,4,'333',[]])"), 3) 364 | self.assertEqual(execute("avg(1)"), 1) 365 | self.assertEqual(execute("round(2/3)"), round(2.0/3)) 366 | self.assertEqual(execute("round(2/3,3)"), round(2.0/3, 3)) 367 | # edge cases 368 | self.assertEqual(execute("avg(1)"), 1) 369 | # should ommit 'sss' 370 | self.assertEqual(execute("avg([1,'sss',3,3,1])"), 2.0) 371 | 372 | def test_builtin_string(self): 373 | self.assertEqual(execute("replace('foobar','oob','baz')"), 'fbazar') 374 | self.assertEqual(execute("""escape('<')"""), "&lt;") 375 | self.assertEqual(execute("""escape('<"&>')"""), "<"&>") 376 | self.assertEqual(execute("""unescape('<"&>')"""), "<\"&>") 377 | self.assertEqual(execute("upper('aaa')"), "AAA") 378 | self.assertEqual(execute("lower('AAA')"), "aaa") 379 | self.assertEqual(execute("title('AAA aaa')"), "Aaa Aaa") 380 | self.assertEqual(execute("capitalize('AAA Aaa')"), "Aaa aaa") 381 | self.assertEqual(execute("split('aaa aaa')"), ["aaa", "aaa"]) 382 | self.assertEqual(execute("split('aaaxaaa','x')"), ["aaa", "aaa"]) 383 | self.assertEqual(execute("join(['aaą','aaę'],'ć')"), "aaąćaaę") 384 | self.assertEqual(execute("join(['aaa','aaa'])"), "aaaaaa") 385 | self.assertEqual(execute("join(['aaa','aaa',3,55])"), "aaaaaa355") 386 | self.assertEqual(execute('slice("Hello world!", [6, 11])'), "world") 387 | self.assertEqual(execute('slice("Hello world!", [6, -1])'), "world") 388 | self.assertEqual( 389 | execute('slice("Hello world!", [[0,5], [6, 11]])'), ["Hello", "world"] 390 | ) 391 | self.assertRaises(ProgrammingError, lambda: execute('slice()')) 392 | self.assertRaises(ExecutionError, lambda: execute('slice("", {})')) 393 | self.assertEqual(execute('map(upper, ["a", "b", "c"])'), ["A", "B", "C"]) 394 | 395 | def test_builtin_arrays(self): 396 | self.assertEqual(execute("sort([1,2,3,4]+[2,4])"), [1, 2, 2, 3, 4, 4]) 397 | self.assertEqual(execute("sort($.._id)"), [1, 2, 3, 4]) 398 | self.assertEqual( 399 | execute("sort($..l.*, _id)"), [{ 400 | '_id': 3, 401 | 'aaa': 'ddd', 402 | 'false': 2 403 | }, { 404 | '_id': 4 405 | }] 406 | ) 407 | self.assertEqual(execute("reverse([1,2,3,4]+[2,4])"), [4, 2, 4, 3, 2, 1]) 408 | self.assertEqual(execute("reverse(sort($.._id))"), [4, 3, 2, 1]) 409 | self.assertEqual(execute("len([1,2,3,4]+[2,4])"), 6) 410 | self.assertEqual(execute("unique([1,1,3,3])"), [1, 3]) 411 | # edge cases 412 | self.assertEqual(execute("len(True)"), True) 413 | self.assertEqual(execute("len('aaa')"), 3) 414 | 415 | def test_builtin_time(self): 416 | import datetime 417 | self.assertIsInstance(execute("now()"), datetime.datetime) 418 | self.assertIsInstance(execute("date()"), datetime.date) 419 | self.assertIsInstance(execute("date(now())"), datetime.date) 420 | self.assertIsInstance(execute("date([2001,12,30])"), datetime.date) 421 | self.assertIsInstance(execute("time()"), datetime.time) 422 | self.assertIsInstance(execute("time(now())"), datetime.time) 423 | self.assertIsInstance(execute("time([12,23])"), datetime.time) 424 | self.assertIsInstance(execute("time([12,23,21,777777])"), datetime.time) 425 | self.assertIsInstance(execute("dateTime(now())"), datetime.datetime) 426 | self.assertIsInstance( 427 | execute("dateTime([2001,12,30,12,23])"), datetime.datetime 428 | ) 429 | self.assertIsInstance( 430 | execute("dateTime([2001,12,30,12,23,21,777777])"), datetime.datetime 431 | ) 432 | self.assertIsInstance(execute('dateTime("1980-05-11 04:22:33", "%Y-%m-%d %H:%M:%S")'), datetime.datetime) 433 | self.assertEqual(str(execute('dateTime("1980-05-11 04:22:33", "%Y-%m-%d %H:%M:%S")')), "1980-05-11 04:22:33") 434 | 435 | self.assertEqual( 436 | execute("toMillis(dateTime([2001,12,30,12,23,21,777777]))"), 437 | 1009715001777 438 | ) 439 | self.assertIsInstance( 440 | execute("dateTime(date(),time())"), datetime.datetime 441 | ) 442 | self.assertIsInstance( 443 | execute("dateTime(date(),[12,23])"), datetime.datetime 444 | ) 445 | self.assertIsInstance( 446 | execute("dateTime(date(),[12,23,21,777777])"), datetime.datetime 447 | ) 448 | self.assertIsInstance( 449 | execute("dateTime([2001,12,30],time())"), datetime.datetime 450 | ) 451 | self.assertEqual( 452 | execute("array(time([12,30])-time([8,00]))"), [4, 30, 0, 0] 453 | ) 454 | self.assertEqual( 455 | execute("array(time([12,12,12,12])-time([8,8,8,8]))"), [4, 4, 4, 4] 456 | ) 457 | self.assertEqual( 458 | execute("array(time([12,12,12,12])-time([1,2,3,4]))"), [11, 10, 9, 8] 459 | ) 460 | self.assertEqual( 461 | execute("array(time([12,00])-time([1,10]))"), [10, 50, 0, 0] 462 | ) 463 | self.assertEqual( 464 | execute("array(time([1,00])-time([1,10]))"), [23, 50, 0, 0] 465 | ) 466 | self.assertEqual( 467 | execute("array(time([0,00])-time([0,0,0,1]))"), [23, 59, 59, 999999] 468 | ) 469 | self.assertEqual( 470 | execute("array(time([0,0])+time([1,1,1,1]))"), [1, 1, 1, 1] 471 | ) 472 | self.assertEqual( 473 | execute("array(time([0,0])+time([1,2,3,4]))"), [1, 2, 3, 4] 474 | ) 475 | self.assertEqual( 476 | execute("array(time([23,59,59,999999])+time([0,0,0,1]))"), [0, 0, 0, 0] 477 | ) 478 | # age tests 479 | self.assertEqual(execute("age(now())"), [0, "seconds"]) 480 | self.assertEqual( 481 | execute("age(dateTime([2000,1,1,1,1]),dateTime([2001,1,1,1,1]))"), 482 | [1, "year"] 483 | ) 484 | self.assertEqual( 485 | execute("age(dateTime([2000,1,1,1,1]),dateTime([2000,2,1,1,1]))"), 486 | [1, "month"] 487 | ) 488 | self.assertEqual( 489 | execute("age(dateTime([2000,1,1,1,1]),dateTime([2000,1,2,1,1]))"), 490 | [1, "day"] 491 | ) 492 | self.assertEqual( 493 | execute("age(dateTime([2000,1,1,1,1]),dateTime([2000,1,1,2,1]))"), 494 | [1, "hour"] 495 | ) 496 | self.assertEqual( 497 | execute("age(dateTime([2000,1,1,1,1]),dateTime([2000,1,1,1,2]))"), 498 | [1, "minute"] 499 | ) 500 | self.assertEqual( 501 | execute("age(dateTime([2000,1,1,1,1,1]),dateTime([2000,1,1,1,1,2]))"), 502 | [1, "second"] 503 | ) 504 | self.assertEqual( 505 | execute("""array(time([0,0]) - time([0,0,0,999999]))"""), 506 | [23, 59, 59, 1] 507 | ) 508 | self.assertEqual( 509 | execute("""array(time([0,0]) + time([0,0,0,999999]))"""), 510 | [0, 0, 0, 999999] 511 | ) 512 | 513 | def test_localize(self): 514 | pass 515 | #these tests are passing on computers with timezone set to UTC - not the case of TravisCI 516 | #test of non-DST time 517 | #if sys.version < "3": 518 | #self.assertEqual(execute("array(localize(dateTime([2000,1,1,10,10,1,0]),'Europe/Warsaw'))"), [2000,1,1,11,10,1,0]) 519 | #test of DST time 520 | #self.assertEqual(execute("array(localize(dateTime([2000,7,1,10,10,1,0]),'Europe/Warsaw'))"), [2000,7,1,12,10,1,0]) 521 | 522 | def test_builtin_type(self): 523 | self.assertEqual(execute("type([1,2,3,4]+[2,4])"), "array") 524 | self.assertEqual(execute("type({})"), "object") 525 | self.assertEqual(execute("type('')"), "str") 526 | 527 | def test_selector_with_empty_result(self): 528 | self.assertEqual(execute("$.missing is None"), True) 529 | self.assertEqual(execute("$.missing is not None"), False) 530 | 531 | def test_misc(self): 532 | self.assertEqual(execute(2), 2) 533 | self.assertEqual(execute('{"@aaa":1}.@aaa'), 1) 534 | self.assertEqual(execute('$ss.a'), None) 535 | self.assertEqual(execute("$..*[10]"), None) 536 | self.assertEqual(sorted(execute("keys({'a':1,'b':2})")), ['a', 'b']) 537 | self.assertRaises(ExecutionError, lambda: execute('keys([])')) 538 | self.assertRaises(ProgrammingError, lambda: execute('blah([])')) 539 | 540 | def test_optimizations(self): 541 | self.assertEqual(execute("$.*[@]"), execute("$.*")) 542 | self.assertIsInstance(execute_raw("$..*"), generator) 543 | self.assertIsInstance(execute_raw("$..* + $..*"), chain) 544 | self.assertIsInstance(execute_raw("$..* + 2"), chain) 545 | self.assertIsInstance(execute_raw("2 + $..*"), chain) 546 | self.assertEqual(execute("$.._id[0]"), 1) 547 | self.assertEqual(execute("sort($.._id + $.._id)[2]"), 2) 548 | self.assertIsInstance(execute("$.._id[2]"), int) 549 | self.assertEqual( 550 | execute2("$.store.book.(price)[0].price"), 551 | execute2("$.store.book[0].price") 552 | ) 553 | 554 | class ObjectPath_Paths(unittest.TestCase): 555 | def assertItemsEqual(self, a, b, m=None): 556 | try: 557 | return self.assertCountEqual(a, b, m) 558 | except: pass 559 | return unittest.TestCase.assertItemsEqual(self, a, b, m) 560 | 561 | def test_simple_paths(self): 562 | self.assertEqual(execute("$.*"), object1) 563 | self.assertEqual(execute("$.a.b.c"), None) 564 | self.assertEqual(execute("$.a.b.c[0]"), None) 565 | self.assertEqual(execute("$.__lang__"), "en") 566 | self.assertEqual(execute("$.test.o._id"), 2) 567 | self.assertEqual(execute("$.test.l._id"), [3, 4]) 568 | self.assertEqual(execute("$.*[test].o._id"), 2) 569 | self.assertEqual(execute("$.*['test'].o._id"), 2) 570 | self.assertEqual( 571 | execute('[1,"aa",{"a":2,"c":3},{"c":3},{"a":1,"b":2}].(a,b)'), [{ 572 | "a": 2 573 | }, { 574 | "a": 1, 575 | "b": 2 576 | }] 577 | ) 578 | self.assertEqual( 579 | execute2("$.store.book.(price,title)[0]"), { 580 | "price": 8.95, 581 | "title": "Sayings of the Century" 582 | } 583 | ) 584 | self.assertEqual(len(execute2("$..*['Lord' in @.title]")), 1) 585 | self.assertEqual( 586 | execute2("$..book.(price,title)"), [{ 587 | 'price': 8.95, 588 | 'title': 'Sayings of the Century' 589 | }, { 590 | 'price': 12.99, 591 | 'title': 'Sword of Honour' 592 | }, { 593 | 'price': 8.99, 594 | 'title': 'Moby Dick' 595 | }, { 596 | 'price': 22.99, 597 | 'title': 'The Lord of the Rings' 598 | }] 599 | ) 600 | self.assertEqual( 601 | execute2("sort($..(price,title),'price')"), 602 | [{ 603 | 'price': 8.95, 604 | 'title': 'Sayings of the Century' 605 | }, { 606 | 'price': 8.99, 607 | 'title': 'Moby Dick' 608 | }, { 609 | 'price': 12.99, 610 | 'title': 'Sword of Honour' 611 | }, { 612 | 'price': 19.95 613 | }, { 614 | 'price': 22.99, 615 | 'title': 'The Lord of the Rings' 616 | }] 617 | ) 618 | self.assertIsInstance(execute("now().year"), int) 619 | 620 | def test_complex_paths(self): 621 | self.assertEqual(sorted(execute("$.._id")), [1, 2, 3, 4]) 622 | self.assertEqual(execute("$..l"), object1["test"]["l"]) 623 | self.assertEqual(execute("$..l.._id"), [3, 4]) 624 | self.assertEqual(execute2("$.store.*"), object2["store"]) 625 | self.assertEqual( 626 | execute2("$.store.book.author"), 627 | ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'] 628 | ) 629 | #print() 630 | #print(execute2("$.store.book.(author,aaa)")) 631 | self.assertEqual( 632 | execute2("$.store.book.(author,aaa)"), [{ 633 | "author": "Nigel Rees" 634 | }, { 635 | "author": "Evelyn Waugh" 636 | }, { 637 | "author": "Herman Melville" 638 | }, { 639 | "author": "J. R. R. Tolkien" 640 | }] 641 | ) 642 | self.assertEqual( 643 | execute2("$.store.book.(author,price)"), [{ 644 | 'price': 8.95, 645 | 'author': 'Nigel Rees' 646 | }, { 647 | 'price': 12.99, 648 | 'author': 'Evelyn Waugh' 649 | }, { 650 | 'price': 8.99, 651 | 'author': 'Herman Melville' 652 | }, { 653 | 'price': 22.99, 654 | 'author': 'J. R. R. Tolkien' 655 | }] 656 | ) 657 | self.assertEqual( 658 | execute2("$.store.book.*[author]"), 659 | ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'] 660 | ) 661 | self.assertEqual( 662 | execute2("$.store.book.*['author']"), 663 | ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'] 664 | ) 665 | self.assertEqual(execute2("$.store.book"), object2["store"]["book"]) 666 | self.assertEqual( 667 | list(execute2("$..author")), 668 | ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'] 669 | ) 670 | 671 | def test_selectors(self): 672 | self.assertEqual( 673 | execute2("$.store.book[@.id is not null]"), 674 | [{ 675 | 'category': 'reference', 676 | 'price': 8.95, 677 | 'title': 'Sayings of the Century', 678 | 'id': 1, 679 | 'author': 'Nigel Rees' 680 | }] 681 | ) 682 | self.assertEqual(len(execute2("$.store.book[@.id is null]")), 3) 683 | self.assertEqual(len(execute("$..*[@._id>2]")), 2) 684 | self.assertEqual(execute("$..*[3 in @.l._id]")[0], object1['test']) 685 | self.assertEqual(execute2("$.store..*[4 in @.k._id]")[0], object2['store']) 686 | self.assertEqual(execute("$..*[@._id>1 and @._id<3][0]"), {'_id': 2}) 687 | # very bad syntax!!! 688 | self.assertEqual( 689 | sorted(execute2("$.store.book[@.price]")), 690 | sorted([8.95, 12.99, 8.99, 22.99]) 691 | ) 692 | self.assertEqual( 693 | execute3("$..*[@.x is 5.6 and @.y is 9.891].value"), ['bar'] 694 | ) 695 | 696 | def test_object_list(self): 697 | self.assertItemsEqual(execute3('values($.*).value'), ['foo', 'bar', 'foobar']) 698 | self.assertItemsEqual(execute3('keys($.*)'), ['item_1', 'item_2', 'item_3']) 699 | self.assertItemsEqual( 700 | execute4('map(values, $..root..response).value'), [5, 4, 0] 701 | ) 702 | 703 | #testcase2=unittest.FunctionTestCase(test_efficiency(2)) 704 | testcase1 = unittest.TestLoader().loadTestsFromTestCase(ObjectPath) 705 | testcase2 = unittest.TestLoader().loadTestsFromTestCase(ObjectPath_Paths) 706 | 707 | op_test = unittest.TestSuite([testcase1, testcase2]) 708 | #utils_interpreter=unittest.TestSuite([testcase2]) 709 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from objectpath.utils import * 5 | from objectpath.utils.json_ext import * 6 | import sys, unittest, os 7 | 8 | sys.setrecursionlimit(20000) 9 | 10 | class Utils_test(unittest.TestCase): 11 | def test_Utils_JSON_compat(self): 12 | self.assertEqual(loads("['ddd']"), ['ddd']) 13 | if sys.version_info.major < 3: 14 | self.assertEqual(loads("[u'ddd']"), ['ddd']) 15 | self.assertRaises(Exception, lambda: loads(['ddd}'])) 16 | self.assertEqual(dumps(['ddd']), '["ddd"]') 17 | self.assertEqual(py2JSON(False), 'false') 18 | self.assertEqual(py2JSON(None), 'null') 19 | self.assertEqual(py2JSON((2, 3, 4)), [2, 3, 4]) 20 | if sys.version_info.major < 3: 21 | self.assertEqual(py2JSON(unicode('')), '') 22 | self.assertEqual(py2JSON(2), 2) 23 | self.assertEqual( 24 | printJSON([1, 2, 3, 4, 5, 6]), 25 | "[\n \x1b[36m\x1b[1m1\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m2\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m3\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m4\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m5\x1b[0m\x1b[0m,\n ... (1 more items)\n]" 26 | ) 27 | self.assertEqual( 28 | printJSON([{}, 1]), '[\n {},\n \x1b[36m\x1b[1m1\x1b[0m\x1b[0m\n]' 29 | ) 30 | self.assertEqual( 31 | printJSON({ 32 | "aaa": 1 33 | }), 34 | '{\x1b[33m\x1b[1m"aaa"\x1b[0m\x1b[0m: \x1b[36m\x1b[1m1\x1b[0m\x1b[0m}' 35 | ) 36 | self.assertEqual( 37 | printJSON({ 38 | "a": [1, 2, 3] 39 | }), 40 | '{\x1b[33m\x1b[1m"a"\x1b[0m\x1b[0m: [\n \x1b[36m\x1b[1m1\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m2\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m3\x1b[0m\x1b[0m\n]}' 41 | ) 42 | self.assertEqual( 43 | printJSON([[1], { 44 | "aa": 2 45 | }]), 46 | '[\n [\x1b[36m\x1b[1m1\x1b[0m\x1b[0m],\n {\x1b[33m\x1b[1m"aa"\x1b[0m\x1b[0m: \x1b[36m\x1b[1m2\x1b[0m\x1b[0m}\n]' 47 | ) 48 | self.assertEqual( 49 | printJSON({ 50 | "aaa": { 51 | "bbb": { 52 | "ccc": { 53 | "ddd": [1, 2, 3, 4, 5] 54 | } 55 | } 56 | } 57 | }), 58 | '{\x1b[33m\x1b[1m"aaa"\x1b[0m\x1b[0m: {\x1b[33m\x1b[1m"bbb"\x1b[0m\x1b[0m: {\x1b[33m\x1b[1m"ccc"\x1b[0m\x1b[0m: {\x1b[33m\x1b[1m"ddd"\x1b[0m\x1b[0m: [\n \x1b[36m\x1b[1m1\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m2\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m3\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m4\x1b[0m\x1b[0m,\n \x1b[36m\x1b[1m5\x1b[0m\x1b[0m\n]}}}}' 59 | ) 60 | if str(sys.version_info.major) + str(sys.version_info.minor) < '33': 61 | self.assertEqual( 62 | printJSON({ 63 | "aaa": { 64 | "bbb": { 65 | "ccc": { 66 | "ddd": { 67 | "eee": [1, 2, 3, 4, 5], 68 | "ddd": {} 69 | } 70 | } 71 | } 72 | } 73 | }), 74 | '{\x1b[33m\x1b[1m"aaa"\x1b[0m\x1b[0m: {\x1b[33m\x1b[1m"bbb"\x1b[0m\x1b[0m: {\x1b[33m\x1b[1m"ccc"\x1b[0m\x1b[0m: {\x1b[33m\x1b[1m"ddd"\x1b[0m\x1b[0m: {\n \x1b[33m\x1b[1m"eee"\x1b[0m\x1b[0m: ,\n \x1b[33m\x1b[1m"ddd"\x1b[0m\x1b[0m: {...}\n}}}}}' 75 | ) 76 | 77 | testcase1 = unittest.TestLoader().loadTestsFromTestCase(Utils_test) 78 | 79 | utils_test = unittest.TestSuite([testcase1]) 80 | --------------------------------------------------------------------------------