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
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.
50class RowLink(BaseModel, Generic[T]): 51 """ 52 A single linking of one row to another row in another table. A link field 53 can have multiple links. Part of `table.TableLinkField`. 54 """ 55 row_id: Optional[int] = Field(alias=str("id")) 56 key: Optional[str] = Field(alias=str("value")) 57 58 model_config = ConfigDict(populate_by_name=True) 59 60 @model_validator(mode="after") 61 def id_or_value_must_be_set(self: "RowLink") -> "RowLink": 62 if self.row_id is None and self.key is None: 63 raise ValueError( 64 "At least one of the row_id and value fields must be set" 65 ) 66 return self 67 68 @model_serializer 69 def serialize(self) -> Union[int, str]: 70 """ 71 Serializes the field into the data structure required by the Baserow 72 API. If an entry has both an id and a value set, the id is used. 73 Otherwise the key field is used. 74 75 From the Baserow API documentation: Accepts an array containing the 76 identifiers or main field text values of the related rows. 77 """ 78 if self.row_id is not None: 79 return self.row_id 80 if self.key is not None: 81 return self.key 82 raise ValueError("both fields id and key are unset for this entry") 83 84 async def query_linked_row(self) -> T: 85 """ 86 Queries and returns the linked row. 87 """ 88 if self.row_id is None: 89 raise ValueError( 90 "query_linked_row is currently only implemented using the row_id", 91 ) 92 table = self.__get_linked_table() 93 return await table.by_id(self.row_id) 94 95 def __get_linked_table(self) -> T: 96 metadata = self.__pydantic_generic_metadata__ 97 if "args" not in metadata: 98 raise PydanticGenericMetadataError.args_missing( 99 self.__class__.__name__, 100 "linked table", 101 ) 102 if len(metadata["args"]) < 1: 103 raise PydanticGenericMetadataError.args_empty( 104 self.__class__.__name__, 105 "linked table", 106 ) 107 return metadata["args"][0]
A single linking of one row to another row in another table. A link field
can have multiple links. Part of table.TableLinkField
.
68 @model_serializer 69 def serialize(self) -> Union[int, str]: 70 """ 71 Serializes the field into the data structure required by the Baserow 72 API. If an entry has both an id and a value set, the id is used. 73 Otherwise the key field is used. 74 75 From the Baserow API documentation: Accepts an array containing the 76 identifiers or main field text values of the related rows. 77 """ 78 if self.row_id is not None: 79 return self.row_id 80 if self.key is not None: 81 return self.key 82 raise ValueError("both fields id and key are unset for this entry")
Serializes the field into the data structure required by the Baserow API. If an entry has both an id and a value set, the id is used. Otherwise the key field is used.
From the Baserow API documentation: Accepts an array containing the identifiers or main field text values of the related rows.
84 async def query_linked_row(self) -> T: 85 """ 86 Queries and returns the linked row. 87 """ 88 if self.row_id is None: 89 raise ValueError( 90 "query_linked_row is currently only implemented using the row_id", 91 ) 92 table = self.__get_linked_table() 93 return await table.by_id(self.row_id)
Queries and returns the linked row.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_config
- 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
- model_fields
- model_computed_fields
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.
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.
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.
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 thatTable.row_id
is set.
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")
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}")
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
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.
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.
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.
If set to true, the parsed dict of the body of each API response is dumped to debug output.
If set to true, the data body for the request is dumped to the debug output.
Fields with this name are ignored when creating tables.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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(),
]
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