@aws/nx-plugin-mcp 1.0.0-rc.13 → 1.0.0-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/aws-nx-mcp.js +28 -1
- package/docs/guides/connection/py-agent-dynamodb.mdx +116 -0
- package/docs/guides/connection/py-fast-api-dynamodb.mdx +56 -0
- package/docs/guides/connection/py-mcp-server-dynamodb.mdx +116 -0
- package/docs/guides/connection/smithy-dynamodb.mdx +2 -2
- package/docs/guides/connection/trpc-dynamodb.mdx +2 -2
- package/docs/guides/connection/ts-agent-dynamodb.mdx +2 -2
- package/docs/guides/connection/ts-mcp-server-dynamodb.mdx +2 -2
- package/docs/guides/connection.mdx +30 -4
- package/docs/guides/py-dynamodb.mdx +449 -0
- package/docs/guides/ts-dynamodb.mdx +21 -250
- package/docs/snippets/connection/py-dynamodb-local-development.mdx +7 -0
- package/docs/snippets/dynamodb/deploying-table.mdx +171 -0
- package/docs/snippets/dynamodb/gsi-config.mdx +38 -0
- package/docs/snippets/dynamodb/infrastructure.mdx +33 -0
- package/docs/snippets/dynamodb/serve-local-start.mdx +13 -0
- package/docs/snippets/dynamodb/serve-local-windows.mdx +15 -0
- package/generators.json +28 -1
- package/package.json +1 -1
- package/src/py/dynamodb/agent-connection/schema.json +22 -0
- package/src/py/dynamodb/fast-api-connection/schema.json +18 -0
- package/src/py/dynamodb/mcp-server-connection/schema.json +22 -0
- package/src/py/dynamodb/schema.json +70 -0
- package/src/ts/dynamodb/schema.json +20 -2
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: py#dynamodb
|
|
3
|
+
description: Create a Python DynamoDB project
|
|
4
|
+
generator: py#dynamodb
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import { FileTree } from '@astrojs/starlight/components';
|
|
8
|
+
import Link from '@components/link.astro';
|
|
9
|
+
import RunGenerator from '@components/run-generator.astro';
|
|
10
|
+
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
11
|
+
import Snippet from '@components/snippet.astro';
|
|
12
|
+
|
|
13
|
+
This generator creates a new Python project backed by [Amazon DynamoDB](https://aws.amazon.com/dynamodb/), using [PynamoDB](https://pynamodb.readthedocs.io/) for entity modelling. It generates the application code and infrastructure needed to provision and manage a DynamoDB table using AWS CDK or Terraform, with single-table design support and built-in local development via DynamoDB Local.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Generate a DynamoDB Project
|
|
18
|
+
|
|
19
|
+
<RunGenerator generator="py#dynamodb" />
|
|
20
|
+
|
|
21
|
+
### Options
|
|
22
|
+
|
|
23
|
+
<GeneratorParameters generator="py#dynamodb" />
|
|
24
|
+
|
|
25
|
+
## Generator Output
|
|
26
|
+
|
|
27
|
+
The generator creates the following project structure in the `<directory>/<name>` directory:
|
|
28
|
+
|
|
29
|
+
<FileTree>
|
|
30
|
+
- \<name>
|
|
31
|
+
- \_\_init\_\_.py Package exports
|
|
32
|
+
- client.py DynamoDB client and table name resolution
|
|
33
|
+
- entities
|
|
34
|
+
- base.py Base PynamoDB model with GSI declarations
|
|
35
|
+
- example.py Example entity definition
|
|
36
|
+
- \_\_init\_\_.py Entity exports
|
|
37
|
+
- config.json Table configuration including GSI definitions and local development settings
|
|
38
|
+
- project.json Project configuration and build targets
|
|
39
|
+
</FileTree>
|
|
40
|
+
|
|
41
|
+
The local development scripts are shared across all DynamoDB projects (both TypeScript and Python) and generated once into:
|
|
42
|
+
|
|
43
|
+
<FileTree>
|
|
44
|
+
- packages/common/scripts/src/dynamodb
|
|
45
|
+
- create-local-table.ts Creates the DynamoDB table in the local DynamoDB Local instance
|
|
46
|
+
- pull-image.ts Pulls the DynamoDB Local image
|
|
47
|
+
- start-container.ts Starts the DynamoDB Local container
|
|
48
|
+
</FileTree>
|
|
49
|
+
|
|
50
|
+
### Infrastructure
|
|
51
|
+
|
|
52
|
+
<Snippet name="dynamodb/infrastructure" />
|
|
53
|
+
|
|
54
|
+
## Local Development
|
|
55
|
+
|
|
56
|
+
### Starting Local DynamoDB
|
|
57
|
+
|
|
58
|
+
<Snippet name="dynamodb/serve-local-start" />
|
|
59
|
+
|
|
60
|
+
### Data Modelling
|
|
61
|
+
|
|
62
|
+
The generated project uses [PynamoDB](https://pynamodb.readthedocs.io/) for entity modelling. All entities **must** inherit from the generated `BaseModel` — it resolves the correct DynamoDB table name at runtime, reading from AWS AppConfig when deployed or from `config.json` when running locally via DynamoDB Local. Without this, PynamoDB will not know which table to use. `BaseModel` also uses [PynamoDB's polymorphism support](https://pynamodb.readthedocs.io/en/stable/polymorphism.html) to store multiple entity types in a single table, following [DynamoDB's single-table design](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/data-modeling-foundations.html).
|
|
63
|
+
|
|
64
|
+
Add or update entity files under `<name>/entities/`, using the generated example entity as a starting point:
|
|
65
|
+
|
|
66
|
+
```python title="packages/my_table/my_table/entities/example.py"
|
|
67
|
+
from collections.abc import Iterator
|
|
68
|
+
from datetime import UTC, datetime
|
|
69
|
+
from pynamodb.attributes import UnicodeAttribute
|
|
70
|
+
from .base import BaseModel
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ExampleModel(BaseModel, discriminator='ExampleModel'):
|
|
74
|
+
"""
|
|
75
|
+
Key design:
|
|
76
|
+
pk=EXAMPLE#<id>, sk=EXAMPLE#<id>
|
|
77
|
+
gsi1pk=CATEGORY#<cat>, gsi1sk=EXAMPLE#<id> <- list items by category
|
|
78
|
+
gsi2pk=EXAMPLE, gsi2sk=<created_at> <- list all items by date
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name = UnicodeAttribute()
|
|
82
|
+
category = UnicodeAttribute()
|
|
83
|
+
created_at = UnicodeAttribute()
|
|
84
|
+
updated_at = UnicodeAttribute()
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def make_pk(cls, id: str) -> str:
|
|
88
|
+
return f'EXAMPLE#{id}'
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def create(cls, id: str, name: str, category: str) -> 'ExampleModel':
|
|
92
|
+
now = datetime.now(UTC).isoformat()
|
|
93
|
+
item = cls(
|
|
94
|
+
pk=cls.make_pk(id),
|
|
95
|
+
sk=cls.make_pk(id),
|
|
96
|
+
gsi1pk=f'CATEGORY#{category}',
|
|
97
|
+
gsi1sk=cls.make_pk(id),
|
|
98
|
+
gsi2pk='EXAMPLE',
|
|
99
|
+
gsi2sk=now,
|
|
100
|
+
name=name,
|
|
101
|
+
category=category,
|
|
102
|
+
created_at=now,
|
|
103
|
+
updated_at=now,
|
|
104
|
+
)
|
|
105
|
+
item.save()
|
|
106
|
+
return item
|
|
107
|
+
|
|
108
|
+
# ── Primary index ─────────────────────────────────────────────────────────
|
|
109
|
+
@classmethod
|
|
110
|
+
def get_by_id(cls, id: str) -> 'ExampleModel':
|
|
111
|
+
return cls.get(cls.make_pk(id), cls.make_pk(id))
|
|
112
|
+
|
|
113
|
+
# ── gsi1_index: partition=category, sort=id ───────────────────────────────
|
|
114
|
+
@classmethod
|
|
115
|
+
def list_by_category(cls, category: str) -> Iterator['ExampleModel']:
|
|
116
|
+
return cls.gsi1_index.query(f'CATEGORY#{category}')
|
|
117
|
+
|
|
118
|
+
# ── gsi2_index: partition=type, sort=created_at ───────────────────────────
|
|
119
|
+
@classmethod
|
|
120
|
+
def list_created_between(cls, start: datetime, end: datetime) -> Iterator['ExampleModel']:
|
|
121
|
+
return cls.gsi2_index.query(
|
|
122
|
+
'EXAMPLE',
|
|
123
|
+
range_key_condition=ExampleModel.gsi2sk.between(
|
|
124
|
+
start.isoformat(), end.isoformat(),
|
|
125
|
+
),
|
|
126
|
+
scan_index_forward=False,
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For more details, see the [PynamoDB tutorial](https://pynamodb.readthedocs.io/en/stable/tutorial.html).
|
|
131
|
+
|
|
132
|
+
#### Designing Around Access Patterns
|
|
133
|
+
|
|
134
|
+
In DynamoDB, schema design starts with your queries, not your data shape. Before writing any model, list every access pattern your application needs, then design `pk`, `sk`, and GSI key values so each pattern is answered by a single table request — no JOINs, no sequential reads.
|
|
135
|
+
|
|
136
|
+
The generated `ExampleModel` demonstrates this for three patterns:
|
|
137
|
+
|
|
138
|
+
- **Get by ID** — primary index, `pk=EXAMPLE#<id>`, `sk=EXAMPLE#<id>`
|
|
139
|
+
- **List by category** — `gsi1`, `pk=CATEGORY#<category>`
|
|
140
|
+
- **List by creation date** — `gsi2`, `pk=EXAMPLE`, sort key between ISO timestamps
|
|
141
|
+
|
|
142
|
+
The **type prefix** convention (e.g. `EXAMPLE#`, `CATEGORY#`) is deliberate: it makes items self-describing when browsing the table, prevents accidental key collisions between entity types that share an index, and allows sort key prefix filtering using `begins_with`.
|
|
143
|
+
|
|
144
|
+
Before writing a new entity, define its key patterns upfront in a docstring. The `OrderModel` in the next section follows this convention:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
class OrderModel(BaseModel, discriminator='OrderModel'):
|
|
148
|
+
"""
|
|
149
|
+
Key design:
|
|
150
|
+
pk=ORDER#<order_id>, sk=ORDER#<order_id>
|
|
151
|
+
gsi1pk=USER#<user_id>, gsi1sk=ORDER#<order_id> <- list orders for a user
|
|
152
|
+
gsi2pk=ORDER, gsi2sk=<created_at> <- list all orders by date
|
|
153
|
+
"""
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Storing Multiple Entity Types
|
|
157
|
+
|
|
158
|
+
PynamoDB's `DiscriminatorAttribute` stores a type label (`entity_type`) in every item. When querying via `BaseModel`, this label is used to instantiate each result as its correct subclass automatically — so a single query can return a mix of `UserModel`, `OrderModel`, and any other entity type registered in the same table.
|
|
159
|
+
|
|
160
|
+
:::caution[PynamoDB is not purpose-built for single-table design]
|
|
161
|
+
PynamoDB's `DiscriminatorAttribute` was designed for class hierarchy polymorphism within a single homogeneous table — not for storing structurally different entity types in one table. The pattern used here works, but keep these caveats in mind:
|
|
162
|
+
|
|
163
|
+
- **Key collision is your responsibility.** PynamoDB does not prevent two entity types from writing the same `pk`/`sk` pair. Always use unique type prefixes (e.g. `ORDER#`, `USER#`).
|
|
164
|
+
- **Cross-entity queries require `BaseModel`.** PynamoDB automatically adds a discriminator filter when querying a specific subclass, so items of other entity types at the same key will never be returned. To retrieve a mix of types from a shared GSI partition key, always query via `BaseModel`.
|
|
165
|
+
- **Shared table settings.** All entities share the `Meta` configuration (table name, region, credentials) defined in `BaseModel`.
|
|
166
|
+
:::
|
|
167
|
+
|
|
168
|
+
Below is a complete two-entity example — a `UserModel` with associated `OrderModel` records stored in the same table:
|
|
169
|
+
|
|
170
|
+
```python title="packages/my_table/my_table/entities/user.py"
|
|
171
|
+
from collections.abc import Iterator
|
|
172
|
+
from datetime import UTC, datetime
|
|
173
|
+
from pynamodb.attributes import UnicodeAttribute
|
|
174
|
+
from .base import BaseModel
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class UserModel(BaseModel, discriminator='UserModel'):
|
|
178
|
+
"""
|
|
179
|
+
Key design:
|
|
180
|
+
pk=USER#<user_id>, sk=USER#<user_id>
|
|
181
|
+
gsi2pk=USER, gsi2sk=<created_at> <- list all users by date
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
username = UnicodeAttribute()
|
|
185
|
+
email = UnicodeAttribute()
|
|
186
|
+
created_at = UnicodeAttribute()
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def make_pk(cls, user_id: str) -> str:
|
|
190
|
+
return f'USER#{user_id}'
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def create(cls, user_id: str, username: str, email: str) -> 'UserModel':
|
|
194
|
+
now = datetime.now(UTC).isoformat()
|
|
195
|
+
item = cls(
|
|
196
|
+
pk=cls.make_pk(user_id),
|
|
197
|
+
sk=cls.make_pk(user_id),
|
|
198
|
+
gsi2pk='USER',
|
|
199
|
+
gsi2sk=now,
|
|
200
|
+
username=username,
|
|
201
|
+
email=email,
|
|
202
|
+
created_at=now,
|
|
203
|
+
)
|
|
204
|
+
item.save()
|
|
205
|
+
return item
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def get_by_id(cls, user_id: str) -> 'UserModel':
|
|
209
|
+
return cls.get(cls.make_pk(user_id), cls.make_pk(user_id))
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def list_recent(cls, limit: int | None = None) -> Iterator['UserModel']:
|
|
213
|
+
return cls.gsi2_index.query('USER', limit=limit, scan_index_forward=False)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```python title="packages/my_table/my_table/entities/order.py"
|
|
217
|
+
from collections.abc import Iterator
|
|
218
|
+
from datetime import UTC, datetime
|
|
219
|
+
from pynamodb.attributes import UnicodeAttribute
|
|
220
|
+
from .base import BaseModel
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class OrderModel(BaseModel, discriminator='OrderModel'):
|
|
224
|
+
"""
|
|
225
|
+
Key design:
|
|
226
|
+
pk=ORDER#<order_id>, sk=ORDER#<order_id>
|
|
227
|
+
gsi1pk=USER#<user_id>, gsi1sk=ORDER#<order_id> <- list orders by user
|
|
228
|
+
gsi2pk=ORDER, gsi2sk=<created_at> <- list all orders by date
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
user_id = UnicodeAttribute()
|
|
232
|
+
total = UnicodeAttribute()
|
|
233
|
+
created_at = UnicodeAttribute()
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def make_pk(cls, order_id: str) -> str:
|
|
237
|
+
return f'ORDER#{order_id}'
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def create(cls, order_id: str, user_id: str, total: str) -> 'OrderModel':
|
|
241
|
+
now = datetime.now(UTC).isoformat()
|
|
242
|
+
item = cls(
|
|
243
|
+
pk=cls.make_pk(order_id),
|
|
244
|
+
sk=cls.make_pk(order_id),
|
|
245
|
+
gsi1pk=f'USER#{user_id}',
|
|
246
|
+
gsi1sk=cls.make_pk(order_id),
|
|
247
|
+
gsi2pk='ORDER',
|
|
248
|
+
gsi2sk=now,
|
|
249
|
+
user_id=user_id,
|
|
250
|
+
total=total,
|
|
251
|
+
created_at=now,
|
|
252
|
+
)
|
|
253
|
+
item.save()
|
|
254
|
+
return item
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def get_by_id(cls, order_id: str) -> 'OrderModel':
|
|
258
|
+
return cls.get(cls.make_pk(order_id), cls.make_pk(order_id))
|
|
259
|
+
|
|
260
|
+
# ── gsi1_index: partition=user, sort=order_id ────────────────────────────
|
|
261
|
+
@classmethod
|
|
262
|
+
def list_by_user(cls, user_id: str) -> Iterator['OrderModel']:
|
|
263
|
+
return cls.gsi1_index.query(f'USER#{user_id}')
|
|
264
|
+
|
|
265
|
+
# ── gsi2_index: partition=type, sort=created_at ───────────────────────────
|
|
266
|
+
@classmethod
|
|
267
|
+
def list_recent(cls, limit: int | None = None) -> Iterator['OrderModel']:
|
|
268
|
+
return cls.gsi2_index.query('ORDER', limit=limit, scan_index_forward=False)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Export the new entities from `__init__.py`:
|
|
272
|
+
|
|
273
|
+
```python title="packages/my_table/my_table/entities/__init__.py"
|
|
274
|
+
from .user import UserModel
|
|
275
|
+
from .order import OrderModel
|
|
276
|
+
from .example import ExampleModel
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### GSI Overloading
|
|
280
|
+
|
|
281
|
+
`BaseModel` provides two shared GSIs (`gsi1_index`, `gsi2_index`). Both `UserModel` and `OrderModel` above write to `gsi2` — but with different `gsi2pk` values (`USER` vs `ORDER`). This is **GSI overloading**: reusing a single physical index to serve multiple independent access patterns without consuming extra GSI capacity.
|
|
282
|
+
|
|
283
|
+
- **`UserModel`** — `gsi2pk=USER`, `gsi2sk=<created_at>` → list all users by date
|
|
284
|
+
- **`OrderModel`** — `gsi2pk=ORDER`, `gsi2sk=<created_at>` → list all orders by date
|
|
285
|
+
|
|
286
|
+
`gsi1` can also be overloaded when multiple entity types share the same parent. If you later add a `ReviewModel` that also belongs to a user, you can assign it `gsi1pk=USER#<user_id>` with a `REVIEW#<id>` sort key — no additional GSI needed. Querying `gsi1` via `BaseModel` then returns both orders and reviews for that user in one request, with PynamoDB instantiating each item as its correct subclass:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from .base import BaseModel
|
|
290
|
+
from .order import OrderModel
|
|
291
|
+
from .review import ReviewModel
|
|
292
|
+
|
|
293
|
+
user_id = 'user-123'
|
|
294
|
+
items = list(BaseModel.gsi1_index.query(f'USER#{user_id}'))
|
|
295
|
+
|
|
296
|
+
orders = [i for i in items if isinstance(i, OrderModel)]
|
|
297
|
+
reviews = [i for i in items if isinstance(i, ReviewModel)]
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
To retrieve only one entity type from an overloaded GSI, use a sort key prefix condition:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
orders_only = list(BaseModel.gsi1_index.query(
|
|
304
|
+
f'USER#{user_id}',
|
|
305
|
+
range_key_condition=BaseModel.gsi1sk.startswith('ORDER#'),
|
|
306
|
+
))
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### One-to-Many Relationships
|
|
310
|
+
|
|
311
|
+
In a **one-to-many** relationship the child entity stores a reference to its parent in a GSI partition key, making the relationship traversable in both directions without duplicating data. The `UserModel` / `OrderModel` example above is exactly this pattern:
|
|
312
|
+
|
|
313
|
+
- **Get a single order by ID** — primary table: `pk=ORDER#<id>`, `sk=ORDER#<id>`
|
|
314
|
+
- **List all orders for a user** — `gsi1`: `pk=USER#<user_id>`
|
|
315
|
+
|
|
316
|
+
An alternative to GSI-based lookups is the **item collection** pattern: give child items the same `pk` as their parent and use the sort key to differentiate them. This lets you retrieve the parent and all its children in a single primary-table query, without a GSI:
|
|
317
|
+
|
|
318
|
+
```python title="packages/my_table/my_table/entities/order.py (item collection variant)"
|
|
319
|
+
class OrderModel(BaseModel, discriminator='OrderModel'):
|
|
320
|
+
"""
|
|
321
|
+
Key design (item collection):
|
|
322
|
+
pk=USER#<user_id>, sk=ORDER#<order_id> <- co-located under the parent user
|
|
323
|
+
"""
|
|
324
|
+
...
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# Retrieve the user and all their orders in one primary-table query
|
|
329
|
+
# BaseModel dispatches each item to its correct subclass via DiscriminatorAttribute
|
|
330
|
+
items = list(BaseModel.query(f'USER#{user_id}'))
|
|
331
|
+
user = next(i for i in items if isinstance(i, UserModel))
|
|
332
|
+
orders = [i for i in items if isinstance(i, OrderModel)]
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The tradeoff: item collections place all children under a single partition key, which is optimal for most workloads but can create a hot partition at extreme write throughput. The GSI approach (used in the examples above) keeps each entity in its own partition and is generally safer to start with.
|
|
336
|
+
|
|
337
|
+
#### Many-to-Many Relationships
|
|
338
|
+
|
|
339
|
+
Many-to-many relationships require a **junction entity** using the [adjacency list pattern](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-adjacency-graphs.html): a dedicated item that records each link, with its GSI key inverting the direction so the relationship can be traversed both ways.
|
|
340
|
+
|
|
341
|
+
Consider `ArticleModel` and `TagModel`, where an article can have many tags and a tag can apply to many articles:
|
|
342
|
+
|
|
343
|
+
```python title="packages/my_table/my_table/entities/article_tag.py"
|
|
344
|
+
from collections.abc import Iterator
|
|
345
|
+
from pynamodb.attributes import UnicodeAttribute
|
|
346
|
+
from .base import BaseModel
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class ArticleTagModel(BaseModel, discriminator='ArticleTag'):
|
|
350
|
+
"""
|
|
351
|
+
Junction entity for the Article ↔ Tag many-to-many relationship.
|
|
352
|
+
|
|
353
|
+
Key design:
|
|
354
|
+
pk=ARTICLE#<article_id>, sk=TAG#<tag_name> <- list tags for an article
|
|
355
|
+
gsi1pk=TAG#<tag_name>, gsi1sk=ARTICLE#<article_id> <- list articles for a tag
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
article_id = UnicodeAttribute()
|
|
359
|
+
tag_name = UnicodeAttribute()
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def add(cls, article_id: str, tag_name: str) -> 'ArticleTagModel':
|
|
363
|
+
item = cls(
|
|
364
|
+
pk=f'ARTICLE#{article_id}',
|
|
365
|
+
sk=f'TAG#{tag_name}',
|
|
366
|
+
gsi1pk=f'TAG#{tag_name}',
|
|
367
|
+
gsi1sk=f'ARTICLE#{article_id}',
|
|
368
|
+
article_id=article_id,
|
|
369
|
+
tag_name=tag_name,
|
|
370
|
+
)
|
|
371
|
+
item.save()
|
|
372
|
+
return item
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def remove(cls, article_id: str, tag_name: str) -> None:
|
|
376
|
+
cls.get(f'ARTICLE#{article_id}', f'TAG#{tag_name}').delete()
|
|
377
|
+
|
|
378
|
+
# ── Primary index: pk=article, sk=tag ─────────────────────────────────────
|
|
379
|
+
@classmethod
|
|
380
|
+
def list_tags_for_article(cls, article_id: str) -> Iterator['ArticleTagModel']:
|
|
381
|
+
return cls.query(f'ARTICLE#{article_id}')
|
|
382
|
+
|
|
383
|
+
# ── gsi1_index: pk=tag, sk=article ────────────────────────────────────────
|
|
384
|
+
@classmethod
|
|
385
|
+
def list_articles_for_tag(cls, tag_name: str) -> Iterator['ArticleTagModel']:
|
|
386
|
+
return cls.gsi1_index.query(f'TAG#{tag_name}')
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Because `ArticleTagModel` uses `pk=ARTICLE#<article_id>` — the same partition as the article itself — you can retrieve an article and all its tags in a single primary-table query:
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
from .base import BaseModel
|
|
393
|
+
from .article import ArticleModel
|
|
394
|
+
from .article_tag import ArticleTagModel
|
|
395
|
+
|
|
396
|
+
items = list(BaseModel.query('ARTICLE#article-123'))
|
|
397
|
+
article = next(i for i in items if isinstance(i, ArticleModel))
|
|
398
|
+
tags = [i.tag_name for i in items if isinstance(i, ArticleTagModel)]
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
For further reading on DynamoDB data modelling, see the [DynamoDB data modelling guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/data-modeling.html) and [Creating a single-table design with Amazon DynamoDB](https://aws.amazon.com/blogs/compute/creating-a-single-table-design-with-amazon-dynamodb/).
|
|
402
|
+
|
|
403
|
+
### Using the DynamoDB Client
|
|
404
|
+
|
|
405
|
+
The generated `client.py` exports two key utilities:
|
|
406
|
+
|
|
407
|
+
- `is_local()` — returns `True` when `SERVE_LOCAL=true`, used to switch between local and AWS behaviour.
|
|
408
|
+
- `get_table_name()` — returns the DynamoDB table name. When `SERVE_LOCAL=true`, reads the table name from `serveLocal.tableName` in `config.json`; otherwise fetches the name from AWS AppConfig using the `RUNTIME_CONFIG_APP_ID` environment variable and caches it for subsequent calls.
|
|
409
|
+
|
|
410
|
+
`BaseModel` in `entities/base.py` uses both to configure PynamoDB automatically:
|
|
411
|
+
|
|
412
|
+
- **Connection** — `BaseModel.Meta` sets `host` from `config.json` and hardcodes `region`, `aws_access_key_id`, and `aws_secret_access_key` when `is_local()` is `True`, pointing PynamoDB at the local DynamoDB instance. In AWS, these are left unset so PynamoDB uses the default credential chain.
|
|
413
|
+
- **Table name** — `BaseModel._get_connection()` calls `get_table_name()` before each operation, so the correct table is resolved at runtime without any manual configuration.
|
|
414
|
+
|
|
415
|
+
### Stopping Local DynamoDB
|
|
416
|
+
|
|
417
|
+
<Snippet name="dynamodb/serve-local-windows" />
|
|
418
|
+
|
|
419
|
+
## Adding/Removing Global Secondary Indexes
|
|
420
|
+
|
|
421
|
+
GSIs are defined in `config.json` at the project root under the `tableConfig.globalSecondaryIndexes` key. Add an entry for each GSI, then reflect the change in `BaseModel` by adding or removing the corresponding `GlobalSecondaryIndex` class and attributes in `<name>/entities/base.py`:
|
|
422
|
+
|
|
423
|
+
<Snippet name="dynamodb/gsi-config" parentHeading="Adding/Removing Global Secondary Indexes" />
|
|
424
|
+
|
|
425
|
+
## Connecting to the Table
|
|
426
|
+
|
|
427
|
+
In any Python project, add the DynamoDB package as a workspace dependency and import entity classes directly:
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
from my_db_package.entities import ExampleModel
|
|
431
|
+
|
|
432
|
+
item = ExampleModel.get_by_id('123')
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
:::note[Runtime config]
|
|
436
|
+
When running in AWS, `get_table_name()` fetches the table name from AWS AppConfig using the `RUNTIME_CONFIG_APP_ID` environment variable. Projects built with this plugin already have this variable configured automatically. For other Python projects, ensure `RUNTIME_CONFIG_APP_ID` is set in the runtime environment with the AppConfig application ID provisioned by your infrastructure. For more information, see the <Link path="guides/runtime-config">Runtime Configuration guide</Link>.
|
|
437
|
+
:::
|
|
438
|
+
|
|
439
|
+
### Connection Generators
|
|
440
|
+
|
|
441
|
+
For specific project types, use the `connection` generator to automatically wire up local development dependencies so DynamoDB Local starts automatically alongside your project, and add the DynamoDB package as a workspace dependency:
|
|
442
|
+
|
|
443
|
+
- <Link path="guides/connection/py-fast-api-dynamodb">FastAPI → DynamoDB</Link>
|
|
444
|
+
- <Link path="guides/connection/py-agent-dynamodb">Python Agent → DynamoDB</Link>
|
|
445
|
+
- <Link path="guides/connection/py-mcp-server-dynamodb">Python MCP Server → DynamoDB</Link>
|
|
446
|
+
|
|
447
|
+
## Deploying your Table
|
|
448
|
+
|
|
449
|
+
<Snippet name="dynamodb/deploying-table" parentHeading="Deploying your Table" />
|