Skip to content

Commit 7301667

Browse files
authored
feat: Update the AccessEntry class with a new condition attribute and unit tests (googleapis#2163)
* feat: adds condition class and assoc. unit tests * Updates AccessEntry with condition setter/getter * Adds condition attr to AccessEntry and unit tests * adds tests for Condition dunder methods to ensure coverage * moves the entity_type logic out of _from_api_repr to entity_type setter * Updates logic in entity_type getter * updates several AccessEntry related tests * Updates AccessEntry condition setter test to use a dict * udpates entity_id handling * Updates _entity_type access * tweaks type hinting * Update tests/unit/test_dataset.py * Update tests/unit/test_dataset.py * Updates DatasetReference in test and __eq__ check * remove debug print statement
1 parent a1c8e9a commit 7301667

File tree

2 files changed

+432
-30
lines changed

2 files changed

+432
-30
lines changed

google/cloud/bigquery/dataset.py

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,15 @@ def __init__(
298298
role: Optional[str] = None,
299299
entity_type: Optional[str] = None,
300300
entity_id: Optional[Union[Dict[str, Any], str]] = None,
301+
**kwargs,
301302
):
302-
self._properties = {}
303+
self._properties: Dict[str, Any] = {}
303304
if entity_type is not None:
304305
self._properties[entity_type] = entity_id
305306
self._properties["role"] = role
306-
self._entity_type = entity_type
307+
self._entity_type: Optional[str] = entity_type
308+
for prop, val in kwargs.items():
309+
setattr(self, prop, val)
307310

308311
@property
309312
def role(self) -> Optional[str]:
@@ -330,6 +333,9 @@ def dataset(self, value):
330333
if isinstance(value, str):
331334
value = DatasetReference.from_string(value).to_api_repr()
332335

336+
if isinstance(value, DatasetReference):
337+
value = value.to_api_repr()
338+
333339
if isinstance(value, (Dataset, DatasetListItem)):
334340
value = value.reference.to_api_repr()
335341

@@ -437,15 +443,65 @@ def special_group(self) -> Optional[str]:
437443
def special_group(self, value):
438444
self._properties["specialGroup"] = value
439445

446+
@property
447+
def condition(self) -> Optional["Condition"]:
448+
"""Optional[Condition]: The IAM condition associated with this entry."""
449+
value = typing.cast(Dict[str, Any], self._properties.get("condition"))
450+
return Condition.from_api_repr(value) if value else None
451+
452+
@condition.setter
453+
def condition(self, value: Union["Condition", dict, None]):
454+
"""Set the IAM condition for this entry."""
455+
if value is None:
456+
self._properties["condition"] = None
457+
elif isinstance(value, Condition):
458+
self._properties["condition"] = value.to_api_repr()
459+
elif isinstance(value, dict):
460+
self._properties["condition"] = value
461+
else:
462+
raise TypeError("condition must be a Condition object, dict, or None")
463+
440464
@property
441465
def entity_type(self) -> Optional[str]:
442466
"""The entity_type of the entry."""
467+
468+
# The api_repr for an AccessEntry object is expected to be a dict with
469+
# only a few keys. Two keys that may be present are role and condition.
470+
# Any additional key is going to have one of ~eight different names:
471+
# userByEmail, groupByEmail, domain, dataset, specialGroup, view,
472+
# routine, iamMember
473+
474+
# if self._entity_type is None, see if it needs setting
475+
# i.e. is there a key: value pair that should be associated with
476+
# entity_type and entity_id?
477+
if self._entity_type is None:
478+
resource = self._properties.copy()
479+
# we are empyting the dict to get to the last `key: value`` pair
480+
# so we don't keep these first entries
481+
_ = resource.pop("role", None)
482+
_ = resource.pop("condition", None)
483+
484+
try:
485+
# we only need entity_type, because entity_id gets set elsewhere.
486+
entity_type, _ = resource.popitem()
487+
except KeyError:
488+
entity_type = None
489+
490+
self._entity_type = entity_type
491+
443492
return self._entity_type
444493

445494
@property
446495
def entity_id(self) -> Optional[Union[Dict[str, Any], str]]:
447496
"""The entity_id of the entry."""
448-
return self._properties.get(self._entity_type) if self._entity_type else None
497+
if self.entity_type:
498+
entity_type = self.entity_type
499+
else:
500+
return None
501+
return typing.cast(
502+
Optional[Union[Dict[str, Any], str]],
503+
self._properties.get(entity_type, None),
504+
)
449505

450506
def __eq__(self, other):
451507
if not isinstance(other, AccessEntry):
@@ -464,7 +520,16 @@ def _key(self):
464520
Returns:
465521
Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
466522
"""
523+
467524
properties = self._properties.copy()
525+
526+
# Dicts are not hashable.
527+
# Convert condition to a hashable datatype(s)
528+
condition = properties.get("condition")
529+
if isinstance(condition, dict):
530+
condition_key = tuple(sorted(condition.items()))
531+
properties["condition"] = condition_key
532+
468533
prop_tup = tuple(sorted(properties.items()))
469534
return (self.role, self._entity_type, self.entity_id, prop_tup)
470535

@@ -491,19 +556,11 @@ def from_api_repr(cls, resource: dict) -> "AccessEntry":
491556
Returns:
492557
google.cloud.bigquery.dataset.AccessEntry:
493558
Access entry parsed from ``resource``.
494-
495-
Raises:
496-
ValueError:
497-
If the resource has more keys than ``role`` and one additional
498-
key.
499559
"""
500-
entry = resource.copy()
501-
role = entry.pop("role", None)
502-
entity_type, entity_id = entry.popitem()
503-
if len(entry) != 0:
504-
raise ValueError("Entry has unexpected keys remaining.", entry)
505560

506-
return cls(role, entity_type, entity_id)
561+
access_entry = cls()
562+
access_entry._properties = resource.copy()
563+
return access_entry
507564

508565

509566
class Dataset(object):
@@ -1160,6 +1217,43 @@ def from_api_repr(cls, resource: Dict[str, Any]) -> "Condition":
11601217

11611218
return cls(
11621219
expression=resource["expression"],
1163-
title=resource.get("title"),
1164-
description=resource.get("description"),
1220+
title=resource.get("title", None),
1221+
description=resource.get("description", None),
11651222
)
1223+
1224+
def __eq__(self, other: object) -> bool:
1225+
"""Check for equality based on expression, title, and description."""
1226+
if not isinstance(other, Condition):
1227+
return NotImplemented
1228+
return self._key() == other._key()
1229+
1230+
def _key(self):
1231+
"""A tuple key that uniquely describes this field.
1232+
Used to compute this instance's hashcode and evaluate equality.
1233+
Returns:
1234+
Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
1235+
"""
1236+
1237+
properties = self._properties.copy()
1238+
1239+
# Dicts are not hashable.
1240+
# Convert object to a hashable datatype(s)
1241+
prop_tup = tuple(sorted(properties.items()))
1242+
return prop_tup
1243+
1244+
def __ne__(self, other: object) -> bool:
1245+
"""Check for inequality."""
1246+
return not self == other
1247+
1248+
def __hash__(self) -> int:
1249+
"""Generate a hash based on expression, title, and description."""
1250+
return hash(self._key())
1251+
1252+
def __repr__(self) -> str:
1253+
"""Return a string representation of the Condition object."""
1254+
parts = [f"expression={self.expression!r}"]
1255+
if self.title is not None:
1256+
parts.append(f"title={self.title!r}")
1257+
if self.description is not None:
1258+
parts.append(f"description={self.description!r}")
1259+
return f"Condition({', '.join(parts)})"

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy