Package: pycql
pycqlpycql is a pure Python parser implementation of the OGC CQL standardInstallationpipinstallpycqlUsageThe basic functionality parses the input string to an abstract syntax tree (AST) representation.
This AST can then be used to build database filters or similar functionality.>>>importpycql>>>ast=pycql.parse(filter_expression)InspectionThe easiest way to inspect the resulting AST is to use theget_reprfunction, which returns a
nice string representation of what was parsed:>>>ast=pycql.parse('id = 10')>>>print(pycql.get_repr(ast))ATTRIBUTEid=LITERAL10.0>>>>>>>>>filter_expr='(number BETWEEN 5 AND 10 AND string NOT LIKE "%B") OR INTERSECTS(geometry, LINESTRING(0 0, 1 1))'>>>print(pycql.get_repr(pycql.parse(filter_expr)))((ATTRIBUTEnumberBETWEENLITERAL5.0ANDLITERAL10.0)AND(ATTRIBUTEstringNOTILIKELITERAL'%B'))OR(INTERSECTS(ATTRIBUTEgeometry,LITERALGEOMETRY'LINESTRING(0 0, 1 1)'))EvaluationIn order to create useful filters from the resulting AST, it has to be evaluated. For the
Django integration, this was done using a recursive descent into the AST, evaluating the
subnodes first and constructing aQobject. Consider having afiltersAPI (for an
example look at the Django one) which creates the filter. Now the evaluator looks something
like this:frompycql.astimport*frommyapiimportfilters# <- this is where the filters are created.# of course, this can also be done in the# evaluator itselfclassFilterEvaluator:def__init__(self,field_mapping=None,mapping_choices=None):self.field_mapping=field_mappingself.mapping_choices=mapping_choicesdefto_filter(self,node):to_filter=self.to_filterifisinstance(node,NotConditionNode):returnfilters.negate(to_filter(node.sub_node))elifisinstance(node,CombinationConditionNode):returnfilters.combine((to_filter(node.lhs),to_filter(node.rhs)),node.op)elifisinstance(node,ComparisonPredicateNode):returnfilters.compare(to_filter(node.lhs),to_filter(node.rhs),node.op,self.mapping_choices)elifisinstance(node,BetweenPredicateNode):returnfilters.between(to_filter(node.lhs),to_filter(node.low),to_filter(node.high),node.not_)elifisinstance(node,BetweenPredicateNode):returnfilters.between(to_filter(node.lhs),to_filter(node.low),to_filter(node.high),node.not_)# ... Some nodes are left out for brevityelifisinstance(node,AttributeExpression):returnfilters.attribute(node.name,self.field_mapping)elifisinstance(node,LiteralExpression):returnnode.valueelifisinstance(node,ArithmeticExpressionNode):returnfilters.arithmetic(to_filter(node.lhs),to_filter(node.rhs),node.op)returnnodeAs mentionend, theto_filtermethod is the recursion.TestingThe basic functionality can be tested usingpytest.python-mpytestThere is a test project/app to test the Django integration. This is tested using the following
command:pythonmanage.pytesttestappDjango integrationFor Django there is a default bridging implementation, where all the filters are translated to the
Django ORM. In order to use this integration, we need two dictionaries, one mapping the available
fields to the Django model fields, and one to map the fields that usechoices. Consider the
following example models:fromdjango.contrib.gis.dbimportmodelsoptional=dict(null=True,blank=True)classRecord(models.Model):identifier=models.CharField(max_length=256,unique=True,null=False)geometry=models.GeometryField()float_attribute=models.FloatField(**optional)int_attribute=models.IntegerField(**optional)str_attribute=models.CharField(max_length=256,**optional)datetime_attribute=models.DateTimeField(**optional)choice_attribute=models.PositiveSmallIntegerField(choices=[(1,'ASCENDING'),(2,'DESCENDING'),],**optional)classRecordMeta(models.Model):record=models.ForeignKey(Record,on_delete=models.CASCADE,related_name='record_metas')float_meta_attribute=models.FloatField(**optional)int_meta_attribute=models.IntegerField(**optional)str_meta_attribute=models.CharField(max_length=256,**optional)datetime_meta_attribute=models.DateTimeField(**optional)choice_meta_attribute=models.PositiveSmallIntegerField(choices=[(1,'X'),(2,'Y'),(3,'Z')],**optional)Now we can specify the field mappings and mapping choices to be used when applying the filters:FIELD_MAPPING={'identifier':'identifier','geometry':'geometry','floatAttribute':'float_attribute','intAttribute':'int_attribute','strAttribute':'str_attribute','datetimeAttribute':'datetime_attribute','choiceAttribute':'choice_attribute',# meta fields'floatMetaAttribute':'record_metas__float_meta_attribute','intMetaAttribute':'record_metas__int_meta_attribute','strMetaAttribute':'record_metas__str_meta_attribute','datetimeMetaAttribute':'record_metas__datetime_meta_attribute','choiceMetaAttribute':'record_metas__choice_meta_attribute',}MAPPING_CHOICES={'choiceAttribute':dict(Record._meta.get_field('choice_attribute').choices),'choiceMetaAttribute':dict(RecordMeta._meta.get_field('choice_meta_attribute').choices),}Finally we are able to connect the CQL AST to the Django database models. We also provide factory
functions to parse the timestamps, durations, geometries and envelopes, so that they can be used
with the ORM layer:frompycql.integrations.djangoimportto_filter,parsecql_expr='strMetaAttribute LIKE "%parent%" AND datetimeAttribute BEFORE 2000-01-01T00:00:01Z'# NOTE: we are using the django integration `parse` wrapper hereast=parse(cql_expr)filters=to_filter(ast,mapping,mapping_choices)qs=Record.objects.filter(**filters)