diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index b0bbd3b..0000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -branch = True - -[xml] -output = tests/reports/coverage.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3237161..dbdbf3d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,14 +26,15 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install -r requirements/dev.txt - pip install -r requirements/test.txt + python -m pip install --upgrade pip + pip install poetry + poetry install - name: Linting run: | - flake8 + poetry run flake8 - name: Testing run: | - python setup.py test + poetry run pytest sonar: name: Quality Analysis by sonar needs: [build] @@ -49,11 +50,12 @@ jobs: python-version: '3.10' - name: Install dependencies run: | - pip install -r requirements/dev.txt - pip install -r requirements/test.txt + python -m pip install --upgrade pip + pip install poetry + poetry install - name: Testing run: | - python setup.py test + poetry run pytest - name: Fix coverage report for Sonar run: | sed -i 's/\/home\/runner\/work\/lib-rql\/lib-rql\//\/github\/workspace\//g' ./tests/reports/coverage.xml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f394921..dbbf9b0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Deploy Python-RQL library +name: Publish Python-RQL library on: push: @@ -21,36 +21,29 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install -r requirements/dev.txt - pip install -r requirements/test.txt - pip install twine + python -m pip install --upgrade pip + pip install poetry + poetry install - name: Linting run: | - flake8 + poetry run flake8 - name: Testing run: | - python setup.py test - - name: Fix coverage report for Sonar - run: | - sed -i 's/\/home\/runner\/work\/lib-rql\/lib-rql\//\/github\/workspace\//g' ./tests/reports/coverage.xml - - name: SonarCloud - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: Wait sonar to process report - uses: jakejarvis/wait-action@master + poetry run pytest + - name: Extract tag name + uses: actions/github-script@v3 + id: tag with: - time: '15s' - - name: SonarQube Quality Gate check - uses: sonarsource/sonarqube-quality-gate-action@master - timeout-minutes: 5 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: Publish to PyPI + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + return context.payload.ref.replace(/refs\/tags\//, '') + - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel - twine upload dist/* + poetry version ${{ steps.tag.outputs.result }} + poetry build + poetry publish -u $TWINE_USERNAME -p $TWINE_PASSWORD + diff --git a/.gitignore b/.gitignore index 41ab210..222db39 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ dist/ tests/reports/ .coverage + +.devcontainer + +htmlcov/ +docs/_build \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 42f8605..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -dist: trusty -language: python -matrix: - include: - - os: linux - python: 3.6 - dist: xenial - sudo: true - - os: linux - python: 3.7 - dist: xenial - sudo: true - env: BUILD=PYPI - - os: linux - python: 3.8 - dist: xenial - sudo: true -install: -- pip install -r requirements/dev.txt -- pip install -r requirements/test.txt -- pip install pytest-cov -script: -- flake8 -- python setup.py test -- sonar-scanner -after_success: -- bash <(curl -s https://codecov.io/bash) -addons: - sonarcloud: - organization: cloudbluesonarcube - token: - secure: VJcQOn8TekNPIEn/vnf4fHGZoHha1hoeIVGGT9sYHfqe6zjofbQqAaGO3HMVR7Oliqxe1GQkiw2g+J5YdnRSvqqt2B6EPRiJXJW2jF0irwZ+zsrs/h5LcnQvp6KcIEjC9XmYh3gtkL7GF4Afola0Co4kic7+G2icExKE9JZCfEPExIgjZ6tkx74i0sIbUpdVNas8ODiPwXYcia0DBBh/1NT6p0X4wduSsWEC98rkxWZEyqtMLgL9hkO8TyihnNxsPCpK+ocyAtVmBG0P52A7iGHOi3JjzxVOQXntXhutdJsFXs+wcnFZfKHZf5lwfSKuY45DBr6s5iXo7mWADau3TJ2WeKasK/Bs20iLu2yJArNVqCAKqyA92YBRltowxYlNt3ewK6NrMyETvK0GVzrnBet0jVBfWPztJYi5HdUQZjfHMOiFE4k2kX1sWWI3uSDMS03vppzwo3T+InaZoj/fJaqsOGXMSlkRyKWa8jeK6556pYKo784WdYthijR2sOxdLck19ZaeplI+t3Pre9mGPlKw1gn/TqCPfQaFstYwede4qk+OzPthfDJh/jSobQgHiGhq9C0vzAn+ftSAEYJupNRt33uX18NSf2nH6N9mBaBF7cuSZrWTKmR1D00f4laIv5w+hI/xVq079BJe9xdK0gtxfZcdGMUowILcA+i2PV4= -deploy: - skip_cleanup: true - skip_existing: true - provider: pypi - user: "__token__" - password: - secure: Wzg60u6udwzVIfquCwyge0x0VFPKMvfi0k/lavgJ4dAIpSRJeZESn9gTPnE8nwmDlK7LLezfHGVZnV94428zthzh3CmeLbMx+eD1l7s5/x0s/FJa6Y1AvO+q82N6AmjkstCFcyhliTL6c39X4sQUpN/CQb1Xo0la7qlooFjXf4UvRD3KTnLi4LbQZ5iAp7eLFFYyfRfP8k8IpCWBzt8vWPmQzL9V+xfS7lt4wk/VOeX0wyTNoSPoSnsTI5bBLc6KhxUweGMdIvGgpEL2QvJ3kL4nnAB31NJ6/wJhHp5busTzNruMFnRMwlIjjVIN6PIpjZ7G1qBP3o19vwLkpZ3DT3ALZexZoeBZtIxxmWoQr4i5D4OGeHRKVOHD7SOELOGp1jvmhMilkNtJwFjtozuBOoJSk0W+Ie1fimPK3WJkTjtJZ1vHf6K3cuV8Ejul7C2Qy9Y8qojyfcQY4EwVriZvRKaCnMfNal6swJrpdYoDIsnZFC8u5jTsWPJCiS60XZbcWv6X/i9kZD/R+OHyDZJ4T4fSVprZ4WQysAKUNoLjftCxGlvUz3ir+D20cRO378kdvNar8XzUhKcaUC5anm+8NvjAKxLEWxo5Eklegg+sBlBF/kJSOVVhJ/KrqHlisaX1UhsZs2Pl3MO+eXRGIrHoeRCoBBrulg9Kb6hYKx2l7b8= - on: - tags: true - all_branches: true - distributions: sdist - condition: "$BUILD = PYPI" - repo: cloudblue/lib-rql diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 734bb02..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include LICENSE -include README.md -include requirements/dev.txt -include requirements/test.txt -prune tests diff --git a/README.md b/README.md index f285bd7..4cbd098 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,54 @@ -Python RQL -========== +# Python RQL + + [![pyversions](https://img.shields.io/pypi/pyversions/lib-rql.svg)](https://pypi.org/project/lib-rql/) [![PyPi Status](https://img.shields.io/pypi/v/lib-rql.svg)](https://pypi.org/project/lib-rql/) [![PyPI status](https://img.shields.io/pypi/status/lib-rql.svg)](https://pypi.org/project/lib-rql/) +[![PyPI Downloads](https://img.shields.io/pypi/dm/lib-rql)](https://pypi.org/project/lib-rql/) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=lib-rql&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=lib-rql) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=lib-rql&metric=coverage)](https://sonarcloud.io/summary/new_code?id=lib-rql) -RQL ---- + + +## Introduction RQL (Resource query language) is designed for modern application development. It is built for the web, ready for NoSQL, and highly extensible with simple syntax. This is a query language fast and convenient database interaction. RQL was designed for use in URLs to request object-style data structures. [RQL Reference](https://connect.cloudblue.com/community/api/rql/) -Notes ------ +## Install + +`Python RQL` can be installed from [pypi.org](https://pypi.org/project/lib-rql/) using pip: + +``` +$ pip install lib-rql +``` + +## Documentation + +[`Python RQL` documentation](https://lib-rql.readthedocs.io/en/latest/) is hosted on the _Read the Docs_ service. + + +## Projects using Python RQL + +`Django RQL` is the Django app, that adds RQL filtering to your application. + +Visit the [Django RQL](https://github.com/cloudblue/django-rql) project repository for more informations. + + +## Notes Parsing is done with [Lark](https://github.com/lark-parser/lark) ([cheatsheet](https://lark-parser.readthedocs.io/en/latest/lark_cheatsheet.pdf)). The current parsing algorithm is [LALR(1)](https://www.wikiwand.com/en/LALR_parser) with standard lexer. -Supported operators -============================= +0. Values with whitespaces or special characters, like ',' need to have "" or '' +1. Supported date format is ISO8601: 2019-02-12 +2. Supported datetime format is ISO8601: 2019-02-12T10:02:00 / 2019-02-12T10:02Z / 2019-02-12T10:02:00+03:00 + + +## Supported operators + 1. Comparison (eq, ne, gt, ge, lt, le, like, ilike, search) 2. List (in, out) 3. Logical (and, or, not) @@ -31,8 +58,11 @@ Supported operators 7. Tuple (t) -Example -======= +## Examples + +### Parsing a RQL query + + ```python from py_rql import parse from py_rql.exceptions import RQLFilterError @@ -43,27 +73,66 @@ except RQLFilterError: pass ``` -Notes -===== -0. Values with whitespaces or special characters, like ',' need to have "" or '' -1. Supported date format is ISO8601: 2019-02-12 -2. Supported datetime format is ISO8601: 2019-02-12T10:02:00 / 2019-02-12T10:02Z / 2019-02-12T10:02:00+03:00 + +### Filter a list of dictionaries + +```python +from py_rql.constants import FilterTypes +from py_rql.filter_cls import FilterClass + + +class BookFilter(FilterClass): + FILTERS = [ + { + 'filter': 'title', + }, + { + 'filter': 'author.name', + }, + { + 'filter': 'status', + }, + { + 'filter': 'pages', + 'type': FilterTypes.INT, + }, + { + 'filter': 'featured', + 'type': FilterTypes.BOOLEAN, + }, + { + 'filter': 'publish_date', + 'type': FilterTypes.DATETIME, + }, + ] + +filters = BookFilter() + +query = 'eq(title,Practical Modern JavaScript)' +results = list(filters.filter(query, DATA)) + +print(results) + +query = 'or(eq(pages,472),lt(pages,400))' +results = list(filters.filter(query, DATA)) + +print(results) +``` -Development -=========== +## Development + 1. Python 3.6+ -0. Install dependencies `requirements/dev.txt` +0. Install dependencies `pip install poetry && poetry install` -Testing -======= +## Testing 1. Python 3.6+ -0. Install dependencies `requirements/test.txt` +0. Install dependencies `pip install poetry && poetry install` -Check code style: `flake8` -Run tests: `pytest` +Check code style: `poetry run flake8` +Run tests: `poetry run pytest` Tests reports are generated in `tests/reports`. * `out.xml` - JUnit test results @@ -72,3 +141,6 @@ Tests reports are generated in `tests/reports`. To generate HTML coverage reports use: `--cov-report html:tests/reports/cov_html` +## License + +`Python RQL` is released under the [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..192b4e4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,56 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +from datetime import datetime + +sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'Python RQL' +copyright = f'{datetime.now().year}, CloudBlue LLC' +author = 'CloudBlue' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosectionlabel', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 0000000..3dbfd93 --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,104 @@ +User guide +========== + + +Getting started +--------------- + + +Install +^^^^^^^ + +*Python RQL* is a small python package that can be installed +from the `pypi.org `_ repository. + + +.. code-block:: sh + + $ pip install lib-rql + + + +First steps with Python RQL +--------------------------- + +Python RQL allows you to filter a list of python dictionary using RQL. + +First of all you have define your filters. To do so you have to create a class that +inherits from :class:`~py_rql.filter_cls.FilterClass` and declare your filters like in the following example: + +.. code-block:: python + + from py_rql.constants import FilterTypes + from py_rql.filter_cls import FilterClass + + + class BookFilter(FilterClass): + FILTERS = [ + { + 'filter': 'title', + }, + { + 'filter': 'author.name', + }, + { + 'filter': 'status', + }, + { + 'filter': 'pages', + 'type': FilterTypes.INT, + }, + { + 'filter': 'featured', + 'type': FilterTypes.BOOLEAN, + }, + { + 'filter': 'publish_date', + 'type': FilterTypes.DATETIME, + }, + ] + + + +Then you can use your BookFilter like in the following example: + +.. code-block:: python + + filters = BookFilter() + + query = 'eq(title,Practical Modern JavaScript)' + results = list(filters.filter(query, DATA)) + + print(results) + + query = 'or(eq(pages,472),lt(pages,400))' + results = list(filters.filter(query, DATA)) + + print(results) + + +See the examples folder in the project root for a demo program. + + +Customize filter lookups +------------------------ + +You can explicitly declare the lookups you want to support for a specific filter. +For example if you want to just support the `eq` and `ne` lookup for a string filter you can do +that like in the following example: + + +.. code-block:: python + + from py_rql.constants import FilterLookups, FilterTypes + from py_rql.filter_cls import FilterClass + + + class BookFilter(FilterClass): + FILTERS = [ + { + 'filter': 'title', + 'lookups': {FilterLookups.EQ, FilterLookups.NE} + }, + ] + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..84237dd --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,29 @@ +.. Python RQL documentation master file, created by + sphinx-quickstart on Mon Feb 14 11:11:54 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Python RQL's documentation! +====================================== + +RQL (Resource query language) is designed for modern application development. It is built for the web, ready for NoSQL, and highly extensible with simple syntax. +This is a query language fast and convenient database interaction. RQL was designed for use in URLs to request object-style data structures. + +`RQL Reference `_. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + guide + + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..153be5e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/examples/books.json b/examples/books.json new file mode 100644 index 0000000..10c04db --- /dev/null +++ b/examples/books.json @@ -0,0 +1,68 @@ +{ + "books": [ + { + "isbn": "9781593279509", + "title": "Eloquent Javascript Third Edition", + "subtitle": "A Modern Introduction to Programming", + "author": { + "name": "Marijn", + "surname": "Haverbeke" + }, + "publish_date": "2018-12-04T00:00:00.000Z", + "publisher": "No Starch Press", + "pages": 472, + "description": "JavaScript lies at the heart of almost every modern web application, from social apps like Twitter to browser-based game frameworks like Phaser and Babylon. Though simple for beginners to pick up and play with, JavaScript is a flexible, complex language that you can use to build full-scale applications.", + "website": "http://eloquentjavascript.net/", + "status": "published", + "featured": true + }, + { + "isbn": "9781491943533", + "title": "Practical Modern JavaScript", + "subtitle": "Dive into ES6 and the Future of JavaScript", + "author": { + "name": "Nicolás", + "surname": "Bevacqua" + }, + "publish_date": "2017-07-16T00:00:00.000Z", + "publisher": "O'Reilly Media", + "pages": 334, + "description": "To get the most out of modern JavaScript, you need learn the latest features of its parent specification, ECMAScript 6 (ES6). This book provides a highly practical look at ES6, without getting lost in the specification or its implementation details.", + "website": "https://github.com/mjavascript/practical-modern-javascript", + "status": "preview", + "featured": false + }, + { + "isbn": "9781593277574", + "title": "Understanding ECMAScript 6", + "subtitle": "The Definitive Guide for JavaScript Developers", + "author": { + "name": "Nicholas", + "surname": "C. Zakas" + }, + "publish_date": "2016-09-03T00:00:00.000Z", + "publisher": "No Starch Press", + "pages": 352, + "description": "ECMAScript 6 represents the biggest update to the core of JavaScript in the history of the language. In Understanding ECMAScript 6, expert developer Nicholas C. Zakas provides a complete guide to the object types, syntax, and other exciting changes that ECMAScript 6 brings to JavaScript.", + "website": "https://leanpub.com/understandinges6/read", + "status": "published", + "featured": false + }, + { + "isbn": "9781449365035", + "title": "Speaking JavaScript", + "subtitle": "An In-Depth Guide for Programmers", + "author": { + "name": "Axel", + "surname": "Rauschmayer" + }, + "publish_date": "2014-04-08T00:00:00.000Z", + "publisher": "O'Reilly Media", + "pages": 460, + "description": "Like it or not, JavaScript is everywhere these days -from browser to server to mobile- and now you, too, need to learn the language or dive deeper than you have. This concise book guides you into and through JavaScript, written by a veteran programmer who once found himself in the same position.", + "website": "http://speakingjs.com/", + "status": null, + "featured": true + } + ] +} \ No newline at end of file diff --git a/examples/books.py b/examples/books.py new file mode 100644 index 0000000..ed2afb6 --- /dev/null +++ b/examples/books.py @@ -0,0 +1,85 @@ +import json +from pprint import pprint + +from py_rql.constants import FilterTypes +from py_rql.filter_cls import FilterClass + + +DATA = json.load(open('./books.json'))['books'] + + +class BookFilter(FilterClass): + FILTERS = [ + { + 'filter': 'title', + }, + { + 'filter': 'author.name', + }, + { + 'filter': 'status', + }, + { + 'filter': 'pages', + 'type': FilterTypes.INT, + }, + { + 'filter': 'featured', + 'type': FilterTypes.BOOLEAN, + }, + { + 'filter': 'publish_date', + 'type': FilterTypes.DATETIME, + }, + ] + + +def main(): + total = len(DATA) + filters = BookFilter() + + query = 'eq(title,Practical Modern JavaScript)' + results = list(filters.filter(query, DATA)) + + print(f'\nquery: {query} -> matches {len(results)}/{total}\n') + pprint(results) + print('*' * 70) + + query = 'or(eq(pages,472),lt(pages,400))' + results = list(filters.filter(query, DATA)) + + print(f'\nquery: {query} -> matches {len(results)}/{total}\n') + pprint(results) + print('*' * 70) + + query = 'and(eq(featured,true),in(status,(published,draft)))' + results = list(filters.filter(query, DATA)) + + print(f'\nquery: {query} -> matches {len(results)}/{total}\n') + pprint(results) + print('*' * 70) + + query = 'eq(status,null())' + results = list(filters.filter(query, DATA)) + + print(f'\nquery: {query} -> matches {len(results)}/{total}\n') + pprint(results) + print('*' * 70) + + query = 'like(title,*JavaScript*)' + results = list(filters.filter(query, DATA)) + + print(f'\nquery: {query} -> matches {len(results)}/{total}\n') + pprint(results) + print('*' * 70) + + query = 'ilike(title,*JavaScript*)' + results = list(filters.filter(query, DATA)) + + print(f'\nquery: {query} -> matches {len(results)}/{total}\n') + pprint(results) + print('*' * 70) + + +if __name__ == '__main__': + main() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3e1aa3a --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1142 @@ +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "appnope" +version = "0.1.2" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "babel" +version = "2.9.1" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "cachetools" +version = "4.2.4" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = false +python-versions = "~=3.5" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "cognitive-complexity" +version = "1.2.0" +description = "Library to calculate Python functions cognitive complexity via code" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.dependencies] +toml = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["toml"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "flake8" +version = "3.8.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[[package]] +name = "flake8-broken-line" +version = "0.3.0" +description = "Flake8 plugin to forbid backslashes for line breaks" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +flake8 = ">=3.5,<4.0" + +[[package]] +name = "flake8-bugbear" +version = "20.11.1" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "black", "hypothesis", "hypothesmith"] + +[[package]] +name = "flake8-cognitive-complexity" +version = "0.1.0" +description = "An extension for flake8 that validates cognitive functions complexity" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cognitive_complexity = "*" + +[[package]] +name = "flake8-commas" +version = "2.0.0" +description = "Flake8 lint for trailing commas." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=2,<4.0.0" + +[[package]] +name = "flake8-future-import" +version = "0.4.6" +description = "__future__ import checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-import-order" +version = "0.18.1" +description = "Flake8 and pylama plugin that checks the ordering of import statements." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycodestyle = "*" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "imagesize" +version = "1.3.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "4.8.3" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "ipython" +version = "7.16.3" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.10,<=0.17.2" +pexpect = {version = "*", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +traitlets = ">=4.2" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "jedi" +version = "0.17.2" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +parso = ">=0.7.0,<0.8.0" + +[package.extras] +qa = ["flake8 (==3.7.9)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] + +[[package]] +name = "jinja2" +version = "3.0.3" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lark-parser" +version = "0.11.0" +description = "a modern parsing library" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +nearley = ["js2py"] +regex = ["regex"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "parso" +version = "0.7.1" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +testing = ["docopt", "pytest (>=3.0.7)"] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.3" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.11.2" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.6.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2021.3" +description = "World timezone definitions, modern and historical" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "sphinx" +version = "4.4.0" +description = "Python documentation generator" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.18" +imagesize = "*" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] + +[[package]] +name = "sphinx-rtd-theme" +version = "1.0.0" +description = "Read the Docs theme for Sphinx" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" + +[package.dependencies] +docutils = "<0.18" +sphinx = ">=1.6" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "traitlets" +version = "4.3.3" +description = "Traitlets Python config system" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +decorator = "*" +ipython-genutils = "*" +six = "*" + +[package.extras] +test = ["pytest", "mock"] + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.8" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.6" +content-hash = "cc560f1ea85de2a57b0189c126c713e8d8b95e8ea410a69cbc55bb010b780709" + +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +appnope = [ + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +babel = [ + {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, + {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +cachetools = [ + {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"}, + {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +cognitive-complexity = [ + {file = "cognitive_complexity-1.2.0.tar.gz", hash = "sha256:3c2b433a9e41502932f6aa629e1f57a5e8f145956c54facbb5241a9492af6fb7"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] +decorator = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] +flake8 = [ + {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, + {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, +] +flake8-broken-line = [ + {file = "flake8-broken-line-0.3.0.tar.gz", hash = "sha256:f74e052833324a9e5f0055032f7ccc54b23faabafe5a26241c2f977e70b10b50"}, + {file = "flake8_broken_line-0.3.0-py3-none-any.whl", hash = "sha256:611f79c7f27118e7e5d3dc098ef7681c40aeadf23783700c5dbee840d2baf3af"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-20.11.1.tar.gz", hash = "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538"}, + {file = "flake8_bugbear-20.11.1-py36.py37.py38-none-any.whl", hash = "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"}, +] +flake8-cognitive-complexity = [ + {file = "flake8_cognitive_complexity-0.1.0.tar.gz", hash = "sha256:f202df054e4f6ff182b659c261922b9c684628a47beb19cb0973c50d6a7831c1"}, +] +flake8-commas = [ + {file = "flake8-commas-2.0.0.tar.gz", hash = "sha256:d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7"}, + {file = "flake8_commas-2.0.0-py2.py3-none-any.whl", hash = "sha256:ee2141a3495ef9789a3894ed8802d03eff1eaaf98ce6d8653a7c573ef101935e"}, +] +flake8-future-import = [ + {file = "flake8-future-import-0.4.6.tar.gz", hash = "sha256:9711df0394a2bb5af47986a816cafc2e7f20db9ebc729676553e13df6cabbcf8"}, + {file = "flake8_future_import-0.4.6-py2.py3-none-any.whl", hash = "sha256:dceb036a81744b59a9ca34811b8a9f952a3585c95eaebdda15e8b04dafc77595"}, +] +flake8-import-order = [ + {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"}, + {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +imagesize = [ + {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, + {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"}, + {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +ipython = [ + {file = "ipython-7.16.3-py3-none-any.whl", hash = "sha256:c0427ed8bc33ac481faf9d3acf7e84e0010cdaada945e0badd1e2e74cc075833"}, + {file = "ipython-7.16.3.tar.gz", hash = "sha256:5ac47dc9af66fc2f5530c12069390877ae372ac905edca75a92a6e363b5d7caa"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +jedi = [ + {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, + {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, +] +jinja2 = [ + {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, + {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, +] +lark-parser = [ + {file = "lark-parser-0.11.0.tar.gz", hash = "sha256:d047e680418221d21be587cd8f36843df97479a5623b74a7d811c1d832e04989"}, + {file = "lark_parser-0.11.0-py2.py3-none-any.whl", hash = "sha256:1d710d4c4664b52579aae80324ff409dedc22b5c592684429f6bd11bfeb7fd49"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +parso = [ + {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, + {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, + {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pygments = [ + {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, + {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, +] +pyparsing = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +pytest-mock = [ + {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, + {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [ + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] +sphinx = [ + {file = "Sphinx-4.4.0-py3-none-any.whl", hash = "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe"}, + {file = "Sphinx-4.4.0.tar.gz", hash = "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, + {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +traitlets = [ + {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, + {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, +] +typing-extensions = [ + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, +] +urllib3 = [ + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +zipp = [ + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, +] diff --git a/py_rql/cast.py b/py_rql/cast.py new file mode 100644 index 0000000..e0fe66c --- /dev/null +++ b/py_rql/cast.py @@ -0,0 +1,38 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +from decimal import Decimal + +from dateutil.parser import isoparse + +from py_rql.constants import FilterTypes, RQL_EMPTY, RQL_FALSE, RQL_NULL + + +def cast_string(val): + if val == RQL_EMPTY: + return '' + + if val == RQL_NULL: + return None + + return str(val) + + +def cast_boolean(val): + if val == RQL_NULL: + return None + + return val.lower() != RQL_FALSE + + +def get_default_cast_func_for_type(filter_type): + mapping = { + FilterTypes.INT: lambda val: None if val == RQL_NULL else int(val), + FilterTypes.DECIMAL: lambda val: None if val == RQL_NULL else Decimal(val), + FilterTypes.FLOAT: lambda val: None if val == RQL_NULL else float(val), + FilterTypes.DATE: lambda val: None if val == RQL_NULL else isoparse(val).date(), + FilterTypes.DATETIME: lambda val: None if val == RQL_NULL else isoparse(val), + FilterTypes.STRING: cast_string, + FilterTypes.BOOLEAN: cast_boolean, + } + return mapping[filter_type] diff --git a/py_rql/filter_cls.py b/py_rql/filter_cls.py new file mode 100644 index 0000000..c4e9007 --- /dev/null +++ b/py_rql/filter_cls.py @@ -0,0 +1,89 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +from cachetools import LFUCache + +from py_rql import parse +from py_rql.cast import get_default_cast_func_for_type +from py_rql.constants import FilterLookups, FilterTypes, RESERVED_FILTER_NAMES +from py_rql.exceptions import RQLFilterLookupError +from py_rql.transformer import RQLToFunctionTransformer + + +class FilterClass: + + FILTERS = [] + + def __init__(self): + self._cache = LFUCache(maxsize=1000) + self._filters = self.init_filters() + + def init_filters(self): + filters = {} + for filter_decl in self.FILTERS: + filter_name = filter_decl['filter'] + e = f"'{filter_name}' is a reserved filter name." + assert filter_name not in RESERVED_FILTER_NAMES, e + + filter_type = filter_decl.get('type', FilterTypes.STRING) + default_lookups = self.default_filter_lookups_for_type(filter_type) + lookups = filter_decl.get('lookups', default_lookups) + e = f"Invalid lookups for filter '{filter_name}' of type '{filter_type}': '{lookups}'" + assert set(lookups).issubset(default_lookups), e + + cast_func = filter_decl.get( + 'cast_func', + get_default_cast_func_for_type(filter_type), + ) + e = f"Invalid cast function for filter '{filter_name}'." + assert callable(cast_func), e + + filters[filter_name] = { + 'type': filter_type, + 'lookups': lookups, + 'cast_func': cast_func, + } + return filters + + def transform_query(self, query): + key = hash(query) + if key in self._cache: + return self._cache[key] + transformer = RQLToFunctionTransformer(self) + filter_func = transformer.transform(parse(query)) + self._cache[key] = filter_func + return filter_func + + def cast_value(self, prop, value): + value = self.remove_quotes(value) + cast_func = self._filters[prop]['cast_func'] + if isinstance(value, list): + return [cast_func(el) for el in value] + return cast_func(value) + + def filter(self, query, iterable): + filter_func = self.transform_query(query) + return filter(filter_func, iterable) + + def validate_lookup(self, prop, lookup): + if lookup not in self._filters[prop]['lookups']: + raise RQLFilterLookupError( + details={'error': f'Lookup {lookup} not available for filter {prop}.'}, + ) + + @staticmethod + def default_filter_lookups_for_type(filter_type): + lookups = { + FilterTypes.INT: FilterLookups.numeric(), + FilterTypes.DECIMAL: FilterLookups.numeric(), + FilterTypes.FLOAT: FilterLookups.numeric(), + FilterTypes.DATE: FilterLookups.numeric(), + FilterTypes.DATETIME: FilterLookups.numeric(), + FilterTypes.STRING: FilterLookups.string(), + FilterTypes.BOOLEAN: FilterLookups.boolean(), + } + return lookups[filter_type] + + @staticmethod + def remove_quotes(str_value): + return str_value[1:-1] if str_value and str_value[0] in ('"', "'") else str_value diff --git a/py_rql/helpers.py b/py_rql/helpers.py new file mode 100644 index 0000000..e384738 --- /dev/null +++ b/py_rql/helpers.py @@ -0,0 +1,25 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +def extract_value(obj, prop): + current = obj + tokens = prop.split('.') + for t in tokens: + if not isinstance(current, dict) or t not in current: + raise KeyError() + current = current[t] + return current + + +def apply_operator(prop, operator, value, obj): + try: + prop_value = extract_value(obj, prop) + except KeyError: + return False + result = operator(prop_value, value) + return result + + +def apply_logical_operator(operator_func, terms, obj): + evaluated = [term(obj) for term in terms] + return operator_func(evaluated) diff --git a/py_rql/operators.py b/py_rql/operators.py new file mode 100644 index 0000000..c272414 --- /dev/null +++ b/py_rql/operators.py @@ -0,0 +1,66 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +from operator import eq, ge, gt, le, lt, ne # noqa: F401 + +from py_rql.constants import ( + ComparisonOperators, + ListOperators, + LogicalOperators, + SearchOperators, +) + + +def and_op(iterable): + return all(iterable) + + +def or_op(iterable): + return any(iterable) + + +def not_op(a): + return not a[0] + + +def in_op(a, b): + return a in b + + +def out_op(a, b): + return a not in b + + +def like(a, b): + if b[0] == '*' and b[-1] == '*': + return b[1:-1] in a + + if b[0] == '*': + return a.endswith(b[1:]) + + if b[-1] == '*': + return a.startswith(b[:-1]) + return a == b + + +def ilike(a, b): + return like(a.lower(), b.lower()) + + +def get_operator_func_by_operator(op): + mapping = { + ComparisonOperators.EQ: eq, + ComparisonOperators.NE: ne, + ComparisonOperators.GE: ge, + ComparisonOperators.GT: gt, + ComparisonOperators.LE: le, + ComparisonOperators.LT: lt, + ListOperators.IN: in_op, + ListOperators.OUT: out_op, + f'{LogicalOperators.AND}_op': and_op, + f'{LogicalOperators.OR}_op': or_op, + f'{LogicalOperators.NOT}_op': not_op, + SearchOperators.LIKE: like, + SearchOperators.I_LIKE: ilike, + } + return mapping[op] diff --git a/py_rql/parser.py b/py_rql/parser.py index 1c5a33a..0d993e1 100644 --- a/py_rql/parser.py +++ b/py_rql/parser.py @@ -3,7 +3,6 @@ # from cachetools import LFUCache - from lark import Lark from lark.exceptions import LarkError diff --git a/py_rql/transformer.py b/py_rql/transformer.py index acaa0e7..c4e2be7 100644 --- a/py_rql/transformer.py +++ b/py_rql/transformer.py @@ -1,10 +1,16 @@ # # Copyright © 2022 Ingram Micro Inc. All rights reserved. # +import functools from lark import Transformer, Tree -from py_rql.constants import ComparisonOperators, RQL_PLUS +from py_rql.constants import ( + ComparisonOperators, + RQL_PLUS, +) +from py_rql.helpers import apply_logical_operator, apply_operator +from py_rql.operators import get_operator_func_by_operator class BaseRQLTransformer(Transformer): @@ -54,3 +60,50 @@ def expr_term(self, args): def start(self, args): return args[0] + + +class RQLToFunctionTransformer(BaseRQLTransformer): + def __init__(self, filter_cls): + + self.filter_cls = filter_cls + + self.__visit_tokens__ = False + + def logical(self, args): + operation = args[0].data + children = args[0].children + + operator = get_operator_func_by_operator(operation) + + return functools.partial( + apply_logical_operator, operator, children, + ) + + def comp(self, args): + prop, operation, val = self._extract_comparison(args) + return self._get_func_for_lookup(prop, operation, val) + + def listing(self, args): + operation, prop = self._get_value(args[0]), self._get_value(args[1]) + return self._get_func_for_lookup( + prop, + operation, + [self._get_value(vtree) for vtree in args[2:]], + ) + + def searching(self, args): + # like, ilike + operation, prop, val = tuple(self._get_value(args[index]) for index in range(3)) + return self._get_func_for_lookup(prop, operation, val) + + def _get_func_for_lookup(self, prop, operation, val): + self.filter_cls.validate_lookup(prop, operation) + + operator = get_operator_func_by_operator(operation) + + return functools.partial( + apply_operator, + prop, + operator, + self.filter_cls.cast_value(prop, val), + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0e954c1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,82 @@ +[tool.poetry] +name = "lib-rql" +version = "25.0.0" +description = "Python RQL Filtering" +authors = ["CloudBlue LLC"] +license = "Apache-2.0" +packages = [ + { include = "py_rql" } +] +readme = "./README.md" +homepage = "https://connect.cloudblue.com/community/api/rql/" +repository = "https://github.com/cloudblue/lib-rql" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: Unix", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Text Processing :: Filters" +] +keywords = [ + "rql", + "filter", +] + + +[tool.poetry.dependencies] +python = "^3.6" +lark-parser = "0.11.0" +cachetools = ">=4.2.4" +python-dateutil = ">=2.8.2" + +[tool.poetry.dev-dependencies] +ipython = ">=7.10.0" +pytest = "^6.1.2" +pytest-cov = "^2.10.1" +pytest-mock = "^3.3.1" +coverage = {extras = ["toml"], version = "^5.3"} +flake8 = "~3.8" +flake8-bugbear = "~20" +flake8-cognitive-complexity = "^0.1" +flake8-commas = "~2.0" +flake8-future-import = "~0.4" +flake8-import-order = "~0.18" +flake8-broken-line = "~0.3" +Sphinx = "^4.4.0" +sphinx-rtd-theme = "^1.0.0" + + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +testpaths = "tests" +log_cli = true +addopts = "--show-capture=no --junitxml=tests/reports/out.xml --cov=py_rql --cov-report xml:tests/reports/coverage.xml --cov-report html:tests/reports/cov_html" + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +omit = [ + "*/migrations/*", + "*/config/*", + "*/settings/*", + "*/manage.py", + "*/wsgi.py", + "*/urls.py" +] + +exclude_lines = [ + "pragma: no cover", + "def __str__", + "def __repr__", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 8c8113a..0000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -lark-parser==0.11.0 -cachetools>=4.2.4 diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index c19f26c..0000000 --- a/requirements/test.txt +++ /dev/null @@ -1,15 +0,0 @@ -coverage==6.0.2 -flake8==3.9.2 -pytest==6.2.5 -pytest-cov==3.0.0 -pytest-mock==3.6.1 -pytest-deadfixtures==2.2.1 -pytest-randomly==3.10.1 -flake8-bugbear==21.9.2 -flake8-broken-line==0.3.0 -flake8-commas==2.0.0 -flake8-comprehensions==3.7.0 -flake8-debugger==4.0.0 -flake8-eradicate==1.1.0 -flake8-import-order==0.18.1 -flake8-string-format==0.3.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index db01f00..0000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[aliases] -test = pytest - -[flake8] -exclude = .idea,.git,venv*/,.eggs/,*.egg-info -max-line-length = 100 -show-source = True -ignore = W503,W605 - -[tool:pytest] -addopts = --show-capture=no --junitxml=tests/reports/out.xml --cov=py_rql --cov-report xml:tests/reports/coverage.xml diff --git a/setup.py b/setup.py deleted file mode 100644 index 08f58a5..0000000 --- a/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -# -# Copyright © 2021 Ingram Micro Inc. All rights reserved. -# - -from setuptools import find_packages, setup - - -def read_file(name): - with open(name, 'r') as f: - content = f.read().rstrip('\n') - return content - - -setup( - name='lib-rql', - author='CloudBlue', - url='https://connect.cloudblue.com/community/api/rql/', - description='Python RQL Filtering', - long_description=read_file('README.md'), - long_description_content_type='text/markdown', - license='Apache License, Version 2.0', - - python_requires='>=3.6', - zip_safe=True, - packages=find_packages(exclude=('tests',)), - include_package_data=True, - install_requires=read_file('requirements/dev.txt').splitlines(), - tests_require=read_file('requirements/test.txt').splitlines(), - setup_requires=['setuptools_scm', 'pytest-runner', 'wheel'], - use_scm_version=True, - - keywords='rql filter', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: Unix', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Text Processing :: Filters', - ], -) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c1697ca --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest + +from py_rql.filter_cls import FilterClass + + +@pytest.fixture +def filter_factory(): + def _filter(filters): + class TestFilter(FilterClass): + FILTERS = filters + return TestFilter() + return _filter diff --git a/tests/test_constants.py b/tests/test_constants.py index 3647d32..da85168 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -1,11 +1,10 @@ # # Copyright © 2022 Ingram Micro Inc. All rights reserved. # +import pytest from py_rql.constants import FilterLookups -import pytest - @pytest.mark.parametrize('func', ('numeric', 'string', 'boolean')) def test_filter_lookups_non_null(func): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 888283e..83dfac6 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,13 +1,13 @@ # # Copyright © 2022 Ingram Micro Inc. All rights reserved. # +import pytest from py_rql.exceptions import ( - RQLFilterError, RQLFilterLookupError, RQLFilterParsingError, RQLFilterValueError, + RQLFilterError, RQLFilterLookupError, + RQLFilterParsingError, RQLFilterValueError, ) -import pytest - @pytest.mark.parametrize('exception_cls,message', [ (RQLFilterError, 'RQL Filtering error.'), diff --git a/tests/test_filter_cls.py b/tests/test_filter_cls.py new file mode 100644 index 0000000..4e9145b --- /dev/null +++ b/tests/test_filter_cls.py @@ -0,0 +1,90 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest + +from py_rql.constants import FilterLookups, FilterTypes, RESERVED_FILTER_NAMES +from py_rql.exceptions import RQLFilterLookupError +from py_rql.filter_cls import FilterClass + + +@pytest.mark.parametrize('filter_name', RESERVED_FILTER_NAMES) +def test_init_filters_reserved_names(filter_factory, filter_name): + with pytest.raises(AssertionError) as cv: + filter_factory([{'filter': filter_name, 'type': FilterTypes.STRING}]) + assert str(cv.value) == f"'{filter_name}' is a reserved filter name." + + +def test_init_filters_invalid_lookup(filter_factory): + with pytest.raises(AssertionError) as cv: + filter_factory( + [ + { + 'filter': 'prop', + 'type': FilterTypes.INT, + 'lookups': {FilterLookups.LIKE}, + }, + ], + ) + assert str(cv.value) == ( + "Invalid lookups for filter 'prop' of type 'int': '{'like'}'" + ) + + +def test_init_filters_invalid_cast_func(filter_factory): + with pytest.raises(AssertionError) as cv: + filter_factory( + [ + { + 'filter': 'prop', + 'cast_func': 'hello', + }, + ], + ) + assert str(cv.value) == "Invalid cast function for filter 'prop'." + + +def test_validate_lookup(filter_factory): + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + assert flt.validate_lookup('prop', FilterLookups.EQ) is None + + +def test_validate_lookup_invalid_lookup(filter_factory): + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + with pytest.raises(RQLFilterLookupError) as cv: + flt.validate_lookup('prop', FilterLookups.LT) + + assert cv.value.details == {'error': 'Lookup lt not available for filter prop.'} + + +def test_transform_query(filter_factory): + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + fn = flt.transform_query('eq(prop,value)') + assert callable(fn) is True + + +def test_transform_query_cache(filter_factory): + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + flt.transform_query('eq(prop,value)') + key = hash('eq(prop,value)') + assert key in flt._cache + + +def test_transform_query_hit_cache(mocker, filter_factory): + mocked_parse = mocker.patch('py_rql.filter_cls.parse') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + fn = mocker.MagicMock() + key = hash('eq(prop,value)') + flt._cache[key] = fn + assert flt.transform_query('eq(prop,value)') == fn + mocked_parse.assert_not_called() + + +def test_filter(mocker, filter_factory): + fn = mocker.MagicMock() + mocked_transform = mocker.patch.object(FilterClass, 'transform_query', return_value=fn) + mocked_filter = mocker.patch('py_rql.filter_cls.filter') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + flt.filter('query', 'iterable') + mocked_transform.assert_called_once_with('query') + mocked_filter.assert_called_once_with(fn, 'iterable') diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..03e67fe --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,61 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest + +from py_rql.helpers import apply_logical_operator, apply_operator, extract_value + + +@pytest.mark.parametrize( + ('src', 'path', 'expected'), + ( + ({'root': 'val'}, 'root', 'val'), + ({'root': None}, 'root', None), + ({'root': True}, 'root', True), + ({'root': {'level1': 'val'}}, 'root.level1', 'val'), + ({'root': {'level1': None}}, 'root.level1', None), + ({'root': {'level1': True}}, 'root.level1', True), + ({'root': {'level1': True}}, 'root', {'level1': True}), + ({'root': {'level1': {'level2': 'val'}}}, 'root.level1.level2', 'val'), + ({'root': {'level1': {'level2': None}}}, 'root.level1.level2', None), + ({'root': {'level1': {'level2': True}}}, 'root.level1.level2', True), + ({'root': {'level1': {'level2': True}}}, 'root.level1', {'level2': True}), + ({'root': {'level1': {'level2': True}}}, 'root', {'level1': {'level2': True}}), + ), +) +def test_extract_value(src, path, expected): + assert extract_value(src, path) == expected + + +@pytest.mark.parametrize( + ('src', 'path'), + ( + ({'root': 'val'}, 'other'), + ({'root': {'level1': 'val'}}, 'root.other'), + ({'root': {'level1': {'level2': 'val'}}}, 'root.level1.other'), + ), +) +def test_extract_value_keyerror(src, path): + with pytest.raises(KeyError): + extract_value(src, path) + + +def test_apply_operator(mocker): + mocked_op = mocker.MagicMock(return_value=True) + assert apply_operator('root', mocked_op, 'value_b', {'root': 'value_a'}) is True + mocked_op.assert_called_once_with('value_a', 'value_b') + + +def test_apply_operator_prop_not_found(mocker): + mocked_op = mocker.MagicMock(return_value=True) + assert apply_operator('other', mocked_op, 'value_b', {'root': 'value_a'}) is False + mocked_op.assert_not_called() + + +def test_apply_logical_operator(mocker): + mocked_op = mocker.MagicMock(return_value=True) + children = [mocker.MagicMock(return_value=True), mocker.MagicMock(return_value=False)] + assert apply_logical_operator(mocked_op, children, {'root': 'value_a'}) is True + mocked_op.assert_called_once_with([True, False]) + for child in children: + child.assert_called_once_with({'root': 'value_a'}) diff --git a/tests/test_init.py b/tests/test_init.py index 5109ddd..e2956b7 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,10 +1,12 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest from lark import Tree from py_rql import parse from py_rql.exceptions import RQLFilterParsingError -import pytest - def test_parse_ok(): assert isinstance(parse('a=b'), Tree) diff --git a/tests/test_operators.py b/tests/test_operators.py new file mode 100644 index 0000000..7f21559 --- /dev/null +++ b/tests/test_operators.py @@ -0,0 +1,100 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest + +from py_rql import operators +from py_rql.constants import ( + ComparisonOperators, ListOperators, + LogicalOperators, SearchOperators, +) + + +@pytest.mark.parametrize( + ('op', 'iterable', 'expected'), + ( + (operators.and_op, [True, True], True), + (operators.and_op, [True, False], False), + (operators.and_op, [False, False], False), + (operators.or_op, [True, True], True), + (operators.or_op, [True, False], True), + (operators.or_op, [False, False], False), + ), +) +def test_logical(op, iterable, expected): + assert op(iterable) is expected + + +@pytest.mark.parametrize( + ('op', 'iterable', 'expected'), + ( + (operators.not_op, [True], False), + (operators.not_op, [False], True), + ), +) +def test_logical_not(op, iterable, expected): + assert op(iterable) is expected + + +@pytest.mark.parametrize( + ('op', 'a', 'b', 'expected'), + ( + (operators.in_op, 'a', ['a', 'b'], True), + (operators.in_op, 'c', ['a', 'b'], False), + (operators.in_op, 'a', [], False), + (operators.out_op, 'a', ['a', 'b'], False), + (operators.out_op, 'c', ['a', 'b'], True), + (operators.out_op, 'a', [], True), + ), +) +def test_list(op, a, b, expected): + assert op(a, b) is expected + + +@pytest.mark.parametrize( + ('op', 'b', 'a', 'expected'), + ( + (operators.like, 'test', 'test', True), + (operators.like, 'test', 'This is a test', False), + (operators.like, '*test', 'This is a test', True), + (operators.like, '*test*', 'This is a test', True), + (operators.like, 'test*', 'This is a test', False), + (operators.like, 'This*', 'This is a test', True), + (operators.like, 'TEST', 'test', False), + (operators.like, 'TEST', 'This is a test', False), + (operators.like, '*TEST', 'This is a test', False), + (operators.like, '*TEST*', 'This is a test', False), + (operators.like, 'TEST*', 'This is a test', False), + (operators.like, 'THIS*', 'This is a test', False), + (operators.ilike, 'test', 'test', True), + (operators.ilike, 'test', 'This is a test', False), + (operators.ilike, '*test', 'This is a test', True), + (operators.ilike, '*test*', 'This is a test', True), + (operators.ilike, 'test*', 'This is a test', False), + (operators.ilike, 'This*', 'This is a test', True), + (operators.ilike, 'TEST', 'test', True), + (operators.ilike, 'TEST', 'This is a test', False), + (operators.ilike, '*TEST', 'This is a test', True), + (operators.ilike, '*TEST*', 'This is a test', True), + (operators.ilike, 'TEST*', 'This is a test', False), + (operators.ilike, 'THIS*', 'This is a test', True), + ), +) +def test_search(op, a, b, expected): + assert op(a, b) is expected + + +def get_operator_func_by_operator(): + assert ComparisonOperators.EQ == operators.eq + assert ComparisonOperators.NE == operators.ne + assert ComparisonOperators.GE == operators.ge + assert ComparisonOperators.GT == operators.gt + assert ComparisonOperators.LE == operators.le + assert ComparisonOperators.LT == operators.lt + assert ListOperators.IN == operators.in_op + assert ListOperators.OUT == operators.out_op + assert LogicalOperators.AND == operators.and_op + assert LogicalOperators.OR == operators.or_op + assert LogicalOperators.NOT == operators.not_op + assert SearchOperators.LIKE == operators.like + assert SearchOperators.I_LIKE == operators.ilike diff --git a/tests/test_parser/test_caching.py b/tests/test_parser/test_caching.py index 9e5bc58..89cce29 100644 --- a/tests/test_parser/test_caching.py +++ b/tests/test_parser/test_caching.py @@ -1,12 +1,11 @@ # # Copyright © 2022 Ingram Micro Inc. All rights reserved. # +import pytest from py_rql.exceptions import RQLFilterParsingError from py_rql.parser import RQLParser -import pytest - def test_cache(): cache = RQLParser._cache diff --git a/tests/test_parser/test_comparison.py b/tests/test_parser/test_comparison.py index a334c7d..ce2a5f0 100644 --- a/tests/test_parser/test_comparison.py +++ b/tests/test_parser/test_comparison.py @@ -4,13 +4,11 @@ from functools import partial +import pytest from lark.exceptions import LarkError from py_rql.constants import ComparisonOperators as CompOp from py_rql.parser import RQLParser - -import pytest - from tests.test_parser.constants import FAIL_PROPS, FAIL_VALUES, OK_PROPS, OK_VALUES from tests.test_parser.utils import ComparisonTransformer diff --git a/tests/test_parser/test_listing.py b/tests/test_parser/test_listing.py index b606967..fe28d72 100644 --- a/tests/test_parser/test_listing.py +++ b/tests/test_parser/test_listing.py @@ -1,14 +1,11 @@ # # Copyright © 2022 Ingram Micro Inc. All rights reserved. # - +import pytest from lark.exceptions import LarkError from py_rql.constants import ListOperators from py_rql.parser import RQLParser - -import pytest - from tests.test_parser.constants import FAIL_PROPS, LIST_FAIL_VALUES, OK_PROPS, OK_VALUES from tests.test_parser.utils import ListTransformer diff --git a/tests/test_parser/test_logical.py b/tests/test_parser/test_logical.py index 5b16a07..08ef952 100644 --- a/tests/test_parser/test_logical.py +++ b/tests/test_parser/test_logical.py @@ -4,13 +4,11 @@ from functools import partial +import pytest from lark.exceptions import LarkError from py_rql.constants import ComparisonOperators, LogicalOperators from py_rql.parser import RQLParser - -import pytest - from tests.test_parser.utils import LogicalTransformer diff --git a/tests/test_parser/test_ordering.py b/tests/test_parser/test_ordering.py index c43cbcc..afda58b 100644 --- a/tests/test_parser/test_ordering.py +++ b/tests/test_parser/test_ordering.py @@ -1,14 +1,11 @@ # # Copyright © 2022 Ingram Micro Inc. All rights reserved. # - +import pytest from lark.exceptions import LarkError from py_rql.constants import RQL_ORDERING_OPERATOR from py_rql.parser import RQLParser - -import pytest - from tests.test_parser.constants import FAIL_PROPS, OK_PROPS from tests.test_parser.utils import OrderingTransformer diff --git a/tests/test_parser/test_searching.py b/tests/test_parser/test_searching.py index 4827de6..3b316c4 100644 --- a/tests/test_parser/test_searching.py +++ b/tests/test_parser/test_searching.py @@ -1,14 +1,11 @@ # # Copyright © 2022 Ingram Micro Inc. All rights reserved. # - +import pytest from lark.exceptions import LarkError from py_rql.constants import SearchOperators from py_rql.parser import RQLParser - -import pytest - from tests.test_parser.constants import FAIL_PROPS, FAIL_VALUES, OK_PROPS, OK_VALUES from tests.test_parser.utils import SearchTransformer diff --git a/tests/test_transformer/__init__.py b/tests/test_transformer/__init__.py new file mode 100644 index 0000000..5af0bcf --- /dev/null +++ b/tests/test_transformer/__init__.py @@ -0,0 +1,3 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# diff --git a/tests/test_transformer/test_comparison.py b/tests/test_transformer/test_comparison.py new file mode 100644 index 0000000..7564301 --- /dev/null +++ b/tests/test_transformer/test_comparison.py @@ -0,0 +1,166 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest + +from py_rql.cast import get_default_cast_func_for_type +from py_rql.constants import ComparisonOperators, FilterTypes, RQL_EMPTY, RQL_NULL +from py_rql.helpers import apply_operator +from py_rql.operators import get_operator_func_by_operator + + +@pytest.mark.parametrize( + 'value', + ('10', '10.3', '0.00004831666666'), +) +@pytest.mark.parametrize( + 'filter_type', + (FilterTypes.DECIMAL, FilterTypes.FLOAT), +) +@pytest.mark.parametrize( + 'op', + ( + ComparisonOperators.EQ, ComparisonOperators.NE, + ComparisonOperators.GE, ComparisonOperators.GT, + ComparisonOperators.LE, ComparisonOperators.LT, + ), +) +def test_numeric(mocker, filter_factory, filter_type, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': filter_type}]) + query = f'{op}(prop,{value})' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(filter_type) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func(value), + ) + + +@pytest.mark.parametrize( + 'value', + ('10', '-10', '0'), +) +@pytest.mark.parametrize( + 'op', + ( + ComparisonOperators.EQ, ComparisonOperators.NE, + ComparisonOperators.GE, ComparisonOperators.GT, + ComparisonOperators.LE, ComparisonOperators.LT, + ), +) +def test_numeric_int(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.INT}]) + query = f'{op}(prop,{value})' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.INT) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func(value), + ) + + +@pytest.mark.parametrize( + 'filter_type', + ( + FilterTypes.INT, FilterTypes.DECIMAL, FilterTypes.FLOAT, + FilterTypes.DATE, FilterTypes.DATETIME, + ), +) +@pytest.mark.parametrize('op', (ComparisonOperators.EQ, ComparisonOperators.NE)) +def test_numeric_null(mocker, filter_factory, filter_type, op): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': filter_type}]) + query = f'{op}(prop,null())' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(filter_type) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func('null()'), + ) + + +@pytest.mark.parametrize( + 'op', + ( + ComparisonOperators.EQ, ComparisonOperators.NE, + ComparisonOperators.GE, ComparisonOperators.GT, + ComparisonOperators.LE, ComparisonOperators.LT, + ), +) +def test_numeric_date(mocker, filter_factory, op): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.DATE}]) + query = f'{op}(prop,2020-01-01)' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.DATE) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func('2020-01-01'), + ) + + +@pytest.mark.parametrize( + 'op', + ( + ComparisonOperators.EQ, ComparisonOperators.NE, + ComparisonOperators.GE, ComparisonOperators.GT, + ComparisonOperators.LE, ComparisonOperators.LT, + ), +) +def test_numeric_datetime(mocker, filter_factory, op): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.DATETIME}]) + query = f'{op}(prop,2022-02-08T07:57:57+01:00)' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.DATETIME) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func('2022-02-08T07:57:57+01:00'), + ) + + +@pytest.mark.parametrize('value', ('value', RQL_EMPTY, RQL_NULL)) +@pytest.mark.parametrize('op', (ComparisonOperators.EQ, ComparisonOperators.NE)) +def test_string(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + query = f'{op}(prop,{value})' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.STRING) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func(value), + ) + + +@pytest.mark.parametrize( + 'value', + ('true', 'false', 'TRUE', 'FALSE', RQL_NULL), +) +@pytest.mark.parametrize('op', (ComparisonOperators.EQ, ComparisonOperators.NE)) +def test_boolean(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.BOOLEAN}]) + query = f'{op}(prop,{value})' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.BOOLEAN) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func(value), + ) diff --git a/tests/test_transformer/test_list.py b/tests/test_transformer/test_list.py new file mode 100644 index 0000000..9691f81 --- /dev/null +++ b/tests/test_transformer/test_list.py @@ -0,0 +1,96 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest + +from py_rql.cast import get_default_cast_func_for_type +from py_rql.constants import FilterTypes, ListOperators, RQL_EMPTY, RQL_NULL +from py_rql.helpers import apply_operator +from py_rql.operators import get_operator_func_by_operator + + +@pytest.mark.parametrize( + 'value', + ( + ['10', '10.3', '0.00004831666666'], + ['10', '-10.3', RQL_NULL], + ), +) +@pytest.mark.parametrize('filter_type', (FilterTypes.DECIMAL, FilterTypes.FLOAT)) +@pytest.mark.parametrize('op', (ListOperators.IN, ListOperators.OUT)) +def test_numeric(mocker, filter_factory, filter_type, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': filter_type}]) + query = f'{op}(prop,({",".join(value)}))' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(filter_type) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + [cast_func(v) for v in value], + ) + + +@pytest.mark.parametrize('value', (['10', '-103', '0', RQL_NULL],)) +@pytest.mark.parametrize('op', (ListOperators.IN, ListOperators.OUT)) +def test_numeric_int(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.INT}]) + query = f'{op}(prop,({",".join(value)}))' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.INT) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + [cast_func(v) for v in value], + ) + + +@pytest.mark.parametrize('value', (['2020-01-01', '1932-03-31', RQL_NULL],)) +@pytest.mark.parametrize('op', (ListOperators.IN, ListOperators.OUT)) +def test_numeric_date(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.DATE}]) + query = f'{op}(prop,({",".join(value)}))' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.DATE) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + [cast_func(v) for v in value], + ) + + +@pytest.mark.parametrize('value', (['2022-02-08T07:57:57+01:00', '2022-02-08T07:57:57', RQL_NULL],)) +@pytest.mark.parametrize('op', (ListOperators.IN, ListOperators.OUT)) +def test_numeric_datetime(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.DATETIME}]) + query = f'{op}(prop,({",".join(value)}))' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.DATETIME) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + [cast_func(v) for v in value], + ) + + +@pytest.mark.parametrize('value', (['value', RQL_EMPTY, RQL_NULL],)) +@pytest.mark.parametrize('op', (ListOperators.IN, ListOperators.OUT)) +def test_string(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + query = f'{op}(prop,({",".join(value)}))' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.STRING) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + [cast_func(v) for v in value], + ) diff --git a/tests/test_transformer/test_logical.py b/tests/test_transformer/test_logical.py new file mode 100644 index 0000000..ba8e908 --- /dev/null +++ b/tests/test_transformer/test_logical.py @@ -0,0 +1,27 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +from unittest.mock import call + +import pytest + +from py_rql.constants import FilterTypes, LogicalOperators +from py_rql.helpers import apply_logical_operator +from py_rql.operators import get_operator_func_by_operator + + +@pytest.mark.parametrize('op', (LogicalOperators.AND, LogicalOperators.OR)) +def test_logical_operators(mocker, filter_factory, op): + functools = mocker.patch('py_rql.transformer.functools') + children = [mocker.MagicMock(), mocker.MagicMock()] + functools.partial.side_effect = children + [mocker.MagicMock()] + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + term1 = 'eq(prop,val1)' + term2 = 'like(prop,*val2*)' + query = f'{op}({term1},{term2})' + flt.filter(query, []) + assert functools.partial.mock_calls[-1] == call( + apply_logical_operator, + get_operator_func_by_operator(f'{op}_op'), + children, + ) diff --git a/tests/test_transformer/test_search.py b/tests/test_transformer/test_search.py new file mode 100644 index 0000000..22fea85 --- /dev/null +++ b/tests/test_transformer/test_search.py @@ -0,0 +1,25 @@ +# +# Copyright © 2022 Ingram Micro Inc. All rights reserved. +# +import pytest + +from py_rql.cast import get_default_cast_func_for_type +from py_rql.constants import FilterTypes, SearchOperators +from py_rql.helpers import apply_operator +from py_rql.operators import get_operator_func_by_operator + + +@pytest.mark.parametrize('value', ('value', '*value', 'value*', '*value*')) +@pytest.mark.parametrize('op', (SearchOperators.LIKE, SearchOperators.I_LIKE)) +def test_string(mocker, filter_factory, op, value): + functools = mocker.patch('py_rql.transformer.functools') + flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.STRING}]) + query = f'{op}(prop,{value})' + flt.filter(query, []) + cast_func = get_default_cast_func_for_type(FilterTypes.STRING) + functools.partial.assert_called_once_with( + apply_operator, + 'prop', + get_operator_func_by_operator(op), + cast_func(value), + ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..51a7b43 --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[flake8] +exclude = .idea,.git,venv*/,.eggs/,*.egg-info +max-line-length = 100 +show-source = True +ignore = FI1,W503,W605 +import-order-style = smarkets +application-import-names = py_rql, tests +max-cognitive-complexity = 15