diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d41e093..effc7203 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 name: Setup Python ${{ matrix.python-version }} with: @@ -19,10 +19,8 @@ jobs: run: | sudo apt-get update sudo apt-get install -y binutils gdal-bin libgdal-dev libproj-dev libsqlite3-mod-spatialite spatialite-bin - pip3 install -r requirements-test.txt - pip3 install -r requirements-dev.txt pip3 install gdal=="`gdal-config --version`.*" - pip3 install . + pip3 install ".[test,dev,fes,backend-django,backend-sqlalchemy,backend-native,backend-elasticsearch,backend-opensearch]" - name: Configure sysctl limits run: | sudo swapoff -a @@ -53,7 +51,7 @@ jobs: - name: Run unit tests run: | pytest - # - name: run pre-commit (code formatting, lint and type checking) - # run: | - # python -m pip3 install pre-commit - # pre-commit run --all-files + - name: Run pre-commit (code formatting, lint and type checking) + run: | + pip3 install pre-commit + pre-commit run --all-files diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 54402da9..f459b28a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,15 +10,15 @@ jobs: publish: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 name: Setup Python with: python-version: '3.11' - name: Install build dependency - run: pip3 install wheel setuptools + run: pip3 install build - name: Build package - run: python3 setup.py sdist bdist_wheel --universal + run: python3 -m build - name: Publish package if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e70fe076..716ce04f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,13 @@ repos: - - repo: https://github.com/psf/black - rev: 25.1.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.13 hooks: - - id: black - language_version: python - - - repo: https://github.com/PyCQA/isort - rev: 6.0.1 - hooks: - - id: isort - language_version: python - - - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 - hooks: - - id: flake8 - language_version: python + - id: ruff + args: [--fix] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v2.1.0 hooks: - id: mypy language_version: python diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 48ea42ce..51fd50c5 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -2,15 +2,19 @@ # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# Required version: 2 -# Build documentation in the docs/ directory with Sphinx +build: + os: ubuntu-22.04 + tools: + python: "3.12" + sphinx: - configuration: docs/conf.py + configuration: docs/conf.py -# Optionally set requirements required to build your docs python: - install: - - requirements: docs/requirements.txt - - requirements: requirements-test.txt + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 7007f9c7..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -recursive-include pygeofilter *.py *.lark -global-include *.lark -include README.md -include LICENSE -include requirements.txt \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 5ba66df5..4d69fa24 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ sys.path.insert(0, os.path.abspath("..")) +from pygeofilter.version import __version__ # noqa: E402 # -- Project information ----------------------------------------------------- @@ -25,9 +26,9 @@ author = "Fabian Schindler" # The short X.Y version -version = "" +version = __version__ # The full version, including alpha/beta/rc tags -release = "0.0.3" +release = __version__ # -- General configuration --------------------------------------------------- @@ -144,7 +145,9 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "pygeofilter", "pygeofilter Documentation", [author], 1)] +man_pages = [ + (master_doc, "pygeofilter", "pygeofilter Documentation", [author], 1) +] # -- Options for Texinfo output ---------------------------------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index d6f12f30..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinxcontrib-apidoc -m2r2 diff --git a/examples/cql2.ipynb b/examples/cql2.ipynb index d13341be..d35f22ac 100644 --- a/examples/cql2.ipynb +++ b/examples/cql2.ipynb @@ -7,12 +7,10 @@ "metadata": {}, "outputs": [], "source": [ - "from pygeofilter.parsers.cql2_json import parse\n", - "from pygeofilter.backends.cql2_json import to_cql2\n", "import json\n", "import traceback\n", - "from lark import lark, logger, v_args\n", - "from pygeofilter.cql2 import BINARY_OP_PREDICATES_MAP\n" + "\n", + "from pygeofilter.backends.cql2_json import to_cql2" ] }, { @@ -34,7 +32,10 @@ ], "source": [ "from pygeofilter.parsers.cql2_text import parse as cql2_parse\n", - "cql2_parse(\"collection = 'landsat8_l1tp' AND gsd <= 30 AND eo:cloud_cover <= 10 AND datetime >= TIMESTAMP('2021-04-08T04:39:23Z')\")" + "\n", + "cql2_parse(\n", + " \"collection = 'landsat8_l1tp' AND gsd <= 30 AND eo:cloud_cover <= 10 AND datetime >= TIMESTAMP('2021-04-08T04:39:23Z')\"\n", + ")" ] }, { @@ -99,42 +100,44 @@ } ], "source": [ - "from pygeofilter.parsers.cql2_text import parse as text_parse\n", - "from pygeofilter.parsers.cql2_json import parse as json_parse\n", - "from pygeofilter.backends.cql2_json import to_cql2\n", "import orjson\n", - "import json\n", - "import pprint\n", + "\n", + "from pygeofilter.parsers.cql2_json import parse as json_parse\n", + "from pygeofilter.parsers.cql2_text import parse as text_parse\n", + "\n", + "\n", "def pp(j):\n", " print(orjson.dumps(j))\n", - "with open('tests/parsers/cql2_json/fixtures.json') as f:\n", + "\n", + "\n", + "with open(\"tests/parsers/cql2_json/fixtures.json\") as f:\n", " examples = json.load(f)\n", "\n", "for k, v in examples.items():\n", " parsed_text = None\n", " parsed_json = None\n", - " print (k)\n", - " t=v['text'].replace('filter=','')\n", - " j=v['json']\n", + " print(k)\n", + " t = v[\"text\"].replace(\"filter=\", \"\")\n", + " j = v[\"json\"]\n", " # print('\\t' + t)\n", " # pp(orjson.loads(j))\n", " # print('*****')\n", " try:\n", - " parsed_text=text_parse(t)\n", - " parsed_json=json_parse(j)\n", + " parsed_text = text_parse(t)\n", + " parsed_json = json_parse(j)\n", " if parsed_text == parsed_json:\n", - " print('*******parsed trees match***************')\n", + " print(\"*******parsed trees match***************\")\n", " else:\n", " print(parsed_text)\n", - " print('-----')\n", + " print(\"-----\")\n", " print(parsed_json)\n", " if parsed_json is None or parsed_text is None:\n", " raise Exception\n", " if to_cql2(parsed_text) == to_cql2(parsed_json):\n", - " print('*******reconstructed json matches*******')\n", + " print(\"*******reconstructed json matches*******\")\n", " else:\n", " pp(to_cql2(parsed_text))\n", - " print('-----')\n", + " print(\"-----\")\n", " pp(to_cql2(parsed_json))\n", " except Exception as e:\n", " print(parsed_text)\n", @@ -142,8 +145,7 @@ " print(j)\n", " traceback.print_exc(f\"Error: {e}\")\n", " pass\n", - " print('____________________________________________________________')\n", - " " + " print(\"____________________________________________________________\")" ] }, { diff --git a/examples/test-solr-queries.py b/examples/test-solr-queries.py index 2732a6d8..1f97a63f 100644 --- a/examples/test-solr-queries.py +++ b/examples/test-solr-queries.py @@ -30,269 +30,268 @@ from pygeofilter.backends.solr import to_filter from pygeofilter.parsers.ecql import parse - # AND -print('Testing AND') +print("Testing AND") ast = parse("title = 'test' AND description = 'test2'") -print('AST AND: ', ast) +print("AST AND: ", ast) solr_filter = to_filter(ast) -print('SOLR filter AND: ', solr_filter) -print('\n') +print("SOLR filter AND: ", solr_filter) +print("\n") # OR -print('Testing OR') +print("Testing OR") ast = parse("title = 'test' OR description = 'test2'") -print('AST OR: ', ast) +print("AST OR: ", ast) solr_filter = to_filter(ast) -print('SOLR filter OR: ', solr_filter) -print('\n') +print("SOLR filter OR: ", solr_filter) +print("\n") # = -print('Testing Equals =') +print("Testing Equals =") ast = parse("int_attribute = 5") -print('AST =: ', ast) +print("AST =: ", ast) solr_filter = to_filter(ast) -print('SOLR filter =: ', solr_filter) -print('\n') +print("SOLR filter =: ", solr_filter) +print("\n") # <> -print('Testing NOT EQUAL <>') +print("Testing NOT EQUAL <>") ast = parse("int_attribute <> 0.0") -print('AST <>: ', ast) +print("AST <>: ", ast) solr_filter = to_filter(ast) -print('SOLR filter <>: ', solr_filter) -print('\n') +print("SOLR filter <>: ", solr_filter) +print("\n") # < -print('Testing LessThan <') +print("Testing LessThan <") ast = parse("float_attribute < 6") -print('AST <: ', ast) +print("AST <: ", ast) solr_filter = to_filter(ast) -print('SOLR filter <: ', solr_filter) -print('\n') +print("SOLR filter <: ", solr_filter) +print("\n") # > -print('Testing GraterThan >') +print("Testing GraterThan >") ast = parse("float_attribute > 6") -print('AST >: ', ast) +print("AST >: ", ast) solr_filter = to_filter(ast) -print('SOLR filter >: ', solr_filter) -print('\n') +print("SOLR filter >: ", solr_filter) +print("\n") # <= -print('Testing LessEqual <=') +print("Testing LessEqual <=") ast = parse("int_attribute <= 6") -print('AST <=: ', ast) +print("AST <=: ", ast) solr_filter = to_filter(ast) -print('SOLR filter <=: ', solr_filter) -print('\n') +print("SOLR filter <=: ", solr_filter) +print("\n") # >= -print('Testing LessEqual >=') +print("Testing LessEqual >=") ast = parse("float_attribute >= 8") -print('AST >=: ', ast) +print("AST >=: ", ast) solr_filter = to_filter(ast) -print('SOLR filter >=: ', solr_filter) -print('\n') +print("SOLR filter >=: ", solr_filter) +print("\n") # Combination AND -print('Testing Combination AND') +print("Testing Combination AND") ast = parse("int_attribute = 5 AND float_attribute < 6.0") -print('AST Combination AND: ', ast) +print("AST Combination AND: ", ast) solr_filter = to_filter(ast) -print('SOLR filter Combination AND: ', solr_filter) -print('\n') +print("SOLR filter Combination AND: ", solr_filter) +print("\n") # Combination OR -print('Testing Combination OR') +print("Testing Combination OR") ast = parse("int_attribute = 6 OR float_attribute < 6.0") -print('AST Combination OR: ', ast) +print("AST Combination OR: ", ast) solr_filter = to_filter(ast) -print('SOLR filter Combination OR: ', solr_filter) -print('\n') +print("SOLR filter Combination OR: ", solr_filter) +print("\n") # Between -print('Testing BETWEEN') +print("Testing BETWEEN") ast = parse("float_attribute BETWEEN -1 AND 1") -print('AST BETWEEN: ', ast) +print("AST BETWEEN: ", ast) solr_filter = to_filter(ast) -print('SOLR filter BETWEEN: ', solr_filter) -print('\n') +print("SOLR filter BETWEEN: ", solr_filter) +print("\n") # NOT Between -print('Testing NOT BETWEEN') +print("Testing NOT BETWEEN") ast = parse("int_attribute NOT BETWEEN 4 AND 6") -print('AST NOT BETWEEN: ', ast) +print("AST NOT BETWEEN: ", ast) solr_filter = to_filter(ast) -print('SOLR filter NOT BETWEEN: ', solr_filter) -print('\n') +print("SOLR filter NOT BETWEEN: ", solr_filter) +print("\n") # NOT Between -print('Testing NOT BETWEEN') +print("Testing NOT BETWEEN") ast = parse("int_attribute NOT BETWEEN 4 AND 6") -print('AST NOT BETWEEN: ', ast) +print("AST NOT BETWEEN: ", ast) solr_filter = to_filter(ast) -print('SOLR filter NOT BETWEEN: ', solr_filter) -print('\n') +print("SOLR filter NOT BETWEEN: ", solr_filter) +print("\n") # IS_NULL -print('Testing IS_NULL') +print("Testing IS_NULL") ast = parse("maybe_str_attribute IS NULL") -print('AST IS_NULL: ', ast) +print("AST IS_NULL: ", ast) solr_filter = to_filter(ast) -print('SOLR filter IS_NULL: ', solr_filter) -print('\n') +print("SOLR filter IS_NULL: ", solr_filter) +print("\n") # IS_NOT_NULL -print('Testing IS_NOT_NULL') +print("Testing IS_NOT_NULL") ast = parse("maybe_str_attribute IS NOT NULL") -print('AST IS_NOT_NULL: ', ast) +print("AST IS_NOT_NULL: ", ast) solr_filter = to_filter(ast) -print('SOLR filter IS_NOT_NULL: ', solr_filter) -print('\n') +print("SOLR filter IS_NOT_NULL: ", solr_filter) +print("\n") # IS_IN -print('Testing IN') +print("Testing IN") ast = parse("int_attribute IN ( 1, 2, 3, 4, 5 )") -print('AST IN: ', ast) +print("AST IN: ", ast) solr_filter = to_filter(ast) -print('SOLR filter IN: ', solr_filter) -print('\n') +print("SOLR filter IN: ", solr_filter) +print("\n") # IS_NOT_IN -print('Testing NOT IN') +print("Testing NOT IN") ast = parse("int_attribute NOT IN ( 1, 2, 3, 4, 5 )") -print('AST NOT IN: ', ast) +print("AST NOT IN: ", ast) solr_filter = to_filter(ast) -print('SOLR filter NOT IN: ', solr_filter) -print('\n') +print("SOLR filter NOT IN: ", solr_filter) +print("\n") # LIKE -print('Testing LIKE') +print("Testing LIKE") ast = parse("str_attribute LIKE 'this is a test'") -print('AST LIKE: ', ast) +print("AST LIKE: ", ast) solr_filter = to_filter(ast) -print('SOLR filter LIKE: ', solr_filter) -print('\n') +print("SOLR filter LIKE: ", solr_filter) +print("\n") # LIKE % -print('Testing LIKE %') +print("Testing LIKE %") ast = parse("str_attribute LIKE 'this is % test'") -print('AST LIKE %: ', ast) +print("AST LIKE %: ", ast) solr_filter = to_filter(ast) -print('SOLR filter LIKE %: ', solr_filter) -print('\n') +print("SOLR filter LIKE %: ", solr_filter) +print("\n") # NOT LIKE % -print('Testing NOT LIKE %') +print("Testing NOT LIKE %") ast = parse("str_attribute NOT LIKE '% another test'") -print('AST NOT LIKE %: ', ast) +print("AST NOT LIKE %: ", ast) solr_filter = to_filter(ast) -print('SOLR filter NOT LIKE %: ', solr_filter) -print('\n') +print("SOLR filter NOT LIKE %: ", solr_filter) +print("\n") # NOT LIKE . -print('Testing NOT LIKE .') +print("Testing NOT LIKE .") ast = parse("str_attribute NOT LIKE 'this is . test'") -print('AST NOT LIKE .: ', ast) +print("AST NOT LIKE .: ", ast) solr_filter = to_filter(ast) -print('SOLR filter NOT LIKE .: ', solr_filter) -print('\n') +print("SOLR filter NOT LIKE .: ", solr_filter) +print("\n") # ILIKE . -print('Testing ILIKE .') +print("Testing ILIKE .") ast = parse("str_attribute ILIKE 'THIS IS . TEST'") -print('AST ILIKE .: ', ast) +print("AST ILIKE .: ", ast) solr_filter = to_filter(ast) -print('SOLR filter ILIKE .: ', solr_filter) -print('\n') +print("SOLR filter ILIKE .: ", solr_filter) +print("\n") # ILIKE % -print('Testing ILIKE %') +print("Testing ILIKE %") ast = parse("str_attribute ILIKE 'THIS IS % TEST'") -print('AST ILIKE %: ', ast) +print("AST ILIKE %: ", ast) solr_filter = to_filter(ast) -print('SOLR filter ILIKE %: ', solr_filter) -print('\n') +print("SOLR filter ILIKE %: ", solr_filter) +print("\n") # EXISTS -print('Testing EXISTS') +print("Testing EXISTS") ast = parse("extra_attr EXISTS") -print('AST EXISTS: ', ast) +print("AST EXISTS: ", ast) solr_filter = to_filter(ast) -print('SOLR filter EXISTS: ', solr_filter) -print('\n') +print("SOLR filter EXISTS: ", solr_filter) +print("\n") # DOES-NOT-EXIST -print('Testing DOES-NOT-EXIST') +print("Testing DOES-NOT-EXIST") ast = parse("extra_attr DOES-NOT-EXIST") -print('AST DOES-NOT-EXIST: ', ast) +print("AST DOES-NOT-EXIST: ", ast) solr_filter = to_filter(ast) -print('SOLR filter DOES-NOT-EXIST: ', solr_filter) -print('\n') +print("SOLR filter DOES-NOT-EXIST: ", solr_filter) +print("\n") # Testing temporal BEFORE -print('Testing datetime attribute BEFORE') +print("Testing datetime attribute BEFORE") ast = parse("datetime_attribute BEFORE 2000-01-01T00:00:05.00Z") -print('AST BEFORE:', ast) +print("AST BEFORE:", ast) solr_filter = to_filter(ast) -print('datetime attribute BEFORE: ', solr_filter) -print('\n') +print("datetime attribute BEFORE: ", solr_filter) +print("\n") # Testing temporal AFTER -print('Testing datetime attribute AFTER') +print("Testing datetime attribute AFTER") ast = parse("datetime_attribute AFTER 2000-01-01T00:00:05.00Z") -print('AST AFTER:', ast) +print("AST AFTER:", ast) solr_filter = to_filter(ast) -print('datetime attribute AFTER: ', solr_filter) -print('\n') +print("datetime attribute AFTER: ", solr_filter) +print("\n") # Testing temporal AFTER # print('Testing datetime attribute DISJOINT') @@ -311,59 +310,59 @@ # Test spatial Intersects -print('Testing Spatial Intersects') +print("Testing Spatial Intersects") ast = parse("INTERSECTS(geometry, ENVELOPE (0.0 1.0 0.0 1.0))") -print('AST Spatial Intersects:', ast) +print("AST Spatial Intersects:", ast) solr_filter = to_filter(ast) -print('Spatial Intersects: ', solr_filter) -print('\n') +print("Spatial Intersects: ", solr_filter) +print("\n") # Test spatial Disjoint -print('Testing Spatial Disjoint') +print("Testing Spatial Disjoint") ast = parse("DISJOINT(geometry, ENVELOPE (0.0 1.0 0.0 1.0))") -print('AST Spatial Disjoint:', ast) +print("AST Spatial Disjoint:", ast) solr_filter = to_filter(ast) -print('Spatial Disjoint: ', solr_filter) -print('\n') +print("Spatial Disjoint: ", solr_filter) +print("\n") # Test spatial Within -print('Testing Spatial Within') +print("Testing Spatial Within") ast = parse("WITHIN(geometry, ENVELOPE (0.0 1.0 0.0 1.0))") -print('AST Spatial Within:', ast) +print("AST Spatial Within:", ast) solr_filter = to_filter(ast) -print('Spatial Within: ', solr_filter) -print('\n') +print("Spatial Within: ", solr_filter) +print("\n") # Test spatial Contains -print('Testing Spatial Contains') +print("Testing Spatial Contains") ast = parse("CONTAINS(geometry, ENVELOPE (0.0 1.0 0.0 1.0))") -print('AST Spatial Contains:', ast) +print("AST Spatial Contains:", ast) solr_filter = to_filter(ast) -print('Spatial Contains: ', solr_filter) -print('\n') +print("Spatial Contains: ", solr_filter) +print("\n") # Test spatial Equals -print('Testing Spatial Equals') +print("Testing Spatial Equals") ast = parse("EQUALS(geometry, ENVELOPE (0.0 1.0 0.0 1.0))") -print('AST Spatial Equals:', ast) +print("AST Spatial Equals:", ast) solr_filter = to_filter(ast) -print('Spatial Equals: ', solr_filter) -print('\n') +print("Spatial Equals: ", solr_filter) +print("\n") # Test spatial BBOX -print('Testing Spatial BBOX') +print("Testing Spatial BBOX") ast = parse("BBOX(center, 2, 2, 3, 3)") -print('AST Spatial BBOX:', ast) +print("AST Spatial BBOX:", ast) solr_filter = to_filter(ast) -print('Spatial BBOX: ', solr_filter) -print('\n') +print("Spatial BBOX: ", solr_filter) +print("\n") diff --git a/pygeofilter/ast.py b/pygeofilter/ast.py index 5f65b79a..8f8e0310 100644 --- a/pygeofilter/ast.py +++ b/pygeofilter/ast.py @@ -225,7 +225,7 @@ class Like(Predicate): """Node class to represent a wildcard sting matching predicate.""" lhs: Node - pattern: ScalarAstType + pattern: str nocase: bool wildcard: str singlechar: str @@ -704,7 +704,9 @@ def get_repr(node: Node, indent_amount: int = 0, indent_incr: int = 4) -> str: args.append( "(\n{}\n)".format( indent( - get_repr(sub_node, indent_amount + indent_incr, indent_incr), + get_repr( + sub_node, indent_amount + indent_incr, indent_incr + ), indent_amount + indent_incr, ) ) diff --git a/pygeofilter/backends/django/evaluate.py b/pygeofilter/backends/django/evaluate.py index 34986413..c8de7ef3 100644 --- a/pygeofilter/backends/django/evaluate.py +++ b/pygeofilter/backends/django/evaluate.py @@ -116,7 +116,9 @@ def spatial_distance(self, node, lhs, rhs): @handle(ast.BBox) def bbox(self, node, lhs): - return filters.bbox(lhs, node.minx, node.miny, node.maxx, node.maxy, node.crs) + return filters.bbox( + lhs, node.minx, node.miny, node.maxx, node.maxy, node.crs + ) @handle(ast.Attribute) def attribute(self, node): diff --git a/pygeofilter/backends/django/filters.py b/pygeofilter/backends/django/filters.py index 6170d2d3..a204218f 100644 --- a/pygeofilter/backends/django/filters.py +++ b/pygeofilter/backends/django/filters.py @@ -55,7 +55,14 @@ def negate(sub_filter: Q) -> Q: return ~sub_filter -OP_TO_COMP = {"<": "lt", "<=": "lte", ">": "gt", ">=": "gte", "<>": None, "=": "exact"} +OP_TO_COMP = { + "<": "lt", + "<=": "lte", + ">": "gt", + ">=": "gte", + "<>": None, + "=": "exact", +} INVERT_COMP: Dict[Optional[str], str] = { "lt": "gt", @@ -171,7 +178,8 @@ def like( # special case when choices are given for the field: # compare statically and use 'in' operator to check if contained cmp_av = [ - (a, a.lower() if nocase else a) for a in mapping_choices[lhs.name].keys() + (a, a.lower() if nocase else a) + for a in mapping_choices[lhs.name].keys() ] for idx, part in enumerate(parts): @@ -190,7 +198,11 @@ def like( cmp_av = [a for a in cmp_av if cmp_p in a[1]] q = Q( - **{"%s__in" % lhs.name: [mapping_choices[lhs.name][a[0]] for a in cmp_av]} + **{ + "%s__in" % lhs.name: [ + mapping_choices[lhs.name][a[0]] for a in cmp_av + ] + } ) else: @@ -288,7 +300,13 @@ def temporal(lhs: F, time_or_period: Value, op: str) -> Q: :return: a comparison expression object :rtype: :class:`django.db.models.Q` """ - assert op in ("BEFORE", "BEFORE OR DURING", "DURING", "DURING OR AFTER", "AFTER") + assert op in ( + "BEFORE", + "BEFORE OR DURING", + "DURING", + "DURING OR AFTER", + "AFTER", + ) time_or_period = time_or_period.value low: Union[datetime, timedelta, None] = None high: Union[datetime, timedelta, None] = None @@ -345,7 +363,12 @@ def time_interval( ) elif high == low: - return Q(**{begin_time_field + "__gte": value, end_time_field + "__lte": value}) + return Q( + **{ + begin_time_field + "__gte": value, + end_time_field + "__lte": value, + } + ) else: q = Q() @@ -431,8 +454,9 @@ def spatial( return Q(**{"%s__%s" % (lhs.name, op.lower()): rhs}) -def spatial_relate(lhs: Union[F, Value], rhs: Union[F, Value], pattern: str) -> Q: - +def spatial_relate( + lhs: Union[F, Value], rhs: Union[F, Value], pattern: str +) -> Q: if not isinstance(lhs, F): # TODO: cannot yet invert pattern -> raise raise ValueError(f"Unable to compare non-field {lhs}") @@ -441,7 +465,11 @@ def spatial_relate(lhs: Union[F, Value], rhs: Union[F, Value], pattern: str) -> def spatial_distance( - lhs: Union[F, Value], rhs: Union[F, Value], op: str, distance: float, units: str + lhs: Union[F, Value], + rhs: Union[F, Value], + op: str, + distance: float, + units: str, ) -> Q: if not isinstance(lhs, F): lhs, rhs = rhs, lhs @@ -516,7 +544,9 @@ def literal(value) -> Value: OP_TO_FUNC = {"+": add, "-": sub, "*": mul, "/": truediv} -def arithmetic(lhs: ArithmeticType, rhs: ArithmeticType, op: str) -> ArithmeticType: +def arithmetic( + lhs: ArithmeticType, rhs: ArithmeticType, op: str +) -> ArithmeticType: """Create an arithmetic filter :param lhs: left hand side of the arithmetic expression. either a diff --git a/pygeofilter/backends/elasticsearch/evaluate.py b/pygeofilter/backends/elasticsearch/evaluate.py index 2ae26e61..5ea4aa7a 100644 --- a/pygeofilter/backends/elasticsearch/evaluate.py +++ b/pygeofilter/backends/elasticsearch/evaluate.py @@ -219,7 +219,9 @@ def temporal(self, node: ast.TemporalPredicate, lhs, rhs): ast.GeometryWithin, ast.GeometryContains, ) - def spatial_comparison(self, node: ast.SpatialComparisonPredicate, lhs: str, rhs): + def spatial_comparison( + self, node: ast.SpatialComparisonPredicate, lhs: str, rhs + ): """Creates a geo_shape query for the give spatial comparison predicate. """ @@ -243,7 +245,9 @@ def bbox(self, node: ast.BBox, lhs): **{ lhs: { "shape": self.envelope( - values.Envelope(node.minx, node.maxx, node.miny, node.maxy) + values.Envelope( + node.minx, node.maxx, node.miny, node.maxy + ) ), "relation": "intersects", }, diff --git a/pygeofilter/backends/evaluator.py b/pygeofilter/backends/evaluator.py index d0f79706..ca95e7ea 100644 --- a/pygeofilter/backends/evaluator.py +++ b/pygeofilter/backends/evaluator.py @@ -100,7 +100,9 @@ def evaluate(self, node: ast.AstType, adopt_result: bool = True) -> Any: subnodes = cast(ast.Node, node).get_sub_nodes() if subnodes: if isinstance(subnodes, list): - sub_args = [self.evaluate(sub_node, False) for sub_node in subnodes] + sub_args = [ + self.evaluate(sub_node, False) for sub_node in subnodes + ] else: sub_args = [self.evaluate(subnodes, False)] @@ -119,7 +121,9 @@ def adopt(self, node, *sub_args): """Interface function for a last resort when trying to evaluate a node and no handler was found. """ - raise NotImplementedError(f"Failed to evaluate node of type {type(node)}") + raise NotImplementedError( + f"Failed to evaluate node of type {type(node)}" + ) def adopt_result(self, result: Any) -> Any: """Interface function for adopting the final evaluation result diff --git a/pygeofilter/backends/geopandas/evaluate.py b/pygeofilter/backends/geopandas/evaluate.py index 98b33ab7..17b236a4 100644 --- a/pygeofilter/backends/geopandas/evaluate.py +++ b/pygeofilter/backends/geopandas/evaluate.py @@ -107,7 +107,9 @@ def spatial_operation(self, node, lhs, rhs): @handle(ast.BBox) def bbox(self, node, lhs): - return filters.bbox(lhs, node.minx, node.miny, node.maxx, node.maxy, node.crs) + return filters.bbox( + lhs, node.minx, node.miny, node.maxx, node.maxy, node.crs + ) @handle(ast.Attribute) def attribute(self, node): diff --git a/pygeofilter/backends/geopandas/filters.py b/pygeofilter/backends/geopandas/filters.py index 23feafdd..2fe4f71b 100644 --- a/pygeofilter/backends/geopandas/filters.py +++ b/pygeofilter/backends/geopandas/filters.py @@ -10,7 +10,9 @@ def combine(sub_filters, combinator: str): """Combine filters using a logical combinator""" assert combinator in ("AND", "OR") op = and_ if combinator == "AND" else or_ - return reduce(lambda acc, q: op(acc, q) if acc is not None else q, sub_filters) + return reduce( + lambda acc, q: op(acc, q) if acc is not None else q, sub_filters + ) def negate(sub_filter): diff --git a/pygeofilter/backends/native/evaluate.py b/pygeofilter/backends/native/evaluate.py index ed5ee499..f8651269 100644 --- a/pygeofilter/backends/native/evaluate.py +++ b/pygeofilter/backends/native/evaluate.py @@ -148,7 +148,11 @@ def between(self, node, lhs, low, high): def like(self, node, lhs): maybe_not_inv = "" if node.not_ else "not " regex = like_pattern_to_re( - node.pattern, node.nocase, node.wildcard, node.singlechar, node.escapechar + node.pattern, + node.nocase, + node.wildcard, + node.singlechar, + node.escapechar, ) key = self._add_local(regex) return f"({key}.match({lhs}) is {maybe_not_inv}None)" @@ -192,11 +196,16 @@ def array(self, node, lhs, rhs): @handle(ast.SpatialComparisonPredicate, subclasses=True) def spatial_operation(self, node, lhs, rhs): - return f"(getattr(ensure_spatial({lhs}), " f"{node.op.value.lower()!r})({rhs}))" + return ( + f"(getattr(ensure_spatial({lhs}), " + f"{node.op.value.lower()!r})({rhs}))" + ) @handle(ast.Relate) def spatial_pattern(self, node, lhs, rhs): - return f"(ensure_spatial({lhs}).relate_pattern({rhs}, {node.pattern!r}))" + return ( + f"(ensure_spatial({lhs}).relate_pattern({rhs}, {node.pattern!r}))" + ) @handle(ast.BBox) def bbox(self, node, lhs): @@ -246,7 +255,9 @@ def geometry(self, node): @handle(values.Envelope) def envelope(self, node): key = self._add_local( - shapely.geometry.Polygon.from_bounds(node.x1, node.y1, node.x2, node.y2) + shapely.geometry.Polygon.from_bounds( + node.x1, node.y1, node.x2, node.y2 + ) ) return key @@ -264,7 +275,8 @@ def adopt_result(self, result): } if not set(globals_).isdisjoint(set(self.function_map)): raise ValueError( - f"globals collision {list(globals_)} and " f"{list(self.function_map)}" + f"globals collision {list(globals_)} and " + f"{list(self.function_map)}" ) globals_.update(self.function_map) diff --git a/pygeofilter/backends/opensearch/evaluate.py b/pygeofilter/backends/opensearch/evaluate.py index 40e0e462..d4bd9257 100644 --- a/pygeofilter/backends/opensearch/evaluate.py +++ b/pygeofilter/backends/opensearch/evaluate.py @@ -219,7 +219,9 @@ def temporal(self, node: ast.TemporalPredicate, lhs, rhs): ast.GeometryWithin, ast.GeometryContains, ) - def spatial_comparison(self, node: ast.SpatialComparisonPredicate, lhs: str, rhs): + def spatial_comparison( + self, node: ast.SpatialComparisonPredicate, lhs: str, rhs + ): """Creates a geo_shape query for the give spatial comparison predicate. """ @@ -243,7 +245,9 @@ def bbox(self, node: ast.BBox, lhs): **{ lhs: { "shape": self.envelope( - values.Envelope(node.minx, node.maxx, node.miny, node.maxy) + values.Envelope( + node.minx, node.maxx, node.miny, node.maxy + ) ), "relation": "intersects", }, diff --git a/pygeofilter/backends/optimize.py b/pygeofilter/backends/optimize.py index 059f8308..382ab3c4 100644 --- a/pygeofilter/backends/optimize.py +++ b/pygeofilter/backends/optimize.py @@ -71,7 +71,11 @@ def is_geometry_literal(value): def is_any_literal(value): - return is_literal(value) or is_temporal_literal(value) or is_geometry_literal(value) + return ( + is_literal(value) + or is_temporal_literal(value) + or is_geometry_literal(value) + ) def to_geometry(value): diff --git a/pygeofilter/backends/oraclesql/evaluate.py b/pygeofilter/backends/oraclesql/evaluate.py index 18a8c040..8638d0d2 100644 --- a/pygeofilter/backends/oraclesql/evaluate.py +++ b/pygeofilter/backends/oraclesql/evaluate.py @@ -65,7 +65,9 @@ class OracleSQLEvaluator(Evaluator): bind_variables: Dict[str, Any] - def __init__(self, attribute_map: Dict[str, str], function_map: Dict[str, str]): + def __init__( + self, attribute_map: Dict[str, str], function_map: Dict[str, str] + ): self.attribute_map = attribute_map self.function_map = function_map @@ -103,7 +105,9 @@ def between(self, node, lhs, low, high): ) self.b_cnt += 1 else: - sql = f"({lhs} {'NOT ' if node.not_ else ''}BETWEEN " f"{low} AND {high})" + sql = ( + f"({lhs} {'NOT ' if node.not_ else ''}BETWEEN {low} AND {high})" + ) return sql @handle(ast.Like) @@ -168,7 +172,7 @@ def bbox(self, node, lhs): self.b_cnt += 1 else: geom_sql = ( - f"SDO_UTIL.FROM_JSON(geometry => '{geo_json}', " f"srid => {srid})" + f"SDO_UTIL.FROM_JSON(geometry => '{geo_json}', srid => {srid})" ) sql = f"SDO_RELATE({lhs}, {geom_sql}, '{param}') = 'TRUE'" @@ -211,7 +215,9 @@ def geometry(self, node: values.Geometry): ) self.b_cnt += 1 else: - sql = f"SDO_UTIL.FROM_JSON(geometry => '{geo_json}', " f"srid => {srid})" + sql = ( + f"SDO_UTIL.FROM_JSON(geometry => '{geo_json}', srid => {srid})" + ) return sql @handle(values.Envelope) @@ -229,7 +235,9 @@ def envelope(self, node: values.Envelope): ) self.b_cnt += 1 else: - sql = f"SDO_UTIL.FROM_JSON(geometry => '{geo_json}', " f"srid => {srid})" + sql = ( + f"SDO_UTIL.FROM_JSON(geometry => '{geo_json}', srid => {srid})" + ) return sql diff --git a/pygeofilter/backends/solr/__init__.py b/pygeofilter/backends/solr/__init__.py index 090a8193..1607256f 100644 --- a/pygeofilter/backends/solr/__init__.py +++ b/pygeofilter/backends/solr/__init__.py @@ -25,8 +25,7 @@ # THE SOFTWARE. # ------------------------------------------------------------------------------ -""" Apache Solr backend for pygeofilter -""" +"""Apache Solr backend for pygeofilter""" from .evaluate import to_filter diff --git a/pygeofilter/backends/solr/evaluate.py b/pygeofilter/backends/solr/evaluate.py index 8e37a60c..3de0ef8e 100644 --- a/pygeofilter/backends/solr/evaluate.py +++ b/pygeofilter/backends/solr/evaluate.py @@ -63,11 +63,20 @@ def _invert_filter_query(filter_query): (e.g., bool dicts), wraps the filter in a bool.must_not structure. """ if isinstance(filter_query, str): - return filter_query[1:] if filter_query.startswith("-") else f"-{filter_query}" - if isinstance(filter_query, dict) and "bool" in filter_query and "must_not" in filter_query["bool"]: + return ( + filter_query[1:] + if filter_query.startswith("-") + else f"-{filter_query}" + ) + if ( + isinstance(filter_query, dict) + and "bool" in filter_query + and "must_not" in filter_query["bool"] + ): return {"bool": {"must": filter_query["bool"]["must_not"]}} return {"bool": {"must_not": [filter_query]}} + def _to_solr_date(value): """Convert input date/datetime to Solr UTC datetime string: YYYY-MM-DDTHH:MM:SSZ. @@ -111,9 +120,10 @@ def _to_solr_date(value): # Solr wants Zulu time with second precision. return dt_utc.strftime("%Y-%m-%dT%H:%M:%SZ") + COMPARISON_OP_MAP = { - ast.ComparisonOp.EQ: "{lhs}:\"{rhs}\"", - ast.ComparisonOp.NE: "-{lhs}:\"{rhs}\"", + ast.ComparisonOp.EQ: '{lhs}:"{rhs}"', + ast.ComparisonOp.NE: '-{lhs}:"{rhs}"', ast.ComparisonOp.GT: "{lhs}:{{{rhs} TO *]", ast.ComparisonOp.GE: "{lhs}:[{rhs} TO *]", ast.ComparisonOp.LT: "{lhs}:[* TO {rhs}}}", @@ -187,10 +197,18 @@ def and_(self, _, lhs, rhs): not be merged into bool.must (they don't work correctly in the query position for Geo3D fields). Non-spatial queries are combined in bool.must as before. """ - lhs_q = lhs.get("query", "*:*") if isinstance(lhs, SolrDSLQuery) else lhs - rhs_q = rhs.get("query", "*:*") if isinstance(rhs, SolrDSLQuery) else rhs - lhs_filters = list(lhs.get("filter", [])) if isinstance(lhs, SolrDSLQuery) else [] - rhs_filters = list(rhs.get("filter", [])) if isinstance(rhs, SolrDSLQuery) else [] + lhs_q = ( + lhs.get("query", "*:*") if isinstance(lhs, SolrDSLQuery) else lhs + ) + rhs_q = ( + rhs.get("query", "*:*") if isinstance(rhs, SolrDSLQuery) else rhs + ) + lhs_filters = ( + list(lhs.get("filter", [])) if isinstance(lhs, SolrDSLQuery) else [] + ) + rhs_filters = ( + list(rhs.get("filter", [])) if isinstance(rhs, SolrDSLQuery) else [] + ) combined_filters = lhs_filters + rhs_filters # Build must list from non-trivial (non-wildcard) query parts @@ -229,7 +247,11 @@ def normalize_clause(clause): return {"bool": {"must": ["*:*"], "must_not": [clause[1:]]}} if isinstance(clause, dict) and "bool" in clause: bool_part = clause["bool"] - if "must_not" in bool_part and "must" not in bool_part and "should" not in bool_part: + if ( + "must_not" in bool_part + and "must" not in bool_part + and "should" not in bool_part + ): normalized = dict(clause) normalized["bool"] = dict(bool_part) normalized["bool"]["must"] = ["*:*"] @@ -266,7 +288,9 @@ def comparison(self, node, lhs, rhs): Creates a range query for comparison operators. """ rhs = _to_solr_date(rhs) - return SolrDSLQuery(f"{COMPARISON_OP_MAP[node.op]}".format(lhs=lhs, rhs=rhs)) + return SolrDSLQuery( + f"{COMPARISON_OP_MAP[node.op]}".format(lhs=lhs, rhs=rhs) + ) @handle(ast.Between) def between(self, node: ast.Between, lhs, low, high): @@ -286,10 +310,12 @@ def in_(self, node, lhs, *options): """ Creates a terms query for `IN` conditions. """ + def _quote(v): if isinstance(v, str): return '"' + v.replace('"', '\\"') + '"' return str(v) + options_str = " OR ".join(_quote(option) for option in options) terms_query = f"{lhs}:({options_str})" if node.not_: @@ -340,22 +366,34 @@ def not_(self, _, sub): result = SolrDSLQuery("*:*") if sub_filters: - result["filter"] = [_invert_filter_query(fq) for fq in sub_filters] + result["filter"] = [ + _invert_filter_query(fq) for fq in sub_filters + ] # A wildcard query contributes no restriction and does not need # query-level negation. if sub_query == "*:*": return result - if isinstance(sub_query, dict) and "bool" in sub_query and "must_not" in sub_query["bool"]: - result["query"] = {"bool": {"must": sub_query["bool"]["must_not"]}} + if ( + isinstance(sub_query, dict) + and "bool" in sub_query + and "must_not" in sub_query["bool"] + ): + result["query"] = { + "bool": {"must": sub_query["bool"]["must_not"]} + } return result result["query"] = {"bool": {"must_not": [sub_query]}} return result # Non-SolrDSLQuery fallback. - if isinstance(sub, dict) and "bool" in sub and "must_not" in sub["bool"]: + if ( + isinstance(sub, dict) + and "bool" in sub + and "must_not" in sub["bool"] + ): return SolrDSLQuery({"bool": {"must": sub["bool"]["must_not"]}}) return SolrDSLQuery({"bool": {"must_not": [sub]}}) @@ -364,7 +402,9 @@ def like(self, node: ast.Like, lhs): """Transforms the provided LIKE pattern to a Solr wildcard pattern. This only works properly on fields that are not tokenized. """ - pattern = like_to_wildcard(node.pattern, node.wildcard, node.singlechar, node.escapechar) + pattern = like_to_wildcard( + node.pattern, node.wildcard, node.singlechar, node.escapechar + ) if "*" in pattern: p = pattern.split("*") if p[0] == "": @@ -393,7 +433,7 @@ def geometry(self, node: values.Geometry): """Geometry values are converted to a Solr spatial query.""" geom_wkt = shape(node).wkt geom = shapely.wkt.loads(geom_wkt) - if geom.geom_type == "Polygon" or geom.geom_type =="MultiPolygon": + if geom.geom_type == "Polygon" or geom.geom_type == "MultiPolygon": # Rectangular polygons (from BBox) must use ENVELOPE format for Geo3D. # WKT polygons with coordinates at ±180/±90 are "coplanar" in 3D space # (poles and antimeridian are degenerate points), causing Solr to reject @@ -408,7 +448,9 @@ def geometry(self, node: values.Geometry): # Global bbox covers the whole Earth — spatial predicate is a no-op. if minx <= -180 and miny <= -90 and maxx >= 180 and maxy >= 90: return None - if (minx <= -179.9999 and maxx >= 179.9999) or (miny <= -89.9999 and maxy >= 89.9999): + if (minx <= -179.9999 and maxx >= 179.9999) or ( + miny <= -89.9999 and maxy >= 89.9999 + ): return f"ENVELOPE({minx}, {maxx}, {maxy}, {miny})" geom = geom.reverse() if not geom.exterior.is_ccw else geom return geom.wkt @@ -436,7 +478,10 @@ def temporal(self, node: ast.TemporalPredicate, lhs, rhs): if isinstance(rhs, (date, datetime)): low = high = rhs.strftime("%Y-%m-%dT%H:%M:%SZ") else: - low, high = rhs[0].strftime("%Y-%m-%dT%H:%M:%SZ"), rhs[1].strftime("%Y-%m-%dT%H:%M:%SZ") + low, high = ( + rhs[0].strftime("%Y-%m-%dT%H:%M:%SZ"), + rhs[1].strftime("%Y-%m-%dT%H:%M:%SZ"), + ) query = None if op == ast.TemporalComparisonOp.DISJOINT: @@ -445,7 +490,10 @@ def temporal(self, node: ast.TemporalPredicate, lhs, rhs): query = f"{lhs}:{{{high} TO *]" elif op == ast.TemporalComparisonOp.BEFORE: query = f"{lhs}:[* TO {low}}}" - elif op == ast.TemporalComparisonOp.TOVERLAPS or op == ast.TemporalComparisonOp.OVERLAPPEDBY: + elif ( + op == ast.TemporalComparisonOp.TOVERLAPS + or op == ast.TemporalComparisonOp.OVERLAPPEDBY + ): query = f"{lhs}:[{low} TO {high}]" elif op == ast.TemporalComparisonOp.BEGINS: query = f"{lhs}:{low}" @@ -470,8 +518,16 @@ def temporal(self, node: ast.TemporalPredicate, lhs, rhs): return SolrDSLQuery(query) - @handle(ast.GeometryIntersects, ast.GeometryDisjoint, ast.GeometryWithin, ast.GeometryContains, ast.GeometryEquals) - def spatial_comparison(self, node: ast.SpatialComparisonPredicate, lhs: str, rhs): + @handle( + ast.GeometryIntersects, + ast.GeometryDisjoint, + ast.GeometryWithin, + ast.GeometryContains, + ast.GeometryEquals, + ) + def spatial_comparison( + self, node: ast.SpatialComparisonPredicate, lhs: str, rhs + ): """Creates a spatial query for the given spatial comparison predicate. Spatial {!field ...} queries MUST go into the Solr filter[] array, not the @@ -492,9 +548,11 @@ def bbox(self, node: ast.BBox, lhs): """Performs a spatial query for the given bounding box. Ignores CRS parameter, as it is not supported by Solr. """ - bbox = self.envelope(values.Envelope(node.minx, node.maxx, node.miny, node.maxy)) + bbox = self.envelope( + values.Envelope(node.minx, node.maxx, node.miny, node.maxy) + ) query = f"{{!field f={lhs} v='Intersects({bbox})'}}" - return SolrDSLQuery('*:*', filters=[query]) + return SolrDSLQuery("*:*", filters=[query]) # @handle(ast.Arithmetic, subclasses=True) # def arithmetic(self, node: ast.Arithmetic, lhs, rhs): @@ -528,7 +586,9 @@ def to_filter( """Shorthand function to convert a pygeofilter AST to an Apache Solr filter structure. """ - return SOLRDSLEvaluator(attribute_map, Version(version) if version else None).evaluate(root) + return SOLRDSLEvaluator( + attribute_map, Version(str(version)) if version else None + ).evaluate(root) def unwrap_query(obj): diff --git a/pygeofilter/backends/solr/util.py b/pygeofilter/backends/solr/util.py index a468249a..2c935ee9 100644 --- a/pygeofilter/backends/solr/util.py +++ b/pygeofilter/backends/solr/util.py @@ -25,8 +25,7 @@ # THE SOFTWARE. # ------------------------------------------------------------------------------ -""" General utilities for the Apache Solr backend. -""" +"""General utilities for the Apache Solr backend.""" import re diff --git a/pygeofilter/backends/sql/evaluate.py b/pygeofilter/backends/sql/evaluate.py index cf813a87..d364efe4 100644 --- a/pygeofilter/backends/sql/evaluate.py +++ b/pygeofilter/backends/sql/evaluate.py @@ -160,7 +160,9 @@ def geometry(self, node: values.Geometry): @handle(values.Envelope) def envelope(self, node: values.Envelope): - wkb_hex = shapely.geometry.box(node.x1, node.y1, node.x2, node.y2).wkb_hex + wkb_hex = shapely.geometry.box( + node.x1, node.y1, node.x2, node.y2 + ).wkb_hex return f"ST_GeomFromWKB(x'{wkb_hex}')" diff --git a/pygeofilter/backends/sqlalchemy/evaluate.py b/pygeofilter/backends/sqlalchemy/evaluate.py index af1536d9..4b4f5b1a 100644 --- a/pygeofilter/backends/sqlalchemy/evaluate.py +++ b/pygeofilter/backends/sqlalchemy/evaluate.py @@ -34,8 +34,10 @@ def between(self, node, lhs, low, high): @handle(ast.Like) def like(self, node, lhs): - if (isinstance(node.pattern, ast.Function)): - node.pattern = filters.function_map[node.pattern.name](*node.pattern.arguments) + if isinstance(node.pattern, ast.Function): + node.pattern = filters.function_map[node.pattern.name]( + *node.pattern.arguments + ) return filters.like( lhs, node.pattern, @@ -104,11 +106,15 @@ def spatial_distance(self, node, lhs, rhs): @handle(ast.BBox) def bbox(self, node, lhs): - return filters.bbox(lhs, node.minx, node.miny, node.maxx, node.maxy, node.crs) + return filters.bbox( + lhs, node.minx, node.miny, node.maxx, node.maxy, node.crs + ) @handle(ast.Attribute) def attribute(self, node): - return filters.attribute(node.name, self.field_mapping, self.undefined_as_null) + return filters.attribute( + node.name, self.field_mapping, self.undefined_as_null + ) @handle(ast.Arithmetic, subclasses=True) def arithmetic(self, node, lhs, rhs): @@ -146,4 +152,6 @@ def to_filter(ast, field_mapping={}, undefined_as_null=None): :type ast: :class:`Node` :returns: a SQLAlchemy query object """ - return SQLAlchemyFilterEvaluator(field_mapping, undefined_as_null).evaluate(ast) + return SQLAlchemyFilterEvaluator(field_mapping, undefined_as_null).evaluate( + ast + ) diff --git a/pygeofilter/backends/sqlalchemy/filters.py b/pygeofilter/backends/sqlalchemy/filters.py index c99c68df..14f4b775 100644 --- a/pygeofilter/backends/sqlalchemy/filters.py +++ b/pygeofilter/backends/sqlalchemy/filters.py @@ -27,16 +27,15 @@ def parse_geometry(geom: dict): wkt = shape(geom).wkt return func.ST_GeomFromEWKT(f"SRID={srid};{wkt}") + # TODO: map functions -function_map = { - "lower": func.lower -} +function_map = {"lower": func.lower} + # ------------------------------------------------------------------------------ # Filters # ------------------------------------------------------------------------------ class Operator: - OPERATORS: Dict[str, Callable] = { "is_null": lambda f, a=None: f.is_(None), "is_not_null": lambda f, a=None: f.isnot(None), diff --git a/pygeofilter/cli.py b/pygeofilter/cli.py index 11055acb..a13b5f7b 100644 --- a/pygeofilter/cli.py +++ b/pygeofilter/cli.py @@ -27,12 +27,13 @@ import logging import sys +from typing import Any import click -from .parsers.cql_json.parser import parse as parse_cql_json from .parsers.cql2_json.parser import parse as parse_cql2_json from .parsers.cql2_text.parser import parse as parse_cql2_text +from .parsers.cql_json.parser import parse as parse_cql_json from .parsers.ecql.parser import parse as parse_ecql from .parsers.fes.parser import parse as parse_fes from .parsers.jfe.parser import parse as parse_jfe @@ -40,28 +41,33 @@ __all__ = ["__version__"] -PARSERS = { - 'cql_json': parse_cql_json, - 'cql2_json': parse_cql2_json, - 'cql2_text': parse_cql2_text, - 'ecql': parse_ecql, - 'fes': parse_fes, - 'jfe': parse_jfe +PARSERS: dict[str, Any] = { + "cql_json": parse_cql_json, + "cql2_json": parse_cql2_json, + "cql2_text": parse_cql2_text, + "ecql": parse_ecql, + "fes": parse_fes, + "jfe": parse_jfe, } def CLI_OPTION_VERBOSITY(f): """Setup click logging output""" + def callback(ctx, param, value): if value is not None: - logging.basicConfig(stream=sys.stdout, - level=getattr(logging, value)) + logging.basicConfig( + stream=sys.stdout, level=getattr(logging, value) + ) return True - return click.option('--verbosity', '-v', - type=click.Choice(['ERROR', 'WARNING', 'INFO', 'DEBUG']), - help='Verbosity', - callback=callback)(f) + return click.option( + "--verbosity", + "-v", + type=click.Choice(["ERROR", "WARNING", "INFO", "DEBUG"]), + help="Verbosity", + callback=callback, + )(f) @click.group() @@ -72,13 +78,13 @@ def cli(): @cli.command() @click.pass_context -@click.argument('parser', type=click.Choice(PARSERS.keys())) -@click.argument('query') +@click.argument("parser", type=click.Choice(PARSERS.keys())) +@click.argument("query") @CLI_OPTION_VERBOSITY def parse(ctx, parser, query, verbosity): """Parse a query into an abstract syntax tree""" - click.echo(f'Parsing {parser} query into AST') + click.echo(f"Parsing {parser} query into AST") try: click.echo(PARSERS[parser](query)) except Exception as err: diff --git a/pygeofilter/parsers/cql2_json/parser.py b/pygeofilter/parsers/cql2_json/parser.py index e3c2e9a7..5686947d 100644 --- a/pygeofilter/parsers/cql2_json/parser.py +++ b/pygeofilter/parsers/cql2_json/parser.py @@ -106,11 +106,15 @@ def walk_cql_json(node: JsonType): # noqa: C901 elif "function" in node: return ast.Function( node["function"]["name"], - cast(List[ast.AstType], walk_cql_json(node["function"]["arguments"])), + cast( + List[ast.AstType], walk_cql_json(node["function"]["arguments"]) + ), ) elif "lower" in node: - return ast.Function("lower", [cast(ast.Node, walk_cql_json(node["lower"]))]) + return ast.Function( + "lower", [cast(ast.Node, walk_cql_json(node["lower"]))] + ) elif "op" in node: op = node["op"] @@ -157,9 +161,11 @@ def walk_cql_json(node: JsonType): # noqa: C901 cast(List[ast.AstType], walk_cql_json(args[1])), not_=False, ) - + elif op == "casei": - return ast.Function("lower", [cast(ast.Node, walk_cql_json(args[0]))]) + return ast.Function( + "lower", [cast(ast.Node, walk_cql_json(args[0]))] + ) elif op in BINARY_OP_PREDICATES_MAP: args = [cast(ast.Node, walk_cql_json(arg)) for arg in args] diff --git a/pygeofilter/parsers/cql_json/parser.py b/pygeofilter/parsers/cql_json/parser.py index cb824c96..b0d969e7 100644 --- a/pygeofilter/parsers/cql_json/parser.py +++ b/pygeofilter/parsers/cql_json/parser.py @@ -109,7 +109,8 @@ def walk_cql_json(node: dict, is_temporal: bool = False) -> ast.AstType: # noqa if isinstance(node, list): result = [ - cast(datetime, walk_cql_json(sub_node, is_temporal)) for sub_node in node + cast(datetime, walk_cql_json(sub_node, is_temporal)) + for sub_node in node ] if is_temporal: return values.Interval(*result) @@ -186,8 +187,14 @@ def walk_cql_json(node: dict, is_temporal: bool = False) -> ast.AstType: # noqa elif name in TEMPORAL_PREDICATES_MAP: return TEMPORAL_PREDICATES_MAP[name]( - cast(ast.TemporalAstType, walk_cql_json(value[0], is_temporal=True)), - cast(ast.TemporalAstType, walk_cql_json(value[1], is_temporal=True)), + cast( + ast.TemporalAstType, + walk_cql_json(value[0], is_temporal=True), + ), + cast( + ast.TemporalAstType, + walk_cql_json(value[1], is_temporal=True), + ), ) elif name in ARRAY_PREDICATES_MAP: diff --git a/pygeofilter/parsers/fes/gml.py b/pygeofilter/parsers/fes/gml.py index 77354206..6c65e6a1 100644 --- a/pygeofilter/parsers/fes/gml.py +++ b/pygeofilter/parsers/fes/gml.py @@ -21,10 +21,12 @@ def _parse_time_instant(node: Element, nsmap: Dict[str, str]) -> datetime: def _parse_time_period(node: Element, nsmap: Dict[str, str]) -> values.Interval: begin = node.xpath( - "gml:begin/gml:TimeInstant/gml:timePosition|gml:beginPosition", namespaces=nsmap + "gml:begin/gml:TimeInstant/gml:timePosition|gml:beginPosition", + namespaces=nsmap, )[0] end = node.xpath( - "gml:end/gml:TimeInstant/gml:timePosition|gml:endPosition", namespaces=nsmap + "gml:end/gml:TimeInstant/gml:timePosition|gml:endPosition", + namespaces=nsmap, )[0] return values.Interval( _parse_time_position(begin, nsmap), diff --git a/pygeofilter/parsers/fes/util.py b/pygeofilter/parsers/fes/util.py index afefbf14..8f146806 100644 --- a/pygeofilter/parsers/fes/util.py +++ b/pygeofilter/parsers/fes/util.py @@ -19,7 +19,9 @@ class Missing: def handle( - *tags: str, namespace: Union[str, Type[Missing]] = Missing, subiter: bool = True + *tags: str, + namespace: Union[str, Type[Missing]] = Missing, + subiter: bool = True, ) -> Callable: """Function-decorator to mark a class function as a handler for a given node type. @@ -68,7 +70,8 @@ def __init__(cls, name, bases, dct): namespace = value.namespace if namespace is Missing: namespace = ( - getattr(cls_, "namespace", None) or cls_namespace + getattr(cls_, "namespace", None) + or cls_namespace ) if namespace: if isinstance(namespace, (list, tuple)): @@ -114,7 +117,9 @@ def _evaluate_node(self, node: etree._Element) -> ast.Node: raise NodeParsingError(f"Cannot parse XML tag {node.tag}") if parse_func.subiter: - sub_nodes = [self._evaluate_node(child) for child in node.iterchildren()] + sub_nodes = [ + self._evaluate_node(child) for child in node.iterchildren() + ] return parse_func(self, node, *sub_nodes) else: return parse_func(self, node) diff --git a/pygeofilter/parsers/iso8601.py b/pygeofilter/parsers/iso8601.py index 04a00274..8c3f8222 100644 --- a/pygeofilter/parsers/iso8601.py +++ b/pygeofilter/parsers/iso8601.py @@ -27,7 +27,7 @@ from lark import Transformer, v_args -from ..util import parse_datetime, parse_duration, parse_date +from ..util import parse_date, parse_datetime, parse_duration @v_args(meta=False, inline=True) @@ -37,6 +37,6 @@ def DATETIME(self, dt): def DURATION(self, duration): return parse_duration(duration) - + def DATE(self, date): return parse_date(date) diff --git a/pygeofilter/parsers/jfe/parser.py b/pygeofilter/parsers/jfe/parser.py index e1da9959..e19e4427 100644 --- a/pygeofilter/parsers/jfe/parser.py +++ b/pygeofilter/parsers/jfe/parser.py @@ -133,26 +133,35 @@ def _parse_node(node: Union[list, dict]) -> ParseResult: # noqa: C901 ) elif op in SPATIAL_PREDICATES_MAP: - return SPATIAL_PREDICATES_MAP[op](*cast(List[ast.SpatialAstType], arguments)) + return SPATIAL_PREDICATES_MAP[op]( + *cast(List[ast.SpatialAstType], arguments) + ) elif op in TEMPORAL_PREDICATES_MAP: # parse strings to datetimes dt_args = [ - parse_datetime(arg) if isinstance(arg, str) else arg for arg in arguments + parse_datetime(arg) if isinstance(arg, str) else arg + for arg in arguments ] if len(arguments) == 3: - if isinstance(dt_args[0], datetime) and isinstance(dt_args[1], datetime): + if isinstance(dt_args[0], datetime) and isinstance( + dt_args[1], datetime + ): dt_args = [ values.Interval(dt_args[0], dt_args[1]), dt_args[2], ] - if isinstance(dt_args[1], datetime) and isinstance(dt_args[2], datetime): + if isinstance(dt_args[1], datetime) and isinstance( + dt_args[2], datetime + ): dt_args = [ dt_args[0], values.Interval(dt_args[1], dt_args[2]), ] - return TEMPORAL_PREDICATES_MAP[op](*cast(List[ast.TemporalAstType], dt_args)) + return TEMPORAL_PREDICATES_MAP[op]( + *cast(List[ast.TemporalAstType], dt_args) + ) # special property getters elif op in ["id", "geometry"]: diff --git a/pygeofilter/py.typed b/pygeofilter/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pygeofilter/values.py b/pygeofilter/values.py index a3b1e7ee..141f43e8 100644 --- a/pygeofilter/values.py +++ b/pygeofilter/values.py @@ -44,6 +44,7 @@ def __geo_interface__(self): def __eq__(self, o: object) -> bool: return shape(self).__geo_interface__ == shape(o).__geo_interface__ + @dataclass class Envelope: x1: float diff --git a/pyproject.toml b/pyproject.toml index bcc090e8..5964a840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,107 @@ [build-system] -requires = ["setuptools>=46.4", "wheel"] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "pygeofilter" +description = "pygeofilter is a pure Python parser implementation of OGC filtering standards" +readme = "README.md" +license = "MIT" +requires-python = ">=3.10" +authors = [ + { name = "Fabian Schindler", email = "fabian.schindler@eox.at" }, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Scientific/Engineering :: GIS", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dynamic = ["version"] +dependencies = [ + "click", + "dateparser", + "lark", + "pygeoif>=1.0.0", +] + +[project.optional-dependencies] +backend-django = ["django"] +backend-sqlalchemy = ["geoalchemy2", "sqlalchemy"] +backend-native = ["shapely"] +backend-elasticsearch = ["elasticsearch", "elasticsearch-dsl"] +backend-opensearch = ["opensearch-py", "opensearch-dsl"] +fes = ["pygml>=0.2"] +dev = [ + "ruff", + "pytest", + "pytest-django", + "mypy", + "types-dateparser", + "pre-commit", +] +test = [ + "pytest", + "pytest-cov", + "pytest-django", + "django", + "geoalchemy2", + "sqlalchemy", + "geopandas", + "fiona", + "pyproj", + "rtree", + "pygml", + "dateparser", + "lark", + "elasticsearch", + "elasticsearch-dsl", + "opensearch-py", + "opensearch-dsl", +] +docs = [ + "sphinxcontrib-apidoc", + "m2r2", +] + +[project.scripts] +pygeofilter = "pygeofilter.cli:cli" + +[project.urls] +Homepage = "https://github.com/geopython/pygeofilter" +Documentation = "https://pygeofilter.readthedocs.io" +Repository = "https://github.com/geopython/pygeofilter" +Issues = "https://github.com/geopython/pygeofilter/issues" + +[tool.setuptools.dynamic] +version = { attr = "pygeofilter.version.__version__" } + +[tool.setuptools.packages.find] +include = ["pygeofilter*"] + +[tool.setuptools.package-data] +pygeofilter = ["py.typed", "**/*.lark"] + +[tool.ruff] +target-version = "py310" +line-length = 80 + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "C90"] +ignore = ["E501", "E203"] + +[tool.ruff.lint.isort] +known-first-party = ["pygeofilter"] + +[tool.ruff.lint.mccabe] +max-complexity = 12 + +[tool.mypy] +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = ["--cov=pygeofilter", "--cov-report=term-missing"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index b41b82be..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -flake8 -pytest -pytest-django -wheel -mypy<=1.10.0 -types-dateparser diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index effaeeb5..00000000 --- a/requirements-test.txt +++ /dev/null @@ -1,14 +0,0 @@ -django -geoalchemy2 -sqlalchemy -geopandas -fiona -pyproj -rtree -pygml -dateparser -lark -elasticsearch -elasticsearch-dsl -opensearch-py -opensearch-dsl diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6cab6ff6..00000000 --- a/setup.cfg +++ /dev/null @@ -1,17 +0,0 @@ -[metadata] -version = attr: pygeofilter.version.__version__ - -###################################################### -# code formating / lint / type checking configurations -[isort] -profile = black -default_section = THIRDPARTY - -[flake8] -ignore = E501,W503,E203 -exclude = .git,__pycache__,docs/conf.py,old,build,dist -max-complexity = 12 -max-line-length = 80 - -[mypy] -ignore_missing_imports = True diff --git a/setup.py b/setup.py deleted file mode 100644 index f5607ce7..00000000 --- a/setup.py +++ /dev/null @@ -1,93 +0,0 @@ -# ------------------------------------------------------------------------------ -# -# Project: pygeofilter -# Authors: Fabian Schindler -# -# ------------------------------------------------------------------------------ -# Copyright (C) 2019 EOX IT Services GmbH -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies of this Software or works derived from this Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# ------------------------------------------------------------------------------ - -"""Install pygeofilter.""" - -import os -import os.path - -from setuptools import find_packages, setup - -# don't install dependencies when building win readthedocs -on_rtd = os.environ.get("READTHEDOCS") == "True" - -# use README.md for project long_description -with open("README.md") as f: - readme = f.read() - -description = ( - "pygeofilter is a pure Python parser implementation of OGC filtering standards" -) - -setup( - name="pygeofilter", - description=description, - long_description=readme, - long_description_content_type="text/markdown", - author="Fabian Schindler", - author_email="fabian.schindler@eox.at", - url="https://github.com/geopython/pygeofilter", - license="MIT", - packages=find_packages(), - include_package_data=True, - install_requires=( - [ - "click", - "dateparser", - "lark", - "pygeoif>=1.0.0" - ] - if not on_rtd - else [] - ), - extras_require={ - "backend-django": ["django"], - "backend-sqlalchemy": ["geoalchemy2", "sqlalchemy"], - "backend-native": ["shapely"], - "backend-elasticsearch": ["elasticsearch", "elasticsearch-dsl"], - "backend-opensearch": ["opensearch-py", "opensearch-dsl"], - "fes": ["pygml>=0.2"], - }, - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Topic :: Scientific/Engineering :: GIS", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - ], - entry_points={ - 'console_scripts': [ - 'pygeofilter=pygeofilter.cli:cli' - ] - }, - tests_require=["pytest"] -) diff --git a/tests/backends/django/test_django_evaluate.py b/tests/backends/django/test_django_evaluate.py index 5df0dda8..ef416414 100644 --- a/tests/backends/django/test_django_evaluate.py +++ b/tests/backends/django/test_django_evaluate.py @@ -42,7 +42,9 @@ def evaluate(cql_expr, expected_ids, model_type=None): qs = model_type.objects.filter(filters) - assert expected_ids == type(expected_ids)(qs.values_list("identifier", flat=True)) + assert expected_ids == type(expected_ids)( + qs.values_list("identifier", flat=True) + ) # common comparisons @@ -276,21 +278,21 @@ def test_before_or_during_dt_dt(): @pytest.mark.django_db def test_before_or_during_dt_td(): evaluate( - "datetimeAttribute BEFORE OR DURING " "2000-01-01T00:00:00Z / PT4S", ("A",) + "datetimeAttribute BEFORE OR DURING 2000-01-01T00:00:00Z / PT4S", ("A",) ) @pytest.mark.django_db def test_before_or_during_td_dt(): evaluate( - "datetimeAttribute BEFORE OR DURING " "PT4S / 2000-01-01T00:00:03Z", ("A",) + "datetimeAttribute BEFORE OR DURING PT4S / 2000-01-01T00:00:03Z", ("A",) ) @pytest.mark.django_db def test_during_td_dt(): evaluate( - "datetimeAttribute BEFORE OR DURING " "PT4S / 2000-01-01T00:00:03Z", ("A",) + "datetimeAttribute BEFORE OR DURING PT4S / 2000-01-01T00:00:03Z", ("A",) ) @@ -341,12 +343,16 @@ def test_intersects_linestring__inv(): @pytest.mark.django_db def test_intersects_multilinestring(): - evaluate("INTERSECTS(geometry, MULTILINESTRING((0 0, 1 1), (2 1, 1 2)))", ("A",)) + evaluate( + "INTERSECTS(geometry, MULTILINESTRING((0 0, 1 1), (2 1, 1 2)))", ("A",) + ) @pytest.mark.django_db def test_intersects_multilinestring_inv(): - evaluate("INTERSECTS(MULTILINESTRING((0 0, 1 1), (2 1, 1 2)), geometry)", ("A",)) + evaluate( + "INTERSECTS(MULTILINESTRING((0 0, 1 1), (2 1, 1 2)), geometry)", ("A",) + ) @pytest.mark.django_db @@ -461,12 +467,12 @@ def test_arith_field_plus_2_inv(): @pytest.mark.django_db def test_arith_field_plus_field(): - evaluate("intMetaAttribute = " "floatMetaAttribute + intAttribute", ("A",)) + evaluate("intMetaAttribute = floatMetaAttribute + intAttribute", ("A",)) @pytest.mark.django_db def test_arith_field_plus_field_inv(): - evaluate("floatMetaAttribute + intAttribute" "= intMetaAttribute", ("A",)) + evaluate("floatMetaAttribute + intAttribute= intMetaAttribute", ("A",)) @pytest.mark.django_db diff --git a/tests/backends/django/testapp/migrations/0001_initial.py b/tests/backends/django/testapp/migrations/0001_initial.py index c276e3f2..6033f1db 100644 --- a/tests/backends/django/testapp/migrations/0001_initial.py +++ b/tests/backends/django/testapp/migrations/0001_initial.py @@ -8,7 +8,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] @@ -29,7 +28,9 @@ class Migration(migrations.Migration): ("identifier", models.CharField(max_length=256, unique=True)), ( "geometry", - django.contrib.gis.db.models.fields.GeometryField(srid=4326), + django.contrib.gis.db.models.fields.GeometryField( + srid=4326 + ), ), ("float_attribute", models.FloatField(blank=True, null=True)), ("int_attribute", models.IntegerField(blank=True, null=True)), @@ -37,11 +38,16 @@ class Migration(migrations.Migration): "str_attribute", models.CharField(blank=True, max_length=256, null=True), ), - ("datetime_attribute", models.DateTimeField(blank=True, null=True)), + ( + "datetime_attribute", + models.DateTimeField(blank=True, null=True), + ), ( "choice_attribute", models.PositiveSmallIntegerField( - blank=True, choices=[(1, "A"), (2, "B"), (3, "C")], null=True + blank=True, + choices=[(1, "A"), (2, "B"), (3, "C")], + null=True, ), ), ], @@ -58,8 +64,14 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("float_meta_attribute", models.FloatField(blank=True, null=True)), - ("int_meta_attribute", models.IntegerField(blank=True, null=True)), + ( + "float_meta_attribute", + models.FloatField(blank=True, null=True), + ), + ( + "int_meta_attribute", + models.IntegerField(blank=True, null=True), + ), ( "str_meta_attribute", models.CharField(blank=True, max_length=256, null=True), @@ -71,7 +83,9 @@ class Migration(migrations.Migration): ( "choice_meta_attribute", models.PositiveSmallIntegerField( - blank=True, choices=[(1, "X"), (2, "Y"), (3, "Z")], null=True + blank=True, + choices=[(1, "X"), (2, "Y"), (3, "Z")], + null=True, ), ), ( diff --git a/tests/backends/django/testapp/models.py b/tests/backends/django/testapp/models.py index 462bed1a..5185ca7c 100644 --- a/tests/backends/django/testapp/models.py +++ b/tests/backends/django/testapp/models.py @@ -45,7 +45,7 @@ class Record(models.Model): (1, "ASCENDING"), (2, "DESCENDING"), ], - **optional + **optional, ) diff --git a/tests/backends/django/testapp/tests.py b/tests/backends/django/testapp/tests.py index 251e178e..4a43e555 100644 --- a/tests/backends/django/testapp/tests.py +++ b/tests/backends/django/testapp/tests.py @@ -47,7 +47,8 @@ def evaluate(self, cql_expr, expected_ids, model_type=None): qs = model_type.objects.filter(filters) self.assertEqual( - expected_ids, type(expected_ids)(qs.values_list("identifier", flat=True)) + expected_ids, + type(expected_ids)(qs.values_list("identifier", flat=True)), ) # common comparisons @@ -189,17 +190,20 @@ def test_before_or_during_dt_dt(self): def test_before_or_during_dt_td(self): self.evaluate( - "datetimeAttribute BEFORE OR DURING " "2000-01-01T00:00:00Z / PT4S", ("A",) + "datetimeAttribute BEFORE OR DURING 2000-01-01T00:00:00Z / PT4S", + ("A",), ) def test_before_or_during_td_dt(self): self.evaluate( - "datetimeAttribute BEFORE OR DURING " "PT4S / 2000-01-01T00:00:03Z", ("A",) + "datetimeAttribute BEFORE OR DURING PT4S / 2000-01-01T00:00:03Z", + ("A",), ) def test_during_td_dt(self): self.evaluate( - "datetimeAttribute BEFORE OR DURING " "PT4S / 2000-01-01T00:00:03Z", ("A",) + "datetimeAttribute BEFORE OR DURING PT4S / 2000-01-01T00:00:03Z", + ("A",), ) # TODO: test DURING OR AFTER / AFTER @@ -220,7 +224,8 @@ def test_intersects_linestring(self): def test_intersects_multilinestring(self): self.evaluate( - "INTERSECTS(geometry, MULTILINESTRING((0 0, 1 1), (2 1, 1 2)))", ("A",) + "INTERSECTS(geometry, MULTILINESTRING((0 0, 1 1), (2 1, 1 2)))", + ("A",), ) def test_intersects_polygon(self): @@ -264,7 +269,9 @@ def test_arith_field_plus_2(self): self.evaluate("intMetaAttribute = 10 + floatMetaAttribute", ("A", "B")) def test_arith_field_plus_field(self): - self.evaluate("intMetaAttribute = " "floatMetaAttribute + intAttribute", ("A",)) + self.evaluate( + "intMetaAttribute = floatMetaAttribute + intAttribute", ("A",) + ) def test_arith_field_plus_mul_1(self): self.evaluate("intMetaAttribute = intAttribute * 1.5 + 5", ("A",)) diff --git a/tests/backends/elasticsearch/test_evaluate.py b/tests/backends/elasticsearch/test_evaluate.py index b17a40d6..5f63a48e 100644 --- a/tests/backends/elasticsearch/test_evaluate.py +++ b/tests/backends/elasticsearch/test_evaluate.py @@ -1,5 +1,7 @@ # pylint: disable=W0621,C0114,C0115,C0116 +from os import environ + import pytest from elasticsearch_dsl import ( Date, @@ -22,7 +24,7 @@ from pygeofilter.backends.elasticsearch import to_filter from pygeofilter.parsers.ecql import parse from pygeofilter.util import parse_datetime -from os import environ + class Wildcard(Field): name = "wildcard" @@ -54,10 +56,12 @@ class Index: @pytest.fixture(autouse=True, scope="session") def connection(): connections.create_connection( - hosts=["http://{}:{}".format( - environ.get("PYGEOFILTER_ELASTIC_HOST", "localhost"), - environ.get("PYGEOFILTER_ELASTIC_PORT", "9200"), - )], + hosts=[ + "http://{}:{}".format( + environ.get("PYGEOFILTER_ELASTIC_HOST", "localhost"), + environ.get("PYGEOFILTER_ELASTIC_PORT", "9200"), + ) + ], ) diff --git a/tests/backends/oraclesql/test_evaluate.py b/tests/backends/oraclesql/test_evaluate.py index af55e0b7..0c31f472 100644 --- a/tests/backends/oraclesql/test_evaluate.py +++ b/tests/backends/oraclesql/test_evaluate.py @@ -54,12 +54,16 @@ def test_between_with_binds(): where, binds = to_sql_where_with_bind_variables( parse("int_attr NOT BETWEEN 4 AND 6"), FIELD_MAPPING, FUNCTION_MAP ) - assert where == "(int_attr NOT BETWEEN :int_attr_low_0 AND :int_attr_high_0)" + assert ( + where == "(int_attr NOT BETWEEN :int_attr_low_0 AND :int_attr_high_0)" + ) assert binds == {"int_attr_low_0": 4, "int_attr_high_0": 6} def test_like(): - where = to_sql_where(parse("str_attr LIKE 'foo%'"), FIELD_MAPPING, FUNCTION_MAP) + where = to_sql_where( + parse("str_attr LIKE 'foo%'"), FIELD_MAPPING, FUNCTION_MAP + ) assert where == "str_attr LIKE 'foo%' ESCAPE '\\'" @@ -82,7 +86,9 @@ def test_combination_with_binds(): where, binds = to_sql_where_with_bind_variables( parse("int_attr = 5 AND float_attr < 6.0"), FIELD_MAPPING, FUNCTION_MAP ) - assert where == "((int_attr = :int_attr_0) AND (float_attr < :float_attr_1))" + assert ( + where == "((int_attr = :int_attr_0) AND (float_attr < :float_attr_1))" + ) assert binds == {"int_attr_0": 5, "float_attr_1": 6.0} @@ -123,7 +129,9 @@ def test_spatial_with_binds(): def test_bbox(): where = to_sql_where( - parse("BBOX(point_attr,-140.99778,41.6751050889,-52.6480987209,83.23324)"), + parse( + "BBOX(point_attr,-140.99778,41.6751050889,-52.6480987209,83.23324)" + ), FIELD_MAPPING, FUNCTION_MAP, ) @@ -144,7 +152,9 @@ def test_bbox(): def test_bbox_with_binds(): where, binds = to_sql_where_with_bind_variables( - parse("BBOX(point_attr,-140.99778,41.6751050889,-52.6480987209,83.23324)"), + parse( + "BBOX(point_attr,-140.99778,41.6751050889,-52.6480987209,83.23324)" + ), FIELD_MAPPING, FUNCTION_MAP, ) diff --git a/tests/backends/solr/test_evaluate.py b/tests/backends/solr/test_evaluate.py index 3bbeb52a..aedc46fc 100644 --- a/tests/backends/solr/test_evaluate.py +++ b/tests/backends/solr/test_evaluate.py @@ -38,8 +38,10 @@ from pygeofilter.parsers.ecql import parse from pygeofilter.util import parse_datetime -PORT=8983 -SOLR_BASE_URL = f"http://localhost:{PORT}/solr/test" # replace with your Solr URL +PORT = 8983 +SOLR_BASE_URL = ( + f"http://localhost:{PORT}/solr/test" # replace with your Solr URL +) HEADERS = { "Content-type": "application/json", } @@ -103,7 +105,9 @@ def prepare(): for field_type in field_types: data = json.dumps({"add-field-type": field_type}) requests.post( - f"http://localhost:{PORT}/api/cores/test/schema", headers={"Content-type": "application/json"}, data=data + f"http://localhost:{PORT}/api/cores/test/schema", + headers={"Content-type": "application/json"}, + data=data, ) # Define the fields to be added @@ -115,7 +119,11 @@ def prepare(): {"name": "str_attribute", "type": "text_general"}, {"name": "center", "type": "location"}, {"name": "geometry_jts", "type": "spatial_jts", "multiValued": "false"}, - {"name": "geometry_geo3d", "type": "spatial_geo3d", "multiValued": "false"}, + { + "name": "geometry_geo3d", + "type": "spatial_geo3d", + "multiValued": "false", + }, {"name": "daterange_attribute", "type": "date_range"}, ] @@ -130,13 +138,17 @@ def prepare(): index = "ok" yield index print("cleaning up") - requests.get(SOLR_BASE_URL + "/admin/cores?action=UNLOAD&core=test&deleteIndex=true") + requests.get( + SOLR_BASE_URL + "/admin/cores?action=UNLOAD&core=test&deleteIndex=true" + ) @pytest.fixture(autouse=True, scope="session") def index(prepare): # Add test documents - response = requests.post(SOLR_BASE_URL + "/update", data=json.dumps(INPUT_DOCS), headers=HEADERS) + response = requests.post( + SOLR_BASE_URL + "/update", data=json.dumps(INPUT_DOCS), headers=HEADERS + ) print(response.json()) # Commit index res = requests.get(SOLR_BASE_URL + "/update?commit=true") @@ -149,7 +161,9 @@ def data(index): data = { "query": "id:A", # Query } - response = requests.get(SOLR_BASE_URL + "/query", data=json.dumps(data), headers=HEADERS) + response = requests.get( + SOLR_BASE_URL + "/query", data=json.dumps(data), headers=HEADERS + ) response_json = response.json() if response_json["responseHeader"]["status"] == 0: # Print the response @@ -241,22 +255,34 @@ def test_combination_like_not(data): result = filter_(parse("NOT str_attribute LIKE 'another'")) assert len(result) == 1 and result[0]["id"] is data[0]["id"] - result = filter_(parse("str_attribute LIKE 'test' AND NOT str_attribute LIKE 'another'")) + result = filter_( + parse("str_attribute LIKE 'test' AND NOT str_attribute LIKE 'another'") + ) assert len(result) == 1 and result[0]["id"] is data[0]["id"] - result = filter_(parse("NOT str_attribute LIKE 'another' AND str_attribute LIKE 'test'")) + result = filter_( + parse("NOT str_attribute LIKE 'another' AND str_attribute LIKE 'test'") + ) assert len(result) == 1 and result[0]["id"] is data[0]["id"] - result = filter_(parse("NOT str_attribute LIKE 'test' AND str_attribute LIKE 'another'")) + result = filter_( + parse("NOT str_attribute LIKE 'test' AND str_attribute LIKE 'another'") + ) assert len(result) == 0 - result = filter_(parse("str_attribute LIKE 'test' OR NOT str_attribute LIKE 'another'")) + result = filter_( + parse("str_attribute LIKE 'test' OR NOT str_attribute LIKE 'another'") + ) assert len(result) == 2 - result = filter_(parse("str_attribute LIKE 'test' OR NOT str_attribute LIKE 'another'")) + result = filter_( + parse("str_attribute LIKE 'test' OR NOT str_attribute LIKE 'another'") + ) assert len(result) == 2 - result = filter_(parse("NOT str_attribute LIKE 'another' OR str_attribute LIKE 'test'")) + result = filter_( + parse("NOT str_attribute LIKE 'another' OR str_attribute LIKE 'test'") + ) assert len(result) == 2 @@ -334,13 +360,22 @@ def test_dsl_query_obj(): assert q == {"query": "text:ice", "filter": ["collection:ice"]} q.add_filter("status:active") - assert q == {"query": "text:ice", "filter": ["collection:ice", "status:active"]} + assert q == { + "query": "text:ice", + "filter": ["collection:ice", "status:active"], + } q = SolrDSLQuery(filters=["collection:ice", "int_field:[3 TO 10]"]) - assert q == {"query": "*:*", "filter": ["collection:ice", "int_field:[3 TO 10]"]} + assert q == { + "query": "*:*", + "filter": ["collection:ice", "int_field:[3 TO 10]"], + } q.add_filter("status:active") - assert q == {"query": "*:*", "filter": ["collection:ice", "int_field:[3 TO 10]", "status:active"]} + assert q == { + "query": "*:*", + "filter": ["collection:ice", "int_field:[3 TO 10]", "status:active"], + } def test_attribute_mapping_fallback(): @@ -358,15 +393,23 @@ def test_attribute_mapping_fallback(): def test_spatial_disjoint_uses_negated_intersects_filter(): - query = to_filter(parse("DISJOINT(geometry_jts, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0)))")) + query = to_filter( + parse("DISJOINT(geometry_jts, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0)))") + ) assert query["query"] == "*:*" - assert query["filter"] == ["-{!field f=geometry_jts v='Intersects(POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))'}"] + assert query["filter"] == [ + "-{!field f=geometry_jts v='Intersects(POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))'}" + ] def test_not_disjoint_inverts_filter_to_intersects(): - query = to_filter(parse("NOT DISJOINT(geometry_jts, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0)))")) + query = to_filter( + parse("NOT DISJOINT(geometry_jts, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0)))") + ) assert query["query"] == "*:*" - assert query["filter"] == ["{!field f=geometry_jts v='Intersects(POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))'}"] + assert query["filter"] == [ + "{!field f=geometry_jts v='Intersects(POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))'}" + ] def test_equality_keeps_non_date_string_terms(): @@ -381,7 +424,9 @@ def test_equality_normalizes_date_literals(): def test_or_keeps_spatial_in_filter_context(): query = to_filter( - parse("INTERSECTS(geometry_jts, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))) OR str_attribute LIKE 'this is a test'") + parse( + "INTERSECTS(geometry_jts, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))) OR str_attribute LIKE 'this is a test'" + ) ) assert query["query"] == "*:*" assert query["filter"] == [ @@ -435,16 +480,24 @@ def test_or_keeps_spatial_in_filter_context(): def test_spatial_and_text(data): - ast = parse("INTERSECTS(geometry_jts, ENVELOPE (0.0 1.0 0.0 1.0)) AND str_attribute LIKE 'this is a test'") + ast = parse( + "INTERSECTS(geometry_jts, ENVELOPE (0.0 1.0 0.0 1.0)) AND str_attribute LIKE 'this is a test'" + ) result = filter_(ast) assert len(result) == 1 def test_spatial(data): - result = filter_(parse("INTERSECTS(geometry_jts, ENVELOPE (0.0 1.0 0.0 1.0))")) + result = filter_( + parse("INTERSECTS(geometry_jts, ENVELOPE (0.0 1.0 0.0 1.0))") + ) assert len(result) == 1 and result[0]["id"] is data[0]["id"] - result = filter_(parse("INTERSECTS(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))")) + result = filter_( + parse( + "INTERSECTS(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))" + ) + ) assert len(result) == 1 and result[0]["id"] is data[0]["id"] result = filter_( @@ -452,23 +505,39 @@ def test_spatial(data): ) assert len(result) == 1 and result[0]["id"] is data[0]["id"] - result = filter_(parse("DISJOINT(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))")) + result = filter_( + parse( + "DISJOINT(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))" + ) + ) assert len(result) == 1 and result[0]["id"] is data[1]["id"] - result = filter_(parse("NOT DISJOINT(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))")) + result = filter_( + parse( + "NOT DISJOINT(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))" + ) + ) assert len(result) == 1 and result[0]["id"] is data[0]["id"] - result = filter_(parse("NOT INTERSECTS(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))")) + result = filter_( + parse( + "NOT INTERSECTS(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0)))" + ) + ) assert len(result) == 1 and result[0]["id"] is data[1]["id"] result = filter_( - parse("DISJOINT(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))) OR str_attribute LIKE 'this is a test'") + parse( + "DISJOINT(geometry_jts, POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0))) OR str_attribute LIKE 'this is a test'" + ) ) assert len(result) == 2 def test_spatial_geo3d(data): - result = filter_(parse("INTERSECTS(geometry_geo3d, ENVELOPE (0.0 1.0 0.0 1.0))")) + result = filter_( + parse("INTERSECTS(geometry_geo3d, ENVELOPE (0.0 1.0 0.0 1.0))") + ) assert len(result) == 1 and result[0]["id"] is data[0]["id"] result = filter_( diff --git a/tests/backends/sqlalchemy/test_evaluate.py b/tests/backends/sqlalchemy/test_evaluate.py index d9903083..45c6106d 100644 --- a/tests/backends/sqlalchemy/test_evaluate.py +++ b/tests/backends/sqlalchemy/test_evaluate.py @@ -18,9 +18,9 @@ from sqlalchemy.sql import func, select from pygeofilter.backends.sqlalchemy.evaluate import to_filter -from pygeofilter.parsers.ecql import parse as parse_ecql -from pygeofilter.parsers.cql2_text import parse as parse_cql_text from pygeofilter.parsers.cql2_json import parse as parse_cql2_json +from pygeofilter.parsers.cql2_text import parse as parse_cql_text +from pygeofilter.parsers.ecql import parse as parse_ecql Base = declarative_base() @@ -146,7 +146,9 @@ def setup_database(connection): connection.commit() seed_database( - scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=connection)) + scoped_session( + sessionmaker(autocommit=False, autoflush=False, bind=connection) + ) ) yield @@ -163,7 +165,9 @@ def db_session(setup_database, connection): transaction.rollback() -def evaluate(session, cql_expr, expected_ids, filter_option=None, parser=parse_ecql): +def evaluate( + session, cql_expr, expected_ids, filter_option=None, parser=parse_ecql +): ast = parser(cql_expr) filters = to_filter(ast, FIELD_MAPPING, filter_option) @@ -274,11 +278,18 @@ def test_not_like_endswith(db_session): def test_not_ilike_endswith(db_session): evaluate(db_session, "strMetaAttribute NOT ILIKE '%b'", ("A",)) + def test_not_eq(db_session): - evaluate(db_session, "NOT(strAttribute = 'AAA')", ("B", ), None, parse_cql_text) + evaluate( + db_session, "NOT(strAttribute = 'AAA')", ("B",), None, parse_cql_text + ) + def test_not_gt(db_session): - evaluate(db_session, "NOT(floatAttribute > 1)", ("A", ), None, parse_cql_text) + evaluate( + db_session, "NOT(floatAttribute > 1)", ("A",), None, parse_cql_text + ) + # (NOT) IN @@ -301,30 +312,81 @@ def test_string_null(db_session): def test_string_not_null(db_session): evaluate(db_session, "intAttribute IS NOT NULL", ("A",)) + # CASEI def test_casei_equals(db_session): - evaluate(db_session, "CASEI(strAttribute) = CASEI('aaa')", ("A",), None, parse_cql_text) + evaluate( + db_session, + "CASEI(strAttribute) = CASEI('aaa')", + ("A",), + None, + parse_cql_text, + ) + def test_casei_like(db_session): - evaluate(db_session, "CASEI(strAttribute) LIKE CASEI('aaa')", ("A",), None, parse_cql_text) + evaluate( + db_session, + "CASEI(strAttribute) LIKE CASEI('aaa')", + ("A",), + None, + parse_cql_text, + ) + def test_casei_notlike(db_session): - evaluate(db_session, "CASEI(strAttribute) NOT LIKE CASEI('aaa')", ("B", ), None, parse_cql_text) + evaluate( + db_session, + "CASEI(strAttribute) NOT LIKE CASEI('aaa')", + ("B",), + None, + parse_cql_text, + ) + def test_casei_in(db_session): - evaluate(db_session, "CASEI(strAttribute) IN (CASEI('aaa'), CASEI('bbb'))", ("A", "B", ), None, parse_cql_text) + evaluate( + db_session, + "CASEI(strAttribute) IN (CASEI('aaa'), CASEI('bbb'))", + ( + "A", + "B", + ), + None, + parse_cql_text, + ) + def test_casei_json_like(db_session): - evaluate(db_session, '{"op": "like", "args": [ {"op": "casei", "args": [{"property": "strAttribute"}]}, {"op": "casei", "args": ["AAA"]} ] }', ("A", ), None, parse_cql2_json) + evaluate( + db_session, + '{"op": "like", "args": [ {"op": "casei", "args": [{"property": "strAttribute"}]}, {"op": "casei", "args": ["AAA"]} ] }', + ("A",), + None, + parse_cql2_json, + ) + # temporal predicates + def test_date_gte(db_session): - evaluate(db_session, "datetimeAttribute >= DATE('2000-01-01')", ("A", "B",), None, parse_cql_text) + evaluate( + db_session, + "datetimeAttribute >= DATE('2000-01-01')", + ( + "A", + "B", + ), + None, + parse_cql_text, + ) def test_before(db_session): - evaluate(db_session, "datetimeAttribute BEFORE 2000-01-01T00:00:01Z", ("A",)) + evaluate( + db_session, "datetimeAttribute BEFORE 2000-01-01T00:00:01Z", ("A",) + ) def test_before_or_during_dt_dt(db_session): @@ -339,7 +401,7 @@ def test_before_or_during_dt_dt(db_session): def test_before_or_during_dt_td(db_session): evaluate( db_session, - "datetimeAttribute BEFORE OR DURING " "2000-01-01T00:00:00Z / PT4S", + "datetimeAttribute BEFORE OR DURING 2000-01-01T00:00:00Z / PT4S", ("A",), ) @@ -347,7 +409,7 @@ def test_before_or_during_dt_td(db_session): def test_before_or_during_td_dt(db_session): evaluate( db_session, - "datetimeAttribute BEFORE OR DURING " "PT4S / 2000-01-01T00:00:03Z", + "datetimeAttribute BEFORE OR DURING PT4S / 2000-01-01T00:00:03Z", ("A",), ) @@ -355,7 +417,7 @@ def test_before_or_during_td_dt(db_session): def test_during_td_dt(db_session): evaluate( db_session, - "datetimeAttribute BEFORE OR DURING " "PT4S / 2000-01-01T00:00:03Z", + "datetimeAttribute BEFORE OR DURING PT4S / 2000-01-01T00:00:03Z", ("A",), ) @@ -372,7 +434,9 @@ def test_intersects_mulitipoint_1(db_session): def test_intersects_mulitipoint_2(db_session): - evaluate(db_session, "INTERSECTS(geometry, MULTIPOINT((0 0), (1 1)))", ("A",)) + evaluate( + db_session, "INTERSECTS(geometry, MULTIPOINT((0 0), (1 1)))", ("A",) + ) def test_intersects_linestring(db_session): @@ -421,14 +485,39 @@ def test_intersects_envelope(db_session): def test_bbox(db_session): evaluate(db_session, "BBOX(geometry, 0, 0, 1, 1, '4326')", ("A",)) + def test_bbox_cql2(db_session): - evaluate(db_session, "S_INTERSECTS(geometry, BBOX(0, 0, 1, 1))", ("A",), None, parse_cql_text) + evaluate( + db_session, + "S_INTERSECTS(geometry, BBOX(0, 0, 1, 1))", + ("A",), + None, + parse_cql_text, + ) + def test_empty_bbox_cql2(db_session): - evaluate(db_session, "S_INTERSECTS(geometry, BBOX(178, 88, 181, 91))", (), None, parse_cql_text) + evaluate( + db_session, + "S_INTERSECTS(geometry, BBOX(178, 88, 181, 91))", + (), + None, + parse_cql_text, + ) + def test_global_bbox_cql2(db_session): - evaluate(db_session, "S_INTERSECTS(geometry,BBOX(-180,-90,180,90))", ("A", "B",), None, parse_cql_text) + evaluate( + db_session, + "S_INTERSECTS(geometry,BBOX(-180,-90,180,90))", + ( + "A", + "B", + ), + None, + parse_cql_text, + ) + # arithmethic expressions @@ -438,16 +527,22 @@ def test_arith_simple_plus(db_session): def test_arith_field_plus_1(db_session): - evaluate(db_session, "intMetaAttribute = floatMetaAttribute + 10", ("A", "B")) + evaluate( + db_session, "intMetaAttribute = floatMetaAttribute + 10", ("A", "B") + ) def test_arith_field_plus_2(db_session): - evaluate(db_session, "intMetaAttribute = 10 + floatMetaAttribute", ("A", "B")) + evaluate( + db_session, "intMetaAttribute = 10 + floatMetaAttribute", ("A", "B") + ) def test_arith_field_plus_field(db_session): evaluate( - db_session, "intMetaAttribute = " "floatMetaAttribute + intAttribute", ("A",) + db_session, + "intMetaAttribute = floatMetaAttribute + intAttribute", + ("A",), ) diff --git a/tests/native/test_evaluate.py b/tests/native/test_evaluate.py index c9867b92..ff2d29d4 100644 --- a/tests/native/test_evaluate.py +++ b/tests/native/test_evaluate.py @@ -107,9 +107,9 @@ def data_json(): def filter_json(ast, data): attr_map = {"point_attr": "geometry", "*": "properties.*"} - filter_expr = NativeEvaluator(math.__dict__, attr_map, use_getattr=False).evaluate( - ast - ) + filter_expr = NativeEvaluator( + math.__dict__, attr_map, use_getattr=False + ).evaluate(ast) return [record for record in data if filter_expr(record)] @@ -277,12 +277,16 @@ def test_temporal(data): result = filter_(parse("date_attr BEFORE 2010-01-08T00:00:00.00Z"), data) assert len(result) == 1 and result[0] is data[0] - result = filter_(parse("date_attr AFTER 2010-01-08T00:00:00.00+01:00"), data) + result = filter_( + parse("date_attr AFTER 2010-01-08T00:00:00.00+01:00"), data + ) assert len(result) == 1 and result[0] is data[1] def test_temporal_json(data_json): - result = filter_json(parse("date_attr BEFORE 2010-01-08T00:00:00.00Z"), data_json) + result = filter_json( + parse("date_attr BEFORE 2010-01-08T00:00:00.00Z"), data_json + ) assert len(result) == 1 and result[0] is data_json[0] result = filter_json( diff --git a/tests/parsers/cql2_json/test_parser.py b/tests/parsers/cql2_json/test_parser.py index 01f2474a..dd55fc9b 100644 --- a/tests/parsers/cql2_json/test_parser.py +++ b/tests/parsers/cql2_json/test_parser.py @@ -148,16 +148,21 @@ def test_attribute_is_null(): result = parse({"op": "isNull", "args": [{"property": "attr"}]}) assert result == ast.IsNull(ast.Attribute("attr"), False) + def test_attribute_casei(): result = parse('{"op": "casei", "args": [{"property": "attr"}]}') assert result == ast.Function("lower", [ast.Attribute("attr")]) + def test_literal_casei(): result = parse('{"op": "casei", "args": ["literal"]}') assert result == ast.Function("lower", ["literal"]) + def test_like_casei(): - result = parse('{"op": "like", "args": [ {"op": "casei", "args": [{"property": "stringattr"}]}, {"op": "casei", "args": ["AAA"]} ] }') + result = parse( + '{"op": "like", "args": [ {"op": "casei", "args": [{"property": "stringattr"}]}, {"op": "casei", "args": ["AAA"]} ] }' + ) assert result == ast.Like( ast.Function("lower", [ast.Attribute("stringattr")]), ast.Function("lower", ["AAA"]), @@ -168,6 +173,7 @@ def test_like_casei(): escapechar="\\", ) + def test_attribute_before(): result = parse( { @@ -212,8 +218,12 @@ def test_attribute_after_dt_dt(): assert result == ast.TimeAfter( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -231,7 +241,9 @@ def test_meets_dt_dr(): assert result == ast.TimeMeets( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), timedelta(seconds=4), ), ) @@ -251,7 +263,9 @@ def test_attribute_metby_dr_dt(): ast.Attribute("attr"), values.Interval( timedelta(seconds=4), - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -270,7 +284,9 @@ def test_attribute_toverlaps_open_dt(): ast.Attribute("attr"), values.Interval( None, - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -288,7 +304,9 @@ def test_attribute_overlappedby_dt_open(): assert result == ast.TimeOverlappedBy( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), None, ), ) @@ -298,7 +316,9 @@ def test_attribute_overlappedby_dt_open(): def test_attribute_aequals(): - result = parse({"op": "a_equals", "args": [{"property": "arrayattr"}, [1, 2, 3]]}) + result = parse( + {"op": "a_equals", "args": [{"property": "arrayattr"}, [1, 2, 3]]} + ) assert result == ast.ArrayEquals( ast.Attribute("arrayattr"), [1, 2, 3], @@ -306,7 +326,9 @@ def test_attribute_aequals(): def test_attribute_aoverlaps(): - result = parse({"op": "a_overlaps", "args": [{"property": "arrayattr"}, [1, 2, 3]]}) + result = parse( + {"op": "a_overlaps", "args": [{"property": "arrayattr"}, [1, 2, 3]]} + ) assert result == ast.ArrayOverlaps( ast.Attribute("arrayattr"), [1, 2, 3], @@ -314,7 +336,9 @@ def test_attribute_aoverlaps(): def test_attribute_acontains(): - result = parse({"op": "a_contains", "args": [{"property": "arrayattr"}, [1, 2, 3]]}) + result = parse( + {"op": "a_contains", "args": [{"property": "arrayattr"}, [1, 2, 3]]} + ) assert result == ast.ArrayContains( ast.Attribute("arrayattr"), [1, 2, 3], @@ -369,7 +393,9 @@ def test_disjoint_linestring_attr(): ) assert result == ast.GeometryDisjoint( values.Geometry( - normalize_geom(geometry.LineString([(1, 1), (2, 2)]).__geo_interface__), + normalize_geom( + geometry.LineString([(1, 1), (2, 2)]).__geo_interface__ + ), ), ast.Attribute("geometry"), ) @@ -393,7 +419,9 @@ def test_contains_attr_polygon(): ast.Attribute("geometry"), values.Geometry( normalize_geom( - geometry.Polygon([(1, 1), (2, 2), (0, 3), (1, 1)]).__geo_interface__ + geometry.Polygon( + [(1, 1), (2, 2), (0, 3), (1, 1)] + ).__geo_interface__ ), ), ) diff --git a/tests/parsers/cql2_text/test_parser.py b/tests/parsers/cql2_text/test_parser.py index 7d4c38ea..5bceec73 100644 --- a/tests/parsers/cql2_text/test_parser.py +++ b/tests/parsers/cql2_text/test_parser.py @@ -1,24 +1,26 @@ -from datetime import datetime, timedelta, date +from datetime import date, datetime, timedelta from dateparser.timezone_parser import StaticTzInfo from pygeofilter import ast, values from pygeofilter.parsers.cql2_text import parse + def test_intersect_bbox(): result = parse("S_INTERSECTS(geometry,BBOX(-180,-90,180,90))") assert result == ast.GeometryIntersects( - ast.Attribute("geometry"), - values.Envelope(-180, 180, -90, 90) + ast.Attribute("geometry"), values.Envelope(-180, 180, -90, 90) ) + def test_intersect_point(): result = parse("S_INTERSECTS(geometry,POINT(7.02 49.92))") assert result == ast.GeometryIntersects( ast.Attribute("geometry"), - values.Geometry({'type': 'Point', 'coordinates': (7.02, 49.92)}) + values.Geometry({"type": "Point", "coordinates": (7.02, 49.92)}), ) + def test_attribute_eq_true_uppercase(): result = parse("attr = TRUE") assert result == ast.Equal( @@ -214,6 +216,7 @@ def test_attribute_before(): datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))), ) + def test_attribute_lt_date(): result = parse("attr < DATE('2000-01-01')") assert result == ast.LessThan( @@ -221,6 +224,7 @@ def test_attribute_lt_date(): date(2000, 1, 1), ) + def test_attribute_t_intersects(): # Using INTERVAL function with properly quoted timestamps result = parse( @@ -229,8 +233,12 @@ def test_attribute_t_intersects(): assert result == ast.TimeOverlaps( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -242,8 +250,12 @@ def test_attribute_tintersects_dt_dr(): assert result == ast.TimeOverlaps( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2000, 1, 1, 0, 0, 4, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2000, 1, 1, 0, 0, 4, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -409,7 +421,9 @@ def test_complex_expression(): assert isinstance(result, ast.And) assert isinstance(result.lhs, ast.And) assert isinstance(result.lhs.lhs, ast.And) - assert result.lhs.lhs.lhs == ast.Equal(ast.Attribute("collection"), "landsat8_l1tp") + assert result.lhs.lhs.lhs == ast.Equal( + ast.Attribute("collection"), "landsat8_l1tp" + ) assert result.lhs.lhs.rhs == ast.LessEqual(ast.Attribute("gsd"), 30) assert result.lhs.rhs == ast.LessEqual(ast.Attribute("eo:cloud_cover"), 10) # The exact datetime comparison depends on implementation details @@ -450,6 +464,7 @@ def test_casei_like(): assert result.pattern.name == "lower" assert result.pattern.arguments == ["coolsat"] + def test_casei_notlike(): result = parse("CASEI(provider) NOT LIKE CASEI('coolsat')") # Assuming CASEI maps to 'lower' in the implementation @@ -460,20 +475,17 @@ def test_casei_notlike(): assert result.pattern.name == "lower" assert result.pattern.arguments == ["coolsat"] + def test_not_gt(): result = parse("NOT(attr > 2)") - assert result == ast.Not( - ast.GreaterThan(ast.Attribute("attr"), 2) - ) + assert result == ast.Not(ast.GreaterThan(ast.Attribute("attr"), 2)) + def test_not_lt(): result = parse("NOT(attr < 2)") - assert result == ast.Not( - ast.LessThan(ast.Attribute("attr"), 2) - ) + assert result == ast.Not(ast.LessThan(ast.Attribute("attr"), 2)) + def test_not_eq(): result = parse("NOT(attr = 2)") - assert result == ast.Not( - ast.Equal(ast.Attribute("attr"), 2) - ) + assert result == ast.Not(ast.Equal(ast.Attribute("attr"), 2)) diff --git a/tests/parsers/cql_json/test_parser.py b/tests/parsers/cql_json/test_parser.py index 7712a885..5d45142c 100644 --- a/tests/parsers/cql_json/test_parser.py +++ b/tests/parsers/cql_json/test_parser.py @@ -284,30 +284,42 @@ def test_attribute_after_dt_dt(): assert result == ast.TimeAfter( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) def test_meets_dt_dr(): - result = parse({"meets": [{"property": "attr"}, ["2000-01-01T00:00:00Z", "PT4S"]]}) + result = parse( + {"meets": [{"property": "attr"}, ["2000-01-01T00:00:00Z", "PT4S"]]} + ) assert result == ast.TimeMeets( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), timedelta(seconds=4), ), ) def test_attribute_metby_dr_dt(): - result = parse({"metby": [{"property": "attr"}, ["PT4S", "2000-01-01T00:00:03Z"]]}) + result = parse( + {"metby": [{"property": "attr"}, ["PT4S", "2000-01-01T00:00:03Z"]]} + ) assert result == ast.TimeMetBy( ast.Attribute("attr"), values.Interval( timedelta(seconds=4), - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -320,7 +332,9 @@ def test_attribute_toverlaps_open_dt(): ast.Attribute("attr"), values.Interval( None, - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -332,7 +346,9 @@ def test_attribute_overlappedby_dt_open(): assert result == ast.TimeOverlappedBy( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), None, ), ) @@ -409,7 +425,9 @@ def test_disjoint_linestring_attr(): ) assert result == ast.GeometryDisjoint( values.Geometry( - normalize_geom(geometry.LineString([(1, 1), (2, 2)]).__geo_interface__), + normalize_geom( + geometry.LineString([(1, 1), (2, 2)]).__geo_interface__ + ), ), ast.Attribute("geometry"), ) @@ -432,7 +450,9 @@ def test_contains_attr_polygon(): ast.Attribute("geometry"), values.Geometry( normalize_geom( - geometry.Polygon([(1, 1), (2, 2), (0, 3), (1, 1)]).__geo_interface__ + geometry.Polygon( + [(1, 1), (2, 2), (0, 3), (1, 1)] + ).__geo_interface__ ), ), ) diff --git a/tests/parsers/ecql/test_parser.py b/tests/parsers/ecql/test_parser.py index 5266b23a..b00e9805 100644 --- a/tests/parsers/ecql/test_parser.py +++ b/tests/parsers/ecql/test_parser.py @@ -250,12 +250,18 @@ def test_attribute_before(): def test_attribute_before_or_during_dt_dt(): - result = parse("attr BEFORE OR DURING 2000-01-01T00:00:00Z / 2000-01-01T00:00:01Z") + result = parse( + "attr BEFORE OR DURING 2000-01-01T00:00:00Z / 2000-01-01T00:00:01Z" + ) assert result == ast.TimeBeforeOrDuring( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -265,7 +271,9 @@ def test_attribute_before_or_during_dt_dr(): assert result == ast.TimeBeforeOrDuring( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), timedelta(seconds=4), ), ) @@ -277,7 +285,9 @@ def test_attribute_before_or_during_dr_dt(): ast.Attribute("attr"), values.Interval( timedelta(seconds=4), - datetime(2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 3, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -308,7 +318,9 @@ def test_contains_attr_polygon(): assert result == ast.GeometryContains( ast.Attribute("geometry"), values.Geometry( - geometry.Polygon([(1, 1), (2, 2), (0, 3), (1, 1)]).__geo_interface__, + geometry.Polygon( + [(1, 1), (2, 2), (0, 3), (1, 1)] + ).__geo_interface__, ), ) @@ -367,7 +379,8 @@ def test_overlaps_attr_multilinestring(): def test_intersects_attr_point_ewkt(): result = parse("INTERSECTS(geometry, SRID=4326;POINT(1 1))") assert ( - result.rhs.geometry["crs"]["properties"]["name"] == "urn:ogc:def:crs:EPSG::4326" + result.rhs.geometry["crs"]["properties"]["name"] + == "urn:ogc:def:crs:EPSG::4326" ) assert result == ast.GeometryIntersects( ast.Attribute("geometry"), @@ -404,7 +417,9 @@ def test_relate_attr_polygon(): assert result == ast.Relate( ast.Attribute("geometry"), values.Geometry( - geometry.Polygon([(1, 1), (2, 2), (0, 3), (1, 1)]).__geo_interface__, + geometry.Polygon( + [(1, 1), (2, 2), (0, 3), (1, 1)] + ).__geo_interface__, ), pattern="1*T***T**", ) @@ -418,7 +433,9 @@ def test_dwithin_attr_polygon(): assert result == ast.DistanceWithin( ast.Attribute("geometry"), values.Geometry( - geometry.Polygon([(1, 1), (2, 2), (0, 3), (1, 1)]).__geo_interface__, + geometry.Polygon( + [(1, 1), (2, 2), (0, 3), (1, 1)] + ).__geo_interface__, ), distance=5, units="feet", @@ -426,11 +443,15 @@ def test_dwithin_attr_polygon(): def test_beyond_attr_polygon(): - result = parse("BEYOND(geometry, POLYGON((1 1,2 2,0 3,1 1)), 5, nautical miles)") + result = parse( + "BEYOND(geometry, POLYGON((1 1,2 2,0 3,1 1)), 5, nautical miles)" + ) assert result == ast.DistanceBeyond( ast.Attribute("geometry"), values.Geometry( - geometry.Polygon([(1, 1), (2, 2), (0, 3), (1, 1)]).__geo_interface__, + geometry.Polygon( + [(1, 1), (2, 2), (0, 3), (1, 1)] + ).__geo_interface__, ), distance=5, units="nautical miles", diff --git a/tests/parsers/fes/test_v11.py b/tests/parsers/fes/test_v11.py index d44c4193..821889ad 100644 --- a/tests/parsers/fes/test_v11.py +++ b/tests/parsers/fes/test_v11.py @@ -387,8 +387,18 @@ def test_geom_overlaps(): [(0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2)], ], [ - [(10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0)], - [(10.2, 10.2), (10.5, 10.2), (10.2, 10.5), (10.2, 10.2)], + [ + (10.0, 10.0), + (11.0, 10.0), + (10.0, 11.0), + (10.0, 10.0), + ], + [ + (10.2, 10.2), + (10.5, 10.2), + (10.2, 10.5), + (10.2, 10.2), + ], ], ], } diff --git a/tests/parsers/fes/test_v20.py b/tests/parsers/fes/test_v20.py index ee063145..4bfaf49a 100644 --- a/tests/parsers/fes/test_v20.py +++ b/tests/parsers/fes/test_v20.py @@ -391,8 +391,18 @@ def test_geom_overlaps(): [(0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2)], ], [ - [(10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0)], - [(10.2, 10.2), (10.5, 10.2), (10.2, 10.5), (10.2, 10.2)], + [ + (10.0, 10.0), + (11.0, 10.0), + (10.0, 11.0), + (10.0, 10.0), + ], + [ + (10.2, 10.2), + (10.5, 10.2), + (10.2, 10.5), + (10.2, 10.2), + ], ], ], } @@ -574,7 +584,11 @@ def test_begins(): assert result == ast.TimeBegins( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2001, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2001, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) diff --git a/tests/parsers/jfe/test_parser.py b/tests/parsers/jfe/test_parser.py index 21a4c654..aafa6c05 100644 --- a/tests/parsers/jfe/test_parser.py +++ b/tests/parsers/jfe/test_parser.py @@ -144,28 +144,46 @@ def test_attribute_before(): def test_attribute_after_dt_dt(): result = parse( - ["after", ["get", "attr"], "2000-01-01T00:00:00Z", "2000-01-01T00:00:01Z"] + [ + "after", + ["get", "attr"], + "2000-01-01T00:00:00Z", + "2000-01-01T00:00:01Z", + ] ) assert result == ast.TimeAfter( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) def test_attribute_during_dt_dt(): result = parse( - ["during", ["get", "attr"], "2000-01-01T00:00:00Z", "2000-01-01T00:00:01Z"] + [ + "during", + ["get", "attr"], + "2000-01-01T00:00:00Z", + "2000-01-01T00:00:01Z", + ] ) assert result == ast.TimeDuring( ast.Attribute("attr"), values.Interval( - datetime(2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))), - datetime(2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))), + datetime( + 2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), + datetime( + 2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0)) + ), ), ) @@ -233,7 +251,9 @@ def test_logical_all(): def test_logical_any(): - result = parse(["any", ["<", ["get", "height"], 50], ["!", ["get", "occupied"]]]) + result = parse( + ["any", ["<", ["get", "height"], 50], ["!", ["get", "occupied"]]] + ) assert result == ast.Or( ast.LessThan( ast.Attribute("height"), diff --git a/tests/test_optimize.py b/tests/test_optimize.py index 1a73b2d3..0303b6d2 100644 --- a/tests/test_optimize.py +++ b/tests/test_optimize.py @@ -54,7 +54,8 @@ def test_combination(): # can' reduce result = optimize(parse("attr = 1 AND other = 2")) assert result == ast.And( - ast.Equal(ast.Attribute("attr"), 1), ast.Equal(ast.Attribute("other"), 2) + ast.Equal(ast.Attribute("attr"), 1), + ast.Equal(ast.Attribute("other"), 2), ) # reduce AND to an INCLUDE if both sides evaluate to true @@ -122,7 +123,9 @@ def test_like(): # allow reduction result = optimize(parse("'This is a test' LIKE 'This is %' AND attr = 1")) assert result == ast.Equal(ast.Attribute("attr"), 1) - result = optimize(parse("'This is a test' LIKE 'This is . test' AND attr = 1")) + result = optimize( + parse("'This is a test' LIKE 'This is . test' AND attr = 1") + ) assert result == ast.Equal(ast.Attribute("attr"), 1) # don't reduction when an attribute is referenced @@ -215,10 +218,13 @@ def myadder(a, b): # can't optimize a function referencing an attribute result = optimize(parse("attr = myadder(other, 2)"), {"myadder": myadder}) assert result == ast.Equal( - ast.Attribute("attr"), ast.Function("myadder", [ast.Attribute("other"), 2]) + ast.Attribute("attr"), + ast.Function("myadder", [ast.Attribute("other"), 2]), ) # can't optimize a function with a nested reference to an attribute - result = optimize(parse("attr = myadder(other + 2, 2)"), {"myadder": myadder}) + result = optimize( + parse("attr = myadder(other + 2, 2)"), {"myadder": myadder} + ) assert result == ast.Equal( ast.Attribute("attr"), ast.Function("myadder", [ast.Add(ast.Attribute("other"), 2), 2]), diff --git a/tests/test_sql/test_evaluate.py b/tests/test_sql/test_evaluate.py index ee7fe3c1..665b7285 100644 --- a/tests/test_sql/test_evaluate.py +++ b/tests/test_sql/test_evaluate.py @@ -113,55 +113,83 @@ def filter_(ast, data): def test_comparison(data): result = filter_(parse("int_attr = 5"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("int_attr < 6"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("int_attr > 6"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) result = filter_(parse("int_attr <= 5"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("int_attr >= 8"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) result = filter_(parse("int_attr <> 5"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) def test_combination(data): result = filter_(parse("int_attr = 5 AND float_attr < 6.0"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("int_attr = 5 AND float_attr < 6.0"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) def test_between(data): result = filter_(parse("float_attr BETWEEN 4 AND 6"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("int_attr NOT BETWEEN 4 AND 6"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) def test_like(data): result = filter_(parse("str_attr LIKE 'this is . test'"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("str_attr LIKE 'this is % test'"), data) assert result.GetFeatureCount() == 2 result = filter_(parse("str_attr NOT LIKE '% another test'"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("str_attr NOT LIKE 'this is . test'"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) result = filter_(parse("str_attr ILIKE 'THIS IS . TEST'"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("str_attr ILIKE 'THIS IS % TEST'"), data) assert result.GetFeatureCount() == 2 @@ -169,18 +197,26 @@ def test_like(data): def test_in(data): result = filter_(parse("int_attr IN ( 1, 2, 3, 4, 5 )"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("int_attr NOT IN ( 1, 2, 3, 4, 5 )"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) def test_null(data): result = filter_(parse("maybe_str_attr IS NULL"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_(parse("maybe_str_attr IS NOT NULL"), data) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) # TODO: possible? @@ -211,13 +247,17 @@ def test_spatial(data): parse("INTERSECTS(point_attr, ENVELOPE (0 1 0 1))"), data, ) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) result = filter_( parse("EQUALS(point_attr, POINT(2 2))"), data, ) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 1 + ) def test_arithmetic(data): @@ -231,7 +271,9 @@ def test_arithmetic(data): parse("int_attr = 5 + 20 / 2 - 10"), data, ) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + ) def test_function(data): @@ -239,4 +281,6 @@ def test_function(data): parse("sin(float_attr) BETWEEN -0.75 AND -0.70"), data, ) - assert result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + assert ( + result.GetFeatureCount() == 1 and result.GetFeature(0).GetField(0) == 0 + )