pynamodb / PynamoDB

A pythonic interface to Amazon's DynamoDB
http://pynamodb.readthedocs.io
MIT License
2.46k stars 427 forks source link

Decimal support #551

Open mscansian opened 6 years ago

mscansian commented 6 years ago

Is there a way to use decimal.Decimal in PynamoDB? To my knowledge, the only way to save using the NumberAttribute is by converting it to float first. I tried sending a Decimal to the NumberAttribute, but since it uses JSON as a serializer, it does not work.

I'm working with financial data where precision is mandatory and float is not an option for me. Am I missing something or Decimal support is not yet implemented?

numberoverzero commented 5 years ago

From these docs you could create a custom attribute that uses a decimal.Context to translate between a decimal.Decimal instance and a string.

I haven't tested this, and it's based on how I implemented the same in bloop, but I'm pretty sure it will do what you want. Note the round trip through create_decimal is required in serialize to ensure the value meets your context's requirements.

import decimal

from pynamodb.attributes import Attribute
from pynamodb.constants import NUMBER

DYNAMODB_CONTEXT = decimal.Context(
    Emin=-128, Emax=126, rounding=None, prec=38,
    traps=[
        decimal.Clamped, decimal.Overflow, decimal.Inexact,
        decimal.Rounded, decimal.Underflow
    ]
)

class DecimalAttribute(Attribute):
    """
    Serialize and deserialize decimal.Decimal instances with a given decimal.Context.

    The default decimal.Context comes from the definition used by boto3.
    """
    def __init__(self, context=None):
        context = context or DYNAMODB_CONTEXT
        self.context = context

    def serialize(self, value):
        # decimal.Decimal -> str
        return str(self.context.create_decimal(value))

    def deserialize(self, value):
        # str -> decimal.Decimal
        return self.context.create_decimal(value)
mscansian commented 5 years ago

@numberoverzero Thanks for the explanation. Haven't tested it yet, but it looks like it's gonna work. I just think that is also necessary to include attr_type = NUMBER in the class to avoid DynamoDB saving data as a string (though I'm not sure).

Anyway, don't you guys think this is an attribute worth of a place in the lib itself? I will gladly send a PR if that's the case.

numberoverzero commented 5 years ago

good call! i imported and forgot to add the line.

ikonst commented 5 years ago

The maintainers told me they'd rather keep only the primitive attributes in pynamodb.attributes, which is why I created pynamodb-attributes to complement it.

acorrea-B commented 3 years ago

@numberoverzero I was use your code but I had something problems but I was solve it, the next way:

class DecimalAttribute(Attribute):

    attr_type = NUMBER

    def __init__(self, context=None, **kwargs):
        super().__init__(**kwargs)
        context = context or DYNAMODB_CONTEXT
        self.context = context

    def serialize(self, value):
        # decimal.Decimal -> str
        return str(self.context.create_decimal(str(value)))

    def deserialize(self, value):
        # str -> decimal.Decimal
        return self.context.create_decimal(value)

I hope that this code help someone