diff --git a/dlt/common/schema/exceptions.py b/dlt/common/schema/exceptions.py index f040325da9..7f73bcbf36 100644 --- a/dlt/common/schema/exceptions.py +++ b/dlt/common/schema/exceptions.py @@ -108,14 +108,14 @@ def __init__( schema_name: str, table_name: str, column_name: str, - contract_entity: TSchemaContractEntities, + schema_entity: TSchemaContractEntities, contract_mode: TSchemaEvolutionMode, table_schema: Any, schema_contract: TSchemaContractDict, data_item: Any = None, extended_info: str = None, ) -> None: - """Raised when `data_item` violates `contract_mode` on a `contract_entity` as defined by `table_schema` + """Raised when `data_item` violates `contract_mode` on a `schema_entity` as defined by `table_schema` Schema, table and column names are given as a context and full `schema_contract` and causing `data_item` as an evidence. """ @@ -128,7 +128,7 @@ def __init__( msg = ( "In " + msg - + f" . Contract on {contract_entity} with mode {contract_mode} is violated. " + + f" . Contract on {schema_entity} with mode {contract_mode} is violated. " + (extended_info or "") ) super().__init__(msg) @@ -137,7 +137,7 @@ def __init__( self.column_name = column_name # violated contract - self.contract_entity = contract_entity + self.schema_entity = schema_entity self.contract_mode = contract_mode # some evidence diff --git a/docs/examples/chess_production/chess.py b/docs/examples/chess_production/chess.py index b902134785..5b767f0eb6 100644 --- a/docs/examples/chess_production/chess.py +++ b/docs/examples/chess_production/chess.py @@ -6,6 +6,7 @@ from dlt.common.typing import StrAny, TDataItems from dlt.sources.helpers.requests import client + @dlt.source def chess( chess_url: str = dlt.config.value, @@ -59,6 +60,7 @@ def players_games(username: Any) -> Iterator[TDataItems]: MAX_PLAYERS = 5 + def load_data_with_retry(pipeline, data): try: for attempt in Retrying( @@ -68,9 +70,7 @@ def load_data_with_retry(pipeline, data): reraise=True, ): with attempt: - logger.info( - f"Running the pipeline, attempt={attempt.retry_state.attempt_number}" - ) + logger.info(f"Running the pipeline, attempt={attempt.retry_state.attempt_number}") load_info = pipeline.run(data) logger.info(str(load_info)) @@ -92,9 +92,7 @@ def load_data_with_retry(pipeline, data): # print the information on the first load package and all jobs inside logger.info(f"First load package info: {load_info.load_packages[0]}") # print the information on the first completed job in first load package - logger.info( - f"First completed job info: {load_info.load_packages[0].jobs['completed_jobs'][0]}" - ) + logger.info(f"First completed job info: {load_info.load_packages[0].jobs['completed_jobs'][0]}") # check for schema updates: schema_updates = [p.schema_update for p in load_info.load_packages] @@ -152,4 +150,4 @@ def load_data_with_retry(pipeline, data): ) # get data for a few famous players data = chess(chess_url="https://api.chess.com/pub/", max_players=MAX_PLAYERS) - load_data_with_retry(pipeline, data) \ No newline at end of file + load_data_with_retry(pipeline, data) diff --git a/docs/examples/incremental_loading/zendesk.py b/docs/examples/incremental_loading/zendesk.py index 4b8597886a..6113f98793 100644 --- a/docs/examples/incremental_loading/zendesk.py +++ b/docs/examples/incremental_loading/zendesk.py @@ -6,12 +6,11 @@ from dlt.common.typing import TAnyDateTime from dlt.sources.helpers.requests import client + @dlt.source(max_table_nesting=2) def zendesk_support( credentials: Dict[str, str] = dlt.secrets.value, - start_date: Optional[TAnyDateTime] = pendulum.datetime( # noqa: B008 - year=2000, month=1, day=1 - ), + start_date: Optional[TAnyDateTime] = pendulum.datetime(year=2000, month=1, day=1), # noqa: B008 end_date: Optional[TAnyDateTime] = None, ): """ @@ -113,6 +112,7 @@ def get_pages( if not response_json["end_of_stream"]: get_url = response_json["next_page"] + if __name__ == "__main__": # create dlt pipeline pipeline = dlt.pipeline( @@ -120,4 +120,4 @@ def get_pages( ) load_info = pipeline.run(zendesk_support()) - print(load_info) \ No newline at end of file + print(load_info) diff --git a/docs/examples/nested_data/nested_data.py b/docs/examples/nested_data/nested_data.py index 3464448de6..7f85f0522e 100644 --- a/docs/examples/nested_data/nested_data.py +++ b/docs/examples/nested_data/nested_data.py @@ -13,6 +13,7 @@ CHUNK_SIZE = 10000 + # You can limit how deep dlt goes when generating child tables. # By default, the library will descend and generate child tables # for all nested lists, without a limit. @@ -81,6 +82,7 @@ def load_documents(self) -> Iterator[TDataItem]: while docs_slice := list(islice(cursor, CHUNK_SIZE)): yield map_nested_in_place(convert_mongo_objs, docs_slice) + def convert_mongo_objs(value: Any) -> Any: if isinstance(value, (ObjectId, Decimal128)): return str(value) diff --git a/docs/examples/transformers/pokemon.py b/docs/examples/transformers/pokemon.py index c17beff6a8..97b9a98b11 100644 --- a/docs/examples/transformers/pokemon.py +++ b/docs/examples/transformers/pokemon.py @@ -1,6 +1,7 @@ import dlt from dlt.sources.helpers import requests + @dlt.source(max_table_nesting=2) def source(pokemon_api_url: str): """""" @@ -46,6 +47,7 @@ def species(pokemon_details): return (pokemon_list | pokemon, pokemon_list | pokemon | species) + if __name__ == "__main__": # build duck db pipeline pipeline = dlt.pipeline( @@ -54,4 +56,4 @@ def species(pokemon_details): # the pokemon_list resource does not need to be loaded load_info = pipeline.run(source("https://pokeapi.co/api/v2/pokemon")) - print(load_info) \ No newline at end of file + print(load_info) diff --git a/docs/website/docs/dlt-ecosystem/destinations/motherduck.md b/docs/website/docs/dlt-ecosystem/destinations/motherduck.md index e228aab045..939563d0f8 100644 --- a/docs/website/docs/dlt-ecosystem/destinations/motherduck.md +++ b/docs/website/docs/dlt-ecosystem/destinations/motherduck.md @@ -93,5 +93,4 @@ We also see them. My observation is that if you write a lot of data into the database then close the connection and then open it again to write, there's a chance of such timeout. Possible **WAL** file is being written to the remote duckdb database. ### Invalid Input Error: Initialization function "motherduck_init" from file -Use `duckdb 0.8.1` - +Use `duckdb 0.8.1` or above. diff --git a/docs/website/docs/general-usage/credentials/configuration.md b/docs/website/docs/general-usage/credentials/configuration.md index 4cb3e17468..867c47ccba 100644 --- a/docs/website/docs/general-usage/credentials/configuration.md +++ b/docs/website/docs/general-usage/credentials/configuration.md @@ -210,7 +210,7 @@ You can pass destination credentials and ignore the default lookup: pipeline = dlt.pipeline(destination="postgres", credentials=dlt.secrets["postgres_dsn"]) ``` -:::Note +:::note **dlt.config** and **dlt.secrets** can be also used as setters. For example: ```python dlt.config["sheet_id"] = "23029402349032049" diff --git a/docs/website/docs/general-usage/data-contracts.md b/docs/website/docs/general-usage/data-contracts.md deleted file mode 100644 index 543edf2502..0000000000 --- a/docs/website/docs/general-usage/data-contracts.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Data Contracts -description: Data contracts and controlling schema evolution -keywords: [data contracts, schema, dlt schema, pydantic] ---- - -## Data contracts and controlling schema evolution - -`dlt` will evolve the schema of the destination to accomodate the structure and data types of the extracted data. There are several settings -that you can use to control this automatic schema evolution, from the default settings where all changes to the schema are accepted to -a frozen schema that does not change at all. - -Consider this example: - -```py -@dlt.resource(schema_contract={"tables": "evolve", "columns": "freeze"}) -def items(): - ... -``` - -This resource will allow new subtables to be created, but will throw an exception if data is extracted for an existing table which -contains a new column. - -### Possible settings - -The `schema_contract` exists on the `source` decorator as a directive for all resources of that source and on the -`resource` decorator as a directive for the individual resource. Additionally it exists on the `pipeline.run()` method, which will override all existing settings. -The `schema_contract` is a dictionary with keys that control the following: - -* `table` creating of new tables and subtables -* `columns` creating of new columns on an existing table -* `data_type` creating of new variant columns, which happens if a different datatype is discovered in the extracted data than exists in the schema - -Each property can be set to one of three values: -* `freeze`: This will raise an exception if data is encountered that does not fit the existing schema, so no data will be loaded to the destination -* `discard_row`: This will discard any extracted row if it does not adhere to the existing schema, and this row will not be loaded to the destination. All other rows will be. -* `discard_value`: This will discard data in an extracted row that does not adhere to the existing schema and the row will be loaded without this data. - -If a table is a new table that has not been created on the destination yet, dlt will allow the creation of all columns and variants on the first run - -### Code Examples - -The below code will silently ignore new subtables, allow new columns to be added to existing tables and raise an error if a variant of a column is discovered. - -```py -@dlt.resource(schema_contract={"tables": "discard_row", "columns": "evolve", "data_type": "freeze"}) -def items(): - ... -``` - -The below Code will raise on any encountered schema change. Note: You can always set a string which will be interpreted as though all keys are set to these values. - -```py -pipeline.run(my_source(), schema_contract="freeze") -``` - -The below code defines some settings on the source which can be overwritten on the resource which in turn can be overwritten by the global override on the `run` method. -Here for all resources variant columns are frozen and raise an error if encountered, on `items` new columns are allowed but `other_items` inherits the `freeze` setting from -the source, thus new columns are frozen there. New tables are allowed. - -```py -@dlt.resource(schema_contract={"columns": "evolve"}) -def items(): - ... - -@dlt.resource() -def other_items(): - ... - -@dlt.source(schema_contract={"columns": "freeze", "data_type": "freeze"}): -def source(): - return [items(), other_items()] - - -# this will use the settings defined by the decorators -pipeline.run(source()) - -# this will freeze the whole schema, regardless of the decorator settings -pipeline.run(source(), schema_contract="freeze") - -``` \ No newline at end of file diff --git a/docs/website/docs/general-usage/resource.md b/docs/website/docs/general-usage/resource.md index a7f68fadd1..c4b9cac105 100644 --- a/docs/website/docs/general-usage/resource.md +++ b/docs/website/docs/general-usage/resource.md @@ -73,6 +73,9 @@ accepts following arguments: > ๐Ÿ’ก You can mark some resource arguments as [configuration and credentials](credentials) > values so `dlt` can pass them automatically to your functions. +### Put a contract on a tables, columns and data +Use the `schema_contract` argument to tell dlt how to [deal with new tables, data types and bad data types](schema-contracts.md). For example if you set it to **freeze**, `dlt` will not allow for any new tables, columns or data types to be introduced to the schema - it will raise an exception. Learn more in on available contract modes [here](schema-contracts.md#setting-up-the-contract) + ### Define a schema with Pydantic You can alternatively use a [Pydantic](https://pydantic-docs.helpmanual.io/) model to define the schema. @@ -106,6 +109,8 @@ def get_users(): The data types of the table columns are inferred from the types of the pydantic fields. These use the same type conversions as when the schema is automatically generated from the data. +Pydantic models integrate well with [schema contracts](schema-contracts.md) as data validators. + Things to note: - Fields with an `Optional` type are marked as `nullable` @@ -131,6 +136,7 @@ behaviour of creating child tables for these fields. We do not support `RootModel` that validate simple types. You can add such validator yourself, see [data filtering section](#filter-transform-and-pivot-data). + ### Dispatch data to many tables You can load data to many tables from a single resource. The most common case is a stream of events @@ -307,7 +313,7 @@ assert list(r) == list(range(10)) > ๐Ÿ’ก You cannot limit transformers. They should process all the data they receive fully to avoid > inconsistencies in generated datasets. -### Set table and adjust schema +### Set table name and adjust schema You can change the schema of a resource, be it standalone or as a part of a source. Look for method named `apply_hints` which takes the same arguments as resource decorator. Obviously you should call diff --git a/docs/website/docs/general-usage/schema-contracts.md b/docs/website/docs/general-usage/schema-contracts.md new file mode 100644 index 0000000000..26fa231cae --- /dev/null +++ b/docs/website/docs/general-usage/schema-contracts.md @@ -0,0 +1,207 @@ +--- +title: ๐Ÿงช Schema and Data Contracts +description: Controlling schema evolution and validating data +keywords: [data contracts, schema, dlt schema, pydantic] +--- + +## Schema and Data Contracts + +`dlt` will evolve the schema at the destination by following the structure and data types of the extracted data. There are several modes +that you can use to control this automatic schema evolution, from the default modes where all changes to the schema are accepted to +a frozen schema that does not change at all. + +Consider this example: + +```py +@dlt.resource(schema_contract={"tables": "evolve", "columns": "freeze"}) +def items(): + ... +``` + +This resource will allow new tables (both child tables and [tables with dynamic names](resource.md#dispatch-data-to-many-tables)) to be created, but will throw an exception if data is extracted for an existing table which contains a new column. + +### Setting up the contract +You can control the following **schema entities**: +* `tables` - contract is applied when a new table is created +* `columns` - contract is applied when a new column is created on an existing table +* `data_type` - contract is applied when data cannot be coerced into a data type associate with existing column. + +You can use **contract modes** to tell `dlt` how to apply contract for a particular entity: +* `evolve`: No constraints on schema changes. +* `freeze`: This will raise an exception if data is encountered that does not fit the existing schema, so no data will be loaded to the destination +* `discard_row`: This will discard any extracted row if it does not adhere to the existing schema, and this row will not be loaded to the destination. +* `discard_value`: This will discard data in an extracted row that does not adhere to the existing schema and the row will be loaded without this data. + +:::note +The default mode (**evolve**) works as follows: +1. New tables may be always created +2. New columns may be always appended to the existing table +3. Data that do not coerce to existing data type of a particular column will be sent to a [variant column](schema.md#variant-columns) created for this particular type. +::: + +#### Passing schema_contract argument +The `schema_contract` exists on the [dlt.source](source.md) decorator as a default for all resources in that source and on the +[dlt.resource](source.md) decorator as a directive for the individual resource - and as a consequence - on all tables created by this resource. +Additionally it exists on the `pipeline.run()` method, which will override all existing settings. + +The `schema_contract` argument accepts two forms: +1. **full**: a mapping of schema entities to contract modes +2. **shorthand** a contract mode (string) that will be applied to all schema entities. + +For example setting `schema_contract` to *freeze* will expand to the full form: +```python +{"tables": "freeze", "columns": "freeze", "data_type": "freeze"} +``` + +You can change the contract on the **source** instance via `schema_contract` property. For **resource** you can use [apply_hints](resource#set-table-name-and-adjust-schema). + + +#### Nuances of contract modes. +1. Contracts are applied **after names of tables and columns are normalized**. +2. Contract defined on a resource is applied to all tables and child tables created by that resource. +3. `discard_row` works on table level. So for example if you have two tables in parent-child relationship ie. *users* and *users__addresses* and contract is violated in *users__addresses* table, the row of that table is discarded while the parent row in *users* table will be loaded. + +### Use Pydantic models for data validation +Pydantic models can be used to [define table schemas and validate incoming data](resource.md#define-a-schema-with-pydantic). You can use any model you already have. `dlt` will internally synthesize (if necessary) new models that conform with the **schema contract** on the resource. + +Just passing a model in `column` argument of the [dlt.resource](resource.md#define-a-schema-with-pydantic) sets a schema contract that conforms to default Pydantic behavior: +```python +{ + "tables": "evolve", + "columns": "discard_value", + "data_type": "freeze" +} +``` +New tables are allowed, extra fields are ignored and invalid data raises an exception. + +If you pass schema contract explicitly the following happens to schema entities: +1. **tables** do not impact the Pydantic models +2. **columns** modes are mapped into the **extra** modes of Pydantic (see below). `dlt` will apply this setting recursively if models contain other models. +3. **data_type** supports following modes for Pydantic: **evolve** will synthesize lenient model that allows for any data type. This may result with variant columns upstream. +**freeze** will re-raise `ValidationException`. **discard_row** will remove the non-validating data items. +**discard_value** is not currently supported. We may eventually do that on Pydantic v2. + +`dlt` maps column contract modes into the extra fields settings as follows. + +Note that this works in two directions. If you use a model with such setting explicitly configured, `dlt` sets the column contract mode accordingly. This also avoids synthesizing modified models. + +| column mode | pydantic extra | +| ------------- | -------------- | +| evolve | allow | +| freeze | forbid | +| discard_value | ignore | +| discard_row | forbid | + +`discard_row` requires additional handling when ValidationError is raised. + +:::tip +Model validation is added as a [transform step](resource.md#filter-transform-and-pivot-data) to the resource. This step will convert the incoming data items into instances of validating models. You could easily convert them back to dictionaries by using `add_map(lambda item: item.dict())` on a resource. +::: + +:::note +Pydantic models work on the **extracted** data **before names are normalized or child relationships are created**. Make sure to name model fields as in your input data and handle nested data with the nested models. + +As a consequence, `discard_row` will drop the whole data item - even if nested model was affected. +::: + +### Set contracts on Arrow Tables and Pandas +All contract settings apply to [arrow tables and panda frames](../dlt-ecosystem/verified-sources/arrow-pandas.md) as well. +1. **tables** mode the same - no matter what is the data item type +2. **columns** will allow new columns, raise an exception or modify tables/frames still in extract step to avoid re-writing parquet files. +3. **data_type** changes to data types in tables/frames are not allowed and will result in data type schema clash. We could allow for more modes (evolving data types in Arrow tables sounds weird but ping us on Slack if you need it.) + +Here's how `dlt` deals with column modes: +1. **evolve** new columns are allowed (table may be reordered to put them at the end) +2. **discard_value** column will be deleted +3. **discard_row** rows with the column present will be deleted and then column will be deleted +4. **freeze** exception on a new column + + +### Get context from DataValidationError in freeze mode +When contract is violated in freeze mode, `dlt` raises `DataValidationError` exception. This exception gives access to the full context and passes the evidence to the caller. +As with any other exception coming from pipeline run, it will be re-raised via `PipelineStepFailed` exception which you should catch in except: + +```python +try: + pipeline.run() +except as pip_ex: + if pip_ex.step == "normalize": + if isinstance(pip_ex.__context__, DataValidationError): + ... + if pip_ex.step == "extract": + # wrapped in resource exception + +``` + +`DataValidationError` provides the following context: +1. `schema_name`, `table_name` and `column_name` provide the logical "location" at which the contract was violated. +2. `schema_entity` and `contract_mode` tell which contract was violated +3. `table_schema` contains the schema against which the contract was validated. May be Pydantic model or `dlt` TTableSchema instance +4. `schema_contract` the full, expanded schema contract +5. `data_item` causing data item (Python dict, arrow table, pydantic model or list of there of) + + +### Contracts on new tables +If a table is a **new table** that has not been created on the destination yet, dlt will allow the creation of new columns. For a single pipeline run, the column mode is changed (internally) to **evolve** and then reverted back to the original mode. This allows for initial schema inference to happen and then on subsequent run, the inferred contract will be applied to a new data. + +Following tables are considered new: +1. Child tables inferred from the nested data +2. Dynamic tables created from the data during extraction +3. Tables containing **incomplete** columns - columns without data type bound to them. + +For example such table is considered new because column **number** is incomplete (define primary key and NOT null but no data type) +```yaml + blocks: + description: Ethereum blocks + write_disposition: append + columns: + number: + nullable: false + primary_key: true + name: number +``` + +What tables are not considered new: +1. Those with columns defined by Pydantic modes + +### Code Examples + +The below code will silently ignore new subtables, allow new columns to be added to existing tables and raise an error if a variant of a column is discovered. + +```py +@dlt.resource(schema_contract={"tables": "discard_row", "columns": "evolve", "data_type": "freeze"}) +def items(): + ... +``` + +The below Code will raise on any encountered schema change. Note: You can always set a string which will be interpreted as though all keys are set to these values. + +```py +pipeline.run(my_source(), schema_contract="freeze") +``` + +The below code defines some settings on the source which can be overwritten on the resource which in turn can be overwritten by the global override on the `run` method. +Here for all resources variant columns are frozen and raise an error if encountered, on `items` new columns are allowed but `other_items` inherits the `freeze` setting from +the source, thus new columns are frozen there. New tables are allowed. + +```py +@dlt.resource(schema_contract={"columns": "evolve"}) +def items(): + ... + +@dlt.resource() +def other_items(): + ... + +@dlt.source(schema_contract={"columns": "freeze", "data_type": "freeze"}): +def source(): + return [items(), other_items()] + + +# this will use the settings defined by the decorators +pipeline.run(source()) + +# this will freeze the whole schema, regardless of the decorator settings +pipeline.run(source(), schema_contract="freeze") + +``` \ No newline at end of file diff --git a/docs/website/sidebars.js b/docs/website/sidebars.js index 3fae972fe3..0e3584bddd 100644 --- a/docs/website/sidebars.js +++ b/docs/website/sidebars.js @@ -106,7 +106,7 @@ const sidebars = { 'general-usage/incremental-loading', 'general-usage/full-loading', 'general-usage/schema', - 'general-usage/data-contracts', + 'general-usage/schema-contracts', { type: 'category', label: 'Configuration', diff --git a/tests/common/schema/test_schema_contract.py b/tests/common/schema/test_schema_contract.py index 7fdeed5408..32f9583b26 100644 --- a/tests/common/schema/test_schema_contract.py +++ b/tests/common/schema/test_schema_contract.py @@ -185,7 +185,7 @@ def test_check_adding_table(base_settings) -> None: assert val_ex.value.schema_name == schema.name assert val_ex.value.table_name == "new_table" assert val_ex.value.column_name is None - assert val_ex.value.contract_entity == "tables" + assert val_ex.value.schema_entity == "tables" assert val_ex.value.contract_mode == "freeze" assert val_ex.value.table_schema is None # there's no validating schema on new table assert val_ex.value.data_item == {"item": 1} @@ -236,7 +236,7 @@ def assert_new_column(table_update: TTableSchema, column_name: str) -> None: assert val_ex.value.schema_name == schema.name assert val_ex.value.table_name == table_update["name"] assert val_ex.value.column_name == column_name - assert val_ex.value.contract_entity == "columns" + assert val_ex.value.schema_entity == "columns" assert val_ex.value.contract_mode == "freeze" assert val_ex.value.table_schema == schema.get_table(table_update["name"]) assert val_ex.value.data_item == {column_name: 1} @@ -332,7 +332,7 @@ def test_check_adding_new_variant() -> None: assert val_ex.value.schema_name == schema.name assert val_ex.value.table_name == table_update["name"] assert val_ex.value.column_name == "column_2_variant" - assert val_ex.value.contract_entity == "data_type" + assert val_ex.value.schema_entity == "data_type" assert val_ex.value.contract_mode == "freeze" assert val_ex.value.table_schema == schema.get_table(table_update["name"]) assert val_ex.value.data_item is None # we do not pass it to apply_schema_contract diff --git a/tests/extract/test_validation.py b/tests/extract/test_validation.py index 45d75e0b92..045f75ab73 100644 --- a/tests/extract/test_validation.py +++ b/tests/extract/test_validation.py @@ -172,7 +172,7 @@ def some_data() -> t.Iterator[TDataItems]: assert val_ex.table_name == "some_data" assert val_ex.column_name == "('items', 1, 'a')" if yield_list else "('a',)" assert val_ex.data_item == {"a": "not_int", "b": "x"} - assert val_ex.contract_entity == "data_type" + assert val_ex.schema_entity == "data_type" # fail in pipeline @dlt.resource(columns=SimpleModel) @@ -191,7 +191,7 @@ def some_data_extra() -> t.Iterator[TDataItems]: assert isinstance(py_ex.value.__cause__.__cause__, DataValidationError) val_ex = py_ex.value.__cause__.__cause__ assert val_ex.table_name == "some_data_extra" - assert val_ex.contract_entity == "data_type" # extra field is the cause + assert val_ex.schema_entity == "data_type" # extra field is the cause assert val_ex.data_item == {"a": "not_int", "b": "x"} diff --git a/tests/libs/test_pydantic.py b/tests/libs/test_pydantic.py index 1021a3037d..7f9ec52468 100644 --- a/tests/libs/test_pydantic.py +++ b/tests/libs/test_pydantic.py @@ -308,7 +308,7 @@ class ItemModel(BaseModel): assert val_ex.value.schema_name is None assert val_ex.value.table_name == "items" assert val_ex.value.column_name == str(("items", 1, "b")) # pydantic location - assert val_ex.value.contract_entity == "data_type" + assert val_ex.value.schema_entity == "data_type" assert val_ex.value.contract_mode == "freeze" assert val_ex.value.table_schema is freeze_list_model assert val_ex.value.data_item == {"b": 2} @@ -324,7 +324,7 @@ class ItemModel(BaseModel): assert val_ex.value.schema_name is None assert val_ex.value.table_name == "items" assert val_ex.value.column_name == str(("items", 1, "a")) # pydantic location - assert val_ex.value.contract_entity == "columns" + assert val_ex.value.schema_entity == "columns" assert val_ex.value.contract_mode == "freeze" assert val_ex.value.table_schema is freeze_list_model assert val_ex.value.data_item == {"a": 2, "b": False} @@ -423,7 +423,7 @@ class ItemModel(BaseModel): assert val_ex.value.schema_name is None assert val_ex.value.table_name == "items" assert val_ex.value.column_name == str(("b",)) # pydantic location - assert val_ex.value.contract_entity == "data_type" + assert val_ex.value.schema_entity == "data_type" assert val_ex.value.contract_mode == "freeze" assert val_ex.value.table_schema is freeze_model assert val_ex.value.data_item == {"b": 2} @@ -433,7 +433,7 @@ class ItemModel(BaseModel): assert val_ex.value.schema_name is None assert val_ex.value.table_name == "items" assert val_ex.value.column_name == str(("a",)) # pydantic location - assert val_ex.value.contract_entity == "columns" + assert val_ex.value.schema_entity == "columns" assert val_ex.value.contract_mode == "freeze" assert val_ex.value.table_schema is freeze_model assert val_ex.value.data_item == {"a": 2, "b": False}