Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ replacements:
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
)
from google.cloud.firestore_v1.client import Client
Expand Down Expand Up @@ -183,6 +184,7 @@ replacements:
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"Client",
"CountAggregation",
Expand Down Expand Up @@ -261,6 +263,7 @@ replacements:
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
Client,
CollectionGroup,
Expand Down Expand Up @@ -324,6 +327,7 @@ replacements:
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"Client",
"CountAggregation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
Client,
CollectionGroup,
Expand Down Expand Up @@ -103,6 +104,7 @@
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"Client",
"CountAggregation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
)
from google.cloud.firestore_v1.client import Client
Expand Down Expand Up @@ -160,6 +161,7 @@
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONRegex",
"BSONTimestamp",
"Client",
"CountAggregation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"BSONInt32",
"BSONBinary",
"BSONTimestamp",
"BSONRegex",
]

_OBJECT_ID_BYTES_LEN = 12
Expand Down Expand Up @@ -342,3 +343,65 @@ def __eq__(self, other: Any) -> bool:

def __hash__(self) -> int:
return hash((type(self), self._seconds, self._increment))


class BSONRegex(_BSONType):
"""Represents a BSON Regular Expression container for Firestore.

Args:
pattern (str): The regular expression pattern string.
options (str, optional): BSON regex option flags as a string
(e.g. "i", "m", "s", "x", "u"). Defaults to "".

Raises:
TypeError: If pattern is not a string or options is not a string.

Example:
>>> regex = BSONRegex("^hello.*$", options="i")
>>> regex.pattern
'^hello.*$'
>>> regex.options
'i'
"""

__slots__ = ("_pattern", "_options")

def __init__(self, pattern: str, options: str = ""):
if not isinstance(pattern, str):
raise TypeError("BSONRegex pattern must be a str.")

if not isinstance(options, str):
raise TypeError("BSONRegex options must be a str.")

self._pattern: str = pattern
self._options: str = "".join(sorted(set(options)))

@property
def pattern(self) -> str:
"""str: The regular expression pattern string."""
return self._pattern

@property
def options(self) -> str:
"""str: The normalized BSON regex option flags sorted alphabetically."""
return self._options

def _to_map_value(self) -> Dict[str, Dict[str, str]]:
"""Returns map dictionary representation for wire serialization."""
return {
"__regex__": {
"pattern": self._pattern,
"options": self._options,
}
}

def __repr__(self) -> str:
return f"BSONRegex({self._pattern!r}, options={self._options!r})"

def __eq__(self, other: Any) -> bool:
if isinstance(other, BSONRegex):
return self._pattern == other._pattern and self._options == other._options
return NotImplemented

def __hash__(self) -> int:
return hash((type(self), self._pattern, self._options))
8 changes: 8 additions & 0 deletions packages/google-cloud-firestore/tests/system/test_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
)
from google.cloud.firestore_v1.vector import Vector
Expand Down Expand Up @@ -1296,6 +1297,7 @@ def test_bson_document_writes(client, cleanup, database):
"int32_val": BSONInt32(42),
"binary_val_sub128": BSONBinary(b"world", subtype=128),
"timestamp_val": BSONTimestamp(1700000000, 1),
"regex_val": BSONRegex("^hello.*$", options="i"),
Comment thread
ohmayr marked this conversation as resolved.
}

doc_ref.set(bson_payload)
Expand All @@ -1314,6 +1316,12 @@ def test_bson_document_writes(client, cleanup, database):
"increment": 1,
}
},
"regex_val": {
"__regex__": {
"pattern": "^hello.*$",
"options": "i",
}
},
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
)
from google.cloud.firestore_v1.query_profile import (
Expand Down Expand Up @@ -1269,6 +1270,7 @@ async def test_async_bson_document_writes(client, cleanup, database):
"int32_val": BSONInt32(42),
"binary_val_sub128": BSONBinary(b"world", subtype=128),
"timestamp_val": BSONTimestamp(1700000000, 1),
"regex_val": BSONRegex("^hello.*$", options="i"),
}

await doc_ref.set(bson_payload)
Expand All @@ -1287,6 +1289,12 @@ async def test_async_bson_document_writes(client, cleanup, database):
"increment": 1,
}
},
"regex_val": {
"__regex__": {
"pattern": "^hello.*$",
"options": "i",
}
},
}


Expand Down
69 changes: 69 additions & 0 deletions packages/google-cloud-firestore/tests/unit/v1/test_bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONRegex,
BSONTimestamp,
_BSONType,
)
Expand Down Expand Up @@ -411,3 +412,71 @@ def test_bson_timestamp_copy():
def test_bson_timestamp_pickle():
ts = BSONTimestamp(100, 1)
assert pickle.loads(pickle.dumps(ts)) == ts


def test_bson_regex_valid():
rx = BSONRegex("^hello.*$", options="i")
assert rx.pattern == "^hello.*$"
assert rx.options == "i"
assert rx._to_map_value() == {
"__regex__": {
"pattern": "^hello.*$",
"options": "i",
}
}
assert repr(rx) == "BSONRegex('^hello.*$', options='i')"


def test_bson_regex_options_sorting_and_deduplication():
rx1 = BSONRegex("foo", options="msi")
assert rx1.options == "ims"

rx2 = BSONRegex("foo", options="mmiis")
assert rx2.options == "ims"

rx3 = BSONRegex("foo", options="xl")
assert rx3.options == "lx"


@pytest.mark.parametrize(
"pattern_input, options_input, exc_type, match_msg",
[
(123, "i", TypeError, "pattern must be a str"),
(None, "i", TypeError, "pattern must be a str"),
("foo", 123, TypeError, "options must be a str"),
("foo", True, TypeError, "options must be a str"),
("foo", [1, 2], TypeError, "options must be a str"),
],
)
def test_bson_regex_invalid_inputs(pattern_input, options_input, exc_type, match_msg):
with pytest.raises(exc_type, match=match_msg):
BSONRegex(pattern_input, options_input)


def test_bson_regex_equality():
rx1 = BSONRegex("^abc", options="i")
rx2 = BSONRegex("^abc", options="i")
rx3 = BSONRegex("^abc", options="m")
rx4 = BSONRegex("^xyz", options="i")
assert rx1 == rx2
assert rx1 != rx3
assert rx1 != rx4
assert rx1 != "^abc"


def test_bson_regex_hash_and_dict_key():
rx1 = BSONRegex("^abc", options="i")
rx2 = BSONRegex("^abc", options="i")
assert hash(rx1) == hash(rx2)
assert len({rx1, rx2}) == 1


def test_bson_regex_copy():
rx = BSONRegex("^abc", options="i")
assert copy.copy(rx) == rx
assert copy.deepcopy(rx) == rx


def test_bson_regex_pickle():
rx = BSONRegex("^abc", options="i")
assert pickle.loads(pickle.dumps(rx)) == rx
Loading