Search code examples
python-3.xazure-table-storagepylintpydantic-v2

How do I define a not required Pydantic Field in a ModelBase that needs other fields to be defined without my Lintern complaining?


So, I am working in creating a kind of ORM for Azure Tables for my job, the problem is, I need to not pre-define the field called table_client because that doesn't make sense, I need to build the table_client after all validations because it needs self.connection_string and self.table_name (example below). Also, I need that attribute to be persistent between all the existence of the object, so I am not re-creating Table Clients every time I call for a method or something like that. So I need this attribute to be part of the model (i.e. I need self.table_client), the problem is, I cannot make it Optional or typed as table_client: TableClient | None = None because, below that, I receive a bunch of warnings from my linter saying "create_table" is not a known attribute of "None", or something like that.

What is the approach here? I cannot use default_factory because I need validated fields (connection_string and table_name) obviously (I think). Here is my current code (Assume all the imports are correct):

class AzureTable(BaseModel):
    connection_string: str
    table_name: str
    table_client: TableClient | None = None

    def __post_init__(self):
        self._create_connection()

    def _create_connection(self):
        self.table_client = TableServiceClient.from_connection_string( # Here is the freaking waning
            conn_str=self.connection_string
        ).get_table_client(self.table_name)

        return self

    def create_table(self) -> None:
        try:
            self.table_client.create_table(table_name=self.table_name)
        except HttpResponseError:
            raise TableAlreadyExistsError()

    def get_all(self) -> list:
        return list(self.table_client.list_entities())

More info:

Python: 3.12
Pydantic: 2.8.2
azure-data-tables: 12.5.0

Also tried:

class AzureTable(BaseModel):
    connection_string: str
    table_name: str

    def __post_init__(self):
        self._create_connection()

    def _create_connection(self):
        self.table_client = TableServiceClient.from_connection_string(
            conn_str=self.connection_string
        ).get_table_client(self.table_name)

        return self

    def create_table(self) -> None:
        try:
            self.table_client.create_table(table_name=self.table_name)
        except HttpResponseError:
            raise TableAlreadyExistsError()

    def get_all(self) -> list:
        return list(self.table_client.list_entities())

But gives me an error:

File "/home/azureuser/dev/LangBotAPI/venv/lib/python3.12/site-packages/pydantic/main.py", line 828, in __getattr__
    raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
AttributeError: 'BotTable' object has no attribute 'table_client'

Solution

  • Maybe you want a computed field? That would look something like this:

    from functools import cached_property
    from pydantic import BaseModel, computed_field
    
    class AzureTable(BaseModel):
        connection_string: str
        table_name: str
    
        @computed_field
        @cached_propery
        def table_client(self) -> AppropriateReturnType:
            table_client = (
                TableServiceClient.from_connection_string(
                    conn_str=self.connection_string
                ).get_table_client(self.table_name)
            )
    
            return table_client
    

    I'm not at all familiar with the Azure modules (which is why the above example is annoted with AppropriateReturnType), but here's a runnable example using only native types to show how it works. Given the following:

    from functools import cached_property
    from pydantic import BaseModel, computed_field
    
    
    class TableClient(BaseModel):
        pass
    
    
    class AzureTable(BaseModel):
        connection_string: str
        table_name: str
    
        @computed_field
        @cached_property
        def table_client(self) -> str:
            return f"{self.table_name}@{self.connection_string}"
    

    We can do this:

    >>> x = AzureTable(connection_string='user@host', table_name='example')
    >>> x
    AzureTable(connection_string='user@host', table_name='example', table_client='example@user@host')
    >>> x.table_client
    'example@user@host'
    

    Note that because we're using the cached_property decorator, table_client is only computed once.