Coverage for dynamodx / keys.py: 84%
81 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-17 19:10 -0300
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-17 19:10 -0300
1from abc import ABC, abstractmethod
2from dataclasses import dataclass
3from typing import Any, Self
6class Key(dict, ABC):
7 @abstractmethod
8 def expr_attr_names(self) -> dict[str, str]: ...
10 @abstractmethod
11 def expr_attr_values(self) -> dict[str, Any]: ...
14class SortKey(str):
15 """
16 SortKey encapsulates a sort key value and optionally stores metadata
17 used for nested data extraction and output transformation.
19 Parameters
20 ----------
21 path_spec : str, optional
22 JMESPath expression used to project nested data from the item.
24 rename_key : str, optional
25 If provided, renames the sort key in the output.
27 projection_expr : str, optional
28 Projection expression associated with the key.
30 expr_attr_names : dict[str, str], optional
31 Attribute name mapping used with projection expressions.
32 """
34 __slots__ = (
35 'sk',
36 'path_spec',
37 'rename_key',
38 'projection_expr',
39 'expr_attr_names',
40 )
42 sk: str | None
43 path_spec: str | None
44 rename_key: str | None
45 projection_expr: str | None
46 expr_attr_names: dict[str, str] | None
48 def __new__(
49 cls,
50 *,
51 path_spec: str | None = None,
52 rename_key: str | None = None,
53 projection_expr: str | None = None,
54 expr_attr_names: dict[str, str] | None = None,
55 **kwargs: str,
56 ) -> Self:
57 if len(kwargs) != 1:
58 raise TypeError(
59 f'SortKey() takes exactly one keyword argument ({len(kwargs)} given)'
60 )
62 ((name_sk, value_sk),) = kwargs.items()
64 obj = super().__new__(cls, value_sk)
66 obj.sk = name_sk
67 obj.path_spec = path_spec
68 obj.rename_key = rename_key
69 obj.projection_expr = projection_expr
70 obj.expr_attr_names = expr_attr_names
72 return obj
75class PartitionKey(Key):
76 """Represents a partition key"""
78 def __init__(
79 self,
80 *,
81 table_name: str | None = None,
82 **kwargs,
83 ) -> None:
84 if len(kwargs) != 1:
85 raise TypeError(
86 f'PartitionKey() takes exactly one key=value argument '
87 f'({len(kwargs)} given)'
88 )
90 ((name_pk, value_pk),) = kwargs.items()
91 super().__init__(**{name_pk: value_pk})
93 self.name_pk = name_pk
94 self.table_name = table_name
96 def expr_attr_names(self) -> dict[str, str]:
97 return {'#pk': self.name_pk}
99 def expr_attr_values(self) -> dict[str, Any]:
100 return {':pk': self[self.name_pk]}
102 def __add__(self, other: SortKey) -> 'PrimaryKey':
103 pk = self.name_pk
104 sk = other.sk
105 kwargs = {
106 pk: self[pk],
107 sk: other,
108 'table_name': self.table_name,
109 }
110 return PrimaryKey(**kwargs)
113class PrimaryKey(Key):
114 """Represents a composite key (partition key and sort key)"""
116 def __init__(
117 self,
118 *,
119 table_name: str | None = None,
120 **kwargs,
121 ) -> None:
122 """
123 Initializes a composite key using partition and sort key.
125 Parameters
126 ----------
127 pk : str
128 The partition key.
129 sk : str
130 The sort key.
131 table_name : str, optional
132 """
133 if len(kwargs) != 2:
134 raise TypeError(
135 f'PrimaryKey() takes exactly two key=value arguments '
136 f'({len(kwargs)} given)'
137 )
139 (name_pk, value_pk), (name_sk, value_sk) = kwargs.items()
140 super().__init__(**{name_pk: value_pk, name_sk: value_sk})
142 self.name_pk = name_pk
143 self.name_sk = name_sk
144 self.table_name = table_name
146 @property
147 def sk(self) -> SortKey | str:
148 return self[self.name_sk]
150 def expr_attr_names(self) -> dict[str, str]:
151 return {
152 '#pk': self.name_pk,
153 '#sk': self.name_sk,
154 }
156 def expr_attr_values(self) -> dict[str, Any]:
157 sk = self[self.name_sk]
158 return {
159 ':pk': self[self.name_pk],
160 ':sk': str(sk) if isinstance(sk, SortKey) else sk,
161 }
163 def __add__(self, other: Self | SortKey) -> 'PrimaryKeySet':
164 if isinstance(other, PrimaryKey):
165 return PrimaryKeySet((self, other))
167 if isinstance(other, SortKey):
168 if other.sk is None:
169 raise ValueError('SortKey name is required')
171 pk, sk = self.name_pk, other.sk
172 kwargs = {pk: self[pk], sk: other}
173 return PrimaryKeySet((self, PrimaryKey(**kwargs)))
175 return NotImplemented
177 def __radd__(self, other: Any):
178 if isinstance(other, PrimaryKeySet):
179 return other + self
181 return NotImplemented
184@dataclass(frozen=True)
185class PrimaryKeySet:
186 pairs: tuple[PrimaryKey, ...] = ()
188 def __add__(self, other: PrimaryKey | SortKey) -> 'PrimaryKeySet':
189 if not isinstance(other, (PrimaryKey, SortKey)):
190 return NotImplemented
192 if isinstance(other, PrimaryKey):
193 return PrimaryKeySet(pairs=self.pairs + (other,))
195 if not self.pairs:
196 raise ValueError('Cannot add SortKey to empty PrimaryKeySet')
198 last_pair = self.pairs[-1]
199 kwargs = {
200 last_pair.name_pk: last_pair[last_pair.name_pk],
201 last_pair.name_sk: other,
202 'table_name': last_pair.table_name,
203 }
204 next_pair = PrimaryKey(**kwargs)
206 if next_pair in self.pairs:
207 return self
209 return PrimaryKeySet(pairs=self.pairs + (next_pair,))