KeyPaths

Attention

Hashquery is currently in public preview. Here are some caveats to keep in mind for early adopters:

  • Backwards compatibility may not be preserved between version upgrades, and you may need to update your package regularly.

  • There may be differences in the SQL logic generated by hashquery compared to the Hashboard app.

  • You may encounter dialect-specific SQL syntax errors or other unexpected errors. If you come across one, let us know by reporting an issue.

Advanced API

We don’t expect many users to need to deal with the KeyPath type directly, unless they are building their own packages on top of the Hashquery framework.

For more detailed information on KeyPaths, see Advanced KeyPaths.

class KeyPath(components: List[KeyPathComponent])

A KeyPath is an ordered list of KeyPathComponent types which describes an accessor to an value. While this is modeled as data, this can be thought of as an accessor function from “root” to the result.

For example, the KeyPath of _.path.to.property is analogous to a function of the form: (value) => value.path.to.property, and is represented internally as:

KeyPath([
    KeyPathComponentProperty("path"),
    KeyPathComponentProperty("to"),
    KeyPathComponentProperty("property")
])

To turn a KeyPath into a value (which we call “resolving” the keypath), use one of the functions in the .resolve module.


KeyPaths can describe more kinds of access than just .property access, such as subscript access: _.some_dict[“subscript”] is an accessor of the form (root) => root[“subscript”], and is represented internally as:

KeyPath([
    KeyPathComponentProperty("some_dict"),
    KeyPathComponentSubscript("subscript")
])

Method calls are also modeled: _.method(arg1, arg2) is an accessor of the form (root) => root.method(arg1, arg2), and is represented internally as:

KeyPath([
    KeyPathComponentProperty("method"),
    KeyPathComponentCall(args=[arg1, arg2], kwargs={})
])

Certain operators are also deferred, for example, you can add two KeyPaths together to form a KeyPath representing the addition: _.x + _.y is an accessor of the form: (root) => root.x + root.y, and is represented internally by mapping the operator to the underlying method calls:

KeyPath([
    KeyPathComponentProperty("x"),
    KeyPathComponentProperty("__add__"),
    KeyPathComponentCall(kwargs={}, args=[
        KeyPath([
            KeyPathComponentProperty("y")
        ])
    ])
])

Finally, KeyPaths can represent chaining a value into the argument of another function. For example, func(_.x _.y).result can be represented as:

BoundKeyPath(
    func,
    [
        KeyPathComponentCall(kwargs={}, args=[
            KeyPath([
                KeyPathComponentProperty("x")
            ]),
            KeyPath([
                KeyPathComponentProperty("y")
            ]),
        ])
        KeyPathComponentProperty("result")
    ]
)

though doing so requires explicit support defined within func to accept the argument keypath(s) and return a BoundKeyPath.

class KeyPathComponentProperty(name: str)

Component of a KeyPath which represents property access. root.property

class KeyPathComponentSubscript(key: str | int)

Component of a KeyPath which represents subscript access. root[“item”]

class KeyPathComponentCall(
args: List[Any],
kwargs: Dict[str, Any],
include_keypath_ctx: bool = False,
)

Component of a KeyPath which represents calling a value as a function. root(arg1, arg2) or root.__call__(arg1, arg2)

class BoundKeyPath(
bound_root,
components: List[KeyPathComponent],
)

Represents a KeyPath where the root is already known. This is useful for when a calling function is known, but the arguments aren’t. See the example at the end of the docs of KeyPath.

class IterItemKeyPath(
base: KeyPath,
components: List[KeyPathComponent],
)

Represents a templated item inside of an iterator context. When KeyPath.__iter__() is called, we must immediately return something, so we return a real list containing a single IterItemKeyPath. Users can chain off that single item to form a more complex expression (like they can for any KeyPath).

When the list is resolved with resolve_all_nested_keypaths, `IterItemKeyPath`s are expanded out – so instead of being a nested list they appear alongside things at the top level.

Take the following example:

[1, 2, *(s + 1 for s in _.some_numbers)]
# translates immediately into
[1, 2, IterItemKeyPath(
    _.some_strings,
    _.__add__(1)
)]
# and during resolve, that single `IterItemKeyPath` may turn into
# multiple values, all at the top level
[1, 2, 3, 4, 5, 6]

Resolving Keypaths

resolve_keypath(root: Any, keypath: KeyPath) Any

Given a root and a KeyPath, resolves the keypath for that root and returns the final result.

If the given keypath argument is not a KeyPath, this will just return it as is, since it already represents a resolved value.

resolve_all_nested_keypaths(root: Any, values) Any

Given a data structure that may have KeyPaths in it, resolves all of them recursively with the provided root. This is helpful for handling collections of values that may or may not be KeyPaths, and turning them all into real values.

resolve_keypath_args_from(root_keypath: KeyPath)

Decorates a function to convert its arguments to KeyPaths, using one of the arguments as the root for the others.

The passed KeyPath is used to point to the root. It must start with a property access against a value matching one of the parameters’ by name.

For example, you can easily allow accessing self properties within a method. Let’s assume I have a method of the form:

def set_primary_date(self, date_column):

and I want to allow consumers to write:

model.set_primary_date(_.timestamp)

instead of:

model.set_primary_date(model.attributes.timestamp)

I can do so by applying the decorator:

@resolve_keypath_args_from(_.self.attributes)
def set_primary_date(self, date_column):
defer_keypath_args(func)

Decorates a function to allows its arguments to be KeyPaths, and if they are, the function as a whole will be deferred as a new KeyPath, which can be run later.

For example, let’s assume I have a method of the form:

def count(column_expression):

and I want to allow consumers to write:

model.with_measure(count(_.user_id))

instead of:

model.with_measure(count(model.attributes.user_id))

I can do so by applying the decorator:

@defer_keypath_args
def count(column_expression):