2023-02-14 15:03:56 -08:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
#
|
|
|
|
|
# The OpenSearch Contributors require contributions made to
|
|
|
|
|
# this file be licensed under the Apache-2.0 license or a
|
|
|
|
|
# compatible open source license.
|
|
|
|
|
#
|
|
|
|
|
# Modifications Copyright OpenSearch Contributors. See
|
|
|
|
|
# GitHub history for details.
|
|
|
|
|
#
|
|
|
|
|
# Licensed to Elasticsearch B.V. under one or more contributor
|
|
|
|
|
# license agreements. See the NOTICE file distributed with
|
|
|
|
|
# this work for additional information regarding copyright
|
|
|
|
|
# ownership. Elasticsearch B.V. licenses this file to you under
|
|
|
|
|
# the Apache License, Version 2.0 (the "License"); you may
|
|
|
|
|
# not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing,
|
|
|
|
|
# software distributed under the License is distributed on an
|
|
|
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
|
|
# KIND, either express or implied. See the License for the
|
|
|
|
|
# specific language governing permissions and limitations
|
|
|
|
|
# under the License.
|
|
|
|
|
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
import collections.abc as collections_abc
|
2023-02-14 15:03:56 -08:00
|
|
|
from copy import copy
|
2023-11-06 13:08:19 -05:00
|
|
|
from typing import Any, Callable, Dict, Optional, Tuple
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
from opensearchpy.exceptions import UnknownDslObject, ValidationException
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
SKIP_VALUES: Tuple[str, None] = ("", None)
|
2023-02-14 15:03:56 -08:00
|
|
|
EXPAND__TO_DOT = True
|
|
|
|
|
|
|
|
|
|
DOC_META_FIELDS = frozenset(
|
|
|
|
|
(
|
|
|
|
|
"id",
|
|
|
|
|
"routing",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
META_FIELDS = frozenset(
|
|
|
|
|
(
|
|
|
|
|
# OpenSearch metadata fields, except 'type'
|
|
|
|
|
"index",
|
|
|
|
|
"using",
|
|
|
|
|
"score",
|
|
|
|
|
"version",
|
|
|
|
|
"seq_no",
|
|
|
|
|
"primary_term",
|
|
|
|
|
)
|
|
|
|
|
).union(DOC_META_FIELDS)
|
|
|
|
|
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def _wrap(val: Any, obj_wrapper: Optional[Callable[..., Any]] = None) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
if isinstance(val, collections_abc.Mapping):
|
|
|
|
|
return AttrDict(val) if obj_wrapper is None else obj_wrapper(val)
|
|
|
|
|
if isinstance(val, list):
|
|
|
|
|
return AttrList(val)
|
|
|
|
|
return val
|
|
|
|
|
|
|
|
|
|
|
2024-07-20 19:30:37 +03:00
|
|
|
class AttrList:
|
2023-11-06 13:08:19 -05:00
|
|
|
def __init__(
|
|
|
|
|
self, p: Any, obj_wrapper: Optional[Callable[..., Any]] = None
|
|
|
|
|
) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
# make iterables into lists
|
|
|
|
|
if not isinstance(p, list):
|
|
|
|
|
p = list(p)
|
|
|
|
|
self._l_ = p
|
|
|
|
|
self._obj_wrapper = obj_wrapper
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __repr__(self) -> str:
|
2023-02-14 15:03:56 -08:00
|
|
|
return repr(self._l_)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __eq__(self, other: Any) -> bool:
|
2023-02-14 15:03:56 -08:00
|
|
|
if isinstance(other, AttrList):
|
2023-11-06 13:08:19 -05:00
|
|
|
return bool(other._l_ == self._l_)
|
2023-02-14 15:03:56 -08:00
|
|
|
# make sure we still equal to a dict with the same data
|
2023-11-06 13:08:19 -05:00
|
|
|
return bool(other == self._l_)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __ne__(self, other: Any) -> bool:
|
|
|
|
|
return bool(not self == other)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getitem__(self, k: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
p = self._l_[k]
|
|
|
|
|
if isinstance(k, slice):
|
|
|
|
|
return AttrList(p, obj_wrapper=self._obj_wrapper)
|
|
|
|
|
return _wrap(p, self._obj_wrapper)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __setitem__(self, k: Any, value: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
self._l_[k] = value
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __iter__(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return map(lambda i: _wrap(i, self._obj_wrapper), self._l_)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __len__(self) -> int:
|
2023-02-14 15:03:56 -08:00
|
|
|
return len(self._l_)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __nonzero__(self) -> bool:
|
2023-02-14 15:03:56 -08:00
|
|
|
return bool(self._l_)
|
|
|
|
|
|
|
|
|
|
__bool__ = __nonzero__
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getattr__(self, name: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return getattr(self._l_, name)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getstate__(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return self._l_, self._obj_wrapper
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __setstate__(self, state: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
self._l_, self._obj_wrapper = state
|
|
|
|
|
|
|
|
|
|
|
2024-07-20 19:30:37 +03:00
|
|
|
class AttrDict:
|
2023-02-14 15:03:56 -08:00
|
|
|
"""
|
|
|
|
|
Helper class to provide attribute like access (read and write) to
|
|
|
|
|
dictionaries. Used to provide a convenient way to access both results and
|
|
|
|
|
nested dsl dicts.
|
|
|
|
|
"""
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __init__(self, d: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
# assign the inner dict manually to prevent __setattr__ from firing
|
2024-07-20 19:30:37 +03:00
|
|
|
super().__setattr__("_d_", d)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __contains__(self, key: Any) -> bool:
|
2023-02-14 15:03:56 -08:00
|
|
|
return key in self._d_
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __nonzero__(self) -> bool:
|
2023-02-14 15:03:56 -08:00
|
|
|
return bool(self._d_)
|
|
|
|
|
|
|
|
|
|
__bool__ = __nonzero__
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __dir__(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
# introspection for auto-complete in IPython etc
|
|
|
|
|
return list(self._d_.keys())
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __eq__(self, other: Any) -> bool:
|
2023-02-14 15:03:56 -08:00
|
|
|
if isinstance(other, AttrDict):
|
2023-11-06 13:08:19 -05:00
|
|
|
return bool(other._d_ == self._d_)
|
2023-02-14 15:03:56 -08:00
|
|
|
# make sure we still equal to a dict with the same data
|
2023-11-06 13:08:19 -05:00
|
|
|
return bool(other == self._d_)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __ne__(self, other: Any) -> bool:
|
|
|
|
|
return bool(not self == other)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __repr__(self) -> str:
|
2023-02-14 15:03:56 -08:00
|
|
|
r = repr(self._d_)
|
|
|
|
|
if len(r) > 60:
|
|
|
|
|
r = r[:60] + "...}"
|
|
|
|
|
return r
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getstate__(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return (self._d_,)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __setstate__(self, state: Any) -> None:
|
2024-07-20 19:30:37 +03:00
|
|
|
super().__setattr__("_d_", state[0])
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getattr__(self, attr_name: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
try:
|
|
|
|
|
return self.__getitem__(attr_name)
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise AttributeError(
|
|
|
|
|
"{!r} object has no attribute {!r}".format(
|
|
|
|
|
self.__class__.__name__, attr_name
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def get(self, key: Any, default: Any = None) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
try:
|
2024-01-22 09:12:57 -05:00
|
|
|
return self.__getattr__(key) # pylint: disable=unnecessary-dunder-call
|
2023-02-14 15:03:56 -08:00
|
|
|
except AttributeError:
|
|
|
|
|
if default is not None:
|
|
|
|
|
return default
|
|
|
|
|
raise
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __delattr__(self, attr_name: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
try:
|
|
|
|
|
del self._d_[attr_name]
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise AttributeError(
|
|
|
|
|
"{!r} object has no attribute {!r}".format(
|
|
|
|
|
self.__class__.__name__, attr_name
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getitem__(self, key: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return _wrap(self._d_[key])
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __setitem__(self, key: Any, value: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
self._d_[key] = value
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __delitem__(self, key: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
del self._d_[key]
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __setattr__(self, name: Any, value: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
if name in self._d_ or not hasattr(self.__class__, name):
|
|
|
|
|
self._d_[name] = value
|
|
|
|
|
else:
|
|
|
|
|
# there is an attribute on the class (could be property, ..) - don't add it as field
|
2024-07-20 19:30:37 +03:00
|
|
|
super().__setattr__(name, value)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __iter__(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return iter(self._d_)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def to_dict(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return self._d_
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DslMeta(type):
|
|
|
|
|
"""
|
|
|
|
|
Base Metaclass for DslBase subclasses that builds a registry of all classes
|
|
|
|
|
for given DslBase subclass (== all the query types for the Query subclass
|
|
|
|
|
of DslBase).
|
|
|
|
|
|
|
|
|
|
It then uses the information from that registry (as well as `name` and
|
|
|
|
|
`shortcut` attributes from the base class) to construct any subclass based
|
2023-10-09 13:45:18 -04:00
|
|
|
on its name.
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
For typical use see `QueryMeta` and `Query` in `opensearchpy.query`.
|
|
|
|
|
"""
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
_types: Dict[str, Any] = {}
|
2023-02-14 15:03:56 -08:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __init__(cls: Any, name: str, bases: Any, attrs: Any) -> None:
|
|
|
|
|
# TODO: why is it calling itself?!
|
2024-07-20 19:30:37 +03:00
|
|
|
super().__init__(name, bases, attrs)
|
2023-02-14 15:03:56 -08:00
|
|
|
# skip for DslBase
|
|
|
|
|
if not hasattr(cls, "_type_shortcut"):
|
|
|
|
|
return
|
|
|
|
|
if cls.name is None:
|
2023-10-09 13:45:18 -04:00
|
|
|
# abstract base class, register its shortcut
|
2023-02-14 15:03:56 -08:00
|
|
|
cls._types[cls._type_name] = cls._type_shortcut
|
|
|
|
|
# and create a registry for subclasses
|
|
|
|
|
if not hasattr(cls, "_classes"):
|
|
|
|
|
cls._classes = {}
|
|
|
|
|
elif cls.name not in cls._classes:
|
|
|
|
|
# normal class, register it
|
|
|
|
|
cls._classes[cls.name] = cls
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-11-06 13:08:19 -05:00
|
|
|
def get_dsl_type(cls, name: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
try:
|
|
|
|
|
return cls._types[name]
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise UnknownDslObject("DSL type %s does not exist." % name)
|
|
|
|
|
|
|
|
|
|
|
2024-07-20 19:30:37 +03:00
|
|
|
class DslBase(metaclass=DslMeta):
|
2023-02-14 15:03:56 -08:00
|
|
|
"""
|
|
|
|
|
Base class for all DSL objects - queries, filters, aggregations etc. Wraps
|
|
|
|
|
a dictionary representing the object's json.
|
|
|
|
|
|
|
|
|
|
Provides several feature:
|
|
|
|
|
- attribute access to the wrapped dictionary (.field instead of ['field'])
|
|
|
|
|
- _clone method returning a copy of self
|
|
|
|
|
- to_dict method to serialize into dict (to be sent via opensearch-py)
|
|
|
|
|
- basic logical operators (&, | and ~) using a Bool(Filter|Query) TODO:
|
|
|
|
|
move into a class specific for Query/Filter
|
2023-10-09 13:45:18 -04:00
|
|
|
- respects the definition of the class and (de)serializes its
|
2023-02-14 15:03:56 -08:00
|
|
|
attributes based on the `_param_defs` definition (for example turning
|
|
|
|
|
all values in the `must` attribute into Query objects)
|
|
|
|
|
"""
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
_param_defs: Dict[str, Any] = {}
|
|
|
|
|
_params: Dict[str, Any]
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
@classmethod
|
2023-11-06 13:08:19 -05:00
|
|
|
def get_dsl_class(cls: Any, name: Any, default: Optional[bool] = None) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
try:
|
|
|
|
|
return cls._classes[name]
|
|
|
|
|
except KeyError:
|
|
|
|
|
if default is not None:
|
|
|
|
|
return cls._classes[default]
|
|
|
|
|
raise UnknownDslObject(
|
|
|
|
|
"DSL class `{}` does not exist in {}.".format(name, cls._type_name)
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def __init__(self, _expand__to_dot: Any = EXPAND__TO_DOT, **params: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
self._params = {}
|
2024-07-20 19:30:37 +03:00
|
|
|
for pname, pvalue in params.items():
|
2023-02-14 15:03:56 -08:00
|
|
|
if "__" in pname and _expand__to_dot:
|
|
|
|
|
pname = pname.replace("__", ".")
|
|
|
|
|
self._setattr(pname, pvalue)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def _repr_params(self) -> str:
|
2023-02-14 15:03:56 -08:00
|
|
|
"""Produce a repr of all our parameters to be used in __repr__."""
|
|
|
|
|
return ", ".join(
|
|
|
|
|
"{}={!r}".format(n.replace(".", "__"), v)
|
2024-07-20 19:30:37 +03:00
|
|
|
for (n, v) in sorted(self._params.items())
|
2023-02-14 15:03:56 -08:00
|
|
|
# make sure we don't include empty typed params
|
|
|
|
|
if "type" not in self._param_defs.get(n, {}) or v
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __repr__(self) -> str:
|
2023-02-14 15:03:56 -08:00
|
|
|
return "{}({})".format(self.__class__.__name__, self._repr_params())
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __eq__(self, other: Any) -> bool:
|
2023-02-14 15:03:56 -08:00
|
|
|
return isinstance(other, self.__class__) and other.to_dict() == self.to_dict()
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __ne__(self, other: Any) -> bool:
|
2023-02-14 15:03:56 -08:00
|
|
|
return not self == other
|
|
|
|
|
|
2023-11-17 16:54:54 -05:00
|
|
|
def __setattr__(self, name: str, value: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
if name.startswith("_"):
|
2024-07-20 19:30:37 +03:00
|
|
|
return super().__setattr__(name, value)
|
2023-02-14 15:03:56 -08:00
|
|
|
return self._setattr(name, value)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def _setattr(self, name: Any, value: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
# if this attribute has special type assigned to it...
|
|
|
|
|
if name in self._param_defs:
|
|
|
|
|
pinfo = self._param_defs[name]
|
|
|
|
|
|
|
|
|
|
if "type" in pinfo:
|
|
|
|
|
# get the shortcut used to construct this type (query.Q, aggs.A, etc)
|
|
|
|
|
shortcut = self.__class__.get_dsl_type(pinfo["type"])
|
|
|
|
|
|
|
|
|
|
# list of dict(name -> DslBase)
|
|
|
|
|
if pinfo.get("multi") and pinfo.get("hash"):
|
|
|
|
|
if not isinstance(value, (tuple, list)):
|
|
|
|
|
value = (value,)
|
|
|
|
|
value = list(
|
2024-07-20 19:30:37 +03:00
|
|
|
{k: shortcut(v) for (k, v) in obj.items()} for obj in value
|
2023-02-14 15:03:56 -08:00
|
|
|
)
|
|
|
|
|
elif pinfo.get("multi"):
|
|
|
|
|
if not isinstance(value, (tuple, list)):
|
|
|
|
|
value = (value,)
|
|
|
|
|
value = list(map(shortcut, value))
|
|
|
|
|
|
|
|
|
|
# dict(name -> DslBase), make sure we pickup all the objs
|
|
|
|
|
elif pinfo.get("hash"):
|
2024-07-20 19:30:37 +03:00
|
|
|
value = {k: shortcut(v) for (k, v) in value.items()}
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
# single value object, just convert
|
|
|
|
|
else:
|
|
|
|
|
value = shortcut(value)
|
|
|
|
|
self._params[name] = value
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getattr__(self, name: str) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
if name.startswith("_"):
|
|
|
|
|
raise AttributeError(
|
|
|
|
|
"{!r} object has no attribute {!r}".format(
|
|
|
|
|
self.__class__.__name__, name
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
value = None
|
|
|
|
|
try:
|
|
|
|
|
value = self._params[name]
|
|
|
|
|
except KeyError:
|
|
|
|
|
# compound types should never throw AttributeError and return empty
|
|
|
|
|
# container instead
|
|
|
|
|
if name in self._param_defs:
|
|
|
|
|
pinfo = self._param_defs[name]
|
|
|
|
|
if pinfo.get("multi"):
|
|
|
|
|
value = self._params.setdefault(name, [])
|
|
|
|
|
elif pinfo.get("hash"):
|
|
|
|
|
value = self._params.setdefault(name, {})
|
|
|
|
|
if value is None:
|
|
|
|
|
raise AttributeError(
|
|
|
|
|
"{!r} object has no attribute {!r}".format(
|
|
|
|
|
self.__class__.__name__, name
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# wrap nested dicts in AttrDict for convenient access
|
|
|
|
|
if isinstance(value, collections_abc.Mapping):
|
|
|
|
|
return AttrDict(value)
|
|
|
|
|
return value
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def to_dict(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
"""
|
|
|
|
|
Serialize the DSL object to plain dict
|
|
|
|
|
"""
|
|
|
|
|
d = {}
|
2024-07-20 19:30:37 +03:00
|
|
|
for pname, value in self._params.items():
|
2023-02-14 15:03:56 -08:00
|
|
|
pinfo = self._param_defs.get(pname)
|
|
|
|
|
|
|
|
|
|
# typed param
|
|
|
|
|
if pinfo and "type" in pinfo:
|
|
|
|
|
# don't serialize empty lists and dicts for typed fields
|
|
|
|
|
if value in ({}, []):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# list of dict(name -> DslBase)
|
|
|
|
|
if pinfo.get("multi") and pinfo.get("hash"):
|
|
|
|
|
value = list(
|
2024-07-20 19:30:37 +03:00
|
|
|
{k: v.to_dict() for k, v in obj.items()} for obj in value
|
2023-02-14 15:03:56 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# multi-values are serialized as list of dicts
|
|
|
|
|
elif pinfo.get("multi"):
|
|
|
|
|
value = list(map(lambda x: x.to_dict(), value))
|
|
|
|
|
|
|
|
|
|
# squash all the hash values into one dict
|
|
|
|
|
elif pinfo.get("hash"):
|
2024-07-20 19:30:37 +03:00
|
|
|
value = {k: v.to_dict() for k, v in value.items()}
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
# serialize single values
|
|
|
|
|
else:
|
|
|
|
|
value = value.to_dict()
|
|
|
|
|
|
|
|
|
|
# serialize anything with to_dict method
|
|
|
|
|
elif hasattr(value, "to_dict"):
|
|
|
|
|
value = value.to_dict()
|
|
|
|
|
|
|
|
|
|
d[pname] = value
|
|
|
|
|
return {self.name: d}
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def _clone(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
c = self.__class__()
|
|
|
|
|
for attr in self._params:
|
|
|
|
|
c._params[attr] = copy(self._params[attr])
|
|
|
|
|
return c
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HitMeta(AttrDict):
|
2023-11-06 13:08:19 -05:00
|
|
|
def __init__(
|
|
|
|
|
self, document: Dict[str, Any], exclude: Any = ("_source", "_fields")
|
|
|
|
|
) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
d = {
|
|
|
|
|
k[1:] if k.startswith("_") else k: v
|
2024-07-20 19:30:37 +03:00
|
|
|
for (k, v) in document.items()
|
2023-02-14 15:03:56 -08:00
|
|
|
if k not in exclude
|
|
|
|
|
}
|
|
|
|
|
if "type" in d:
|
|
|
|
|
# make sure we are consistent everywhere in python
|
|
|
|
|
d["doc_type"] = d.pop("type")
|
2024-07-20 19:30:37 +03:00
|
|
|
super().__init__(d)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ObjectBase(AttrDict):
|
2023-11-09 10:51:20 -05:00
|
|
|
_doc_type: Any
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __init__(self, meta: Any = None, **kwargs: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
meta = meta or {}
|
|
|
|
|
for k in list(kwargs):
|
|
|
|
|
if k.startswith("_") and k[1:] in META_FIELDS:
|
|
|
|
|
meta[k] = kwargs.pop(k)
|
|
|
|
|
|
|
|
|
|
super(AttrDict, self).__setattr__("meta", HitMeta(meta))
|
|
|
|
|
|
2024-07-20 19:30:37 +03:00
|
|
|
super().__init__(kwargs)
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
@classmethod
|
2023-11-06 13:08:19 -05:00
|
|
|
def __list_fields(cls: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
"""
|
|
|
|
|
Get all the fields defined for our class, if we have an Index, try
|
|
|
|
|
looking at the index mappings as well, mark the fields from Index as
|
|
|
|
|
optional.
|
|
|
|
|
"""
|
|
|
|
|
for name in cls._doc_type.mapping:
|
|
|
|
|
field = cls._doc_type.mapping[name]
|
|
|
|
|
yield name, field, False
|
|
|
|
|
|
|
|
|
|
if hasattr(cls.__class__, "_index"):
|
|
|
|
|
if not cls._index._mapping:
|
|
|
|
|
return
|
|
|
|
|
for name in cls._index._mapping:
|
|
|
|
|
# don't return fields that are in _doc_type
|
|
|
|
|
if name in cls._doc_type.mapping:
|
|
|
|
|
continue
|
|
|
|
|
field = cls._index._mapping[name]
|
|
|
|
|
yield name, field, True
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-11-06 13:08:19 -05:00
|
|
|
def __get_field(cls: Any, name: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
try:
|
|
|
|
|
return cls._doc_type.mapping[name]
|
|
|
|
|
except KeyError:
|
|
|
|
|
# fallback to fields on the Index
|
|
|
|
|
if hasattr(cls, "_index") and cls._index._mapping:
|
|
|
|
|
try:
|
|
|
|
|
return cls._index._mapping[name]
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-11-06 13:08:19 -05:00
|
|
|
def from_opensearch(cls: Any, hit: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
meta = hit.copy()
|
|
|
|
|
data = meta.pop("_source", {})
|
|
|
|
|
doc = cls(meta=meta)
|
|
|
|
|
doc._from_dict(data)
|
|
|
|
|
return doc
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def _from_dict(self, data: Any) -> None:
|
2024-07-20 19:30:37 +03:00
|
|
|
for k, v in data.items():
|
2023-02-14 15:03:56 -08:00
|
|
|
f = self.__get_field(k)
|
|
|
|
|
if f and f._coerce:
|
|
|
|
|
v = f.deserialize(v)
|
|
|
|
|
setattr(self, k, v)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getstate__(self) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
return self.to_dict(), self.meta._d_
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __setstate__(self, state: Any) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
data, meta = state
|
|
|
|
|
super(AttrDict, self).__setattr__("_d_", {})
|
|
|
|
|
super(AttrDict, self).__setattr__("meta", HitMeta(meta))
|
|
|
|
|
self._from_dict(data)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getattr__(self, name: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
try:
|
2024-07-20 19:30:37 +03:00
|
|
|
return super().__getattr__(name)
|
2023-02-14 15:03:56 -08:00
|
|
|
except AttributeError:
|
|
|
|
|
f = self.__get_field(name)
|
|
|
|
|
if hasattr(f, "empty"):
|
|
|
|
|
value = f.empty()
|
|
|
|
|
if value not in SKIP_VALUES:
|
|
|
|
|
setattr(self, name, value)
|
|
|
|
|
value = getattr(self, name)
|
|
|
|
|
return value
|
|
|
|
|
raise
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def to_dict(self, skip_empty: Optional[bool] = True) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
out = {}
|
2024-07-20 19:30:37 +03:00
|
|
|
for k, v in self._d_.items():
|
2023-02-14 15:03:56 -08:00
|
|
|
# if this is a mapped field,
|
|
|
|
|
f = self.__get_field(k)
|
|
|
|
|
if f and f._coerce:
|
|
|
|
|
v = f.serialize(v)
|
|
|
|
|
|
|
|
|
|
# if someone assigned AttrList, unwrap it
|
|
|
|
|
if isinstance(v, AttrList):
|
|
|
|
|
v = v._l_
|
|
|
|
|
|
|
|
|
|
if skip_empty:
|
|
|
|
|
# don't serialize empty values
|
|
|
|
|
# careful not to include numeric zeros
|
|
|
|
|
if v in ([], {}, None):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
out[k] = v
|
|
|
|
|
return out
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def clean_fields(self) -> None:
|
|
|
|
|
errors: Dict[str, Any] = {}
|
2023-02-14 15:03:56 -08:00
|
|
|
for name, field, optional in self.__list_fields():
|
|
|
|
|
data = self._d_.get(name, None)
|
|
|
|
|
if data is None and optional:
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
# save the cleaned value
|
|
|
|
|
data = field.clean(data)
|
|
|
|
|
except ValidationException as e:
|
|
|
|
|
errors.setdefault(name, []).append(e)
|
|
|
|
|
|
|
|
|
|
if name in self._d_ or data not in ([], {}, None):
|
|
|
|
|
self._d_[name] = data
|
|
|
|
|
|
|
|
|
|
if errors:
|
|
|
|
|
raise ValidationException(errors)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def clean(self) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
pass
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def full_clean(self) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
self.clean_fields()
|
|
|
|
|
self.clean()
|
|
|
|
|
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def merge(data: Any, new_data: Any, raise_on_conflict: bool = False) -> None:
|
2023-02-14 15:03:56 -08:00
|
|
|
if not (
|
|
|
|
|
isinstance(data, (AttrDict, collections_abc.Mapping))
|
|
|
|
|
and isinstance(new_data, (AttrDict, collections_abc.Mapping))
|
|
|
|
|
):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"You can only merge two dicts! Got {!r} and {!r} instead.".format(
|
|
|
|
|
data, new_data
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
if not isinstance(new_data, Dict):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"You can only merge two dicts! Got {!r} and {!r} instead.".format(
|
|
|
|
|
data, new_data
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2024-07-20 19:30:37 +03:00
|
|
|
for key, value in new_data.items():
|
2023-02-14 15:03:56 -08:00
|
|
|
if (
|
|
|
|
|
key in data
|
|
|
|
|
and isinstance(data[key], (AttrDict, collections_abc.Mapping))
|
|
|
|
|
and isinstance(value, (AttrDict, collections_abc.Mapping))
|
|
|
|
|
):
|
|
|
|
|
merge(data[key], value, raise_on_conflict)
|
|
|
|
|
elif key in data and data[key] != value and raise_on_conflict:
|
|
|
|
|
raise ValueError("Incompatible data for key %r, cannot be merged." % key)
|
|
|
|
|
else:
|
2023-11-06 13:08:19 -05:00
|
|
|
data[key] = value # type: ignore
|
2023-02-14 15:03:56 -08:00
|
|
|
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def recursive_to_dict(data: Any) -> Any:
|
2023-02-14 15:03:56 -08:00
|
|
|
"""Recursively transform objects that potentially have .to_dict()
|
|
|
|
|
into dictionary literals by traversing AttrList, AttrDict, list,
|
|
|
|
|
tuple, and Mapping types.
|
|
|
|
|
"""
|
|
|
|
|
if isinstance(data, AttrList):
|
|
|
|
|
data = list(data._l_)
|
|
|
|
|
elif hasattr(data, "to_dict"):
|
|
|
|
|
data = data.to_dict()
|
|
|
|
|
if isinstance(data, (list, tuple)):
|
|
|
|
|
return type(data)(recursive_to_dict(inner) for inner in data)
|
|
|
|
|
elif isinstance(data, collections_abc.Mapping):
|
|
|
|
|
return {key: recursive_to_dict(val) for key, val in data.items()}
|
|
|
|
|
return data
|