Skip to content

Commit

Permalink
Moved query field class into onyx-client, my little library was not b…
Browse files Browse the repository at this point in the history
…eing used by anyone and if it just makes code maintaining even a little more awkward, its not worth it in this case
  • Loading branch information
tombch committed Nov 6, 2023
1 parent d1afadf commit 0b0108b
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 13 deletions.
3 changes: 2 additions & 1 deletion onyx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""`~ O N Y X ~`"""
from .config import OnyxConfig, OnyxEnv
from .api import OnyxClient, OnyxField
from .api import OnyxClient
from .field import OnyxField
from . import exceptions
10 changes: 1 addition & 9 deletions onyx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import requests
from requests import HTTPError, RequestException
from typing import Any, Generator, List, Dict, TextIO, Optional, Union
from django_query_tools.client import F
from .config import OnyxConfig
from .field import OnyxField
from .exceptions import (
OnyxClientError,
OnyxConnectionError,
Expand All @@ -14,14 +14,6 @@
)


class OnyxField(F):
"""
Class that represents a single field-value pair for use in Onyx queries.
"""

pass


class OnyxClientBase:
__slots__ = "config", "_request_handler", "_session"
ENDPOINTS = {
Expand Down
10 changes: 8 additions & 2 deletions onyx/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,26 @@ class OnyxError(Exception):

class OnyxConfigError(OnyxError):
"""
Onyx config validation error.
OnyxConfig validation error.
"""

pass


class OnyxClientError(OnyxError):
"""
Onyx client validation error.
OnyxClient validation error.
"""

pass


class OnyxFieldError(OnyxError):
"""
OnyxField validation error.
"""


class OnyxConnectionError(OnyxError):
"""
Onyx connection error.
Expand Down
111 changes: 111 additions & 0 deletions onyx/field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations
from .exceptions import OnyxFieldError


class OnyxOperator:
AND = "&"
OR = "|"
XOR = "^"
NOT = "~"


class OnyxField:
"""
Class that represents a single field-value pair for use in Onyx queries.
"""

def __init__(self, **kwargs) -> None:
if len(kwargs) != 1:
raise OnyxFieldError(
f"Expected exactly one field-value pair as a keyword argument. Received: {len(kwargs)}"
)

# Get the field-value pair from the kwargs
field, value = next(iter(kwargs.items()))

# If the field is not an operation and it is a multi-value lookup
# Join the values into a comma-separated string
if field not in {
OnyxOperator.AND,
OnyxOperator.OR,
OnyxOperator.XOR,
OnyxOperator.NOT,
}:
if type(value) in [list, tuple, set]:
value = ",".join(map(str, value))

self.query = {field: value}

def _validate_field(self, field: OnyxField) -> None:
"""
Ensure an instance with the correct type has been provided.
"""
if not isinstance(field, OnyxField):
raise OnyxFieldError(
f"Expected another instance of {type(self)}. Received: {type(field)}"
)

def _combine_on_associative(self, field: OnyxField, operation: str) -> OnyxField:
"""
Combine two pre-existing queries on an ASSOCIATIVE operation (`AND`, `OR`, `XOR`), and use this to reduce nested JSON.
E.g. take the following query:
`((X AND Y) AND Z) OR (W AND (X AND (Y OR (Z OR X))))`
We can use associativity of `AND` and `OR` to reduce how deeply nested the JSON request body is.
By associativity of `AND` and `OR`, the following is unambiguous and logically equivalent to the previous query:
`(X AND Y AND Z) OR (W AND X AND (Y OR Z OR X))`
With the former corresponding to more deeply-nested JSON than the latter.
This is useful! There is normally a limit on how deeply nested a JSON request body can be.
So by preventing unnecessary nesting, users can programatically construct and execute a broader range of queries.
"""

# For each field, if the topmost key is equal to the current operation, we pull up the values.
# Otherwise, they stay self-contained within their existing operation.
# For example, if operation the operation is '&' and we have self = {"&" : [{...}]} and field = {"|" : [{...}]}
# Then this function would take [{...}] from self, and create [{"|" : [{...}]}] from field
# And then return {"&" : [{...}] + [{"|" : [{...}]}]} or {"&" : [{...}, {"|" : [{...}]}]}
self_key, self_value = next(iter(self.query.items()))
if self_key == operation:
self_query = self_value
else:
self_query = [self.query]

field_key, field_value = next(iter(field.query.items()))
if field_key == operation:
field_query = field_value
else:
field_query = [field.query]

return OnyxField(**{operation: self_query + field_query})

def __eq__(self, field: OnyxField) -> bool:
self._validate_field(field)
return self.query == field.query

def __and__(self, field: OnyxField) -> OnyxField:
self._validate_field(field)
return self._combine_on_associative(field, OnyxOperator.AND)

def __or__(self, field: OnyxField) -> OnyxField:
self._validate_field(field)
return self._combine_on_associative(field, OnyxOperator.OR)

def __xor__(self, field: OnyxField) -> OnyxField:
self._validate_field(field)
return self._combine_on_associative(field, OnyxOperator.XOR)

def __invert__(self) -> OnyxField:
# Here we account for double negatives to also reduce nesting
# Not really needed as people are (unlikely) to be putting multiple negations one after the other
# But hey you never know

# Get the top-most key of the current query
# If its also a NOT, we pull out the value and initialise that as the query
self_key, self_value = next(iter(self.query.items()))
if self_key == OnyxOperator.NOT:
return OnyxField(**self_value)
else:
return OnyxField(**{OnyxOperator.NOT: self.query})
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@
"requests",
"typer>=0.6.0",
"rich",
"django-query-tools>=0.3.3",
],
)

0 comments on commit 0b0108b

Please sign in to comment.