Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parsers-to-frontend #77

Open
wants to merge 3 commits into
base: v1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pygeofilter/backends/cql2_json/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
from typing import Dict, Optional

from ... import ast, values
from ...cql2 import get_op
from ..evaluator import Evaluator, handle
from ...frontends.cql2 import get_op
from ...evaluator import Evaluator, handle


def json_serializer(obj):
Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/backends/django/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from django.contrib.gis.geos import GEOSGeometry, Polygon

from ... import ast, values
from ..evaluator import Evaluator, handle
from ...evaluator import Evaluator, handle
from . import filters


Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/backends/elasticsearch/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from packaging.version import Version

from ... import ast, values
from ..evaluator import Evaluator, handle
from ...evaluator import Evaluator, handle
from .util import like_to_wildcard

VERSION_7_10_0 = Version("7.10.0")
Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/backends/geopandas/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from shapely import geometry

from ... import ast, values
from ..evaluator import Evaluator, handle
from ...evaluator import Evaluator, handle
from . import filters

LITERALS = (str, float, int, bool, datetime, date, time, timedelta)
Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/backends/native/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

from ... import ast, values
from ...util import like_pattern_to_re, parse_datetime
from ..evaluator import Evaluator, handle
from ...evaluator import Evaluator, handle

COMPARISON_MAP = {
ast.ComparisonOp.EQ: "==",
Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/backends/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

from .. import ast, values
from ..util import like_pattern_to_re
from .evaluator import Evaluator, handle
from ..evaluator import Evaluator, handle

COMPARISON_MAP = {
"=": operator.eq,
Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/backends/sql/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import shapely.geometry

from ... import ast, values
from ..evaluator import Evaluator, handle
from ...evaluator import Evaluator, handle

COMPARISON_OP_MAP = {
ast.ComparisonOp.EQ: "=",
Expand Down
2 changes: 1 addition & 1 deletion pygeofilter/backends/sqlalchemy/evaluate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import date, datetime, time, timedelta

from ... import ast, values
from ..evaluator import Evaluator, handle
from ...evaluator import Evaluator, handle
from . import filters

LITERALS = (str, float, int, bool, datetime, date, time, timedelta)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from functools import wraps
from typing import Any, Callable, Dict, List, Type, cast

from .. import ast
from . import ast


def get_all_subclasses(*classes: Type) -> List[Type]:
Expand Down
File renamed without changes.
14 changes: 14 additions & 0 deletions pygeofilter/frontends/abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from abc import ABC, abstractmethod

from ..ast import Node


class Frontend(ABC):

@abstractmethod
def parse(self, raw: str) -> Node:
...

@abstractmethod
def encode(self, root: Node) -> str:
...
2 changes: 1 addition & 1 deletion pygeofilter/cql2.py → pygeofilter/frontends/cql2.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Common configurations for cql2 parsers and evaluators.
from typing import Dict, Type, Union

from . import ast
from .. import ast

# https://github.com/opengeospatial/ogcapi-features/tree/master/cql2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from typing import List, Union, cast

from ... import ast, values
from ...cql2 import BINARY_OP_PREDICATES_MAP
from ..cql2 import BINARY_OP_PREDICATES_MAP
from ...util import parse_date, parse_datetime, parse_duration

# https://github.com/opengeospatial/ogcapi-features/tree/master/cql2
Expand Down
11 changes: 11 additions & 0 deletions pygeofilter/frontends/cql2_text/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ...ast import Node
from ..abc import Frontend
from .parser import parse


__all__ = ["parse"]


class CQL2TextFrontend(Frontend):
def parse(self, raw: str) -> Node:
return super().parse(raw)
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from lark import Lark, logger, v_args

from ... import ast, values
from ...cql2 import SPATIAL_PREDICATES_MAP, TEMPORAL_PREDICATES_MAP
from ..cql2 import SPATIAL_PREDICATES_MAP, TEMPORAL_PREDICATES_MAP
from ..iso8601 import ISO8601Transformer
from ..wkt import WKTTransformer

Expand Down
174 changes: 174 additions & 0 deletions pygeofilter/frontends/ecql/encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from datetime import date, datetime, timedelta
import re

import shapely.geometry

from ... import ast
from ... import values
from ...evaluator import Evaluator, handle
from ...util import encode_duration


COMPARISON_OP_MAP = {
ast.ComparisonOp.EQ: "=",
ast.ComparisonOp.NE: "<>",
ast.ComparisonOp.LT: "<",
ast.ComparisonOp.LE: "<=",
ast.ComparisonOp.GT: ">",
ast.ComparisonOp.GE: ">=",
}

ARITHMETIC_OP_MAP = {
ast.ArithmeticOp.ADD: "+",
ast.ArithmeticOp.SUB: "-",
ast.ArithmeticOp.MUL: "*",
ast.ArithmeticOp.DIV: "/",
}


def maybe_bracket(node: ast.Node, encoded: str) -> str:
if isinstance(node, (ast.Not, ast.Combination, ast.Comparison, ast.Arithmetic)):
return f"({encoded})"
return encoded


class ECQLEvaluator(Evaluator):
@handle(ast.Not)
def not_(self, node: ast.Not, sub):
return f"NOT {sub}"

@handle(ast.And, ast.Or)
def combination(self, node: ast.Combination, lhs, rhs):
if isinstance(node.lhs, ast.Combination):
lhs = f"({lhs})"
if isinstance(node.rhs, ast.Combination):
rhs = f"({rhs})"
return f"{lhs} {node.op.value} {rhs}"

@handle(ast.Comparison, subclasses=True)
def comparison(self, node: ast.Comparison, lhs, rhs):
return f"{lhs} {COMPARISON_OP_MAP[node.op]} {rhs}"

@handle(ast.Between)
def between(self, node, lhs, low, high):
return f"{lhs} {'NOT ' if node.not_ else ''}BETWEEN {low} AND {high}"

@handle(ast.Like)
def like(self, node: ast.Like, lhs):
pattern = node.pattern
if node.wildcard != "%":
# TODO: not preceded by escapechar
pattern = pattern.replace(node.wildcard, "%")
if node.singlechar != "_":
# TODO: not preceded by escapechar
pattern = pattern.replace(node.singlechar, "_")

return f"{lhs} {'NOT ' if node.not_ else ''}{'I' if node.nocase else ''}LIKE '{pattern}'"

@handle(ast.In)
def in_(self, node: ast.In, lhs, *options):
return f"{lhs} {'NOT ' if node.not_ else ''}IN ({', '.join(options)})"

@handle(ast.IsNull)
def null(self, node: ast.IsNull, lhs):
return f"{lhs} IS {'NOT ' if node.not_ else ''}NULL"

@handle(ast.Exists)
def exists(self, node: ast.Exists, lhs):
return f"{lhs} {'DOES-NOT-EXIST' if node.not_ else 'EXISTS'}"

@handle(ast.Include)
def include(self, node: ast.Include):
return "EXCLUDE" if node.not_ else "INCLUDE"

@handle(ast.TemporalPredicate, subclasses=True)
def temporal(self, node: ast.TemporalPredicate, lhs, rhs):
if isinstance(node, ast.TimeBefore):
return f"{lhs} BEFORE {rhs}"
elif isinstance(node, ast.TimeBeforeOrDuring):
return f"{lhs} BEFORE OR DURING {rhs}"
elif isinstance(node, ast.TimeDuring):
return f"{lhs} DURING {rhs}"
elif isinstance(node, ast.TimeDuringOrAfter):
return f"{lhs} DURING OR AFTER {rhs}"
elif isinstance(node, ast.TimeAfter):
return f"{lhs} AFTER {rhs}"
else:
raise NotImplementedError(f"{node.op} is not implemented")

@handle(ast.SpatialComparisonPredicate, subclasses=True)
def spatial_operation(self, node: ast.SpatialComparisonPredicate, lhs, rhs):
return f"{node.op.value}({lhs}, {rhs})"

@handle(ast.BBox)
def bbox(self, node: ast.BBox, lhs):
if not node.crs:
return f"BBOX({lhs}, {node.minx}, {node.miny}, {node.maxx}, {node.maxy})"
else:
return f"BBOX({lhs}, {node.minx}, {node.miny}, {node.maxx}, {node.maxy}, '{node.crs}')"

@handle(ast.Relate)
def relate(self, node: ast.Relate, lhs, rhs):
return f"RELATE({lhs}, {rhs}, '{node.pattern}')"

@handle(ast.SpatialDistancePredicate, subclasses=True)
def spatial_distance_predicate(self, node: ast.SpatialDistancePredicate, lhs, rhs):
return f"{node.op.value}({lhs}, {rhs}, {node.distance}, {node.units})"

@handle(ast.Attribute)
def attribute(self, node: ast.Attribute):
is_cname = re.match(r"[a-zA-Z_][a-zA-Z0-9_]*", node.name) is not None
return node.name if is_cname else f'"{node.name}"'

@handle(ast.Arithmetic, subclasses=True)
def arithmetic(self, node: ast.Arithmetic, lhs, rhs):
def arity(node):
if isinstance(node, (ast.Sub, ast.Add)):
return 1
elif isinstance(node, (ast.Div, ast.Mul)):
return 2

node_arity = arity(node)
lhs_arity = arity(node.lhs)
rhs_arity = arity(node.rhs)
if lhs_arity and node_arity > lhs_arity:
lhs = f"({lhs})"
if rhs_arity and node_arity > rhs_arity:
rhs = f"({rhs})"

return f"{lhs} {node.op.value} {rhs}"

@handle(ast.Function)
def function(self, node: ast.Function, *arguments):
return f"{node.name}({', '.join(arguments)})"

@handle(*values.LITERALS)
def literal(self, node):
if isinstance(node, str):
return f"'{node}'"
elif isinstance(node, (datetime, date)):
return node.isoformat().replace("+00:00", "Z")
elif isinstance(node, timedelta):
return encode_duration(node)
elif isinstance(node, bool):
return str(node).upper()
elif isinstance(node, float):
return str(int(node) if node.is_integer() else node)
else:
return str(node)

@handle(values.Interval)
def interval(self, node: values.Interval, start, end):
return f"{self.literal(node.start)} / {self.literal(node.end)}"

@handle(values.Geometry)
def geometry(self, node: values.Geometry):
return shapely.geometry.shape(node).wkt

@handle(values.Envelope)
def envelope(self, node: values.Envelope):
return f"ENVELOPE ({node.x1} {node.y1} {node.x2} {node.y2})"


def encode(root: ast.Node) -> str:
return ECQLEvaluator().evaluate(root)
13 changes: 13 additions & 0 deletions pygeofilter/frontends/ecql/frontend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from ..abc import Frontend
from ...ast import Node

from .parser import parse
from .encoder import encode


class ECQLFrontend(Frontend):
def parse(self, raw: str) -> Node:
return parse(raw)

def encode(self, root: Node) -> str:
return encode(root)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 0 additions & 3 deletions pygeofilter/parsers/jfe/__init__.py

This file was deleted.

29 changes: 22 additions & 7 deletions pygeofilter/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@
from datetime import date, datetime, timedelta

from dateparser import parse as _parse_datetime
import isodate

__all__ = [
"parse_datetime",
"RE_ISO_8601",
"parse_duration",
"like_pattern_to_re_pattern",
"like_pattern_to_re",
]

# __all__ = [
# "parse_datetime",
# "encode_duration",
# "RE_ISO_8601",
# "parse_duration",
# "like_pattern_to_re_pattern",
# "like_pattern_to_re",
# ]

RE_ISO_8601 = re.compile(
r"^(?P<sign>[+-])?P"
Expand Down Expand Up @@ -87,6 +90,18 @@ def parse_datetime(value: str) -> datetime:
return parsed


def encode_duration(value: timedelta) -> str:
return isodate.duration_isoformat(value)


def encode_date(value: date) -> str:
return value.isoformat()


def encode_datetime(value: datetime) -> str:
return datetime.isoformat().replace("+00:00", "Z")


def like_pattern_to_re_pattern(like, wildcard, single_char, escape_char):
x_wildcard = re.escape(wildcard)
x_single_char = re.escape(single_char)
Expand Down
2 changes: 1 addition & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pyproj
rtree
pygml
dateparser
pygeoif==0.7
pygeoif==1.0
lark
elasticsearch
elasticsearch-dsl
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"lark<1.0",
"pygeoif>=1.0.0",
"dataclasses;python_version<'3.7'",
"isodate",
]
if not on_rtd
else [],
Expand Down
Loading