ddb-single


Nameddb-single JSON
Version 0.4.7 PyPI version JSON
download
home_pagehttps://github.com/medaka0213/DynamoDB-SingleTable
SummaryPython DynamoDB interface, specialized in single-table design.
upload_time2024-03-23 07:49:46
maintainerNone
docs_urlNone
authormedaka
requires_pythonNone
licenseMIT
keywords aws dynamodb serverless
VCS
bugtrack_url
requirements boto3
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # DynamoDB SingleTable

https://pypi.org/project/ddb-single/

Python DynamoDB interface, specialized in single-table design.
DynamoDB is high-performance serverless NoSQL, but difficult to disign tables.

Single-table design needs only single table, and few GSIs (Global Secondary Indexes).
It makes effective and easy to manage your whole data models for single service.

## Getting Started

### Install

```
pip install ddb-single
```

### Init Table

```python
from ddb_single import Table

table = Table(
    table_name="sample",
    endpoint_url="http://localhost:8000",
)
table.init()
```

### Data Models

Each model has al least 3 keys
- primary_key ... Hash key for single item. default: `pk: {__model_name__}_{uuid}` 
- seconday_key ... Range key for item. default: `sk: {__model_name__}_item`
- unique_key ... key to identify the item is the same. Mainly used to update item.

And you can set `serch_key` to enable search via GSI 

```python
from ddb_single import BaseModel, DBField, FieldType

class User(BaseModel):
    __table__=table
    __model_name__ = "user"
    name = DBField(unique_key=True)
    email = DBField(search_key=True)
    age = DBField(type=FieldType.NUMBER, search_key=True)
    description=DBField()
```

## Usage

need "Qurey" object for CRUD
- `query.model(foo).create`
- `query.model(foo).get`
- `query.model(foo).search`
- `query.model(foo).update`
- `query.model(foo).delete`

```python
from ddb_single import Query
query = Query(table)
```


### Create Item

If the item with same value of `unique_key` already exist, exist item is updated.

```python
user = User(name="John", email="john@example.com", description="test")
query.model(user).create()
```

Then, multible items added.

|pk|sk|data|name|email|description|
|-|-|-|-|-|-|
|user_xxxx|user_item||John|john@example.com|test|
|user_xxxx|search_user_name|John|
|user_xxxx|search_user_email|new-john@example.com|

In addition to main item (sk=`user_item`), multiple item (sk=`search_{__model_name__}_{field_name}`) added to table.
Those "search items" are used to search

The GSI `DataSearchIndex` is used to get "search items" to extract target's pk.
Then, `batch_get` items by pk.

|sk = hash|data = range|pk|
|-|-|-|
|search_user_name|John|user_xxxx|
|search_user_email|new-john@example.com|user_xxxx|

### Search Items

```python
user = query.model(User).search(User.name.eq("John"))
print(user)
# -> [{"pk":"user_xxxx", "sk":"user_item", "name":"John", "email":"john@example.com"}]
```

`pk_only=True` to extract pk without `batch_get`

```python
user_pks = query.model(User).search(User.name.eq("John"), pk_only=True)
print(user_pks)
# -> ["user_xxxx"]
```

### Get single item

`get(pk)` to get single item.

```
user = query.model(User).get("user_xxxx")
print(user)
# -> {"pk":"user_xxxx", "sk":"user_item", "name":"John", "email":"john@example.com"}
```

`get_by_unique` to get item by `unique_key`

```python
user = query.model(User).get_by_unique("John")
print(user)
# -> {"pk":"user_xxxx", "sk":"user_item", "name":"John", "email":"john@example.com"}
```

`pk_only=True` option in `get_by_unique` to get `primary key` without `get_item`

```python
pk = query.model(User).get_by_unique("John", pk_only=True)
print(pk)
# -> "user_xxxx"
```

### Update Item

```python
user = query.model(User).search(User.email.eq("john@example.com"))
new_user = User(**user[0])
new_user.email = "new-john@example.com"
query.model(new_user).update()
```

Or use unique value to detect exist item.

```python
new_user = User(name="John", email="new-john@example.com")
query.model(new_user).update()
```

Then, tha value of "main item" and "seach item" changed

|pk|sk|data|name|email|description|
|-|-|-|-|-|-|
|user_xxxx|user_item||John|new-john@example.com|test|
|user_xxxx|search_user_name|John|
|user_xxxx|search_user_email|new-john@example.com|


### Delete Item

```
user = query.model(User).search(User.email.eq("new-john@example.com"))
query.model(user[0]).delete()
```

`primary key` to detect exist item.

```
query.model(User).delete_by_pk("user_xxxx")
```


or `unique key`

```
query.model(User).delete_by_unique("John")
```

## Batch Writer

`table.batch_writer()` to create/update/delete multible items
- `query.model(foo).create(batch=batch)`
- `query.model(foo).update(batch=batch)`
- `query.model(foo).delete(batch=batch)`

### Batch Create

```python
with table.batch_writer() as batch:
    for i in range(3):
        user = User(name=f"test{i}", age=i+10)
        query.model(user).create(batch=batch)
res = query.model(User).search(User.name.begins_with("test"))
print([(r["name"], r["age"]) for r in res])
# -> [("test0", 10), ("test1", 11), ("test2", 12)]
```

### Batch Update

```python
with table.batch_writer() as batch:
    for i in range(3):
        user = User(name=f"test{i}", age=i+20)
        query.model(user).update(batch=batch)
res = query.model(User).search(User.name.begins_with("test"))
print([(r["name"], r["age"]) for r in res])
# -> [("test0", 20), ("test1", 21), ("test2", 22)]
```

### Batch Delete

```python
pks = query.model(User).search(User.name.begins_with("test"), pk_only=True)
with table.batch_writer() as batch:
    for pk in pks:
        query.model(user).delete_by_pk(pk, batch=batch)
res = query.model(User).search(User.name.begins_with("test"))
print(res)
# -> []
```


## Relationship

### Create Model

You can sat relationns to other models
`relation=BaseModel` to set relation.

```python
class BlogPost(BaseModel):
    __model_name__ = "blogpost"
    __table__=table
    name = DBField(unique_key=True)
    content = DBField()
    author = DBField(reletion=User)
```

### Create Item


```python
blogpost = BlogPost(
    name="Hello",
    content="Hello world",
    author=self.user
)
query.model(blogpost).create()
```

Then, tha value "reletion item" added

|pk|sk|data|name|author|content|
|-|-|-|-|-|-|
|user_xxxx|user_item||John|||
|user_xxxx|search_user_name|John|
|blogpost_xxxx|blogpost_item||Hello|John|Hello world|
|blogpost_xxxx|search_blogpost_title|Hello|
|blogpost_xxxx|rel_user_xxxx|author|

In addition to main item (sk=`blogpost_item`), relation item (sk=`rel_{primary_key}`) added to table. The GSI `DataSearchIndex` is used to get "relation items" to extract target's pk.
Then, `batch_get` items by pk.

|sk = hash|data = range|pk|
|-|-|-|
|rel_user_xxxx|author|blogpost_xxxx|

### Search Relations

`get_relation(model=Basemodel)` to search relations

```python
blogpost = query.model(BlogPost).get_by_unique("Hello")
blogpost = BlogPost(**blogpost)

user = query.model(blogpost).get_relation(model=User)
print(user)
# -> [{"pk":"user_xxxx", "sk":"user_item", "name":"John"}]
```

Also `get_relation(field=DBField)` to specify field

```python
user = query.model(blogpost).get_relation(field=BlogPost.author)
print(user)
# -> [{"pk":"user_xxxx", "sk":"user_item", "name":"John"}]
```

### Search Reference

In this library, "reference" is antonym to relation

`get_reference(model=Basemodel)` to search items related to the item

```python
user = query.model(User).get_by_unique("John")
user = User(**blogpost)

blogpost = query.model(blogpost).get_reference(model=BlogPost)
print(blogpost)
# -> [{"pk":"blogpost_xxxx", "sk":"blogpost_item", "name":"Hello"}]
```

Also `get_reference(field=DBField)` to specify field

```python
blogpost = query.model(user).get_reference(field=BlogPost.author)
print(blogpost)
# -> [{"pk":"blogpost_xxxx", "sk":"blogpost_item", "name":"Hello"}]
```

### Update Relation

If relation key's value changed, relationship also changed.


```python
new_user = User(name="Michael")
blogpost = query.model(BlogPost).get_by_unique("Hello")
blogpost["author"] = new_user
blogpost = BlogPost(**blogpost)

query.model(blogpost).update()
```

Then, "reletion item" changed

|pk|sk|data|name|author|content|
|-|-|-|-|-|-|
|user_xxxx|user_item||John|||
|user_xxxx|search_user_name|John|
|user_yyyy|user_item||Michael|||
|user_yyyy|search_user_name|Michael|
|blogpost_xxxx|blogpost_item||Hello|Michael|Hello world|
|blogpost_xxxx|search_blogpost_title|Hello|
|blogpost_xxxx|rel_user_yyyy|author|
### Delete Relation

If related item deleted, relationship also deleted

```python
query.model(user).delete_by_unique("Michael")
```

Then, "reletion item" deleted.
But main item's value is not chenged.

|pk|sk|data|name|author|content|
|-|-|-|-|-|-|
|user_xxxx|user_item||John|||
|user_xxxx|search_user_name|John|
|blogpost_xxxx|blogpost_item||Hello|Michael|Hello world|
|blogpost_xxxx|search_blogpost_title|Hello|

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/medaka0213/DynamoDB-SingleTable",
    "name": "ddb-single",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": "aws dynamodb serverless",
    "author": "medaka",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/d1/de/8899823c20b665e6e029ae3b11cc487dca030a78e3ac942f8d7c6715861c/ddb_single-0.4.7.zip",
    "platform": null,
    "description": "# DynamoDB SingleTable\n\nhttps://pypi.org/project/ddb-single/\n\nPython DynamoDB interface, specialized in single-table design.\nDynamoDB is high-performance serverless NoSQL, but difficult to disign tables.\n\nSingle-table design needs only single table, and few GSIs (Global Secondary Indexes).\nIt makes effective and easy to manage your whole data models for single service.\n\n## Getting Started\n\n### Install\n\n```\npip install ddb-single\n```\n\n### Init Table\n\n```python\nfrom ddb_single import Table\n\ntable = Table(\n    table_name=\"sample\",\n    endpoint_url=\"http://localhost:8000\",\n)\ntable.init()\n```\n\n### Data Models\n\nEach model has al least 3 keys\n- primary_key ... Hash key for single item. default: `pk: {__model_name__}_{uuid}` \n- seconday_key ... Range key for item. default: `sk: {__model_name__}_item`\n- unique_key ... key to identify the item is the same. Mainly used to update item.\n\nAnd you can set `serch_key` to enable search via GSI \n\n```python\nfrom ddb_single import BaseModel, DBField, FieldType\n\nclass User(BaseModel):\n    __table__=table\n    __model_name__ = \"user\"\n    name = DBField(unique_key=True)\n    email = DBField(search_key=True)\n    age = DBField(type=FieldType.NUMBER, search_key=True)\n    description=DBField()\n```\n\n## Usage\n\nneed \"Qurey\" object for CRUD\n- `query.model(foo).create`\n- `query.model(foo).get`\n- `query.model(foo).search`\n- `query.model(foo).update`\n- `query.model(foo).delete`\n\n```python\nfrom ddb_single import Query\nquery = Query(table)\n```\n\n\n### Create Item\n\nIf the item with same value of `unique_key` already exist, exist item is updated.\n\n```python\nuser = User(name=\"John\", email=\"john@example.com\", description=\"test\")\nquery.model(user).create()\n```\n\nThen, multible items added.\n\n|pk|sk|data|name|email|description|\n|-|-|-|-|-|-|\n|user_xxxx|user_item||John|john@example.com|test|\n|user_xxxx|search_user_name|John|\n|user_xxxx|search_user_email|new-john@example.com|\n\nIn addition to main item (sk=`user_item`), multiple item (sk=`search_{__model_name__}_{field_name}`) added to table.\nThose \"search items\" are used to search\n\nThe GSI `DataSearchIndex` is used to get \"search items\" to extract target's pk.\nThen, `batch_get` items by pk.\n\n|sk = hash|data = range|pk|\n|-|-|-|\n|search_user_name|John|user_xxxx|\n|search_user_email|new-john@example.com|user_xxxx|\n\n### Search Items\n\n```python\nuser = query.model(User).search(User.name.eq(\"John\"))\nprint(user)\n# -> [{\"pk\":\"user_xxxx\", \"sk\":\"user_item\", \"name\":\"John\", \"email\":\"john@example.com\"}]\n```\n\n`pk_only=True` to extract pk without `batch_get`\n\n```python\nuser_pks = query.model(User).search(User.name.eq(\"John\"), pk_only=True)\nprint(user_pks)\n# -> [\"user_xxxx\"]\n```\n\n### Get single item\n\n`get(pk)` to get single item.\n\n```\nuser = query.model(User).get(\"user_xxxx\")\nprint(user)\n# -> {\"pk\":\"user_xxxx\", \"sk\":\"user_item\", \"name\":\"John\", \"email\":\"john@example.com\"}\n```\n\n`get_by_unique` to get item by `unique_key`\n\n```python\nuser = query.model(User).get_by_unique(\"John\")\nprint(user)\n# -> {\"pk\":\"user_xxxx\", \"sk\":\"user_item\", \"name\":\"John\", \"email\":\"john@example.com\"}\n```\n\n`pk_only=True` option in `get_by_unique` to get `primary key` without `get_item`\n\n```python\npk = query.model(User).get_by_unique(\"John\", pk_only=True)\nprint(pk)\n# -> \"user_xxxx\"\n```\n\n### Update Item\n\n```python\nuser = query.model(User).search(User.email.eq(\"john@example.com\"))\nnew_user = User(**user[0])\nnew_user.email = \"new-john@example.com\"\nquery.model(new_user).update()\n```\n\nOr use unique value to detect exist item.\n\n```python\nnew_user = User(name=\"John\", email=\"new-john@example.com\")\nquery.model(new_user).update()\n```\n\nThen, tha value of \"main item\" and \"seach item\" changed\n\n|pk|sk|data|name|email|description|\n|-|-|-|-|-|-|\n|user_xxxx|user_item||John|new-john@example.com|test|\n|user_xxxx|search_user_name|John|\n|user_xxxx|search_user_email|new-john@example.com|\n\n\n### Delete Item\n\n```\nuser = query.model(User).search(User.email.eq(\"new-john@example.com\"))\nquery.model(user[0]).delete()\n```\n\n`primary key` to detect exist item.\n\n```\nquery.model(User).delete_by_pk(\"user_xxxx\")\n```\n\n\nor `unique key`\n\n```\nquery.model(User).delete_by_unique(\"John\")\n```\n\n## Batch Writer\n\n`table.batch_writer()` to create/update/delete multible items\n- `query.model(foo).create(batch=batch)`\n- `query.model(foo).update(batch=batch)`\n- `query.model(foo).delete(batch=batch)`\n\n### Batch Create\n\n```python\nwith table.batch_writer() as batch:\n    for i in range(3):\n        user = User(name=f\"test{i}\", age=i+10)\n        query.model(user).create(batch=batch)\nres = query.model(User).search(User.name.begins_with(\"test\"))\nprint([(r[\"name\"], r[\"age\"]) for r in res])\n# -> [(\"test0\", 10), (\"test1\", 11), (\"test2\", 12)]\n```\n\n### Batch Update\n\n```python\nwith table.batch_writer() as batch:\n    for i in range(3):\n        user = User(name=f\"test{i}\", age=i+20)\n        query.model(user).update(batch=batch)\nres = query.model(User).search(User.name.begins_with(\"test\"))\nprint([(r[\"name\"], r[\"age\"]) for r in res])\n# -> [(\"test0\", 20), (\"test1\", 21), (\"test2\", 22)]\n```\n\n### Batch Delete\n\n```python\npks = query.model(User).search(User.name.begins_with(\"test\"), pk_only=True)\nwith table.batch_writer() as batch:\n    for pk in pks:\n        query.model(user).delete_by_pk(pk, batch=batch)\nres = query.model(User).search(User.name.begins_with(\"test\"))\nprint(res)\n# -> []\n```\n\n\n## Relationship\n\n### Create Model\n\nYou can sat relationns to other models\n`relation=BaseModel` to set relation.\n\n```python\nclass BlogPost(BaseModel):\n    __model_name__ = \"blogpost\"\n    __table__=table\n    name = DBField(unique_key=True)\n    content = DBField()\n    author = DBField(reletion=User)\n```\n\n### Create Item\n\n\n```python\nblogpost = BlogPost(\n    name=\"Hello\",\n    content=\"Hello world\",\n    author=self.user\n)\nquery.model(blogpost).create()\n```\n\nThen, tha value \"reletion item\" added\n\n|pk|sk|data|name|author|content|\n|-|-|-|-|-|-|\n|user_xxxx|user_item||John|||\n|user_xxxx|search_user_name|John|\n|blogpost_xxxx|blogpost_item||Hello|John|Hello world|\n|blogpost_xxxx|search_blogpost_title|Hello|\n|blogpost_xxxx|rel_user_xxxx|author|\n\nIn addition to main item (sk=`blogpost_item`), relation item (sk=`rel_{primary_key}`) added to table. The GSI `DataSearchIndex` is used to get \"relation items\" to extract target's pk.\nThen, `batch_get` items by pk.\n\n|sk = hash|data = range|pk|\n|-|-|-|\n|rel_user_xxxx|author|blogpost_xxxx|\n\n### Search Relations\n\n`get_relation(model=Basemodel)` to search relations\n\n```python\nblogpost = query.model(BlogPost).get_by_unique(\"Hello\")\nblogpost = BlogPost(**blogpost)\n\nuser = query.model(blogpost).get_relation(model=User)\nprint(user)\n# -> [{\"pk\":\"user_xxxx\", \"sk\":\"user_item\", \"name\":\"John\"}]\n```\n\nAlso `get_relation(field=DBField)` to specify field\n\n```python\nuser = query.model(blogpost).get_relation(field=BlogPost.author)\nprint(user)\n# -> [{\"pk\":\"user_xxxx\", \"sk\":\"user_item\", \"name\":\"John\"}]\n```\n\n### Search Reference\n\nIn this library, \"reference\" is antonym to relation\n\n`get_reference(model=Basemodel)` to search items related to the item\n\n```python\nuser = query.model(User).get_by_unique(\"John\")\nuser = User(**blogpost)\n\nblogpost = query.model(blogpost).get_reference(model=BlogPost)\nprint(blogpost)\n# -> [{\"pk\":\"blogpost_xxxx\", \"sk\":\"blogpost_item\", \"name\":\"Hello\"}]\n```\n\nAlso `get_reference(field=DBField)` to specify field\n\n```python\nblogpost = query.model(user).get_reference(field=BlogPost.author)\nprint(blogpost)\n# -> [{\"pk\":\"blogpost_xxxx\", \"sk\":\"blogpost_item\", \"name\":\"Hello\"}]\n```\n\n### Update Relation\n\nIf relation key's value changed, relationship also changed.\n\n\n```python\nnew_user = User(name=\"Michael\")\nblogpost = query.model(BlogPost).get_by_unique(\"Hello\")\nblogpost[\"author\"] = new_user\nblogpost = BlogPost(**blogpost)\n\nquery.model(blogpost).update()\n```\n\nThen, \"reletion item\" changed\n\n|pk|sk|data|name|author|content|\n|-|-|-|-|-|-|\n|user_xxxx|user_item||John|||\n|user_xxxx|search_user_name|John|\n|user_yyyy|user_item||Michael|||\n|user_yyyy|search_user_name|Michael|\n|blogpost_xxxx|blogpost_item||Hello|Michael|Hello world|\n|blogpost_xxxx|search_blogpost_title|Hello|\n|blogpost_xxxx|rel_user_yyyy|author|\n### Delete Relation\n\nIf related item deleted, relationship also deleted\n\n```python\nquery.model(user).delete_by_unique(\"Michael\")\n```\n\nThen, \"reletion item\" deleted.\nBut main item's value is not chenged.\n\n|pk|sk|data|name|author|content|\n|-|-|-|-|-|-|\n|user_xxxx|user_item||John|||\n|user_xxxx|search_user_name|John|\n|blogpost_xxxx|blogpost_item||Hello|Michael|Hello world|\n|blogpost_xxxx|search_blogpost_title|Hello|\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Python DynamoDB interface, specialized in single-table design.",
    "version": "0.4.7",
    "project_urls": {
        "Homepage": "https://github.com/medaka0213/DynamoDB-SingleTable"
    },
    "split_keywords": [
        "aws",
        "dynamodb",
        "serverless"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "d1de8899823c20b665e6e029ae3b11cc487dca030a78e3ac942f8d7c6715861c",
                "md5": "3157ecb2ac094c5ff4e8b28fb5b4c591",
                "sha256": "8a189075c720b1c6667fdd8f006b0d0c3e40ecc08a439ec5de386d0375f25159"
            },
            "downloads": -1,
            "filename": "ddb_single-0.4.7.zip",
            "has_sig": false,
            "md5_digest": "3157ecb2ac094c5ff4e8b28fb5b4c591",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 19368,
            "upload_time": "2024-03-23T07:49:46",
            "upload_time_iso_8601": "2024-03-23T07:49:46.803074Z",
            "url": "https://files.pythonhosted.org/packages/d1/de/8899823c20b665e6e029ae3b11cc487dca030a78e3ac942f8d7c6715861c/ddb_single-0.4.7.zip",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-23 07:49:46",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "medaka0213",
    "github_project": "DynamoDB-SingleTable",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [
        {
            "name": "boto3",
            "specs": []
        }
    ],
    "lcname": "ddb-single"
}
        
Elapsed time: 0.23270s