Skip to content

Commit ccd3745

Browse files
authored
Implementing BlobProperty in ndb. (googleapis#6398)
1 parent d94b13d commit ccd3745

3 files changed

Lines changed: 382 additions & 8 deletions

File tree

ndb/MIGRATION_NOTES.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ The primary differences come from:
5757
the limit has been raised by the backend. (FWIW, Danny's opinion is that
5858
the backend should enforce these limits, not the library.)
5959
- `Property.__creation_counter_global` has been removed as it seems to have
60-
been included for a feature that was never implemented. See
60+
been included for a feature that was never implemented. See
6161
[Issue #175][1] for original rationale for including it and [Issue #6317][2]
6262
for discussion of its removal.
6363
- `ndb` uses "private" instance attributes in many places, e.g. `Key.__app`.
@@ -86,6 +86,15 @@ The primary differences come from:
8686
- `eventloop` has been renamed to `_eventloop`. It is believed that `eventloop`
8787
was previously a *de facto* private module, so we've just made that
8888
explicit.
89+
- `BlobProperty._datastore_type` has not been implemented; the base class
90+
implementation is sufficient. The original implementation wrapped a byte
91+
string in a `google.appengine.api.datastore_types.ByteString` instance, but
92+
that type was mostly an alias for `str` in Python 2
93+
- `BlobProperty._validate` used to special case for "too long when indexed"
94+
if `isinstance(self, TextProperty)`. We have removed this check since
95+
the implementation does the same check in `TextProperty._validate`.
96+
- The `BlobProperty` constructor only sets `_compressed` if explicitly
97+
passed. The original set always (and used `False` as default)
8998

9099
## Comments
91100

ndb/src/google/cloud/ndb/model.py

Lines changed: 203 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717

1818
import inspect
19+
import zlib
1920

2021
from google.cloud.ndb import exceptions
2122
from google.cloud.ndb import key as key_module
@@ -80,6 +81,7 @@
8081
]
8182

8283

84+
_MAX_STRING_LENGTH = 1500
8385
Key = key_module.Key
8486
BlobKey = NotImplemented # From `google.appengine.api.datastore_types`
8587
GeoPt = NotImplemented # From `google.appengine.api.datastore_types`
@@ -248,7 +250,7 @@ def __repr__(self):
248250
)
249251

250252
def __eq__(self, other):
251-
"""Compare two indexes."""
253+
"""Compare two index states."""
252254
if not isinstance(other, IndexState):
253255
return NotImplemented
254256

@@ -611,11 +613,11 @@ def _verify_validator(validator):
611613
``value + "$"`` is not.
612614
613615
Args:
614-
validator (Callable[[.Property, Any], bool]): A callable that can
616+
validator (Callable[[Property, Any], bool]): A callable that can
615617
validate a property value.
616618
617619
Returns:
618-
Callable[[.Property, Any], bool]: The ``validator``.
620+
Callable[[Property, Any], bool]: The ``validator``.
619621
620622
Raises:
621623
TypeError: If ``validator`` is not callable. This is determined by
@@ -1627,10 +1629,206 @@ def __init__(self, *args, **kwargs):
16271629
raise NotImplementedError
16281630

16291631

1632+
class _CompressedValue:
1633+
"""A marker object wrapping compressed values.
1634+
1635+
Args:
1636+
z_val (bytes): A return value of ``zlib.compress``.
1637+
"""
1638+
1639+
__slots__ = ("z_val",)
1640+
1641+
def __init__(self, z_val):
1642+
self.z_val = z_val
1643+
1644+
def __repr__(self):
1645+
return "_CompressedValue({!r})".format(self.z_val)
1646+
1647+
def __eq__(self, other):
1648+
"""Compare two compressed values."""
1649+
if not isinstance(other, _CompressedValue):
1650+
return NotImplemented
1651+
1652+
return self.z_val == other.z_val
1653+
1654+
def __ne__(self, other):
1655+
"""Inequality comparison operation."""
1656+
return not self == other
1657+
1658+
def __hash__(self):
1659+
raise TypeError("_CompressedValue is not immutable")
1660+
1661+
16301662
class BlobProperty(Property):
1631-
__slots__ = ()
1663+
"""A property that contains values that are byte strings.
16321664
1633-
def __init__(self, *args, **kwargs):
1665+
.. note::
1666+
1667+
Unlike most property types, a :class:`BlobProperty` is **not**
1668+
indexed by default.
1669+
1670+
Args:
1671+
name (str): The name of the property.
1672+
compressed (bool): Indicates if the value should be compressed (via
1673+
``zlib``).
1674+
indexed (bool): Indicates if the value should be indexed.
1675+
repeated (bool): Indicates if this property is repeated, i.e. contains
1676+
multiple values.
1677+
required (bool): Indicates if this property is required on the given
1678+
model type.
1679+
default (bytes): The default value for this property.
1680+
choices (Iterable[bytes]): A container of allowed values for this
1681+
property.
1682+
validator (Callable[[Property, Any], bool]): A validator to be used
1683+
to check values.
1684+
verbose_name (str): A longer, user-friendly name for this property.
1685+
write_empty_list (bool): Indicates if an empty list should be written
1686+
to the datastore.
1687+
1688+
Raises:
1689+
NotImplementedError: If the property is both compressed and indexed.
1690+
"""
1691+
1692+
_indexed = False
1693+
_compressed = False
1694+
1695+
def __init__(
1696+
self,
1697+
name=None,
1698+
compressed=None,
1699+
*,
1700+
indexed=None,
1701+
repeated=None,
1702+
required=None,
1703+
default=None,
1704+
choices=None,
1705+
validator=None,
1706+
verbose_name=None,
1707+
write_empty_list=None
1708+
):
1709+
super(BlobProperty, self).__init__(
1710+
name=name,
1711+
indexed=indexed,
1712+
repeated=repeated,
1713+
required=required,
1714+
default=default,
1715+
choices=choices,
1716+
validator=validator,
1717+
verbose_name=verbose_name,
1718+
write_empty_list=write_empty_list,
1719+
)
1720+
if compressed is not None:
1721+
self._compressed = compressed
1722+
if self._compressed and self._indexed:
1723+
raise NotImplementedError(
1724+
"BlobProperty {} cannot be compressed and "
1725+
"indexed at the same time.".format(self._name)
1726+
)
1727+
1728+
def _value_to_repr(self, value):
1729+
"""Turn the value into a user friendly representation.
1730+
1731+
.. note::
1732+
1733+
This will truncate the value based on the "visual" length, e.g.
1734+
if it contains many ``\\xXX`` or ``\\uUUUU`` sequences, those
1735+
will count against the length as more than one character.
1736+
1737+
Args:
1738+
value (Any): The value to convert to a pretty-print ``repr``.
1739+
1740+
Returns:
1741+
str: The ``repr`` of the "true" value.
1742+
"""
1743+
long_repr = super(BlobProperty, self)._value_to_repr(value)
1744+
if len(long_repr) > _MAX_STRING_LENGTH + 4:
1745+
# Truncate, assuming the final character is the closing quote.
1746+
long_repr = long_repr[:_MAX_STRING_LENGTH] + "..." + long_repr[-1]
1747+
return long_repr
1748+
1749+
def _validate(self, value):
1750+
"""Validate a ``value`` before setting it.
1751+
1752+
Args:
1753+
value (bytes): The value to check.
1754+
1755+
Raises:
1756+
.BadValueError: If ``value`` is not a :class:`bytes`.
1757+
.BadValueError: If the current property is indexed but the value
1758+
exceeds the maximum length (1500 bytes).
1759+
"""
1760+
if not isinstance(value, bytes):
1761+
raise exceptions.BadValueError(
1762+
"Expected bytes, got {!r}".format(value)
1763+
)
1764+
1765+
if self._indexed and len(value) > _MAX_STRING_LENGTH:
1766+
raise exceptions.BadValueError(
1767+
"Indexed value {} must be at most {:d} "
1768+
"bytes".format(self._name, _MAX_STRING_LENGTH)
1769+
)
1770+
1771+
def _to_base_type(self, value):
1772+
"""Convert a value to the "base" value type for this property.
1773+
1774+
Args:
1775+
value (bytes): The value to be converted.
1776+
1777+
Returns:
1778+
Optional[bytes]: The converted value. If the current property is
1779+
compressed, this will return a wrapped version of the compressed
1780+
value. Otherwise, it will return :data:`None` to indicate that
1781+
the value didn't need to be converted.
1782+
"""
1783+
if self._compressed:
1784+
return _CompressedValue(zlib.compress(value))
1785+
1786+
def _from_base_type(self, value):
1787+
"""Convert a value from the "base" value type for this property.
1788+
1789+
Args:
1790+
value (bytes): The value to be converted.
1791+
1792+
Returns:
1793+
Optional[bytes]: The converted value. If the current property is
1794+
a (wrapped) compressed value, this will unwrap the value and return
1795+
the decompressed form. Otherwise, it will return :data:`None` to
1796+
indicate that the value didn't need to be unwrapped and
1797+
decompressed.
1798+
"""
1799+
if isinstance(value, _CompressedValue):
1800+
return zlib.decompress(value.z_val)
1801+
1802+
def _db_set_value(self, v, unused_p, value):
1803+
"""Helper for :meth:`_serialize`.
1804+
1805+
Raises:
1806+
NotImplementedError: Always. This method is virtual.
1807+
"""
1808+
raise NotImplementedError
1809+
1810+
def _db_set_compressed_meaning(self, p):
1811+
"""Helper for :meth:`_db_set_value`.
1812+
1813+
Raises:
1814+
NotImplementedError: Always. This method is virtual.
1815+
"""
1816+
raise NotImplementedError
1817+
1818+
def _db_set_uncompressed_meaning(self, p):
1819+
"""Helper for :meth:`_db_set_value`.
1820+
1821+
Raises:
1822+
NotImplementedError: Always. This method is virtual.
1823+
"""
1824+
raise NotImplementedError
1825+
1826+
def _db_get_value(self, v, unused_p):
1827+
"""Helper for :meth:`_deserialize`.
1828+
1829+
Raises:
1830+
NotImplementedError: Always. This method is virtual.
1831+
"""
16341832
raise NotImplementedError
16351833

16361834

0 commit comments

Comments
 (0)