Files
saladeaula.digital/http-api/dynamodb.py
2025-03-20 17:48:37 -03:00

285 lines
6.6 KiB
Python

import base64
import json
import os
import urllib.parse as urlparse
from datetime import datetime
from enum import Enum
from typing import Any, Type
from glom import glom
from layercake.dateutils import now, timestamp
from layercake.dynamodb import DynamoDBPersistenceLayer, Key
DELIMITER = '#'
TZ = os.getenv('TZ', 'UTC')
def json_b64encode(obj: dict) -> str:
s = json.dumps(obj).encode('utf-8')
s = base64.urlsafe_b64encode(s).decode('utf-8')
return urlparse.quote(s)
def json_b64decode(s: str) -> dict:
obj = urlparse.unquote(s).encode('utf-8')
obj = base64.urlsafe_b64decode(obj).decode('utf-8')
return json.loads(obj)
class KeyLoc(Enum):
PK = 'pk' # Partition Key
SK = 'sk' # Sort Key
def _join_prefix(
prefix: str | tuple[str, ...],
delimiter: str,
*args,
) -> str:
"""
Joins a prefix (or prefixes) with additional arguments using a specified delimiter.
Parameters
----------
prefix : str | tuple[str, ...]
A prefix or a tuple of prefixes to be added.
delimiter : str
Delimiter to use for joining the strings.
*args
Additional strings to be joined with the prefix.
Returns
-------
str
A single string with the prefix and additional arguments joined by the delimiter.
"""
if isinstance(prefix, str):
prefix = (prefix,)
return delimiter.join(prefix + args)
def _keys(
*,
pk: str,
sk: str,
prefix: str | tuple[str, ...],
keyloc_prefix: KeyLoc,
delimiter: str,
) -> dict:
"""
Parameters
----------
keyloc_prefix : str | tuple[str, ...]
Specifies whether to add the prefix to the Partition Key or Sort Key.
delimiter : str
Delimiter used to join the prefix and the key.
Returns
-------
dict
A dict with keys id and sk representing the constructed keys for the DynamoDB item.
"""
if keyloc_prefix == KeyLoc.PK:
pk = _join_prefix(prefix, delimiter, pk)
sk = sk
else:
pk = pk
sk = _join_prefix(prefix, delimiter, sk)
return {
'id': pk,
'sk': sk,
}
def _ttl_kwargs(ttl: datetime | int | None = None, tz=None):
if not ttl:
return {}
if isinstance(ttl, int):
return {
'ttl': ttl,
'ttl_date': datetime.fromtimestamp(ttl, tz),
}
return {
'ttl': timestamp(ttl),
'ttl_date': ttl,
}
class MissingError(ValueError):
pass
def get_record(
pk: str,
sk: str | tuple[str, ...],
*,
glom_spec: str | None = None,
raise_on_missing: bool = True,
default_on_missing: Any = None,
missing_cls: Type[Exception] = MissingError,
delimiter: str = DELIMITER,
persistence_layer: DynamoDBPersistenceLayer,
) -> Any:
if not issubclass(missing_cls, Exception):
raise TypeError(
f'missing_cls must be a subclass of Exception, got {missing_cls}'
)
record = persistence_layer.get_item(
Key(
pk,
sk if isinstance(sk, str) else delimiter.join(sk),
)
)
if raise_on_missing and not record:
raise missing_cls(f'Record with Key({pk}, {sk}) not found.')
if glom_spec and record:
return glom(record, glom_spec, default=default_on_missing)
return record or default_on_missing
def add_record(
pk: str,
sk: str,
prefix: str | tuple[str, ...],
*,
keyloc_prefix: KeyLoc = KeyLoc.SK,
ttl: datetime | int | None = None,
delimiter: str = DELIMITER,
persistence_layer: DynamoDBPersistenceLayer,
**kwargs: Any,
) -> bool:
"""
Parameters
----------
keyloc_prefix: str | tuple[str, ...]
Specifies whether to add the prefix to the Partition Key or Sort Key.
delimiter: str
Delimiter used to join the prefix and the key.
Returns
-------
bool
"""
current_time = now(TZ)
keys = _keys(
pk=pk,
sk=sk,
prefix=prefix,
keyloc_prefix=keyloc_prefix,
delimiter=delimiter,
)
return persistence_layer.put_item(
item=keys
| kwargs
| {'create_date': current_time}
| _ttl_kwargs(ttl, current_time.tzinfo)
)
def del_record(
pk: str,
sk: str,
prefix: str | tuple[str, ...],
*,
keyloc_prefix: KeyLoc = KeyLoc.SK,
delimiter: str = DELIMITER,
persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
"""
Parameters
----------
keyloc_prefix: str | tuple[str, ...]
Specifies whether to add the prefix to the Partition Key or Sort Key.
delimiter: str
Delimiter used to join the prefix and the key.
Returns
-------
bool
"""
return persistence_layer.delete_item(
key=_keys(
pk=pk,
sk=sk,
prefix=prefix,
keyloc_prefix=keyloc_prefix,
delimiter=delimiter,
)
)
def get_records(
pk: str,
prefix: str | tuple[str, ...],
*,
keyloc_prefix: KeyLoc = KeyLoc.SK,
expr_attr_name: dict = {},
expr_attr_values: dict = {},
limit: int = 10,
start_key: str | None = None,
filter_expr: str | None = None,
delimiter: str = DELIMITER,
persistence_layer: DynamoDBPersistenceLayer,
) -> dict[str, Any]:
"""
Parameters
----------
pk: str
Partition Key value.
keyloc_prefix: str | tuple[str, ...]
Specifies whether to add the prefix to the Partition Key or Sort Key.
delimiter: str
Delimiter used to join the prefix and the key.
Returns
-------
dict
"""
prefix_ = _join_prefix(prefix, delimiter)
pk = f'{prefix_}{delimiter}{pk}' if keyloc_prefix == KeyLoc.PK else pk
sk = f'{prefix_}{delimiter}'
key_cond_expr = (
'id = :pk'
if keyloc_prefix == KeyLoc.PK
else 'id = :pk AND begins_with(sk, :sk)'
)
expr_attr_values_ = (
{':pk': pk}
if keyloc_prefix == KeyLoc.PK
else {
':pk': pk,
':sk': sk,
}
)
result = persistence_layer.query(
key_cond_expr=key_cond_expr,
expr_attr_name=expr_attr_name,
expr_attr_values=expr_attr_values_ | expr_attr_values,
filter_expr=filter_expr,
limit=limit,
index_forward=False,
start_key=json_b64decode(start_key) if start_key else {},
)
items = (
result['items']
if keyloc_prefix == KeyLoc.PK
# Removes the prefix from sk
else [x | {'sk': x['sk'].removeprefix(sk)} for x in result['items']]
)
return {
'items': items,
'last_key': json_b64encode(result['last_key']) if result['last_key'] else None,
}