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