add get_items
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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
2
layercake/uv.lock
generated
@@ -600,7 +600,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.1.16"
|
||||
version = "0.2.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
|
||||
Reference in New Issue
Block a user