Content-Length: 641067 | pFad | https://github.com/googleapis/python-bigquery/commit/7301667272dfbdd04b1a831418a9ad2d037171fb

FF feat: Update the AccessEntry class with a new condition attribute and… · googleapis/python-bigquery@7301667 · GitHub
Skip to content

Commit 7301667

Browse files
authored
feat: Update the AccessEntry class with a new condition attribute and unit tests (#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)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/googleapis/python-bigquery/commit/7301667272dfbdd04b1a831418a9ad2d037171fb

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy