diff --git a/.env b/.env new file mode 100644 index 0000000..69feecb --- /dev/null +++ b/.env @@ -0,0 +1,42 @@ +# This env file is used by docker compose to inject env variables inside the docker-compose.yml file (not provided to container) + +# Project namespace (defaults to the current folder name if not set) +#COMPOSE_PROJECT_NAME=myproject + +# Password for the 'elastic' user (at least 6 characters) +ELASTIC_PASSWORD=changeme + + +# Password for the 'kibana_system' user (at least 6 characters) +KIBANA_PASSWORD=changeme + + +# Version of Elastic products +STACK_VERSION=8.7.1 + + +# Set the cluster name +CLUSTER_NAME=docker-cluster + + +# Set to 'basic' or 'trial' to automatically start the 30-day trial +LICENSE=basic +#LICENSE=trial + + +# Port to expose Elasticsearch HTTP API to the host +ES_PORT=9200 + + +# Port to expose Kibana to the host +KIBANA_PORT=5601 + + +# Increase or decrease based on the available host memory (in bytes) +ES_MEM_LIMIT=1073741824 +KB_MEM_LIMIT=1073741824 +LS_MEM_LIMIT=1073741824 + + +# SAMPLE Predefined Key only to be used in POC environments +ENCRYPTION_KEY=c34d38b3a14956121ff2170e5030b471551370178f43e5626eec58b04a30fae2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a48923f..9d1ed6c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,47 +1,14 @@ -name: build ⚙️ +name: test ⚙️ on: [ push, pull_request ] jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: - matrix: - python-version: ['3.7', '3.8', '3.9'] + fail-fast: false steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - name: Setup Python ${{ matrix.python-version }} - with: - python-version: ${{ matrix.python-version }} - cache: pip - - name: Install requirements + - uses: actions/checkout@v3 + - name: Run test suite docker container 📦 run: | - sudo apt-get update - sudo apt-get install -y binutils libproj-dev gdal-bin libgdal-dev libsqlite3-mod-spatialite spatialite-bin - pip install -r requirements-test.txt - pip install -r requirements-dev.txt - pip install pygdal=="`gdal-config --version`.*" - pip install . - - name: Configure sysctl limits - run: | - sudo swapoff -a - sudo sysctl -w vm.swappiness=1 - sudo sysctl -w fs.file-max=262144 - sudo sysctl -w vm.max_map_count=262144 - - name: Install and run Elasticsearch 📦 - uses: getong/elasticsearch-action@v1.2 - with: - elasticsearch version: '8.2.2' - host port: 9200 - container port: 9200 - host node port: 9300 - node port: 9300 - discovery type: 'single-node' - - name: Run unit tests - run: | - pytest - # - name: run pre-commit (code formatting, lint and type checking) - # run: | - # python -m pip install pre-commit - # pre-commit run --all-files + docker compose up --build test-gdal-322 diff --git a/.gitignore b/.gitignore index 0964046..607cf71 100644 --- a/.gitignore +++ b/.gitignore @@ -102,7 +102,6 @@ celerybeat.pid *.sage.py # Environments -.env .venv env/ venv/ diff --git a/Dockerfile-3.9 b/Dockerfile-3.9 deleted file mode 100644 index 94c019a..0000000 --- a/Dockerfile-3.9 +++ /dev/null @@ -1,31 +0,0 @@ -FROM python:3.9-buster - -LABEL description="Test executor" - -ENV DEBIAN_FRONTEND noninteractive -RUN apt-get update --fix-missing \ - && apt-get install -y --no-install-recommends \ - binutils \ - libproj-dev \ - gdal-bin \ - libsqlite3-mod-spatialite \ - spatialite-bin \ - && rm -rf /var/lib/apt/lists/* - -RUN mkdir /app -WORKDIR /app - -COPY requirements-test.txt . -COPY requirements-dev.txt . -RUN pip install -r requirements-test.txt -RUN pip install -r requirements-dev.txt - -COPY pygeofilter pygeofilter -COPY tests tests -COPY README.md . -COPY setup.py . -RUN pip install -e . - -RUN chmod +x tests/execute-tests.sh - -CMD ["tests/execute-tests.sh"] diff --git a/README.md b/README.md index ad770cc..6b18855 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ pygeofilter is a pure Python parser implementation of OGC filtering standards [![Build Status](https://github.com/geopython/pygeofilter/workflows/build%20%E2%9A%99%EF%B8%8F/badge.svg)](https://github.com/geopython/pygeofilter/actions) [![Documentation Status](https://readthedocs.org/projects/pygeofilter/badge/?version=latest)](https://pygeofilter.readthedocs.io/en/latest/?badge=latest) +* Tested on debian 11 with gdal version 3.2.2 with python versions 3.9, 3.10 in combination with django versions 4.0, 4.1, 4.2. ## Features @@ -173,28 +174,48 @@ class MyAPIEvaluator(Evaluator): ## Testing +### Proconditions + +1. Install [gdal](https://gdal.org/download.html#binaries) on your system. This package and some site-packages will use it to calculate geospatial data. +2. Provide an elasticsearch instance. You can simply use the provided docker compose setup. + + +### Python dependencies + For testing, several requirements must be satisfied. These can be installed, via pip: ```bash +pip install -r requirements-base.txt pip install -r requirements-dev.txt pip install -r requirements-test.txt ``` -The functionality can be tested using `pytest`. +### Docker + +Start the elasticsearch instance: ```bash -python -m pytest +docker compose up es01 + ``` -### Docker +### Excecuting tests -To execute tests in Docker: +The functionality can be tested using `tox`. To run tests in a specific python and django version run + +```bash +tox -e py311-django41 ``` -docker build -t pygeofilter/test -f Dockerfile-3.9 . -docker run --rm pygeofilter/test + + +To execute tests with Docker: + +```bash +docker compose up --build test-gdal-322 ``` + ## Backends The following backends are shipped with `pygeofilter`. Some require additional dependencies, refer to the [installation](#installation) section for further details. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c70e9be --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,154 @@ +version: '3.8' +services: + + test-gdal-322: + build: + context: . + dockerfile: docker/test/bullseye.Dockerfile + tty: true # To support colorized log output. + networks: + - default + volumes: + - type: bind + source: ./pygeofilter + target: /app/pygeofilter + - type: bind + source: ./tests + target: /app/tests + environment: + - ESHOSTNAME=es01 + + depends_on: + - es01 + + # to get debug information run in verbose mode + # command: > + # /bin/sh -c "tox run -v -v -v -v" + + # to debug container + # command: > + # tail -f /dev/null + + command: > + /bin/sh -c "tox run" + + + es01: + depends_on: + - setup + + image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 + ports: + - ${ES_PORT}:9200 + + environment: + - node.name=es01 + - cluster.name=${CLUSTER_NAME} + - discovery.type=single-node + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + - bootstrap.memory_lock=true + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.key=certs/es01/es01.key + - xpack.security.http.ssl.certificate=certs/es01/es01.crt + - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.enabled=true + - xpack.security.transport.ssl.key=certs/es01/es01.key + - xpack.security.transport.ssl.certificate=certs/es01/es01.crt + - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.verification_mode=certificate + - xpack.license.self_generated.type=${LICENSE} + mem_limit: ${ES_MEM_LIMIT} + volumes: + - certs:/usr/share/elasticsearch/config/certs + - esdata01:/usr/share/elasticsearch/data + networks: + - default + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", + ] + interval: 10s + timeout: 10s + retries: 120 + + + setup: + image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 + volumes: + - certs:/usr/share/elasticsearch/config/certs + user: "0" + env_file: + - .env + command: > + bash -c ' + if [ x${ELASTIC_PASSWORD} == x ]; then + echo "Set the ELASTIC_PASSWORD environment variable in the .env file"; + exit 1; + elif [ x${KIBANA_PASSWORD} == x ]; then + echo "Set the KIBANA_PASSWORD environment variable in the .env file"; + exit 1; + fi; + if [ ! -f config/certs/ca.zip ]; then + echo "Creating CA"; + bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; + unzip config/certs/ca.zip -d config/certs; + fi; + if [ ! -f config/certs/certs.zip ]; then + echo "Creating certs"; + echo -ne \ + "instances:\n"\ + " - name: es01\n"\ + " dns:\n"\ + " - es01\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + " - name: kibana\n"\ + " dns:\n"\ + " - kibana\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + > config/certs/instances.yml; + bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; + unzip config/certs/certs.zip -d config/certs; + fi; + echo "Setting file permissions" + chown -R root:root config/certs; + find . -type d -exec chmod 750 \{\} \;; + find . -type f -exec chmod 640 \{\} \;; + echo "Waiting for Elasticsearch availability"; + until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done; + echo "Setting kibana_system password"; + until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done; + echo "All done!"; + ' + healthcheck: + test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"] + interval: 1s + timeout: 5s + retries: 120 + + +volumes: + certs: + driver: local + esdata01: + driver: local + kibanadata: + driver: local + metricbeatdata01: + driver: local + filebeatdata01: + driver: local + logstashdata01: + driver: local + + +networks: + default: + name: elastic + external: false \ No newline at end of file diff --git a/docker/test/bullseye.Dockerfile b/docker/test/bullseye.Dockerfile new file mode 100644 index 0000000..cac1e2b --- /dev/null +++ b/docker/test/bullseye.Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.10-bullseye +# finally there will be python version 3.9, 3.10 to run matrix tests with tox + +LABEL description="Test executor" + + +RUN set -x \ + && pythonVersions='python3.9 python3.9-dev' \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + # diverse python versions + software-properties-common gpg-agent\ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get purge -y --autoremove software-properties-common gpg-agent \ + && apt-get install -y --no-install-recommends $pythonVersions \ + # Project os dependencies + && apt-get install -y --no-install-recommends \ + binutils \ + libproj-dev \ + gdal-bin \ + libgdal-dev \ + libsqlite3-mod-spatialite \ + spatialite-bin \ + # cleanup + && rm -rf /var/lib/apt/lists/* + +RUN mkdir /app +WORKDIR /app + +COPY requirements-test.txt requirements-base.txt README.md setup.py tox.ini ./ + +# Code base will binded by docker compose. Otherwise the container needs to rebuild on any code change. + +# create dynamic requirements file with the current os gdal version +RUN echo "gdal==`gdal-config --version`.*" >> requirements-gdal.txt \ + && pip install tox + diff --git a/pygeofilter/backends/django/evaluate.py b/pygeofilter/backends/django/evaluate.py index 3498641..bc39d8d 100644 --- a/pygeofilter/backends/django/evaluate.py +++ b/pygeofilter/backends/django/evaluate.py @@ -57,6 +57,10 @@ def between(self, node, lhs, low, high): @handle(ast.Like) def like(self, node, lhs): + if node.wildcard != "%": + # FIXME: this will also replace the singlechar and escapechar, + # but they shall not be replaced + node.pattern = node.pattern.replace(node.wildcard, "%") return filters.like( lhs, node.pattern, node.nocase, node.not_, self.mapping_choices ) diff --git a/pygeofilter/parsers/fes/base.py b/pygeofilter/parsers/fes/base.py index 29fb945..ca4b2a1 100644 --- a/pygeofilter/parsers/fes/base.py +++ b/pygeofilter/parsers/fes/base.py @@ -134,10 +134,6 @@ def distance(self, node: Element): # # TODO: ast.BBox() seems incompatible # pass - @handle("ValueReference") - def value_reference(self, node): - return ast.Attribute(node.text) - @handle("Literal") def literal(self, node): type_ = node.get("type", "").rpartition(":")[2] diff --git a/pygeofilter/parsers/fes/v11.py b/pygeofilter/parsers/fes/v11.py index 264aa64..a899c93 100644 --- a/pygeofilter/parsers/fes/v11.py +++ b/pygeofilter/parsers/fes/v11.py @@ -29,6 +29,10 @@ def div( self, node: Element, lhs: ast.ScalarAstType, rhs: ast.ScalarAstType ) -> ast.Node: return ast.Div(lhs, rhs) + + @handle("PropertyName") + def property_name(self, node): + return ast.Attribute(node.text) def parse(input_: ParseInput) -> ast.Node: diff --git a/requirements-base.txt b/requirements-base.txt new file mode 100644 index 0000000..87e8452 --- /dev/null +++ b/requirements-base.txt @@ -0,0 +1,12 @@ +geoalchemy2 +sqlalchemy<2.0.0 +geopandas +fiona +pyproj +rtree +pygml +dateparser +pygeoif +lark +elasticsearch +elasticsearch-dsl diff --git a/requirements-dev.txt b/requirements-dev.txt index 66c2576..3cac3fe 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ flake8 -pytest -pytest-django wheel mypy<=0.982 +django>=3.2,<4.3 \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index 13f35b9..b773494 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,13 +1,4 @@ -django -geoalchemy2 -sqlalchemy<2.0.0 -geopandas -fiona -pyproj -rtree -pygml -dateparser -pygeoif==0.7 -lark -elasticsearch -elasticsearch-dsl +tox==4.5.1 +pytest +pytest-django +setuptools>=58.0.3 \ No newline at end of file diff --git a/setup.py b/setup.py index 47ac4ce..060875f 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ if not on_rtd else [], extras_require={ - "backend-django": ["django"], + "backend-django": ["django>=4.0"], "backend-sqlalchemy": ["geoalchemy2", "sqlalchemy<2.0.0"], "backend-native": ["shapely"], "backend-elasticsearch": ["elasticsearch", "elasticsearch-dsl"], @@ -74,10 +74,8 @@ "Intended Audience :: Developers", "Topic :: Scientific/Engineering :: GIS", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], tests_require=["pytest"], ) diff --git a/tests/backends/django/test_django_evaluate.py b/tests/backends/django/test_django_evaluate_with_cql.py similarity index 100% rename from tests/backends/django/test_django_evaluate.py rename to tests/backends/django/test_django_evaluate_with_cql.py diff --git a/tests/backends/django/test_django_evaluate_with_fes11.py b/tests/backends/django/test_django_evaluate_with_fes11.py new file mode 100644 index 0000000..c2d0b9c --- /dev/null +++ b/tests/backends/django/test_django_evaluate_with_fes11.py @@ -0,0 +1,185 @@ +# ------------------------------------------------------------------------------ +# +# Project: pygeofilter +# Authors: Jonas Kiefer +# +# ------------------------------------------------------------------------------ +# 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. +# ------------------------------------------------------------------------------ + +import pytest +from testapp import models + +from pygeofilter.backends.django.evaluate import to_filter +from pygeofilter.parsers.fes.parser import parse + + +def evaluate(fes_expr, expected_ids, model_type=None): + model_type = model_type or models.Record + mapping = models.FIELD_MAPPING + mapping_choices = models.MAPPING_CHOICES + + ast = parse(fes_expr) + filters = to_filter(ast, mapping, mapping_choices) + + qs = model_type.objects.filter(filters) + + assert expected_ids == type(expected_ids)(qs.values_list("identifier", flat=True)) + + +FILTER_FRAME = """ + + {0} + +""" + + +# common comparisons + +@pytest.mark.django_db +def test_common_value_like(): + CONSTRAINT = ''' + + strAttribute + AA* + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +@pytest.mark.django_db +def test_common_value_like_middle(): + CONSTRAINT = ''' + + strAttribute + A*A + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +@pytest.mark.django_db +def test_like_beginswith(): + CONSTRAINT = ''' + + strAttribute + A* + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +@pytest.mark.django_db +def test_like_endswith(): + CONSTRAINT = ''' + + strAttribute + *a + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +@pytest.mark.django_db +def test_like_middle(): + CONSTRAINT = ''' + + strMetaAttribute + *parent* + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A", "B")) + + +@pytest.mark.django_db +def test_like_startswith_middle(): + CONSTRAINT = ''' + + strMetaAttribute + A*rent* + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +@pytest.mark.django_db +def test_like_startswith_middle_endswith(): + CONSTRAINT = ''' + + strMetaAttribute + A%ren%A + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +@pytest.mark.django_db +def test_not_like_beginswith(): + CONSTRAINT = ''' + + + strMetaAttribute + B% + + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +@pytest.mark.django_db +def test_not_like_endswith(): + CONSTRAINT = ''' + + + strMetaAttribute + %B + + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) + + +# spatial predicates + +@pytest.mark.django_db +def test_intersects_point(): + CONSTRAINT = ''' + + geometry + + 1.0 1.0 + + + + ''' + + evaluate(FILTER_FRAME.format(CONSTRAINT), ("A",)) diff --git a/tests/backends/elasticsearch/test_evaluate.py b/tests/backends/elasticsearch/test_evaluate.py index e3e8904..9b25fb3 100644 --- a/tests/backends/elasticsearch/test_evaluate.py +++ b/tests/backends/elasticsearch/test_evaluate.py @@ -1,5 +1,5 @@ # pylint: disable=W0621,C0114,C0115,C0116 - +import os import pytest from elasticsearch_dsl import ( Date, @@ -53,8 +53,12 @@ class Index: @pytest.fixture(autouse=True, scope="session") def connection(): + hostname = os.environ.get("ESHOSTNAME", "localhost") connections.create_connection( - hosts=["http://localhost:9200"], + hosts=[f"https://{hostname}:9200"], + ca_certs=False, + verify_certs=False, + basic_auth=("elastic", "changeme") ) @@ -107,9 +111,7 @@ def data(index): def filter_(ast_): query = to_filter(ast_, version="8.2") - print(query) result = Record.search().query(query).execute() - print([r.identifier for r in result]) return result @@ -135,35 +137,35 @@ def test_comparison(data): def test_combination(data): result = filter_(parse("int_attribute = 5 AND float_attribute < 6.0")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_(parse("int_attribute = 6 OR float_attribute < 6.0")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier def test_between(data): result = filter_(parse("float_attribute BETWEEN -1 AND 1")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_(parse("int_attribute NOT BETWEEN 4 AND 6")) - assert len(result) == 1 and result[0].identifier is data[1].identifier + assert len(result) == 1 and result[0].identifier == data[1].identifier def test_like(data): result = filter_(parse("str_attribute LIKE 'this is a test'")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_(parse("str_attribute LIKE 'this is % test'")) assert len(result) == 2 result = filter_(parse("str_attribute NOT LIKE '% another test'")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_(parse("str_attribute NOT LIKE 'this is . test'")) - assert len(result) == 1 and result[0].identifier is data[1].identifier + assert len(result) == 1 and result[0].identifier == data[1].identifier result = filter_(parse("str_attribute ILIKE 'THIS IS . TEST'")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_(parse("str_attribute ILIKE 'THIS IS % TEST'")) assert len(result) == 2 @@ -171,18 +173,18 @@ def test_like(data): def test_in(data): result = filter_(parse("int_attribute IN ( 1, 2, 3, 4, 5 )")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_(parse("int_attribute NOT IN ( 1, 2, 3, 4, 5 )")) - assert len(result) == 1 and result[0].identifier is data[1].identifier + assert len(result) == 1 and result[0].identifier == data[1].identifier def test_null(data): result = filter_(parse("maybe_str_attribute IS NULL")) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_(parse("maybe_str_attribute IS NOT NULL")) - assert len(result) == 1 and result[0].identifier is data[1].identifier + assert len(result) == 1 and result[0].identifier == data[1].identifier def test_has_attr(): @@ -203,17 +205,17 @@ def test_temporal(data): ], ) ) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_( parse("datetime_attribute BEFORE 2000-01-01T00:00:05.00Z"), ) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier result = filter_( parse("datetime_attribute AFTER 2000-01-01T00:00:05.00Z"), ) - assert len(result) == 1 and result[0].identifier is data[1].identifier + assert len(result) == 1 and result[0].identifier == data[1].identifier # def test_array(): @@ -258,14 +260,14 @@ def test_spatial(data): result = filter_( parse("INTERSECTS(geometry, ENVELOPE (0.0 1.0 0.0 1.0))"), ) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier # TODO: test more spatial queries result = filter_( parse("BBOX(center, 2, 2, 3, 3)"), ) - assert len(result) == 1 and result[0].identifier is data[0].identifier + assert len(result) == 1 and result[0].identifier == data[0].identifier # def test_arithmetic(): diff --git a/tests/backends/sqlalchemy/test_evaluate.py b/tests/backends/sqlalchemy/test_evaluate.py index 705f3af..b12e019 100644 --- a/tests/backends/sqlalchemy/test_evaluate.py +++ b/tests/backends/sqlalchemy/test_evaluate.py @@ -31,7 +31,6 @@ class Record(Base): geometry_type="MULTIPOLYGON", srid=4326, spatial_index=False, - management=True, ) ) float_attribute = Column(Float) diff --git a/tests/parsers/fes/test_v11.py b/tests/parsers/fes/test_v11.py index c54b3cd..b348cac 100644 --- a/tests/parsers/fes/test_v11.py +++ b/tests/parsers/fes/test_v11.py @@ -9,11 +9,11 @@ def test_and(): xmlns:xsd="http://www.w3.org/2001/XMLSchema-datatypes"> - attr + attr 30 - attr + attr 10 @@ -39,11 +39,11 @@ def test_or(): xmlns:xsd="http://www.w3.org/2001/XMLSchema-datatypes"> - attr + attr 30.5 - attr + attr 10.5 @@ -69,7 +69,7 @@ def test_not(): xmlns:xsd="http://www.w3.org/2001/XMLSchema-datatypes"> - attr + attr value @@ -90,7 +90,7 @@ def test_not_equal(): - attr + attr value @@ -112,7 +112,7 @@ def test_is_like(): singleChar="." escapeChar="\\" matchCase="true"> - attr + attr some% @@ -138,7 +138,7 @@ def test_is_like(): singleChar="." escapeChar="\\" matchCase="false"> - attr + attr some% @@ -161,7 +161,7 @@ def test_is_null(): - attr + attr """ @@ -178,7 +178,7 @@ def test_is_between(): - attr + attr 10.5 @@ -203,7 +203,7 @@ def test_geom_equals(): - attr + attr @@ -236,7 +236,7 @@ def test_geom_disjoint(): - attr + attr 1.0 1.0 2.0 2.0 @@ -264,7 +264,7 @@ def test_geom_touches(): - attr + attr @@ -301,7 +301,7 @@ def test_geom_within(): - attr + attr 0.0 1.0 2.0 3.0 @@ -335,7 +335,7 @@ def test_geom_overlaps(): - attr + attr @@ -402,7 +402,7 @@ def test_geom_crosses(): - attr + attr 1.0 2.0 2.0 1.0 @@ -424,7 +424,7 @@ def test_geom_intersects(): - attr + attr 1.0 0.5 2.0 1.5 @@ -452,7 +452,7 @@ def test_geom_contains(): - attr + attr 1.0 0.5 2.0 0.5 2.0 1.5 1.0 1.5 1.0 0.5 @@ -479,7 +479,7 @@ def test_geom_dwithin(): - attr + attr 1.0 1.0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0e8ba36 --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +requires = + tox>=4 +env_list = + py{3.9,3.10}-django{40,41,42} + +[testenv] +description = run unit tests +deps= + django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 + -rrequirements-gdal.txt + -rrequirements-base.txt + -rrequirements-test.txt + +setenv = + PYTHONPATH = {toxinidir} + +commands = + python -m pytest {posargs} + +passenv = * +