langgraph-store-dynamodb 0.1.0__tar.gz
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.
- langgraph_store_dynamodb-0.1.0/.gitignore +42 -0
- langgraph_store_dynamodb-0.1.0/PKG-INFO +85 -0
- langgraph_store_dynamodb-0.1.0/README.md +64 -0
- langgraph_store_dynamodb-0.1.0/pyproject.toml +42 -0
- langgraph_store_dynamodb-0.1.0/readme.md +64 -0
- langgraph_store_dynamodb-0.1.0/requirement.txt +0 -0
- langgraph_store_dynamodb-0.1.0/src/langgraph_store_dynamodb/dynamodbStore.py +165 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
.env/
|
|
15
|
+
.venv/
|
|
16
|
+
ENV/
|
|
17
|
+
|
|
18
|
+
# IDE specific files
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
23
|
+
|
|
24
|
+
# Unit test / coverage reports
|
|
25
|
+
htmcov/
|
|
26
|
+
.tox/
|
|
27
|
+
.coverage
|
|
28
|
+
.coverage.*
|
|
29
|
+
.cache
|
|
30
|
+
nosetests.xml
|
|
31
|
+
coverage.xml
|
|
32
|
+
*.cover
|
|
33
|
+
|
|
34
|
+
# Jupyter Notebook
|
|
35
|
+
.ipynb_checkpoints
|
|
36
|
+
|
|
37
|
+
# Environment variables
|
|
38
|
+
.env
|
|
39
|
+
|
|
40
|
+
# Local development settings
|
|
41
|
+
*.log
|
|
42
|
+
.DS_Store
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langgraph_store_dynamodb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DynamoDB dtore for LangGraph
|
|
5
|
+
Author-email: Kamal <skamalj@github.com>
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: boto3
|
|
11
|
+
Requires-Dist: botocore
|
|
12
|
+
Requires-Dist: langchain-core
|
|
13
|
+
Requires-Dist: langgraph
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: black; extra == 'dev'
|
|
16
|
+
Requires-Dist: isort; extra == 'dev'
|
|
17
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# LangGraph DynamoDB Store
|
|
23
|
+
|
|
24
|
+
A DynamoDB-based store implementation for LangGraph that allows long term memory implementation
|
|
25
|
+
|
|
26
|
+
bash
|
|
27
|
+
pip install langgraph-store-dynamodb
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Basic Initialization
|
|
33
|
+
|
|
34
|
+
python
|
|
35
|
+
from langgraph_store_dynamodb import DynamoDBStore
|
|
36
|
+
|
|
37
|
+
# Initialize the store with a table name
|
|
38
|
+
store = DynamoDBStore(
|
|
39
|
+
table_name="your-dynamodb-table-name",
|
|
40
|
+
max_read_request_units=10, # Optional, default is 10
|
|
41
|
+
max_write_request_units=10 # Optional, default is 10
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Alternative Initialization Using Context Manager
|
|
46
|
+
|
|
47
|
+
python
|
|
48
|
+
from langgraph_dynamodb_checkpoint import DynamoDBStore
|
|
49
|
+
|
|
50
|
+
with DynamoDBStore.from_conn_info(table_name="your-dynamodb-table-name") as store:
|
|
51
|
+
# Use the store here
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Parameters
|
|
56
|
+
|
|
57
|
+
### DynamoDBStore Constructor
|
|
58
|
+
|
|
59
|
+
- `table_name` (str): Name of the DynamoDB table to use for storing checkpoints
|
|
60
|
+
- `max_read_request_units` (int, optional): Maximum read request units for the DynamoDB table. Defaults to 10
|
|
61
|
+
- `max_write_request_units` (int, optional): Maximum write request units for the DynamoDB table. Defaults to 10
|
|
62
|
+
|
|
63
|
+
## Table Structure
|
|
64
|
+
|
|
65
|
+
The store automatically creates a DynamoDB table if it doesn't exist, with the following structure:
|
|
66
|
+
|
|
67
|
+
- Partition Key (PK): String type, used for namespace
|
|
68
|
+
- Sort Key (SK): String type, used for memory key
|
|
69
|
+
|
|
70
|
+
## AWS Configuration
|
|
71
|
+
|
|
72
|
+
Ensure you have proper AWS credentials configured either through:
|
|
73
|
+
- Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
74
|
+
- AWS credentials file (~/.aws/credentials)
|
|
75
|
+
- IAM role when running on AWS services
|
|
76
|
+
|
|
77
|
+
The AWS credentials should have permissions to:
|
|
78
|
+
- Create DynamoDB tables (if table doesn't exist)
|
|
79
|
+
- Read and write to DynamoDB tables
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
- The store automatically creates the DynamoDB table if it doesn't exist
|
|
84
|
+
- Uses on-demand billing mode for DynamoDB
|
|
85
|
+
- Implements methods required by the LangGraph BaseStore interface
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# LangGraph DynamoDB Store
|
|
2
|
+
|
|
3
|
+
A DynamoDB-based store implementation for LangGraph that allows long term memory implementation
|
|
4
|
+
|
|
5
|
+
bash
|
|
6
|
+
pip install langgraph-store-dynamodb
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### Basic Initialization
|
|
12
|
+
|
|
13
|
+
python
|
|
14
|
+
from langgraph_store_dynamodb import DynamoDBStore
|
|
15
|
+
|
|
16
|
+
# Initialize the store with a table name
|
|
17
|
+
store = DynamoDBStore(
|
|
18
|
+
table_name="your-dynamodb-table-name",
|
|
19
|
+
max_read_request_units=10, # Optional, default is 10
|
|
20
|
+
max_write_request_units=10 # Optional, default is 10
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Alternative Initialization Using Context Manager
|
|
25
|
+
|
|
26
|
+
python
|
|
27
|
+
from langgraph_dynamodb_checkpoint import DynamoDBStore
|
|
28
|
+
|
|
29
|
+
with DynamoDBStore.from_conn_info(table_name="your-dynamodb-table-name") as store:
|
|
30
|
+
# Use the store here
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Parameters
|
|
35
|
+
|
|
36
|
+
### DynamoDBStore Constructor
|
|
37
|
+
|
|
38
|
+
- `table_name` (str): Name of the DynamoDB table to use for storing checkpoints
|
|
39
|
+
- `max_read_request_units` (int, optional): Maximum read request units for the DynamoDB table. Defaults to 10
|
|
40
|
+
- `max_write_request_units` (int, optional): Maximum write request units for the DynamoDB table. Defaults to 10
|
|
41
|
+
|
|
42
|
+
## Table Structure
|
|
43
|
+
|
|
44
|
+
The store automatically creates a DynamoDB table if it doesn't exist, with the following structure:
|
|
45
|
+
|
|
46
|
+
- Partition Key (PK): String type, used for namespace
|
|
47
|
+
- Sort Key (SK): String type, used for memory key
|
|
48
|
+
|
|
49
|
+
## AWS Configuration
|
|
50
|
+
|
|
51
|
+
Ensure you have proper AWS credentials configured either through:
|
|
52
|
+
- Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
53
|
+
- AWS credentials file (~/.aws/credentials)
|
|
54
|
+
- IAM role when running on AWS services
|
|
55
|
+
|
|
56
|
+
The AWS credentials should have permissions to:
|
|
57
|
+
- Create DynamoDB tables (if table doesn't exist)
|
|
58
|
+
- Read and write to DynamoDB tables
|
|
59
|
+
|
|
60
|
+
## Notes
|
|
61
|
+
|
|
62
|
+
- The store automatically creates the DynamoDB table if it doesn't exist
|
|
63
|
+
- Uses on-demand billing mode for DynamoDB
|
|
64
|
+
- Implements methods required by the LangGraph BaseStore interface
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langgraph_store_dynamodb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "DynamoDB dtore for LangGraph"
|
|
9
|
+
authors = [{name = "Kamal", email = "skamalj@github.com"}]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"langchain-core",
|
|
14
|
+
"langgraph",
|
|
15
|
+
"boto3",
|
|
16
|
+
"botocore"
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=7.0",
|
|
27
|
+
"pytest-cov",
|
|
28
|
+
"black",
|
|
29
|
+
"isort",
|
|
30
|
+
"mypy"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/langgraph_store_dynamodb"]
|
|
35
|
+
|
|
36
|
+
[tool.black]
|
|
37
|
+
line-length = 88
|
|
38
|
+
target-version = ['py39']
|
|
39
|
+
|
|
40
|
+
[tool.isort]
|
|
41
|
+
profile = "black"
|
|
42
|
+
multi_line_output = 3
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# LangGraph DynamoDB Store
|
|
2
|
+
|
|
3
|
+
A DynamoDB-based store implementation for LangGraph that allows long term memory implementation
|
|
4
|
+
|
|
5
|
+
bash
|
|
6
|
+
pip install langgraph-store-dynamodb
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
### Basic Initialization
|
|
12
|
+
|
|
13
|
+
python
|
|
14
|
+
from langgraph_store_dynamodb import DynamoDBStore
|
|
15
|
+
|
|
16
|
+
# Initialize the store with a table name
|
|
17
|
+
store = DynamoDBStore(
|
|
18
|
+
table_name="your-dynamodb-table-name",
|
|
19
|
+
max_read_request_units=10, # Optional, default is 10
|
|
20
|
+
max_write_request_units=10 # Optional, default is 10
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Alternative Initialization Using Context Manager
|
|
25
|
+
|
|
26
|
+
python
|
|
27
|
+
from langgraph_dynamodb_checkpoint import DynamoDBStore
|
|
28
|
+
|
|
29
|
+
with DynamoDBStore.from_conn_info(table_name="your-dynamodb-table-name") as store:
|
|
30
|
+
# Use the store here
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Parameters
|
|
35
|
+
|
|
36
|
+
### DynamoDBStore Constructor
|
|
37
|
+
|
|
38
|
+
- `table_name` (str): Name of the DynamoDB table to use for storing checkpoints
|
|
39
|
+
- `max_read_request_units` (int, optional): Maximum read request units for the DynamoDB table. Defaults to 10
|
|
40
|
+
- `max_write_request_units` (int, optional): Maximum write request units for the DynamoDB table. Defaults to 10
|
|
41
|
+
|
|
42
|
+
## Table Structure
|
|
43
|
+
|
|
44
|
+
The store automatically creates a DynamoDB table if it doesn't exist, with the following structure:
|
|
45
|
+
|
|
46
|
+
- Partition Key (PK): String type, used for namespace
|
|
47
|
+
- Sort Key (SK): String type, used for memory key
|
|
48
|
+
|
|
49
|
+
## AWS Configuration
|
|
50
|
+
|
|
51
|
+
Ensure you have proper AWS credentials configured either through:
|
|
52
|
+
- Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
53
|
+
- AWS credentials file (~/.aws/credentials)
|
|
54
|
+
- IAM role when running on AWS services
|
|
55
|
+
|
|
56
|
+
The AWS credentials should have permissions to:
|
|
57
|
+
- Create DynamoDB tables (if table doesn't exist)
|
|
58
|
+
- Read and write to DynamoDB tables
|
|
59
|
+
|
|
60
|
+
## Notes
|
|
61
|
+
|
|
62
|
+
- The store automatically creates the DynamoDB table if it doesn't exist
|
|
63
|
+
- Uses on-demand billing mode for DynamoDB
|
|
64
|
+
- Implements methods required by the LangGraph BaseStore interface
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
|
3
|
+
from boto3.dynamodb.conditions import Key
|
|
4
|
+
from botocore.exceptions import ClientError
|
|
5
|
+
from langgraph.store.base import BaseStore, Item, PutOp, Result, SearchItem, NamespacePath
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DynamoDBStore(BaseStore):
|
|
10
|
+
def __init__(self, table_name: str, max_read_request_units: int = 10, max_write_request_units: int = 10):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.dynamodb = boto3.resource('dynamodb')
|
|
13
|
+
self.table = self._get_or_create_table(table_name, max_read_request_units,max_write_request_units)
|
|
14
|
+
|
|
15
|
+
def _get_or_create_table(self, table_name: str, max_read_request_units: int, max_write_request_units: int):
|
|
16
|
+
try:
|
|
17
|
+
# Attempt to load the table
|
|
18
|
+
table = self.dynamodb.Table(table_name)
|
|
19
|
+
table.load() # This will raise an exception if the table does not exist
|
|
20
|
+
print(f"Table '{table_name}' already exists.")
|
|
21
|
+
return table
|
|
22
|
+
except ClientError as e:
|
|
23
|
+
if e.response['Error']['Code'] == 'ResourceNotFoundException':
|
|
24
|
+
# Table does not exist, create it
|
|
25
|
+
print(f"Table '{table_name}' not found. Creating table...")
|
|
26
|
+
key_schema = [
|
|
27
|
+
{'AttributeName': 'PK', 'KeyType': 'HASH'}, # Partition key
|
|
28
|
+
{'AttributeName': 'SK', 'KeyType': 'RANGE'}, # Sort key
|
|
29
|
+
]
|
|
30
|
+
attribute_definitions = [
|
|
31
|
+
{'AttributeName': 'PK', 'AttributeType': 'S'}, # String type
|
|
32
|
+
{"AttributeName": "SK", "AttributeType": 'S'},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
table = self.dynamodb.create_table(
|
|
36
|
+
TableName=table_name,
|
|
37
|
+
KeySchema=key_schema,
|
|
38
|
+
AttributeDefinitions=attribute_definitions,
|
|
39
|
+
BillingMode='PAY_PER_REQUEST',
|
|
40
|
+
OnDemandThroughput={
|
|
41
|
+
'MaxReadRequestUnits': max_read_request_units,
|
|
42
|
+
'MaxWriteRequestUnits': max_write_request_units
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
table.wait_until_exists() # Wait for the table to become active
|
|
46
|
+
print(f"Table '{table_name}' created successfully.")
|
|
47
|
+
return table
|
|
48
|
+
else:
|
|
49
|
+
raise # Re-raise any other exceptions
|
|
50
|
+
from datetime import datetime
|
|
51
|
+
|
|
52
|
+
def _map_to_item(result_dict, namespace, return_type='Item'):
|
|
53
|
+
# Extract values from the result_dict
|
|
54
|
+
key = result_dict['PK'] # Using 'PK' as the unique key
|
|
55
|
+
value = result_dict['value']
|
|
56
|
+
created_at = result_dict['created_at']
|
|
57
|
+
updated_at = result_dict['updated_at']
|
|
58
|
+
|
|
59
|
+
target_class = Item if return_type == 'Item' else SearchItem
|
|
60
|
+
|
|
61
|
+
# Create an Item object
|
|
62
|
+
item = target_class(
|
|
63
|
+
value=value,
|
|
64
|
+
key=key,
|
|
65
|
+
namespace=namespace,
|
|
66
|
+
created_at=created_at,
|
|
67
|
+
updated_at=updated_at
|
|
68
|
+
)
|
|
69
|
+
return item
|
|
70
|
+
|
|
71
|
+
def batch(self, ops: Iterable[PutOp]) -> list[Result]:
|
|
72
|
+
results = []
|
|
73
|
+
for op in ops:
|
|
74
|
+
composite_key = self._construct_composite_key(op.namespace, op.key)
|
|
75
|
+
if op.value is None:
|
|
76
|
+
# Delete operation
|
|
77
|
+
result = self.delete(namespace=composite_key[0], key=composite_key[1])
|
|
78
|
+
else:
|
|
79
|
+
result = self.put(
|
|
80
|
+
namespace=composite_key[0], key=composite_key[1], value=op.value
|
|
81
|
+
)
|
|
82
|
+
# Append the operation result
|
|
83
|
+
results.append(result)
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
def get(self, namespace: Tuple[str, ...], key: str) -> Optional[Item]:
|
|
87
|
+
"""
|
|
88
|
+
Retrieve an item based on its composite key (namespace, key).
|
|
89
|
+
"""
|
|
90
|
+
composite_key = self._construct_composite_key(namespace, key)
|
|
91
|
+
response = self.table.get_item(Key={'PK': composite_key[0], 'SK': composite_key[1]})
|
|
92
|
+
item = response.get('Item')
|
|
93
|
+
if item:
|
|
94
|
+
return self._map_to_item(item, namespace)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def search(
|
|
98
|
+
self,
|
|
99
|
+
namespace_prefix: Tuple[str, ...],
|
|
100
|
+
*,
|
|
101
|
+
query: Optional[str] = None,
|
|
102
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
103
|
+
limit: int = 10,
|
|
104
|
+
offset: int = 0
|
|
105
|
+
) -> List[SearchItem]:
|
|
106
|
+
"""
|
|
107
|
+
Search for items in a given namespace, applying optional query and filter.
|
|
108
|
+
"""
|
|
109
|
+
namespace = ':'.join(namespace_prefix)
|
|
110
|
+
key_condition = "PK = :partitionkeyval AND SK = :sortkeyval"
|
|
111
|
+
filter_expression = None
|
|
112
|
+
|
|
113
|
+
response = self.table.query(
|
|
114
|
+
ExpressionAttributeValues={
|
|
115
|
+
':PK': namespace
|
|
116
|
+
},
|
|
117
|
+
KeyConditionExpression='PK = :PK',
|
|
118
|
+
Limit=limit
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
items = response.get('Items', [])
|
|
122
|
+
return [self._map_to_item(item, namespace, 'SearchItem') for item in items]
|
|
123
|
+
|
|
124
|
+
def put(
|
|
125
|
+
self,
|
|
126
|
+
namespace: Tuple[str, ...],
|
|
127
|
+
key: str,
|
|
128
|
+
value: Dict[str, Any],
|
|
129
|
+
index: Optional[Union[Literal[False], List[str]]] = None
|
|
130
|
+
) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Insert or update an item in the table.
|
|
133
|
+
"""
|
|
134
|
+
composite_key = self._construct_composite_key(namespace, key)
|
|
135
|
+
existing_item = self.get(namespace=composite_key[0], key=composite_key[0])
|
|
136
|
+
current_time = datetime.utcnow()
|
|
137
|
+
item = {
|
|
138
|
+
'PK': composite_key[0],
|
|
139
|
+
'SK': composite_key[0],
|
|
140
|
+
'value': value,
|
|
141
|
+
'created_at': existing_item.created_at if existing_item else current_time,
|
|
142
|
+
'updated_at': current_time,
|
|
143
|
+
}
|
|
144
|
+
self.table.put_item(Item=item)
|
|
145
|
+
|
|
146
|
+
def delete(self, namespace: Tuple[str, ...], key: str) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Delete an item based on its composite key (namespace, key).
|
|
149
|
+
"""
|
|
150
|
+
composite_key = self._construct_composite_key(namespace, key)
|
|
151
|
+
self.table.delete_item(Key={'PK': composite_key[0], 'SK': composite_key[1]})
|
|
152
|
+
|
|
153
|
+
def list_namespaces(self, *, prefix: Optional[NamespacePath] = None,
|
|
154
|
+
suffix: Optional[NamespacePath] = None,
|
|
155
|
+
max_depth: Optional[int] = None,
|
|
156
|
+
limit: int = 100, offset: int = 0) -> list[tuple[str, ...]]:
|
|
157
|
+
raise NotImplementedError("The 'list_namespaces' method is not implemented yet.")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _construct_composite_key(self, namespace: Tuple[str, ...], key: str) -> Tuple[str, str]:
|
|
161
|
+
"""
|
|
162
|
+
Combine namespace and key to form a composite key (PK, SK).
|
|
163
|
+
"""
|
|
164
|
+
namespace_str = ':'.join(namespace)
|
|
165
|
+
return (namespace_str, key)
|