Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug: low level api operations don't work without calling describe_table first #1249

Open
bpsoos opened this issue Jul 24, 2024 · 0 comments
Open

Comments

@bpsoos
Copy link

bpsoos commented Jul 24, 2024

The issue

Based on the docs it seems that we should be able to instantiate a Connection or a TableConnection and perform put_item, get_item, update_item and delete_item operations on them. However these all fail with the following error:
Meta-table for '<table name>' not initialized
These operations work if we call describe_table first on the connection, like in the current integration tests.

examples to reproduce the issue:

    table_name = 'pynamodb-ci-connection'
    conn = Connection(host=ddb_url)
    # conn.describe_table(table_name)  # bug: operations don't work without calling describe_table first

    conn.put_item(
        table_name,
        'item1-hash',
        attributes={'foo': {'S': 'bar'}},
    )  # pynamodb.exceptions.TableError: Meta-table for 'pynamodb-ci-connection' not initialized
   table_name = 'pynamodb-ci-connection'
   conn = TableConnection(table_name=table, host=ddb_url)
   # conn.describe_table()  # bug: operations don't work without calling describe_table first
   conn.put_item(
       'item1-hash',
       attributes={'foo': {'S': 'bar'}},
   )  # pynamodb.exceptions.TableError: Meta-table for 'pynamodb-ci-connection' not initialized

See the integration tests in the linked pull request for runnable versions of these examples.

Root cause

Itt seems that Connection.get_identifier_map relies on a MetaTable corresponding to the given table to extract information about it's hash and range keys.

Solution proposal

I propose to add an optional parameter, keyname_details, to all of the relevant operations and to Connection.get_identifier_map. If keyname_details is given use that, otherwise default to the current behaviour, that is try to fetch the key details from a MetaTable.

This could look something like the following:

@dataclass
class TypedName:
    name: str
    name_type: str


@dataclass
class KeyNameDetails:
    hash_keyname: TypedName
    range_keyname: Optional[TypedName] = None


class Connection(object):
    ...
    def get_item(
        self,
        table_name: str,
        hash_key: str,
        range_key: Optional[str] = None,
        consistent_read: bool = False,
        attributes_to_get: Optional[Any] = None,
        keyname_details: Optional[KeyNameDetails] = None,
    ) -> Dict:
        operation_kwargs = self.get_operation_kwargs(
            table_name=table_name,
            hash_key=hash_key,
            range_key=range_key,
            consistent_read=consistent_read,
            attributes_to_get=attributes_to_get
            keyname_details=keyname_details,
        )
        ...

    def get_operation_kwargs(
        self,
        table_name: str,
        hash_key: str,
        range_key: Optional[str] = None,
        key: str = KEY,
        attributes: Optional[Any] = None,
        attributes_to_get: Optional[Any] = None,
        actions: Optional[Sequence[Action]] = None,
        condition: Optional[Condition] = None,
        consistent_read: Optional[bool] = None,
        keyname_details: Optional[KeyNameDetails] = None,
        return_values: Optional[str] = None,
        return_consumed_capacity: Optional[str] = None,
        return_item_collection_metrics: Optional[str] = None,
        return_values_on_condition_failure: Optional[str] = None
    ) -> Dict:
        ...
        operation_kwargs.update(
            self.get_identifier_map(
                table_name,
                hash_key,
                range_key,
                key=key,
                keyname_details= keyname_details,
            ),
        )

Connection.get_identifier_map:

 class Connection(object):
    ...
        def get_identifier_map(
        self,
        table_name: str,
        hash_key: str,
        range_key: Optional[str] = None,
        key: str = KEY,
        keyname_details: Optional[KeyNameDetails] = None,
    ) -> Dict:
        if keyname_details is None:
            tbl = self.get_meta_table(table_name)
            if tbl is None:
                raise TableError("No such table {}".format(table_name))
            return tbl.get_identifier_map(hash_key, range_key=range_key, key=key)

        kwargs: Dict[str, Any] = {
            key: {
                keyname_details.hash_keyname.name: {
                    keyname_details.hash_keyname.name_type: hash_key
                }
            }
        }
        if keyname_details.range_keyname is not None:
            kwargs[key][keyname_details.range_keyname.name] = {
                keyname_details.range_keyname.name_type: range_key
            }
        return kwargs
@bpsoos bpsoos changed the title low level api operations don't work without calling describe_table first bug: low level api operations don't work without calling describe_table first Aug 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant