Skip to content

Commit

Permalink
Add UUID attribute type (#6)
Browse files Browse the repository at this point in the history
* Adds UUIDAttribute to pynamodb_attributes
  * Custom attribute is backed by unicode 'S' type in DynamoDB type
* Adds docs and bumps number
* Adds tests
  • Loading branch information
NazarioJL authored Jul 16, 2019
1 parent 12b1014 commit 446b922
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ build/
.coverage
.mypy_cache
.pytest_cache
.dmypy.json
coverage.xml
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ This Python 3 library contains compound and high-level PynamoDB attributes:
- `UnicodeDelimitedTupleAttribute` - a delimiter-separated value, useful for storing composite keys
- `UnicodeEnumAttribute` - serializes a string-valued `Enum` into a Unicode (`S`-typed) attribute
- `TimestampAttribute`, `TimestampMsAttribute`, `TimestampUsAttribute` – serializes `datetime`s as Unix epoch seconds, milliseconds (ms) or microseconds (µs)
- `IntegerDateAttribute` - serializes `date` as an integer representing the Gregorian date (e.g. `20181231`)
- `IntegerDateAttribute` - serializes `date` as an integer representing the Gregorian date (_e.g._ `20181231`)
- `UUIDAttribute` - serializes a `UUID` Python object as a `S` type attribute (_e.g._ `'a8098c1a-f86e-11da-bd1a-00112444be1e'`)
2 changes: 2 additions & 0 deletions pynamodb_attributes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .timestamp import TimestampUsAttribute
from .unicode_delimited_tuple import UnicodeDelimitedTupleAttribute
from .unicode_enum import UnicodeEnumAttribute
from .uuid import UUIDAttribute

__all__ = [
'FloatAttribute',
Expand All @@ -18,4 +19,5 @@
'TimestampAttribute',
'TimestampMsAttribute',
'TimestampUsAttribute',
'UUIDAttribute',
]
39 changes: 39 additions & 0 deletions pynamodb_attributes/uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import Any
from typing import TYPE_CHECKING
from uuid import UUID

import pynamodb.constants
from pynamodb.attributes import Attribute


class UUIDAttribute(Attribute):
"""
PynamoDB attribute to for UUIDs. These are backed by DynamoDB unicode (`S`) types.
"""

attr_type = pynamodb.constants.STRING

def __init__(self, remove_dashes: bool = False, **kwargs: Any) -> None:
"""
Initializes a UUIDAttribute object.
:param remove_dashes: if set, the string serialization will be without dashes.
Defaults to False.
"""
super().__init__(**kwargs)
self._remove_dashes = remove_dashes

def serialize(self, value: UUID) -> str:
result = str(value)

if self._remove_dashes:
result = result.replace('-', '')

return result

def deserialize(self, value: str) -> UUID:
return UUID(value)

if TYPE_CHECKING:
def __get__(self, instance: Any, owner: Any) -> UUID:
...
6 changes: 6 additions & 0 deletions pynamodb_attributes/uuid.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from uuid import UUID

from ._typing import Attribute

class UUIDAttribute(Attribute[UUID]):
...
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def find_stubs(package): # type: ignore

setup(
name='pynamodb-attributes',
version='0.2.3',
version='0.2.5',
description='Common attributes for PynamoDB',
url='https://www.github.com/lyft/pynamodb-attributes',
maintainer='Lyft',
Expand Down
13 changes: 13 additions & 0 deletions tests/mypy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,16 @@ class MyModel(Model):
m.ts_ms = 42 # E: Incompatible types in assignment (expression has type "int", variable has type "datetime")
m.ts_us = 42 # E: Incompatible types in assignment (expression has type "int", variable has type "datetime")
""")


def test_uuid_attribute():
assert_mypy_output("""
from pynamodb.models import Model
from pynamodb_attributes import UUIDAttribute
class MyModel(Model):
my_attr = UUIDAttribute()
reveal_type(MyModel.my_attr) # E: Revealed type is 'pynamodb_attributes.uuid.UUIDAttribute'
reveal_type(MyModel().my_attr) # E: Revealed type is 'uuid.UUID*'
""")
68 changes: 68 additions & 0 deletions tests/uuid_attribute_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from uuid import UUID

import pytest
from pynamodb.attributes import UnicodeAttribute
from pynamodb.models import Model

from pynamodb_attributes import UUIDAttribute
from tests.meta import dynamodb_table_meta


class MyModel(Model):
Meta = dynamodb_table_meta(__name__)

key = UnicodeAttribute(hash_key=True)
value = UUIDAttribute(null=True)


@pytest.fixture(scope="module", autouse=True)
def create_table():
MyModel.create_table()


def test_deserialization_no_dashes():
uuid_attribute = UUIDAttribute(remove_dashes=True)
uuid_str_no_dashes = "19c4f2515e364cc0bfeb983dd5d2bacd"

assert UUID("19c4f251-5e36-4cc0-bfeb-983dd5d2bacd") == uuid_attribute.deserialize(
uuid_str_no_dashes,
)


def test_serialization_no_dashes():
uuid_attribute = UUIDAttribute(remove_dashes=True)
uuid_value = UUID("19c4f251-5e36-4cc0-bfeb-983dd5d2bacd")

assert "19c4f2515e364cc0bfeb983dd5d2bacd" == uuid_attribute.serialize(uuid_value)


def test_serialization_non_null(uuid_key):
model = MyModel()
model.key = uuid_key
uuid_str = "19c4f251-5e36-4cc0-bfeb-983dd5d2bacd"
uuid_value = UUID(uuid_str)
model.value = uuid_value
model.save()

# verify underlying storage
item = MyModel._get_connection().get_item(uuid_key)
assert item["Item"]["value"] == {"S": uuid_str}

# verify deserialization
model = MyModel.get(uuid_key)
assert model.value == uuid_value


def test_serialization_null(uuid_key):
model = MyModel()
model.key = uuid_key
model.value = None
model.save()

# verify underlying storage
item = MyModel._get_connection().get_item(uuid_key)
assert "value" not in item["Item"]

# verify deserialization
model = MyModel.get(uuid_key)
assert model.value is None

0 comments on commit 446b922

Please sign in to comment.