Skip to main content

A DSL for robust querying of tags in MongoDB

This module converts a text query into a dict that can be directly used in a MongoDB query of a string array field.

To get started, install this package with python3 -m pip install tag_query.

Example Usage

To query a collection for any documents for which the field field_name contains value1 and value2:

from tag_query import compile_query, exceptions

try:
  mongo_query = compile_query(
    expression = 'value1 and value2',
    field = 'field_name'
  )
  # Or just `compile_query('value1 and value2', 'field_name')`
except exceptions.ParseError as e:
  print(e)
  exit(1)

print(mongo_query) #will output -> {'$and': [{'field_name': 'value1'}, {'field_name': 'value2'}]}

Syntax

All query expressions are case-insensitive; that is, the mongodb query is always output in lowercase. The only exception is the field name, which is case-sensitive. Also, note that query expressions are evaluated from left to right, unless otherwise denoted by parentheses. So a and b or c is the same as (a and b) or c, and a or b and c is the same as (a or b) and c.

The following is the entire syntax specification:

expression := binary

binary :=
  | binary (`and`|`+`) value
  | binary (`or`|`/`) value
  | binary (`not`|`-`) value
  | value

value :=
  | `(` expression `)`
  | `not` value
  | extra_field
  | function
  | glob
  | regex
  | tag

extra_field :=
  | literal `:` value

function :=
  | (`eq`|`equal`|`equals`|`exact`|`exactly`|`=`) number
  | (`ge`|`min`|`minimum`|`>=`) number
  | (`le`|`max`|`maximum`|`<=`) number
  | (`lt`|`fewer`|`below`|`<`) number
  | (`gt`|`greater`|`above`|`>`) number

glob :=
  | tag `*`
  | `*` tag
  | `*` tag `*`

tag := literal+

regex := `\{[^\}]*\}`

literal :=
  | `[a-z0-9_\.]+`
  | `"(\\"|[^"])*"`

number := `[0-9]+`

Parentheses

In some cases, left-to-right parsing may not be wanted, so parentheses can be used to group expressions together in a precise order. For example, a or (b and not c) is different than a or b and not c (the latter is equivalent to (a or b) and not c).

Tags

Any non-keyword, non-function, non-special-character text is a single tag. Additionally, any text inside double quotes (") is a tag. If multiple tags are adjacent, they are concatenated together into a single tag, using a single space for the delimiter.

So for example, some_tag, ThisIsATag123 and "another tag" each count as a single tag (but keep in mind they're case-insensitive!), and tag1 and tag2 tag3 is the same as "tag1" and "tag2 tag3".

Globbing

The Glob operator (*) may be used for simple pattern matching. E.g. *test* will match any tag that begins or ends with "test", such as "contested". *test or test* are also valid, and match tags that end and begin with "test", respectively. Note that only plain text tags can have globbing applied; no regex or any compound expressions.

Regex

Any string inside curly braces is interpreted as a regex pattern. E.g. {^[A-Za-z0-9]+$} matches any purely alphanumeric tag.

Operators

Operators select based on the contents of a field.

  • and, +: Require both operands to be true. E.g. tag1 and tag2 or tag1 + tag2 means that the field must contain both "tag1" and "tag2".
  • or, /: Require either of the operands to be true. E.g. tag1 or tag2 or tag1 / tag2 means that the field must contain "tag1" or "tag2" (or both!).
  • not, -: Invert the selection. E.g. not tag1 means that the field must not contain "tag1", and not (tag1 and tag2) means it must not contain both "tag1" and "tag2", but it may contain one of them, or neither.
    • Note that in a binary expression, not can mean and not. For example tag1 not tag2 or tag1 - tag2 is the same as tag1 and not tag2.

Functions

Instead of selecting the contents of a field, functions select based on how many values the field has.

  • eq,equals,exact,exactly, =: Require the field to have exactly that many tags. E.g. exactly 5.
  • lt,fewer,below, <: Require the field to have fewer than that many tags. E.g. fewer 5.
  • gt,greater,above, >: Require the field to have more than that many tags. E.g. greater 5.
  • le,max,maximum, <=: Require the field to have at most that many tags. E.g. maximum 5.
  • ge,min,minimum, >=: Require the field to have at least that many tags. E.g. minimum 5.

Explicit Fields

Depending on the use case, queries on additional fields may be enabled, in addition to the default field. If enabled, these fields must be explicitly queried with the syntax field_name : field_value. Here, field_value can be any expression, or have restrictions imposed on it.

Briefly, these restrictions are:

  • Field value is any string.
  • Field value is one of a list of strings.
  • Field value must satisfy a given parsing function.

In addition, you may indicate whether the field is an array or not, which essentially just allows applying functions to that field.

See the Explicit Field Restrictions section below for examples.

Optimizations

In certain trivial cases, the parser will optimize queries. For example, a and b and a has a redundant value a, so it will be optimized into just a and b. Likewise, a or not a and not b would be optimized into not b, because a or not a just selects everything. As for functions, they may be optimized away if their range includes all numbers (e.g. < 5 or >= 5), and redundant ranges will be removed (e.g. > 2 or = 5 is the same as just > 2).

Contradictions

Any queries that are obviously contradictory are not allowed and will raise an error. For example, a and not a can never match any documents, so it's considered an error. Likewise, expressions like > 3 and < 2 are impossible and also raise an error.


Examples

Here are some example tag queries and their corresponding outputs. The outputs can be directly passed to MongoDB as selection criteria.

Query Expression MongoDB Query Output
tag1 and tag2 {'$and': [{'field_name': 'tag1'}, {'field_name': 'tag2'}]}
tag1 or tag2 {'$or': [{'field_name': 'tag1'}, {'field_name': 'tag2'}]}
not tag1 {'field_name': {'$ne': 'tag1'}}
tag1 and not tag2 {'$and': [{'field_name': 'tag1'}, {'field_name': {'$ne': 'tag2'}}]}
"tag with spaces" {'field_name': 'tag with spaces'}
three tags concatenated {'field_name': 'three tags concatenated'}
{^foo.*} {'field_name': {'$regex': '^foo.*'}}
*test* {'field_name': {'$regex': 'test'}}
exactly 3 {'field_name': {'$size': 3}}
fewer 2 {'field_name.1': {'$exists': False}}
minimum 5 {'field_name.4': {'$exists': True}}
tag1 or (tag2 and not tag3) {'$or': [{'field_name': 'tag1'}, {'$and': [{'field_name': 'tag2'}, {'field_name': {'$ne': 'tag3'}}]}]}

You can use any of these expressions with compile_query(expression, field='field_name').

Explicit Field Restrictions

from tag_query import Alias, ArrayField, compile_query, exceptions

# A filtering function that converts field values to integers,
# or errors if the value contains a non-digit character.
def to_integer(field_name: str, field_value: str) -> int:
	if not re.match(r'^\d+$', field_value):
		raise exceptions.InvalidFieldValue(field_name)
	return int(val)

# field_name can be an array, and individual values have no restrictions.
compile_query('field_name:value', 'tags', field_name=ArrayField(None)) # OK
compile_query('field_name:(gt 1)', 'tags', field_name=ArrayField(None)) # OK
compile_query('field_name:(gt 1 or value)', 'tags', field_name=ArrayField(None)) # OK
compile_query('field_name:*value*', 'tags', field_name=ArrayField(None)) # OK

# field_name is NOT an array, but value has no restriction.
compile_query('field_name:value', 'tags', field_name=None) # OK
compile_query('field_name:(gt 1)', 'tags', field_name=None) # ERROR
compile_query('field_name:*value*', 'tags', field_name=None) # OK

# field_name is NOT an array, and value must explicitly be from a given list.
compile_query('field_name:value', 'tags', field_name=['value', 'val2']) # OK
compile_query('field_name:other', 'tags', field_name=['value', 'val2']) # ERROR
compile_query('field_name:(gt 1)', 'tags', field_name=['value', 'val2']) # ERROR
compile_query('field_name:*value*', 'tags', field_name=['value', 'val2']) # ERROR

# field_name is NOT an array, and value must satisfy a filtering function.
compile_query('field_name:123', 'tags', field_name=to_integer) # OK
compile_query('field_name:value', 'tags', field_name=to_integer) # ERROR
compile_query('field_name:(gt 1)', 'tags', field_name=to_integer) # ERROR
compile_query('field_name:*value*', 'tags', field_name=to_integer) # ERROR

# You can of course mix and match restriction levels to allow arrays on any of them.
ArrayField(None)
ArrayField(['value', 'val2'])
ArrayField(to_integer)

# And if you want a field to be aliased to a different MongoDB field than
# what the user inputs, you can use an Alias.
compile_query('myfield:value', 'tags', myfield=Alias('field_name', ArrayField(None))) # OK
compile_query('field_name:value', 'tags', myfield=Alias('field_name', ArrayField(None))) # ERROR

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tag_query-1.2.1.tar.gz (28.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tag_query-1.2.1-py3-none-any.whl (27.5 kB view details)

Uploaded Python 3

File details

Details for the file tag_query-1.2.1.tar.gz.

File metadata

  • Download URL: tag_query-1.2.1.tar.gz
  • Upload date:
  • Size: 28.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for tag_query-1.2.1.tar.gz
Algorithm Hash digest
SHA256 7fe9365bcd9f3dd4c956c8e289d10f05f6e62e95fa7f3d271a34721bfdbd4fce
MD5 2dabc00f241215c09624098b13386d44
BLAKE2b-256 d0abc347ab43ca01c40831ef8b7fc1dcbb93cad53d2b804cfdfd1cf7a212909f

See more details on using hashes here.

File details

Details for the file tag_query-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: tag_query-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 27.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for tag_query-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e312486f23ba6a2c66af82ea29912e9b1ebf86ad6cb1875029889341ff857f67
MD5 9b768cd3ae6cf10eb1ecb78a12912bf8
BLAKE2b-256 685a270fb19faaee070efabf4382cbec679e894669974f0cf8c83bfc6d3e7cf6

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.2.1 This release

2 files

1.2.0

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.1

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page