PynamoDB icon indicating copy to clipboard operation
PynamoDB copied to clipboard

unable to create ListAttribute of custom attribute

Open rsmekala opened this issue 2 years ago • 2 comments

Steps to reproduce

  1. Following class definition Attribute definition
UnicodeEnumAttribute
from enum import Enum
from typing import Any
from typing import Optional
from typing import Type
from typing import TypeVar

import pynamodb.constants
from pynamodb.attributes import Attribute

from app.config.llmconfig import AvailableLLMEnum

T = TypeVar("T", bound=Enum)
_fail: Any = object()


class UnicodeEnumAttribute(Attribute[T]):
    attr_type = pynamodb.constants.STRING

    def __init__(
        self, enum_type: Type[T], unknown_value: Optional[T] = _fail, **kwargs: Any
    ) -> None:
        """
        :param enum_type: The type of the enum
        """
        super().__init__(**kwargs)
        self.enum_type = enum_type
        self.unknown_value = unknown_value
        if not all(isinstance(e.value, str) for e in self.enum_type):
            raise TypeError(
                f"Enumeration '{self.enum_type}' values must be all strings",
            )

    def deserialize(self, value: str) -> Optional[T]:
        try:
            return self.enum_type(value)
        except ValueError:
            if self.unknown_value is _fail:
                raise
            return self.unknown_value

    def serialize(self, value: T) -> str:
        if not isinstance(value, self.enum_type):
            raise TypeError(
                f"value has invalid type '{type(value)}'; expected '{self.enum_type}'",
            )
        return value.value

Model defintion

from typing import Optional

from pynamodb.attributes import UnicodeAttribute, ListAttribute, NumberAttribute

from app.config.dynamodbconfig import (
    DynamoDbTableEnum,
    DYNAMO_ENDPOINT,
    DYNAMO_REGION,
)
from app.config.llmconfig import AvailableLLMEnum
from app.models.basemodel import BaseModel
from app.models.custom_attributes.unicodeenum import UnicodeEnumAttribute, AvailableLLMUnicodeEnumListAttribute


class Channel(BaseModel):
    class Meta:
        table_name = DynamoDbTableEnum.CHANNELS_TABLE_NAME
        host = DYNAMO_ENDPOINT
        region = DYNAMO_REGION

    name = UnicodeAttribute(hash_key=True)
    # list of enums cannot be generated with the way of pynamo implements it, bec
    whitelisted_llms = ListAttribute(default=[], of=type(UnicodeEnumAttribute(AvailableLLMEnum)))
    temperature = NumberAttribute(null=True)
    presence_penalty = NumberAttribute(null=True)

    @staticmethod
    def get_channel_name(tenant_name: str, channel_name: str):
        return tenant_name + "|" + channel_name
  1. try saving an item
  2. issue
{"@timestamp":"2023-09-05T04:53:52.442Z","log.level":"error","message":"(TypeError(\"__init__() missing 1 required positional argument: 'enum_type'\"),)","ecs":{"version":"1.6.0"},"error":{"message":"unhandled errors in a TaskGroup (1 sub-exception)","stack_trace":"  File \"/Users/rajashekar/workspace/gai/maverick/venv/lib/python3.9/site-packages/starlette/middleware/errors.py\", line 162, in __call__\n    await self.app(scope, receive, _send)\n  File \"/Users/rajashekar/workspace/gai/maverick/venv/lib/python3.9/site-packages/starlette/middleware/base.py\", line 110, in __call__\n    response_sent.set()\n  File \"/Users/rajashekar/workspace/gai/maverick/venv/lib/python3.9/site-packages/anyio/_backends/_asyncio.py\", line 664, in __aexit__\n    raise BaseExceptionGroup(\n","type":"ExceptionGroup"},"log":{"logger":"app.exception_handlers","origin":{"file":{"line":1,"name":"<string>"},"function":"<module>"},"original":"(TypeError(\"__init__() missing 1 required positional argument: 'enum_type'\"),)"},"process":{"name":"SpawnProcess-3","pid":99594,"thread":{"id":8658525696,"name":"MainThread"}}}

Suspected cause

https://github.com/pynamodb/PynamoDB/blob/335c7cde6732c5121347207e60479d96e47338f6/pynamodb/attributes.py#L1396

Temporary Workaround

Custom Attribute


class AvailableLLMUnicodeEnumListAttribute(UnicodeEnumAttribute[AvailableLLMEnum]):
    def __init__(self, **kwargs: Any) -> None:
        """
        :param enum_type: The type of the enum
        """
        super().__init__(AvailableLLMEnum, **kwargs)

Model

class Channel(BaseModel):
    class Meta:
        table_name = DynamoDbTableEnum.CHANNELS_TABLE_NAME
        host = DYNAMO_ENDPOINT
        region = DYNAMO_REGION

    name = UnicodeAttribute(hash_key=True)
    # list of enums cannot be generated with the way of pynamo implements it, bec
    whitelisted_llms = ListAttribute(default=[], of=AvailableLLMUnicodeEnumListAttribute)
    temperature = NumberAttribute(null=True)
    presence_penalty = NumberAttribute(null=True)

    @staticmethod
    def get_channel_name(tenant_name: str, channel_name: str):
        return tenant_name + "|" + channel_name

rsmekala avatar Sep 05 '23 05:09 rsmekala

@ikonst is this the right way to achieve this ??

rsmekala avatar Sep 05 '23 05:09 rsmekala

Your solution seems reasonable.

ikonst avatar Sep 18 '23 02:09 ikonst