add get_items

This commit is contained in:
2025-04-08 13:44:06 -03:00
parent 68ff6f282b
commit 6450e5fa7c
18 changed files with 393 additions and 195 deletions

View File

@@ -14,6 +14,7 @@ from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
from botocore.exceptions import ClientError
from .dateutils import now, timestamp
from .funcs import omit
TZ = os.getenv('TZ', 'UTC')
PK = os.getenv('DYNAMODB_PARTITION_KEY', 'pk')
@@ -124,6 +125,44 @@ else:
self.delimiter = delimiter
if TYPE_CHECKING:
@dataclass
class SortKey(str):
sk: str
table_name: str | None = None
else:
class SortKey(str):
def __new__(cls, sk: str, *, table_name: str | None = None) -> str:
return super().__new__(cls, sk)
def __init__(self, sk: str, *, table_name: str | None = None) -> None:
# __init__ is used to store the parameters for later reference.
# For immutable types like str, __init__ cannot change the instance's value.
self.sk = sk
self.table_name = table_name
@dataclass
class TransactKey:
"""
Example
-------
>>> TransactKey('e9bb7dc6-c7b2-4d34-8931-d298353758ec')
... + SortKey('0')
... + SortKey('tenant')
"""
pk: str
sk: tuple[SortKey, ...] = ()
def __add__(self, sk: SortKey) -> 'TransactKey':
if isinstance(sk, SortKey):
return TransactKey(pk=self.pk, sk=self.sk + (sk,))
raise TypeError('Can only add a SortKey to a TransactKey')
class Key(ABC, dict):
@abstractmethod
def expr_attr_name(self) -> dict: ...
@@ -485,7 +524,7 @@ class DynamoDBPersistenceLayer:
else:
return True
def transact_get_items(self, transact_items: TransactItems) -> list[dict]:
def transact_get_items(self, transact_items: TransactItems) -> list[dict[str, Any]]:
try:
response = self.dynamodb_client.transact_get_items(
TransactItems=transact_items.items
@@ -521,36 +560,6 @@ class PaginatedResult(TypedDict):
class DynamoDBCollection:
"""
Example
-------
**Get an item using a composed sort key**
collect = DynamoDBCollection(...)
collect.get_item(
KeyPair(
'b3511b5a-cb32-4833-a373-f8223f2088d4',
ComposeKey('sergio@somosbeta.com.br', prefix='emails'),
),
)
**Get items using a composed partition key**
collect = DynamoDBCollection(...)
collect.get_items(
PartitionKey(
ComposeKey('b3511b5a-cb32-4833-a373-f8223f2088d4', prefix='logs')
),
)
**Get items using a key pair**
collect = DynamoDBCollection(...)
collect.get_items(
KeyPair('b3511b5a-cb32-4833-a373-f8223f2088d4', 'emails'),
)
"""
def __init__(
self,
persistence_layer: DynamoDBPersistenceLayer,
@@ -572,6 +581,45 @@ class DynamoDBCollection:
default: Any = None,
delimiter: str = '#',
) -> Any:
"""Get an item with the given key.
Example
-------
**Get an item using a composed sort key**
collect = DynamoDBCollection(...)
collect.get_item(
KeyPair(
'b3511b5a-cb32-4833-a373-f8223f2088d4',
ComposeKey('username@domain.com', prefix='emails'),
),
)
Parameters
----------
key : Key
Key of the item to be retrieved.
path_spec : str, optional
A path specification for nested data extraction, default is None.
raise_on_error : bool, optional
If True, raises an exception when the item is not found, default is True.
exception_cls : Type[Exception], optional
Exception class to be used if the item is not found, default is MissingError.
default : Any, optional
Default value returned if the item is not found, default is None.
delimiter : str, optional
Delimiter used in key composition, default is '#'.
Returns
-------
Any
Data of the retrieved item or the default value if not found.
Raises
------
Exception
Raises the provided exception if the item is not found and raise_on_error is True.
"""
exc_cls = exception_cls or self.exception_cls
data = self.persistence_layer.get_item(key)
@@ -592,6 +640,22 @@ class DynamoDBCollection:
ttl: int | datetime | None = None,
**kwargs: Any,
) -> bool:
"""Creates a new item, or replaces an old item with a new item.
Parameters
----------
key : Key
Key for the item to be inserted or updated.
ttl : int or datetime, optional
Time-to-live for the item, specified as a timestamp integer or datetime object, default is None.
**kwargs
Additional data to be stored with the item.
Returns
-------
bool
True if the operation is successful, False otherwise.
"""
now_ = now(self.tz)
if isinstance(ttl, int):
@@ -620,6 +684,24 @@ class DynamoDBCollection:
expr_attr_names: dict | None = None,
expr_attr_values: dict | None = None,
) -> bool:
"""Deletes a single item in a table by key.
Parameters
----------
key : Key
Key of the item to be deleted.
cond_expr : str, optional
Conditional expression for deletion, default is None.
expr_attr_names : dict, optional
Mapping of attribute names for the expression, default is None.
expr_attr_values : dict, optional
Mapping of attribute values for the expression, default is None.
Returns
-------
bool
True if the item is successfully deleted, False otherwise.
"""
return self.persistence_layer.delete_item(
key=key,
cond_expr=cond_expr,
@@ -627,7 +709,57 @@ class DynamoDBCollection:
expr_attr_values=expr_attr_values,
)
def get_items(
def get_items(self, key: TransactKey) -> dict[str, Any]:
"""Get multiple items via a transaction based on the provided TransactKey.
Example
-------
**Get items using chained sort keys**
key = TransactKey('b3511b5a-cb32-4833-a373-f8223f2088d4') + SortKey('sk-1') + SortKey('sk-2')
collect = DynamoDBCollection(...)
items = collect.get_items(key)
Parameters
----------
key : TransactKey
A TransactKey instance that contains a partition key and one or more sort keys.
If no sort key is provided, the transaction is skipped.
Returns
-------
dict[str, Any]
A dict of items retrieved from the transaction.
"""
# If no sort key is provided, the query is skipped
if not key.sk:
return {}
table_name = self.persistence_layer.table_name
transact = TransactItems(table_name)
# Add a get operation for each sort key for the transaction
for sk in key.sk:
transact.get(
key=KeyPair(key.pk, sk),
table_name=sk.table_name,
)
data, *rest = self.persistence_layer.transact_get_items(transact)
return data | {
k: omit(
(
PK,
SK,
),
item,
)
for k, item in zip(key.sk[1:], rest)
if item
}
def query(
self,
key: PartitionKey | KeyPair,
*,
@@ -638,6 +770,52 @@ class DynamoDBCollection:
index_forward: bool = False,
limit: int = LIMIT,
) -> PaginatedResult:
"""Query returns all items with that partition key or key pair.
Example
-------
**Query using a composed partition key**
collect = DynamoDBCollection(...)
collect.query(
PartitionKey(
ComposeKey('b3511b5a-cb32-4833-a373-f8223f2088d4', prefix='logs')
),
)
**Query using a key pair**
collect = DynamoDBCollection(...)
collect.query(
KeyPair('b3511b5a-cb32-4833-a373-f8223f2088d4', 'emails'),
)
Parameters
----------
key : PartitionKey or KeyPair
Partition key or Key pair used for the query.
expr_attr_name : dict, optional
Additional mapping for attribute names, default is {}.
expr_attr_values : dict, optional
Additional mapping for attribute values, default is {}.
start_key : str, optional
Starting key for pagination, default is None.
filter_expr : str, optional
Filter expression for the query, default is None.
index_forward : bool, optional
Order of the results; True for ascending order, default is False.
limit : int, optional
Maximum number of items to return, default is LIMIT.
Returns
-------
PaginatedResult
Dict containing the queried items and the key for the next batch.
See Also
--------
DynamoDB.Client.query : https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/query.html
"""
key_cond_expr = (
'#pk = :pk AND begins_with(#sk, :sk)'
if isinstance(key, KeyPair)

View File

@@ -1,6 +1,6 @@
[project]
name = "layercake"
version = "0.1.16"
version = "0.2.0"
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
readme = "README.md"
authors = [

View File

@@ -2,3 +2,6 @@
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}}
{"id": {"S": "logs#5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2024-02-08T16:42:33.776409-03:00"}, "action": {"S": "OPEN_EMAIL"}}
{"id": {"S": "logs#5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2019-03-25T00:00:00-03:00"}, "action": {"S": "CLICK_EMAIL"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}, "cnpj": {"S": "15608435000190"}, "email": {"S": "org+15608435000190@users.noreply.betaeducacao.com.br"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policy"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}}

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from decimal import Decimal
from ipaddress import IPv4Address
import pytest
@@ -12,7 +13,9 @@ from layercake.dynamodb import (
KeyPair,
PartitionKey,
PrefixKey,
SortKey,
TransactItems,
TransactKey,
serialize,
)
@@ -189,14 +192,14 @@ def test_collection_delete_item(
)
def test_collection_get_items(
def test_collection_query(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
collect = DynamoDBCollection(dynamodb_persistence_layer)
# This data was added from seeds
logs = collect.get_items(
logs = collect.query(
PartitionKey(
ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='logs'),
),
@@ -219,7 +222,7 @@ def test_collection_get_items(
}
# This data was added from seeds
emails = collect.get_items(
emails = collect.query(
KeyPair('5OxmMjL-ujoR5IMGegQz', PrefixKey('emails')),
)
assert emails == {
@@ -236,3 +239,27 @@ def test_collection_get_items(
],
'last_key': None,
}
def test_collection_get_items(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
collect = DynamoDBCollection(dynamodb_persistence_layer)
doc = collect.get_items(
TransactKey('cJtK9SsnJhKPyxESe7g3DG')
+ SortKey('0')
+ SortKey('billing_policy')
+ SortKey('payment_policy')
)
assert doc == {
'sk': '0',
'name': 'EDUSEG',
'id': 'cJtK9SsnJhKPyxESe7g3DG',
'cnpj': '15608435000190',
'email': 'org+15608435000190@users.noreply.betaeducacao.com.br',
'billing_policy': {'billing_day': Decimal('1'), 'payment_method': 'PIX'},
'payment_policy': {'due_days': Decimal('90')},
}

2
layercake/uv.lock generated
View File

@@ -600,7 +600,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.1.16"
version = "0.2.0"
source = { editable = "." }
dependencies = [
{ name = "arnparse" },