baserow.table

The module provides the ORM-like functionality of Baserowdantic.

  1"""
  2The module provides the ORM-like functionality of Baserowdantic.
  3"""
  4
  5
  6import abc
  7from functools import wraps
  8from typing import Any, ClassVar, Generic, Optional, Tuple, Type, TypeVar, Union, get_args, get_origin
  9import uuid
 10
 11from pydantic import BaseModel, ConfigDict, Field, RootModel, model_serializer, model_validator
 12from pydantic.fields import FieldInfo
 13
 14from baserow.client import Client, GlobalClient, MinimalRow
 15from baserow.error import InvalidFieldForCreateTableError, InvalidTableConfigurationError, MultiplePrimaryFieldsError, NoClientAvailableError, NoPrimaryFieldError, PydanticGenericMetadataError, RowIDNotSetError
 16from baserow.field import BaserowField
 17from baserow.field_config import DEFAULT_CONFIG_FOR_BUILT_IN_TYPES, Config, FieldConfigType, LinkFieldConfig, PrimaryField
 18from baserow.filter import Filter
 19
 20
 21def valid_configuration(func):
 22    """
 23    This decorator checks whether the model configuration has been done
 24    correctly. In addition to validating the class vars Table.table_id and
 25    Table.table_name, it also verifies whether the model config is set with
 26    populate_by_name=True.
 27    """
 28
 29    @wraps(func)
 30    def wrapper(cls, *args, **kwargs):
 31        if not isinstance(cls.table_id, int):
 32            raise InvalidTableConfigurationError(
 33                cls.__name__, "table_id not set")
 34        if not isinstance(cls.table_name, str):
 35            raise InvalidTableConfigurationError(
 36                cls.__name__, "table_name not set")
 37        if "populate_by_name" not in cls.model_config:
 38            raise InvalidTableConfigurationError(
 39                cls.__name__,
 40                "populate_by_name is not set in the model config; it should most likely be set to true"
 41            )
 42        return func(cls, *args, **kwargs)
 43    return wrapper
 44
 45
 46T = TypeVar("T", bound="Table")
 47
 48
 49class RowLink(BaseModel, Generic[T]):
 50    """
 51    A single linking of one row to another row in another table. A link field
 52    can have multiple links. Part of `table.TableLinkField`.
 53    """
 54    row_id: Optional[int] = Field(alias=str("id"))
 55    key: Optional[str] = Field(alias=str("value"))
 56
 57    model_config = ConfigDict(populate_by_name=True)
 58
 59    @model_validator(mode="after")
 60    def id_or_value_must_be_set(self: "RowLink") -> "RowLink":
 61        if self.row_id is None and self.key is None:
 62            raise ValueError(
 63                "At least one of the row_id and value fields must be set"
 64            )
 65        return self
 66
 67    @model_serializer
 68    def serialize(self) -> Union[int, str]:
 69        """
 70        Serializes the field into the data structure required by the Baserow
 71        API. If an entry has both an id and a value set, the id is used.
 72        Otherwise the key field is used.
 73
 74        From the Baserow API documentation: Accepts an array containing the
 75        identifiers or main field text values of the related rows.
 76        """
 77        if self.row_id is not None:
 78            return self.row_id
 79        if self.key is not None:
 80            return self.key
 81        raise ValueError("both fields id and key are unset for this entry")
 82
 83    async def query_linked_row(self) -> T:
 84        """
 85        Queries and returns the linked row.
 86        """
 87        if self.row_id is None:
 88            raise ValueError(
 89                "query_linked_row is currently only implemented using the row_id",
 90            )
 91        table = self.__get_linked_table()
 92        return await table.by_id(self.row_id)
 93
 94    def __get_linked_table(self) -> T:
 95        metadata = self.__pydantic_generic_metadata__
 96        if "args" not in metadata:
 97            raise PydanticGenericMetadataError.args_missing(
 98                self.__class__.__name__,
 99                "linked table",
100            )
101        if len(metadata["args"]) < 1:
102            raise PydanticGenericMetadataError.args_empty(
103                self.__class__.__name__,
104                "linked table",
105            )
106        return metadata["args"][0]
107
108
109class TableLinkField(BaserowField, RootModel[list[RowLink]], Generic[T]):
110    """
111    A link to table field creates a link between two existing tables by
112    connecting data across tables with linked rows.
113    """
114    root: list[RowLink[T]]
115    _cache: Optional[list[T]] = None
116
117    @classmethod
118    def default_config(cls) -> FieldConfigType:
119        metadata = cls.__pydantic_generic_metadata__
120        if "args" not in metadata:
121            raise PydanticGenericMetadataError.args_missing(
122                cls.__class__.__name__,
123                "linked table",
124            )
125        if len(metadata["args"]) < 1:
126            raise PydanticGenericMetadataError.args_empty(
127                cls.__class__.__name__,
128                "linked table",
129            )
130        linked_table = metadata["args"][0]
131        return LinkFieldConfig(link_row_table_id=linked_table.table_id)
132
133    @classmethod
134    def read_only(cls) -> bool:
135        return False
136
137    @classmethod
138    def from_value(cls, *instances: Union[int, T]):
139        """
140        Instantiates a link field from a referencing value. Can be used to set a
141        link directly when instantiating a table model using a parameter. This
142        is a quality of life feature and replace the tedious way of manually
143        defining a link. For more information please refer to the example below.
144
145        ```python
146        class Author(Table):
147            [...] name: str
148
149        class Book(Table):
150            [...] title: str author: Optional[TableLinkField[Author]] =
151            Field(default=None)
152
153        # Instead of...
154        new_book = await Book(
155            title="The Great Adventure", author=TableLinkField[Author](
156                root=[RowLink[Author](row_id=23, key=None)]
157            )
158        ).create()
159
160        # ...this method allows this (link to author row with id=23) new_book =
161        await Book(
162            title="The Great Adventure",
163            author=TableLinkField[Author].from_value(23),
164        ).create() ```
165
166        Args:
167            *instance (int | T): Instance(s) or row id(s) to be
168                linked.
169        """
170        rsl = cls(root=[])
171        for item in instances:
172            if isinstance(item, int):
173                rsl.root.append(RowLink[T](row_id=item, key=None))
174            elif item.row_id is None:
175                raise RowIDNotSetError(
176                    cls.__name__,
177                    "TableLinkField.link()",
178                )
179            else:
180                rsl.root.append(RowLink[T](row_id=item.row_id, key=None))
181        return rsl
182
183    def id_str(self) -> str:
184        """Returns a list of all ID's as string for debugging."""
185        return ",".join([str(link.row_id) for link in self.root])
186
187    def append(self, *instances: Union[int, T]):
188        """
189        Add a link to the given table row(s). Please note that this method does
190        not update the record on Baserow. You have to call `Table.update()`
191        to apply the changes.
192
193        ```python
194        author = await Author.by_id(AUTHOR_ID)
195        book = await Book.by_id(BOOK_ROW_ID)
196        await book.author.append(ANOTHER_AUTHOR_ID, author)
197        await book.update()
198        ```
199
200        Args:
201            instance (int | T | list[int | T]): Instance(s) or row id(s) to be
202                added. When using a `Table` instance make sure that
203                `Table.row_id` is set.
204        """
205        for item in instances:
206            if isinstance(item, int):
207                row_id = item
208            elif item.row_id is None:
209                raise RowIDNotSetError(
210                    self.__class__.__name__,
211                    "TableLinkField.link()",
212                )
213            else:
214                row_id = item.row_id
215            self.root.append(RowLink(
216                row_id=row_id,
217                key=None,
218            ))
219            self.register_pending_change(f"link to entry {row_id} added")
220
221    def clear(self):
222        """
223        Deletes all linked entries. After that, `Table.update()` must be called
224        to apply the changes.
225
226        ```python
227        book = await Book.by_id(BOOK_ROW_ID)
228        book.author.clear()
229        await book.update()
230        print("Removed all authors from the book")
231        ```
232        """
233        self.root.clear()
234        self.register_pending_change("all links removed")
235
236    async def query_linked_rows(self) -> list[T]:
237        """
238        Queries and returns all linked rows.
239
240        ```python
241        book = await Book.by_id(BOOK_ROW_ID)
242        authors = await book.author.query_linked_rows()
243        print(f"Author(s) of book {book.title}: {authors}")
244        ```
245        """
246        rsl: list[T] = []
247        for link in self.root:
248            rsl.append(await link.query_linked_row())
249        self._cache = rsl
250        return rsl
251
252    async def cached_query_linked_rows(self) -> list[T]:
253        """
254        Same as `TableLinkField.query_linked_rows()` with cached results. The
255        Baserow API is called only the first time. After that, the cached result
256        is returned directly. This will also use the last result of
257        `TableLinkField.query_linked_rows()`.
258        """
259        if self._cache is None:
260            self._cache = await self.query_linked_rows()
261        return self._cache
262
263
264class Table(BaseModel, abc.ABC):
265    """
266    The model derived from pydantic's BaseModel provides ORM-like access to the
267    CRUD (create, read, update, delete) functionalities of a table in Baserow.
268    The design of the class is quite opinionated. Therefore, if a certain use
269    case cannot be well covered with this abstraction, it may be more effective
270    to directly use the `Client` class.
271
272    Every inheritance/implementation of this class provides access to a table in
273    a Baserow instance. A client instance can be specified; if not, the
274    `GlobalClient` is used. Ensure that it is configured before use.
275    """
276
277    row_id: Optional[int] = Field(default=None, alias=str("id"))
278    """
279    All rows in Baserow have a unique ID.
280    """
281
282    @property
283    @abc.abstractmethod
284    def table_id(cls) -> int:  # type: ignore
285        """
286        The Baserow table ID. Every table in Baserow has a unique ID. This means
287        that each model is linked to a specific table. It's not currently
288        possible to bind a table model to multiple tables.
289        """
290        raise NotImplementedError()
291
292    @property
293    @abc.abstractmethod
294    def table_name(cls) -> str:  # type: ignore
295        """
296        Each table model must have a human-readable table name. The name is used
297        for debugging information only and has no role in addressing/interacting
298        with the Baserow table. Ideally this should be the same name used for
299        the table within the Baserow UI.
300        """
301        raise NotImplementedError()
302
303    table_id: ClassVar[int]
304    table_name: ClassVar[str]
305
306    client: ClassVar[Optional[Client]] = None
307    """
308    Optional client instance for accessing Baserow. If not set, the
309    GlobalClient is used.
310    """
311    dump_response: ClassVar[bool] = False
312    """
313    If set to true, the parsed dict of the body of each API response is dumped
314    to debug output.
315    """
316    dump_payload: ClassVar[bool] = False
317    """
318    If set to true, the data body for the request is dumped to the debug output.
319    """
320    ignore_fields_during_table_creation: ClassVar[list[str]] = ["order", "id"]
321    """Fields with this name are ignored when creating tables."""
322    model_config = ConfigDict(ser_json_timedelta="float")
323
324    @classmethod
325    def __req_client(cls) -> Client:
326        """
327        Returns the client for API requests to Baserow. If no specific client is
328        set for the model (Table.client is None), the packet-wide GlobalClient
329        is used.
330        """
331        if cls.client is None and not GlobalClient.is_configured:
332            raise NoClientAvailableError(cls.table_name)
333        if cls.client is None:
334            return GlobalClient()
335        return cls.client
336
337    @classmethod
338    @valid_configuration
339    async def by_id(cls: Type[T], row_id: int) -> T:
340        """
341        Fetch a single row/entry from the table by the row ID.
342
343        Args:
344            row_id (int): The ID of the row to be returned.
345        """
346        return await cls.__req_client().get_row(cls.table_id, row_id, True, cls)
347
348    @classmethod
349    @valid_configuration
350    async def query(
351        cls: Type[T],
352        filter: Optional[Filter] = None,
353        order_by: Optional[list[str]] = None,
354        page: Optional[int] = None,
355        size: Optional[int] = None,
356    ) -> list[T]:
357        """
358        Queries for rows in the Baserow table. Note that Baserow uses paging. If
359        all rows of a table (in line with the optional filter) are needed, set
360        `size` to `-1`. Even though this option allows for resolving paging, it
361        should be noted that in Baserow, a maximum of 200 rows can be received
362        per API call. This can lead to significant waiting times and system load
363        for large datasets. Therefore, this option should be used with caution.
364
365        Args:
366            filter (Optional[list[Filter]], optional): Allows the dataset to be
367                filtered.
368            order_by (Optional[list[str]], optional): A list of field names/IDs
369                by which the result should be sorted. If the field name is
370                prepended with a +, the sorting is ascending; if with a -, it is
371                descending.
372            page (Optional[int], optional): The page of the paging.
373            size (Optional[int], optional): How many records should be returned
374                at max. Defaults to 100 and cannot exceed 200. If set to -1 the
375                method wil resolve Baserow's paging and returns all rows
376                corresponding to the query.
377        """
378        if size == -1 and page:
379            raise ValueError(
380                "it's not possible to request a specific page when requesting all results (potentially from multiple pages) with size=-1",
381            )
382        if size is not None and size == -1:
383            rsl = await cls.__req_client().list_all_table_rows(
384                cls.table_id,
385                True,
386                cls,
387                filter=filter,
388                order_by=order_by,
389            )
390        else:
391            rsl = await cls.__req_client().list_table_rows(
392                cls.table_id,
393                True,
394                cls,
395                filter=filter,
396                order_by=order_by,
397                page=page,
398                size=size,
399            )
400        return rsl.results
401
402    @classmethod
403    @valid_configuration
404    async def update_fields_by_id(
405        cls: Type[T],
406        row_id: int,
407        by_alias: bool = True,
408        **kwargs: Any,
409    ) -> Union[T, MinimalRow]:
410        """
411        Update the fields in a row (defined by its ID) given by the kwargs
412        parameter. The keys provided must be valid field names in the model.
413        values will be validated against the model. If the value type is
414        inherited by the BaseModel, its serializer will be applied to the value
415        and submitted to the database. Please note that custom _Field_
416        serializers for any other types are not taken into account.
417
418        The custom model serializer is used in the module because the structure
419        of some Baserow fields differs between the GET result and the required
420        POST data for modification. For example, the MultipleSelectField returns
421        ID, text value, and color with the GET request. However, only a list of
422        IDs or values is required for updating the field using a POST request.
423
424        Args:
425            row_id (int): ID of row in Baserow to be updated.
426            by_alias (bool, optional): Specify whether to use alias values to
427                address field names in Baserow. Note that this value is set to
428                True by default, contrary to pydantic's usual practice. In the
429                context of the table model (which is specifically used to
430                represent Baserow tables), setting an alias typically indicates
431                that the field name in Baserow is not a valid Python variable
432                name.
433        """
434        payload = cls.__model_dump_subset(by_alias, **kwargs)
435        # if cls.dump_payload:
436        #     logger.debug(payload)
437        return await cls.__req_client().update_row(
438            cls.table_id,
439            row_id,
440            payload,
441            True,
442        )
443
444    @classmethod
445    @valid_configuration
446    async def delete_by_id(cls: Type[T], row_id: Union[int, list[int]]):
447        """
448        Deletes one or more rows in the Baserow table. If a list of IDs is
449        passed, deletion occurs as a batch command. To delete a single Table
450        instance with the Table.row_id set, the Table.delete() method can also
451        be used.
452
453        Args:
454            row_id (Union[int, list[int]]): ID or ID list of row(s) in Baserow
455            to be deleted.
456        """
457        await cls.__req_client().delete_row(cls.table_id, row_id)
458
459    @classmethod
460    @valid_configuration
461    def batch_update(cls, data: dict[int, dict[str, Any]], by_alias: bool = True):
462        """
463        Updates multiple fields in the database. The given data dict must map
464        the unique row id to the data to be updated. The input is validated
465        against the model. See the update method documentation for more
466        information about its limitations and underlying ideas.
467
468        Args:
469            data: A dict mapping the unique row id to the data to be updated.
470            by_alias: Please refer to the documentation on the update method to
471                learn more about this arg.
472        """
473        payload = []
474        for key, value in data.items():
475            entry = cls.__model_dump_subset(by_alias, **value)
476            entry["id"] = key
477            payload.append(entry)
478        # if cls.dump_payload:
479        #     logger.debug(payload)
480        raise NotImplementedError(
481            "Baserow client library currently does not support batch update operations on rows"
482        )
483
484    @valid_configuration
485    async def create(self) -> MinimalRow:
486        """
487        Creates a new row in the table with the data from the instance. Please
488        note that this method does not check whether the fields defined by the
489        model actually exist.
490        """
491        rsl = await self.__req_client().create_row(
492            self.table_id,
493            self.model_dump(by_alias=True, mode="json", exclude_none=True),
494            True,
495        )
496        if not isinstance(rsl, MinimalRow):
497            raise RuntimeError(
498                f" expected MinimalRow instance got {type(rsl)} instead",
499            )
500        return rsl
501
502    @valid_configuration
503    async def update_fields(
504        self: T,
505        by_alias: bool = True,
506        **kwargs: Any,
507    ) -> Union[T, MinimalRow]:
508        """
509        Updates the row with the ID of this instance. Short-hand for the
510        `Table.update_by_id()` method, for instances with the `Table.row_id`
511        set. For more information on how to use this, please refer to the
512        documentation of this method.
513        """
514        if self.row_id is None:
515            raise RowIDNotSetError(self.__class__.__name__, "field_update")
516        return await self.update_fields_by_id(self.row_id, by_alias, **kwargs)
517
518    @valid_configuration
519    async def update(self: T) -> Union[T, MinimalRow]:
520        """
521        Updates all fields of a row with the data of this model instance. The
522        row_id field must be set.
523        """
524        if self.row_id is None:
525            raise RowIDNotSetError(self.__class__.__name__, "update")
526
527        excluded: list[str] = []
528        for key, field in self.__dict__.items():
529            if isinstance(field, BaserowField) and field.read_only():
530                excluded.append(key)
531            elif isinstance(field, uuid.UUID):
532                excluded.append(key)
533
534        rsl = await self.__req_client().update_row(
535            self.table_id,
536            self.row_id,
537            self.model_dump(
538                by_alias=True,
539                mode="json",
540                exclude_none=True,
541                exclude=set(excluded),
542            ),
543            True
544        )
545        for _, field in self.__dict__.items():
546            if isinstance(field, BaserowField):
547                field.changes_applied()
548        return rsl
549
550    @valid_configuration
551    async def delete(self):
552        """
553        Deletes the row with the ID of this instance. Short-hand for the
554        `Table.delete_by_id()` method, for instances with the `Table.row_id`
555        set. For more information on how to use this, please refer to the
556        documentation of this method.
557        """
558        if self.row_id is None:
559            raise RowIDNotSetError(self.__class__.__name__, "delete")
560        await self.delete_by_id(self.row_id)
561
562    @classmethod
563    async def create_table(cls, database_id: int):
564        """
565        This method creates a new table in the given database based on the
566        structure and fields of the model.
567
568        Args:
569            database_id (int): The ID of the database in which the new table
570                should be created.
571        """
572        # Name is needed for table creation.
573        if not isinstance(cls.table_name, str):
574            raise InvalidTableConfigurationError(
575                cls.__name__, "table_name not set")
576
577        # The primary field is determined at this point to ensure that any
578        # exceptions (none, more than one primary field) occur before the
579        # expensive API calls.
580        primary_name, _ = cls.primary_field()
581
582        # Create the new table itself.
583        table_rsl = await cls.__req_client().create_database_table(database_id, cls.table_name)
584        cls.table_id = table_rsl.id
585        primary_id, unused_fields = await cls.__scramble_all_field_names()
586
587        # Create the fields.
588        for key, field in cls.model_fields.items():
589            await cls.__create_table_field(key, field, primary_id, primary_name)
590
591        # Delete unused fields.
592        for field in unused_fields:
593            await cls.__req_client().delete_database_table_field(field)
594
595    @classmethod
596    def primary_field(cls) -> Tuple[str, FieldInfo]:
597        """
598        This method returns a tuple of the field name and pydantic.FieldInfo of
599        the field that has been marked as the primary field. Only one primary
600        field is allowed per table. This is done by adding PrimaryField as a
601        type annotation. Example for such a Model:
602
603        ```python
604        class Person(Table):
605            table_id = 23
606            table_name = "Person"
607            model_config = ConfigDict(populate_by_name=True)
608
609            name: Annotated[
610                str,
611                Field(alias=str("Name")),
612                PrimaryField(),
613            ]
614        ```
615        """
616        rsl: Optional[Tuple[str, FieldInfo]] = None
617        for name, field in cls.model_fields.items():
618            if any(isinstance(item, PrimaryField) for item in field.metadata):
619                if rsl is not None:
620                    raise MultiplePrimaryFieldsError(cls.__name__)
621                rsl = (name, field)
622        if rsl is None:
623            raise NoPrimaryFieldError(cls.__name__)
624        return rsl
625
626    @classmethod
627    async def __create_table_field(cls, name: str, field: FieldInfo, primary_id: int, primary_name: str):
628        if name in cls.ignore_fields_during_table_creation or field.alias in cls.ignore_fields_during_table_creation:
629            return
630
631        config: Optional[FieldConfigType] = None
632        for item in field.metadata:
633            if isinstance(item, Config):
634                config = item.config
635        field_type = cls.__type_for_field(name, field)
636        if config is None and field_type in DEFAULT_CONFIG_FOR_BUILT_IN_TYPES:
637            config = DEFAULT_CONFIG_FOR_BUILT_IN_TYPES[field_type]
638        elif config is None and issubclass(field_type, BaserowField):
639            config = field_type.default_config()
640        elif config is None:
641            raise InvalidFieldForCreateTableError(
642                name,
643                f"{field_type} is not supported"
644            )
645        if field.alias is not None:
646            config.name = field.alias
647        else:
648            config.name = name
649
650        config.description = field.description
651
652        if name == primary_name:
653            await cls.__req_client().update_database_table_field(primary_id, config)
654        else:
655            await cls.__req_client().create_database_table_field(cls.table_id, config)
656
657    @staticmethod
658    def __type_for_field(name: str, field: FieldInfo) -> Type[Any]:
659        if get_origin(field.annotation) is Union:
660            args = get_args(field.annotation)
661            not_none_args = [arg for arg in args if arg is not type(None)]
662            if len(not_none_args) == 1:
663                return not_none_args[0]
664            else:
665                raise InvalidFieldForCreateTableError(
666                    name,
667                    "Union type is not supported",
668                )
669        elif field.annotation is not None:
670            return field.annotation
671        else:
672            raise InvalidFieldForCreateTableError(
673                name,
674                "None type is not supported",
675            )
676
677    @classmethod
678    async def __scramble_all_field_names(cls) -> Tuple[int, list[int]]:
679        """
680        Changes the names of all existing fields in a Baserow table to random
681        UUIDs and returns the ID of the primary file and a list of the other
682        modified fields. This is used to ensure that the automatically created
683        fields in a new table do not collide with the names of subsequently
684        created fields.
685        """
686        fields = await cls.__req_client().list_fields(cls.table_id)
687        primary: int = -1
688        to_delete: list[int] = []
689        for field in fields.root:
690            if field.root.id is None:
691                raise ValueError("field id is None")
692            if field.root.primary:
693                primary = field.root.id
694            else:
695                to_delete.append(field.root.id)
696            await cls.__req_client().update_database_table_field(
697                field.root.id,
698                {"name": str(uuid.uuid4())},
699            )
700        return (primary, to_delete)
701
702    @classmethod
703    def __validate_single_field(
704        cls,
705        field_name: str,
706        value: Any,
707    ) -> Union[
708        dict[str, Any],
709        tuple[dict[str, Any], dict[str, Any], set[str]],
710        Any,
711    ]:
712        return cls.__pydantic_validator__.validate_assignment(
713            cls.model_construct(), field_name, value
714        )
715
716    @classmethod
717    def __model_dump_subset(cls, by_alias: bool, **kwargs: Any) -> dict[str, Any]:
718        """
719        This method takes a dictionary of keyword arguments (kwargs) and
720        validates it against the model before serializing it as a dictionary. It
721        is used for the update and batch_update methods. If a field value is
722        inherited from a BaseModel, it will be serialized using model_dump.
723
724        Please refer to the documentation on the update method to learn more
725        about its limitations and underlying ideas.
726        """
727        rsl = {}
728        for key, value in kwargs.items():
729            # Check, whether the submitted key-value pairs are in the model and
730            # the value passes the validation specified by the field.
731            cls.__validate_single_field(key, value)
732
733            # If a field has an alias, replace the key with the alias.
734            rsl_key = key
735            alias = cls.model_fields[key].alias
736            if by_alias and alias:
737                rsl_key = alias
738
739            # When the field value is a pydantic model, serialize it.
740            rsl[rsl_key] = value
741            if isinstance(value, BaseModel):
742                rsl[rsl_key] = value.model_dump(by_alias=by_alias)
743        return rsl
def valid_configuration(func):
22def valid_configuration(func):
23    """
24    This decorator checks whether the model configuration has been done
25    correctly. In addition to validating the class vars Table.table_id and
26    Table.table_name, it also verifies whether the model config is set with
27    populate_by_name=True.
28    """
29
30    @wraps(func)
31    def wrapper(cls, *args, **kwargs):
32        if not isinstance(cls.table_id, int):
33            raise InvalidTableConfigurationError(
34                cls.__name__, "table_id not set")
35        if not isinstance(cls.table_name, str):
36            raise InvalidTableConfigurationError(
37                cls.__name__, "table_name not set")
38        if "populate_by_name" not in cls.model_config:
39            raise InvalidTableConfigurationError(
40                cls.__name__,
41                "populate_by_name is not set in the model config; it should most likely be set to true"
42            )
43        return func(cls, *args, **kwargs)
44    return wrapper

This decorator checks whether the model configuration has been done correctly. In addition to validating the class vars Table.table_id and Table.table_name, it also verifies whether the model config is set with populate_by_name=True.

class TableLinkField(baserow.field.BaserowField, pydantic.root_model.RootModel[list[RowLink]], typing.Generic[~T]):
110class TableLinkField(BaserowField, RootModel[list[RowLink]], Generic[T]):
111    """
112    A link to table field creates a link between two existing tables by
113    connecting data across tables with linked rows.
114    """
115    root: list[RowLink[T]]
116    _cache: Optional[list[T]] = None
117
118    @classmethod
119    def default_config(cls) -> FieldConfigType:
120        metadata = cls.__pydantic_generic_metadata__
121        if "args" not in metadata:
122            raise PydanticGenericMetadataError.args_missing(
123                cls.__class__.__name__,
124                "linked table",
125            )
126        if len(metadata["args"]) < 1:
127            raise PydanticGenericMetadataError.args_empty(
128                cls.__class__.__name__,
129                "linked table",
130            )
131        linked_table = metadata["args"][0]
132        return LinkFieldConfig(link_row_table_id=linked_table.table_id)
133
134    @classmethod
135    def read_only(cls) -> bool:
136        return False
137
138    @classmethod
139    def from_value(cls, *instances: Union[int, T]):
140        """
141        Instantiates a link field from a referencing value. Can be used to set a
142        link directly when instantiating a table model using a parameter. This
143        is a quality of life feature and replace the tedious way of manually
144        defining a link. For more information please refer to the example below.
145
146        ```python
147        class Author(Table):
148            [...] name: str
149
150        class Book(Table):
151            [...] title: str author: Optional[TableLinkField[Author]] =
152            Field(default=None)
153
154        # Instead of...
155        new_book = await Book(
156            title="The Great Adventure", author=TableLinkField[Author](
157                root=[RowLink[Author](row_id=23, key=None)]
158            )
159        ).create()
160
161        # ...this method allows this (link to author row with id=23) new_book =
162        await Book(
163            title="The Great Adventure",
164            author=TableLinkField[Author].from_value(23),
165        ).create() ```
166
167        Args:
168            *instance (int | T): Instance(s) or row id(s) to be
169                linked.
170        """
171        rsl = cls(root=[])
172        for item in instances:
173            if isinstance(item, int):
174                rsl.root.append(RowLink[T](row_id=item, key=None))
175            elif item.row_id is None:
176                raise RowIDNotSetError(
177                    cls.__name__,
178                    "TableLinkField.link()",
179                )
180            else:
181                rsl.root.append(RowLink[T](row_id=item.row_id, key=None))
182        return rsl
183
184    def id_str(self) -> str:
185        """Returns a list of all ID's as string for debugging."""
186        return ",".join([str(link.row_id) for link in self.root])
187
188    def append(self, *instances: Union[int, T]):
189        """
190        Add a link to the given table row(s). Please note that this method does
191        not update the record on Baserow. You have to call `Table.update()`
192        to apply the changes.
193
194        ```python
195        author = await Author.by_id(AUTHOR_ID)
196        book = await Book.by_id(BOOK_ROW_ID)
197        await book.author.append(ANOTHER_AUTHOR_ID, author)
198        await book.update()
199        ```
200
201        Args:
202            instance (int | T | list[int | T]): Instance(s) or row id(s) to be
203                added. When using a `Table` instance make sure that
204                `Table.row_id` is set.
205        """
206        for item in instances:
207            if isinstance(item, int):
208                row_id = item
209            elif item.row_id is None:
210                raise RowIDNotSetError(
211                    self.__class__.__name__,
212                    "TableLinkField.link()",
213                )
214            else:
215                row_id = item.row_id
216            self.root.append(RowLink(
217                row_id=row_id,
218                key=None,
219            ))
220            self.register_pending_change(f"link to entry {row_id} added")
221
222    def clear(self):
223        """
224        Deletes all linked entries. After that, `Table.update()` must be called
225        to apply the changes.
226
227        ```python
228        book = await Book.by_id(BOOK_ROW_ID)
229        book.author.clear()
230        await book.update()
231        print("Removed all authors from the book")
232        ```
233        """
234        self.root.clear()
235        self.register_pending_change("all links removed")
236
237    async def query_linked_rows(self) -> list[T]:
238        """
239        Queries and returns all linked rows.
240
241        ```python
242        book = await Book.by_id(BOOK_ROW_ID)
243        authors = await book.author.query_linked_rows()
244        print(f"Author(s) of book {book.title}: {authors}")
245        ```
246        """
247        rsl: list[T] = []
248        for link in self.root:
249            rsl.append(await link.query_linked_row())
250        self._cache = rsl
251        return rsl
252
253    async def cached_query_linked_rows(self) -> list[T]:
254        """
255        Same as `TableLinkField.query_linked_rows()` with cached results. The
256        Baserow API is called only the first time. After that, the cached result
257        is returned directly. This will also use the last result of
258        `TableLinkField.query_linked_rows()`.
259        """
260        if self._cache is None:
261            self._cache = await self.query_linked_rows()
262        return self._cache

A link to table field creates a link between two existing tables by connecting data across tables with linked rows.

root: list[RowLink]
@classmethod
def from_value(cls, *instances: Union[int, ~T]):
138    @classmethod
139    def from_value(cls, *instances: Union[int, T]):
140        """
141        Instantiates a link field from a referencing value. Can be used to set a
142        link directly when instantiating a table model using a parameter. This
143        is a quality of life feature and replace the tedious way of manually
144        defining a link. For more information please refer to the example below.
145
146        ```python
147        class Author(Table):
148            [...] name: str
149
150        class Book(Table):
151            [...] title: str author: Optional[TableLinkField[Author]] =
152            Field(default=None)
153
154        # Instead of...
155        new_book = await Book(
156            title="The Great Adventure", author=TableLinkField[Author](
157                root=[RowLink[Author](row_id=23, key=None)]
158            )
159        ).create()
160
161        # ...this method allows this (link to author row with id=23) new_book =
162        await Book(
163            title="The Great Adventure",
164            author=TableLinkField[Author].from_value(23),
165        ).create() ```
166
167        Args:
168            *instance (int | T): Instance(s) or row id(s) to be
169                linked.
170        """
171        rsl = cls(root=[])
172        for item in instances:
173            if isinstance(item, int):
174                rsl.root.append(RowLink[T](row_id=item, key=None))
175            elif item.row_id is None:
176                raise RowIDNotSetError(
177                    cls.__name__,
178                    "TableLinkField.link()",
179                )
180            else:
181                rsl.root.append(RowLink[T](row_id=item.row_id, key=None))
182        return rsl

Instantiates a link field from a referencing value. Can be used to set a link directly when instantiating a table model using a parameter. This is a quality of life feature and replace the tedious way of manually defining a link. For more information please refer to the example below.

class Author(Table):
    [...] name: str

class Book(Table):
    [...] title: str author: Optional[TableLinkField[Author]] =
    Field(default=None)

# Instead of...
new_book = await Book(
    title="The Great Adventure", author=TableLinkField[Author](
        root=[RowLink[Author](row_id=23, key=None)]
    )
).create()

# ...this method allows this (link to author row with id=23) new_book =
await Book(
    title="The Great Adventure",
    author=TableLinkField[Author].from_value(23),
).create()
Arguments:
  • *instance (int | T): Instance(s) or row id(s) to be linked.
def id_str(self) -> str:
184    def id_str(self) -> str:
185        """Returns a list of all ID's as string for debugging."""
186        return ",".join([str(link.row_id) for link in self.root])

Returns a list of all ID's as string for debugging.

def append(self, *instances: Union[int, ~T]):
188    def append(self, *instances: Union[int, T]):
189        """
190        Add a link to the given table row(s). Please note that this method does
191        not update the record on Baserow. You have to call `Table.update()`
192        to apply the changes.
193
194        ```python
195        author = await Author.by_id(AUTHOR_ID)
196        book = await Book.by_id(BOOK_ROW_ID)
197        await book.author.append(ANOTHER_AUTHOR_ID, author)
198        await book.update()
199        ```
200
201        Args:
202            instance (int | T | list[int | T]): Instance(s) or row id(s) to be
203                added. When using a `Table` instance make sure that
204                `Table.row_id` is set.
205        """
206        for item in instances:
207            if isinstance(item, int):
208                row_id = item
209            elif item.row_id is None:
210                raise RowIDNotSetError(
211                    self.__class__.__name__,
212                    "TableLinkField.link()",
213                )
214            else:
215                row_id = item.row_id
216            self.root.append(RowLink(
217                row_id=row_id,
218                key=None,
219            ))
220            self.register_pending_change(f"link to entry {row_id} added")

Add a link to the given table row(s). Please note that this method does not update the record on Baserow. You have to call Table.update() to apply the changes.

author = await Author.by_id(AUTHOR_ID)
book = await Book.by_id(BOOK_ROW_ID)
await book.author.append(ANOTHER_AUTHOR_ID, author)
await book.update()
Arguments:
  • instance (int | T | list[int | T]): Instance(s) or row id(s) to be added. When using a Table instance make sure that Table.row_id is set.
def clear(self):
222    def clear(self):
223        """
224        Deletes all linked entries. After that, `Table.update()` must be called
225        to apply the changes.
226
227        ```python
228        book = await Book.by_id(BOOK_ROW_ID)
229        book.author.clear()
230        await book.update()
231        print("Removed all authors from the book")
232        ```
233        """
234        self.root.clear()
235        self.register_pending_change("all links removed")

Deletes all linked entries. After that, Table.update() must be called to apply the changes.

book = await Book.by_id(BOOK_ROW_ID)
book.author.clear()
await book.update()
print("Removed all authors from the book")
async def query_linked_rows(self) -> list[~T]:
237    async def query_linked_rows(self) -> list[T]:
238        """
239        Queries and returns all linked rows.
240
241        ```python
242        book = await Book.by_id(BOOK_ROW_ID)
243        authors = await book.author.query_linked_rows()
244        print(f"Author(s) of book {book.title}: {authors}")
245        ```
246        """
247        rsl: list[T] = []
248        for link in self.root:
249            rsl.append(await link.query_linked_row())
250        self._cache = rsl
251        return rsl

Queries and returns all linked rows.

book = await Book.by_id(BOOK_ROW_ID)
authors = await book.author.query_linked_rows()
print(f"Author(s) of book {book.title}: {authors}")
async def cached_query_linked_rows(self) -> list[~T]:
253    async def cached_query_linked_rows(self) -> list[T]:
254        """
255        Same as `TableLinkField.query_linked_rows()` with cached results. The
256        Baserow API is called only the first time. After that, the cached result
257        is returned directly. This will also use the last result of
258        `TableLinkField.query_linked_rows()`.
259        """
260        if self._cache is None:
261            self._cache = await self.query_linked_rows()
262        return self._cache

Same as TableLinkField.query_linked_rows() with cached results. The Baserow API is called only the first time. After that, the cached result is returned directly. This will also use the last result of TableLinkField.query_linked_rows().

Inherited Members
pydantic.root_model.RootModel
RootModel
model_construct
baserow.field.BaserowField
default_config
read_only
register_pending_change
changes_applied
model_config
model_post_init
model_fields
model_computed_fields
pydantic.main.BaseModel
model_extra
model_fields_set
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_rebuild
model_validate
model_validate_json
model_validate_strings
dict
json
parse_obj
parse_raw
parse_file
from_orm
construct
copy
schema
schema_json
validate
update_forward_refs
class Table(pydantic.main.BaseModel, abc.ABC):
265class Table(BaseModel, abc.ABC):
266    """
267    The model derived from pydantic's BaseModel provides ORM-like access to the
268    CRUD (create, read, update, delete) functionalities of a table in Baserow.
269    The design of the class is quite opinionated. Therefore, if a certain use
270    case cannot be well covered with this abstraction, it may be more effective
271    to directly use the `Client` class.
272
273    Every inheritance/implementation of this class provides access to a table in
274    a Baserow instance. A client instance can be specified; if not, the
275    `GlobalClient` is used. Ensure that it is configured before use.
276    """
277
278    row_id: Optional[int] = Field(default=None, alias=str("id"))
279    """
280    All rows in Baserow have a unique ID.
281    """
282
283    @property
284    @abc.abstractmethod
285    def table_id(cls) -> int:  # type: ignore
286        """
287        The Baserow table ID. Every table in Baserow has a unique ID. This means
288        that each model is linked to a specific table. It's not currently
289        possible to bind a table model to multiple tables.
290        """
291        raise NotImplementedError()
292
293    @property
294    @abc.abstractmethod
295    def table_name(cls) -> str:  # type: ignore
296        """
297        Each table model must have a human-readable table name. The name is used
298        for debugging information only and has no role in addressing/interacting
299        with the Baserow table. Ideally this should be the same name used for
300        the table within the Baserow UI.
301        """
302        raise NotImplementedError()
303
304    table_id: ClassVar[int]
305    table_name: ClassVar[str]
306
307    client: ClassVar[Optional[Client]] = None
308    """
309    Optional client instance for accessing Baserow. If not set, the
310    GlobalClient is used.
311    """
312    dump_response: ClassVar[bool] = False
313    """
314    If set to true, the parsed dict of the body of each API response is dumped
315    to debug output.
316    """
317    dump_payload: ClassVar[bool] = False
318    """
319    If set to true, the data body for the request is dumped to the debug output.
320    """
321    ignore_fields_during_table_creation: ClassVar[list[str]] = ["order", "id"]
322    """Fields with this name are ignored when creating tables."""
323    model_config = ConfigDict(ser_json_timedelta="float")
324
325    @classmethod
326    def __req_client(cls) -> Client:
327        """
328        Returns the client for API requests to Baserow. If no specific client is
329        set for the model (Table.client is None), the packet-wide GlobalClient
330        is used.
331        """
332        if cls.client is None and not GlobalClient.is_configured:
333            raise NoClientAvailableError(cls.table_name)
334        if cls.client is None:
335            return GlobalClient()
336        return cls.client
337
338    @classmethod
339    @valid_configuration
340    async def by_id(cls: Type[T], row_id: int) -> T:
341        """
342        Fetch a single row/entry from the table by the row ID.
343
344        Args:
345            row_id (int): The ID of the row to be returned.
346        """
347        return await cls.__req_client().get_row(cls.table_id, row_id, True, cls)
348
349    @classmethod
350    @valid_configuration
351    async def query(
352        cls: Type[T],
353        filter: Optional[Filter] = None,
354        order_by: Optional[list[str]] = None,
355        page: Optional[int] = None,
356        size: Optional[int] = None,
357    ) -> list[T]:
358        """
359        Queries for rows in the Baserow table. Note that Baserow uses paging. If
360        all rows of a table (in line with the optional filter) are needed, set
361        `size` to `-1`. Even though this option allows for resolving paging, it
362        should be noted that in Baserow, a maximum of 200 rows can be received
363        per API call. This can lead to significant waiting times and system load
364        for large datasets. Therefore, this option should be used with caution.
365
366        Args:
367            filter (Optional[list[Filter]], optional): Allows the dataset to be
368                filtered.
369            order_by (Optional[list[str]], optional): A list of field names/IDs
370                by which the result should be sorted. If the field name is
371                prepended with a +, the sorting is ascending; if with a -, it is
372                descending.
373            page (Optional[int], optional): The page of the paging.
374            size (Optional[int], optional): How many records should be returned
375                at max. Defaults to 100 and cannot exceed 200. If set to -1 the
376                method wil resolve Baserow's paging and returns all rows
377                corresponding to the query.
378        """
379        if size == -1 and page:
380            raise ValueError(
381                "it's not possible to request a specific page when requesting all results (potentially from multiple pages) with size=-1",
382            )
383        if size is not None and size == -1:
384            rsl = await cls.__req_client().list_all_table_rows(
385                cls.table_id,
386                True,
387                cls,
388                filter=filter,
389                order_by=order_by,
390            )
391        else:
392            rsl = await cls.__req_client().list_table_rows(
393                cls.table_id,
394                True,
395                cls,
396                filter=filter,
397                order_by=order_by,
398                page=page,
399                size=size,
400            )
401        return rsl.results
402
403    @classmethod
404    @valid_configuration
405    async def update_fields_by_id(
406        cls: Type[T],
407        row_id: int,
408        by_alias: bool = True,
409        **kwargs: Any,
410    ) -> Union[T, MinimalRow]:
411        """
412        Update the fields in a row (defined by its ID) given by the kwargs
413        parameter. The keys provided must be valid field names in the model.
414        values will be validated against the model. If the value type is
415        inherited by the BaseModel, its serializer will be applied to the value
416        and submitted to the database. Please note that custom _Field_
417        serializers for any other types are not taken into account.
418
419        The custom model serializer is used in the module because the structure
420        of some Baserow fields differs between the GET result and the required
421        POST data for modification. For example, the MultipleSelectField returns
422        ID, text value, and color with the GET request. However, only a list of
423        IDs or values is required for updating the field using a POST request.
424
425        Args:
426            row_id (int): ID of row in Baserow to be updated.
427            by_alias (bool, optional): Specify whether to use alias values to
428                address field names in Baserow. Note that this value is set to
429                True by default, contrary to pydantic's usual practice. In the
430                context of the table model (which is specifically used to
431                represent Baserow tables), setting an alias typically indicates
432                that the field name in Baserow is not a valid Python variable
433                name.
434        """
435        payload = cls.__model_dump_subset(by_alias, **kwargs)
436        # if cls.dump_payload:
437        #     logger.debug(payload)
438        return await cls.__req_client().update_row(
439            cls.table_id,
440            row_id,
441            payload,
442            True,
443        )
444
445    @classmethod
446    @valid_configuration
447    async def delete_by_id(cls: Type[T], row_id: Union[int, list[int]]):
448        """
449        Deletes one or more rows in the Baserow table. If a list of IDs is
450        passed, deletion occurs as a batch command. To delete a single Table
451        instance with the Table.row_id set, the Table.delete() method can also
452        be used.
453
454        Args:
455            row_id (Union[int, list[int]]): ID or ID list of row(s) in Baserow
456            to be deleted.
457        """
458        await cls.__req_client().delete_row(cls.table_id, row_id)
459
460    @classmethod
461    @valid_configuration
462    def batch_update(cls, data: dict[int, dict[str, Any]], by_alias: bool = True):
463        """
464        Updates multiple fields in the database. The given data dict must map
465        the unique row id to the data to be updated. The input is validated
466        against the model. See the update method documentation for more
467        information about its limitations and underlying ideas.
468
469        Args:
470            data: A dict mapping the unique row id to the data to be updated.
471            by_alias: Please refer to the documentation on the update method to
472                learn more about this arg.
473        """
474        payload = []
475        for key, value in data.items():
476            entry = cls.__model_dump_subset(by_alias, **value)
477            entry["id"] = key
478            payload.append(entry)
479        # if cls.dump_payload:
480        #     logger.debug(payload)
481        raise NotImplementedError(
482            "Baserow client library currently does not support batch update operations on rows"
483        )
484
485    @valid_configuration
486    async def create(self) -> MinimalRow:
487        """
488        Creates a new row in the table with the data from the instance. Please
489        note that this method does not check whether the fields defined by the
490        model actually exist.
491        """
492        rsl = await self.__req_client().create_row(
493            self.table_id,
494            self.model_dump(by_alias=True, mode="json", exclude_none=True),
495            True,
496        )
497        if not isinstance(rsl, MinimalRow):
498            raise RuntimeError(
499                f" expected MinimalRow instance got {type(rsl)} instead",
500            )
501        return rsl
502
503    @valid_configuration
504    async def update_fields(
505        self: T,
506        by_alias: bool = True,
507        **kwargs: Any,
508    ) -> Union[T, MinimalRow]:
509        """
510        Updates the row with the ID of this instance. Short-hand for the
511        `Table.update_by_id()` method, for instances with the `Table.row_id`
512        set. For more information on how to use this, please refer to the
513        documentation of this method.
514        """
515        if self.row_id is None:
516            raise RowIDNotSetError(self.__class__.__name__, "field_update")
517        return await self.update_fields_by_id(self.row_id, by_alias, **kwargs)
518
519    @valid_configuration
520    async def update(self: T) -> Union[T, MinimalRow]:
521        """
522        Updates all fields of a row with the data of this model instance. The
523        row_id field must be set.
524        """
525        if self.row_id is None:
526            raise RowIDNotSetError(self.__class__.__name__, "update")
527
528        excluded: list[str] = []
529        for key, field in self.__dict__.items():
530            if isinstance(field, BaserowField) and field.read_only():
531                excluded.append(key)
532            elif isinstance(field, uuid.UUID):
533                excluded.append(key)
534
535        rsl = await self.__req_client().update_row(
536            self.table_id,
537            self.row_id,
538            self.model_dump(
539                by_alias=True,
540                mode="json",
541                exclude_none=True,
542                exclude=set(excluded),
543            ),
544            True
545        )
546        for _, field in self.__dict__.items():
547            if isinstance(field, BaserowField):
548                field.changes_applied()
549        return rsl
550
551    @valid_configuration
552    async def delete(self):
553        """
554        Deletes the row with the ID of this instance. Short-hand for the
555        `Table.delete_by_id()` method, for instances with the `Table.row_id`
556        set. For more information on how to use this, please refer to the
557        documentation of this method.
558        """
559        if self.row_id is None:
560            raise RowIDNotSetError(self.__class__.__name__, "delete")
561        await self.delete_by_id(self.row_id)
562
563    @classmethod
564    async def create_table(cls, database_id: int):
565        """
566        This method creates a new table in the given database based on the
567        structure and fields of the model.
568
569        Args:
570            database_id (int): The ID of the database in which the new table
571                should be created.
572        """
573        # Name is needed for table creation.
574        if not isinstance(cls.table_name, str):
575            raise InvalidTableConfigurationError(
576                cls.__name__, "table_name not set")
577
578        # The primary field is determined at this point to ensure that any
579        # exceptions (none, more than one primary field) occur before the
580        # expensive API calls.
581        primary_name, _ = cls.primary_field()
582
583        # Create the new table itself.
584        table_rsl = await cls.__req_client().create_database_table(database_id, cls.table_name)
585        cls.table_id = table_rsl.id
586        primary_id, unused_fields = await cls.__scramble_all_field_names()
587
588        # Create the fields.
589        for key, field in cls.model_fields.items():
590            await cls.__create_table_field(key, field, primary_id, primary_name)
591
592        # Delete unused fields.
593        for field in unused_fields:
594            await cls.__req_client().delete_database_table_field(field)
595
596    @classmethod
597    def primary_field(cls) -> Tuple[str, FieldInfo]:
598        """
599        This method returns a tuple of the field name and pydantic.FieldInfo of
600        the field that has been marked as the primary field. Only one primary
601        field is allowed per table. This is done by adding PrimaryField as a
602        type annotation. Example for such a Model:
603
604        ```python
605        class Person(Table):
606            table_id = 23
607            table_name = "Person"
608            model_config = ConfigDict(populate_by_name=True)
609
610            name: Annotated[
611                str,
612                Field(alias=str("Name")),
613                PrimaryField(),
614            ]
615        ```
616        """
617        rsl: Optional[Tuple[str, FieldInfo]] = None
618        for name, field in cls.model_fields.items():
619            if any(isinstance(item, PrimaryField) for item in field.metadata):
620                if rsl is not None:
621                    raise MultiplePrimaryFieldsError(cls.__name__)
622                rsl = (name, field)
623        if rsl is None:
624            raise NoPrimaryFieldError(cls.__name__)
625        return rsl
626
627    @classmethod
628    async def __create_table_field(cls, name: str, field: FieldInfo, primary_id: int, primary_name: str):
629        if name in cls.ignore_fields_during_table_creation or field.alias in cls.ignore_fields_during_table_creation:
630            return
631
632        config: Optional[FieldConfigType] = None
633        for item in field.metadata:
634            if isinstance(item, Config):
635                config = item.config
636        field_type = cls.__type_for_field(name, field)
637        if config is None and field_type in DEFAULT_CONFIG_FOR_BUILT_IN_TYPES:
638            config = DEFAULT_CONFIG_FOR_BUILT_IN_TYPES[field_type]
639        elif config is None and issubclass(field_type, BaserowField):
640            config = field_type.default_config()
641        elif config is None:
642            raise InvalidFieldForCreateTableError(
643                name,
644                f"{field_type} is not supported"
645            )
646        if field.alias is not None:
647            config.name = field.alias
648        else:
649            config.name = name
650
651        config.description = field.description
652
653        if name == primary_name:
654            await cls.__req_client().update_database_table_field(primary_id, config)
655        else:
656            await cls.__req_client().create_database_table_field(cls.table_id, config)
657
658    @staticmethod
659    def __type_for_field(name: str, field: FieldInfo) -> Type[Any]:
660        if get_origin(field.annotation) is Union:
661            args = get_args(field.annotation)
662            not_none_args = [arg for arg in args if arg is not type(None)]
663            if len(not_none_args) == 1:
664                return not_none_args[0]
665            else:
666                raise InvalidFieldForCreateTableError(
667                    name,
668                    "Union type is not supported",
669                )
670        elif field.annotation is not None:
671            return field.annotation
672        else:
673            raise InvalidFieldForCreateTableError(
674                name,
675                "None type is not supported",
676            )
677
678    @classmethod
679    async def __scramble_all_field_names(cls) -> Tuple[int, list[int]]:
680        """
681        Changes the names of all existing fields in a Baserow table to random
682        UUIDs and returns the ID of the primary file and a list of the other
683        modified fields. This is used to ensure that the automatically created
684        fields in a new table do not collide with the names of subsequently
685        created fields.
686        """
687        fields = await cls.__req_client().list_fields(cls.table_id)
688        primary: int = -1
689        to_delete: list[int] = []
690        for field in fields.root:
691            if field.root.id is None:
692                raise ValueError("field id is None")
693            if field.root.primary:
694                primary = field.root.id
695            else:
696                to_delete.append(field.root.id)
697            await cls.__req_client().update_database_table_field(
698                field.root.id,
699                {"name": str(uuid.uuid4())},
700            )
701        return (primary, to_delete)
702
703    @classmethod
704    def __validate_single_field(
705        cls,
706        field_name: str,
707        value: Any,
708    ) -> Union[
709        dict[str, Any],
710        tuple[dict[str, Any], dict[str, Any], set[str]],
711        Any,
712    ]:
713        return cls.__pydantic_validator__.validate_assignment(
714            cls.model_construct(), field_name, value
715        )
716
717    @classmethod
718    def __model_dump_subset(cls, by_alias: bool, **kwargs: Any) -> dict[str, Any]:
719        """
720        This method takes a dictionary of keyword arguments (kwargs) and
721        validates it against the model before serializing it as a dictionary. It
722        is used for the update and batch_update methods. If a field value is
723        inherited from a BaseModel, it will be serialized using model_dump.
724
725        Please refer to the documentation on the update method to learn more
726        about its limitations and underlying ideas.
727        """
728        rsl = {}
729        for key, value in kwargs.items():
730            # Check, whether the submitted key-value pairs are in the model and
731            # the value passes the validation specified by the field.
732            cls.__validate_single_field(key, value)
733
734            # If a field has an alias, replace the key with the alias.
735            rsl_key = key
736            alias = cls.model_fields[key].alias
737            if by_alias and alias:
738                rsl_key = alias
739
740            # When the field value is a pydantic model, serialize it.
741            rsl[rsl_key] = value
742            if isinstance(value, BaseModel):
743                rsl[rsl_key] = value.model_dump(by_alias=by_alias)
744        return rsl

The model derived from pydantic's BaseModel provides ORM-like access to the CRUD (create, read, update, delete) functionalities of a table in Baserow. The design of the class is quite opinionated. Therefore, if a certain use case cannot be well covered with this abstraction, it may be more effective to directly use the Client class.

Every inheritance/implementation of this class provides access to a table in a Baserow instance. A client instance can be specified; if not, the GlobalClient is used. Ensure that it is configured before use.

row_id: Optional[int]

All rows in Baserow have a unique ID.

table_id: int
283    @property
284    @abc.abstractmethod
285    def table_id(cls) -> int:  # type: ignore
286        """
287        The Baserow table ID. Every table in Baserow has a unique ID. This means
288        that each model is linked to a specific table. It's not currently
289        possible to bind a table model to multiple tables.
290        """
291        raise NotImplementedError()

The Baserow table ID. Every table in Baserow has a unique ID. This means that each model is linked to a specific table. It's not currently possible to bind a table model to multiple tables.

table_name: str
293    @property
294    @abc.abstractmethod
295    def table_name(cls) -> str:  # type: ignore
296        """
297        Each table model must have a human-readable table name. The name is used
298        for debugging information only and has no role in addressing/interacting
299        with the Baserow table. Ideally this should be the same name used for
300        the table within the Baserow UI.
301        """
302        raise NotImplementedError()

Each table model must have a human-readable table name. The name is used for debugging information only and has no role in addressing/interacting with the Baserow table. Ideally this should be the same name used for the table within the Baserow UI.

client: ClassVar[Optional[baserow.client.Client]] = None

Optional client instance for accessing Baserow. If not set, the GlobalClient is used.

dump_response: ClassVar[bool] = False

If set to true, the parsed dict of the body of each API response is dumped to debug output.

dump_payload: ClassVar[bool] = False

If set to true, the data body for the request is dumped to the debug output.

ignore_fields_during_table_creation: ClassVar[list[str]] = ['order', 'id']

Fields with this name are ignored when creating tables.

model_config = {'ser_json_timedelta': 'float'}
@classmethod
@valid_configuration
def by_id(cls: Type[~T], row_id: int) -> ~T:
338    @classmethod
339    @valid_configuration
340    async def by_id(cls: Type[T], row_id: int) -> T:
341        """
342        Fetch a single row/entry from the table by the row ID.
343
344        Args:
345            row_id (int): The ID of the row to be returned.
346        """
347        return await cls.__req_client().get_row(cls.table_id, row_id, True, cls)

Fetch a single row/entry from the table by the row ID.

Arguments:
  • row_id (int): The ID of the row to be returned.
@classmethod
@valid_configuration
def query( cls: Type[~T], filter: Optional[baserow.filter.Filter] = None, order_by: Optional[list[str]] = None, page: Optional[int] = None, size: Optional[int] = None) -> list[~T]:
349    @classmethod
350    @valid_configuration
351    async def query(
352        cls: Type[T],
353        filter: Optional[Filter] = None,
354        order_by: Optional[list[str]] = None,
355        page: Optional[int] = None,
356        size: Optional[int] = None,
357    ) -> list[T]:
358        """
359        Queries for rows in the Baserow table. Note that Baserow uses paging. If
360        all rows of a table (in line with the optional filter) are needed, set
361        `size` to `-1`. Even though this option allows for resolving paging, it
362        should be noted that in Baserow, a maximum of 200 rows can be received
363        per API call. This can lead to significant waiting times and system load
364        for large datasets. Therefore, this option should be used with caution.
365
366        Args:
367            filter (Optional[list[Filter]], optional): Allows the dataset to be
368                filtered.
369            order_by (Optional[list[str]], optional): A list of field names/IDs
370                by which the result should be sorted. If the field name is
371                prepended with a +, the sorting is ascending; if with a -, it is
372                descending.
373            page (Optional[int], optional): The page of the paging.
374            size (Optional[int], optional): How many records should be returned
375                at max. Defaults to 100 and cannot exceed 200. If set to -1 the
376                method wil resolve Baserow's paging and returns all rows
377                corresponding to the query.
378        """
379        if size == -1 and page:
380            raise ValueError(
381                "it's not possible to request a specific page when requesting all results (potentially from multiple pages) with size=-1",
382            )
383        if size is not None and size == -1:
384            rsl = await cls.__req_client().list_all_table_rows(
385                cls.table_id,
386                True,
387                cls,
388                filter=filter,
389                order_by=order_by,
390            )
391        else:
392            rsl = await cls.__req_client().list_table_rows(
393                cls.table_id,
394                True,
395                cls,
396                filter=filter,
397                order_by=order_by,
398                page=page,
399                size=size,
400            )
401        return rsl.results

Queries for rows in the Baserow table. Note that Baserow uses paging. If all rows of a table (in line with the optional filter) are needed, set size to -1. Even though this option allows for resolving paging, it should be noted that in Baserow, a maximum of 200 rows can be received per API call. This can lead to significant waiting times and system load for large datasets. Therefore, this option should be used with caution.

Arguments:
  • filter (Optional[list[Filter]], optional): Allows the dataset to be filtered.
  • order_by (Optional[list[str]], optional): A list of field names/IDs by which the result should be sorted. If the field name is prepended with a +, the sorting is ascending; if with a -, it is descending.
  • page (Optional[int], optional): The page of the paging.
  • size (Optional[int], optional): How many records should be returned at max. Defaults to 100 and cannot exceed 200. If set to -1 the method wil resolve Baserow's paging and returns all rows corresponding to the query.
@classmethod
@valid_configuration
def update_fields_by_id( cls: Type[~T], row_id: int, by_alias: bool = True, **kwargs: Any) -> Union[~T, baserow.client.MinimalRow]:
403    @classmethod
404    @valid_configuration
405    async def update_fields_by_id(
406        cls: Type[T],
407        row_id: int,
408        by_alias: bool = True,
409        **kwargs: Any,
410    ) -> Union[T, MinimalRow]:
411        """
412        Update the fields in a row (defined by its ID) given by the kwargs
413        parameter. The keys provided must be valid field names in the model.
414        values will be validated against the model. If the value type is
415        inherited by the BaseModel, its serializer will be applied to the value
416        and submitted to the database. Please note that custom _Field_
417        serializers for any other types are not taken into account.
418
419        The custom model serializer is used in the module because the structure
420        of some Baserow fields differs between the GET result and the required
421        POST data for modification. For example, the MultipleSelectField returns
422        ID, text value, and color with the GET request. However, only a list of
423        IDs or values is required for updating the field using a POST request.
424
425        Args:
426            row_id (int): ID of row in Baserow to be updated.
427            by_alias (bool, optional): Specify whether to use alias values to
428                address field names in Baserow. Note that this value is set to
429                True by default, contrary to pydantic's usual practice. In the
430                context of the table model (which is specifically used to
431                represent Baserow tables), setting an alias typically indicates
432                that the field name in Baserow is not a valid Python variable
433                name.
434        """
435        payload = cls.__model_dump_subset(by_alias, **kwargs)
436        # if cls.dump_payload:
437        #     logger.debug(payload)
438        return await cls.__req_client().update_row(
439            cls.table_id,
440            row_id,
441            payload,
442            True,
443        )

Update the fields in a row (defined by its ID) given by the kwargs parameter. The keys provided must be valid field names in the model. values will be validated against the model. If the value type is inherited by the BaseModel, its serializer will be applied to the value and submitted to the database. Please note that custom _Field_ serializers for any other types are not taken into account.

The custom model serializer is used in the module because the structure of some Baserow fields differs between the GET result and the required POST data for modification. For example, the MultipleSelectField returns ID, text value, and color with the GET request. However, only a list of IDs or values is required for updating the field using a POST request.

Arguments:
  • row_id (int): ID of row in Baserow to be updated.
  • by_alias (bool, optional): Specify whether to use alias values to address field names in Baserow. Note that this value is set to True by default, contrary to pydantic's usual practice. In the context of the table model (which is specifically used to represent Baserow tables), setting an alias typically indicates that the field name in Baserow is not a valid Python variable name.
@classmethod
@valid_configuration
def delete_by_id(cls: Type[~T], row_id: Union[int, list[int]]):
445    @classmethod
446    @valid_configuration
447    async def delete_by_id(cls: Type[T], row_id: Union[int, list[int]]):
448        """
449        Deletes one or more rows in the Baserow table. If a list of IDs is
450        passed, deletion occurs as a batch command. To delete a single Table
451        instance with the Table.row_id set, the Table.delete() method can also
452        be used.
453
454        Args:
455            row_id (Union[int, list[int]]): ID or ID list of row(s) in Baserow
456            to be deleted.
457        """
458        await cls.__req_client().delete_row(cls.table_id, row_id)

Deletes one or more rows in the Baserow table. If a list of IDs is passed, deletion occurs as a batch command. To delete a single Table instance with the Table.row_id set, the Table.delete() method can also be used.

Arguments:
  • row_id (Union[int, list[int]]): ID or ID list of row(s) in Baserow
  • to be deleted.
@classmethod
@valid_configuration
def batch_update(cls, data: dict[int, dict[str, typing.Any]], by_alias: bool = True):
460    @classmethod
461    @valid_configuration
462    def batch_update(cls, data: dict[int, dict[str, Any]], by_alias: bool = True):
463        """
464        Updates multiple fields in the database. The given data dict must map
465        the unique row id to the data to be updated. The input is validated
466        against the model. See the update method documentation for more
467        information about its limitations and underlying ideas.
468
469        Args:
470            data: A dict mapping the unique row id to the data to be updated.
471            by_alias: Please refer to the documentation on the update method to
472                learn more about this arg.
473        """
474        payload = []
475        for key, value in data.items():
476            entry = cls.__model_dump_subset(by_alias, **value)
477            entry["id"] = key
478            payload.append(entry)
479        # if cls.dump_payload:
480        #     logger.debug(payload)
481        raise NotImplementedError(
482            "Baserow client library currently does not support batch update operations on rows"
483        )

Updates multiple fields in the database. The given data dict must map the unique row id to the data to be updated. The input is validated against the model. See the update method documentation for more information about its limitations and underlying ideas.

Arguments:
  • data: A dict mapping the unique row id to the data to be updated.
  • by_alias: Please refer to the documentation on the update method to learn more about this arg.
@valid_configuration
async def create(self) -> baserow.client.MinimalRow:
485    @valid_configuration
486    async def create(self) -> MinimalRow:
487        """
488        Creates a new row in the table with the data from the instance. Please
489        note that this method does not check whether the fields defined by the
490        model actually exist.
491        """
492        rsl = await self.__req_client().create_row(
493            self.table_id,
494            self.model_dump(by_alias=True, mode="json", exclude_none=True),
495            True,
496        )
497        if not isinstance(rsl, MinimalRow):
498            raise RuntimeError(
499                f" expected MinimalRow instance got {type(rsl)} instead",
500            )
501        return rsl

Creates a new row in the table with the data from the instance. Please note that this method does not check whether the fields defined by the model actually exist.

@valid_configuration
async def update_fields( self: ~T, by_alias: bool = True, **kwargs: Any) -> Union[~T, baserow.client.MinimalRow]:
503    @valid_configuration
504    async def update_fields(
505        self: T,
506        by_alias: bool = True,
507        **kwargs: Any,
508    ) -> Union[T, MinimalRow]:
509        """
510        Updates the row with the ID of this instance. Short-hand for the
511        `Table.update_by_id()` method, for instances with the `Table.row_id`
512        set. For more information on how to use this, please refer to the
513        documentation of this method.
514        """
515        if self.row_id is None:
516            raise RowIDNotSetError(self.__class__.__name__, "field_update")
517        return await self.update_fields_by_id(self.row_id, by_alias, **kwargs)

Updates the row with the ID of this instance. Short-hand for the Table.update_by_id() method, for instances with the Table.row_id set. For more information on how to use this, please refer to the documentation of this method.

@valid_configuration
async def update(self: ~T) -> Union[~T, baserow.client.MinimalRow]:
519    @valid_configuration
520    async def update(self: T) -> Union[T, MinimalRow]:
521        """
522        Updates all fields of a row with the data of this model instance. The
523        row_id field must be set.
524        """
525        if self.row_id is None:
526            raise RowIDNotSetError(self.__class__.__name__, "update")
527
528        excluded: list[str] = []
529        for key, field in self.__dict__.items():
530            if isinstance(field, BaserowField) and field.read_only():
531                excluded.append(key)
532            elif isinstance(field, uuid.UUID):
533                excluded.append(key)
534
535        rsl = await self.__req_client().update_row(
536            self.table_id,
537            self.row_id,
538            self.model_dump(
539                by_alias=True,
540                mode="json",
541                exclude_none=True,
542                exclude=set(excluded),
543            ),
544            True
545        )
546        for _, field in self.__dict__.items():
547            if isinstance(field, BaserowField):
548                field.changes_applied()
549        return rsl

Updates all fields of a row with the data of this model instance. The row_id field must be set.

@valid_configuration
async def delete(self):
551    @valid_configuration
552    async def delete(self):
553        """
554        Deletes the row with the ID of this instance. Short-hand for the
555        `Table.delete_by_id()` method, for instances with the `Table.row_id`
556        set. For more information on how to use this, please refer to the
557        documentation of this method.
558        """
559        if self.row_id is None:
560            raise RowIDNotSetError(self.__class__.__name__, "delete")
561        await self.delete_by_id(self.row_id)

Deletes the row with the ID of this instance. Short-hand for the Table.delete_by_id() method, for instances with the Table.row_id set. For more information on how to use this, please refer to the documentation of this method.

@classmethod
async def create_table(cls, database_id: int):
563    @classmethod
564    async def create_table(cls, database_id: int):
565        """
566        This method creates a new table in the given database based on the
567        structure and fields of the model.
568
569        Args:
570            database_id (int): The ID of the database in which the new table
571                should be created.
572        """
573        # Name is needed for table creation.
574        if not isinstance(cls.table_name, str):
575            raise InvalidTableConfigurationError(
576                cls.__name__, "table_name not set")
577
578        # The primary field is determined at this point to ensure that any
579        # exceptions (none, more than one primary field) occur before the
580        # expensive API calls.
581        primary_name, _ = cls.primary_field()
582
583        # Create the new table itself.
584        table_rsl = await cls.__req_client().create_database_table(database_id, cls.table_name)
585        cls.table_id = table_rsl.id
586        primary_id, unused_fields = await cls.__scramble_all_field_names()
587
588        # Create the fields.
589        for key, field in cls.model_fields.items():
590            await cls.__create_table_field(key, field, primary_id, primary_name)
591
592        # Delete unused fields.
593        for field in unused_fields:
594            await cls.__req_client().delete_database_table_field(field)

This method creates a new table in the given database based on the structure and fields of the model.

Arguments:
  • database_id (int): The ID of the database in which the new table should be created.
@classmethod
def primary_field(cls) -> Tuple[str, pydantic.fields.FieldInfo]:
596    @classmethod
597    def primary_field(cls) -> Tuple[str, FieldInfo]:
598        """
599        This method returns a tuple of the field name and pydantic.FieldInfo of
600        the field that has been marked as the primary field. Only one primary
601        field is allowed per table. This is done by adding PrimaryField as a
602        type annotation. Example for such a Model:
603
604        ```python
605        class Person(Table):
606            table_id = 23
607            table_name = "Person"
608            model_config = ConfigDict(populate_by_name=True)
609
610            name: Annotated[
611                str,
612                Field(alias=str("Name")),
613                PrimaryField(),
614            ]
615        ```
616        """
617        rsl: Optional[Tuple[str, FieldInfo]] = None
618        for name, field in cls.model_fields.items():
619            if any(isinstance(item, PrimaryField) for item in field.metadata):
620                if rsl is not None:
621                    raise MultiplePrimaryFieldsError(cls.__name__)
622                rsl = (name, field)
623        if rsl is None:
624            raise NoPrimaryFieldError(cls.__name__)
625        return rsl

This method returns a tuple of the field name and pydantic.FieldInfo of the field that has been marked as the primary field. Only one primary field is allowed per table. This is done by adding PrimaryField as a type annotation. Example for such a Model:

class Person(Table):
    table_id = 23
    table_name = "Person"
    model_config = ConfigDict(populate_by_name=True)

    name: Annotated[
        str,
        Field(alias=str("Name")),
        PrimaryField(),
    ]
model_fields = {'row_id': FieldInfo(annotation=Union[int, NoneType], required=False, default=None, alias='id', alias_priority=2)}
model_computed_fields = {}
Inherited Members
pydantic.main.BaseModel
BaseModel
model_extra
model_fields_set
model_construct
model_copy
model_dump
model_dump_json
model_json_schema
model_parametrized_name
model_post_init
model_rebuild
model_validate
model_validate_json
model_validate_strings
dict
json
parse_obj
parse_raw
parse_file
from_orm
construct
copy
schema
schema_json
validate
update_forward_refs