diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ee0e72d..4445a1d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # Changelog -# Version 3.26.0 (2022-08-12) +# Version 3.26.1 (2022-08-23) +### Changed +* `ModelRun.get_config()` + * Modifies get_config to return un-nested Model Run config +### Added +* `ModelRun.update_config()` + * Updates model run training metadata +* `ModelRun.reset_config()` + * Resets model run training metadata +* `ModelRun.get_config()` + * Fetches model run training metadata + +### Changed +* `Model.create_model_run()` + * Add training metadata config as a model run creation param + +# Version 3.26.0 (2022-08-15) ## Added * `Batch.delete()` which will delete an existing `Batch` * `Batch.delete_labels()` which will delete all `Label`’s created after a `Project`’s mode has been set to batch. @@ -663,7 +679,3 @@ a `Label`. Default value is 0.0. ## Version 2.2 (2019-10-18) Changelog not maintained before version 2.2. - -### Changed -* `Model.create_model_run()` - * Add training metadata config as a model run creation param diff --git a/docs/source/conf.py b/docs/source/conf.py index 694677233..f339e6045 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,7 +21,7 @@ copyright = '2021, Labelbox' author = 'Labelbox' -release = '3.25.1' +release = '3.26.1' # -- General configuration --------------------------------------------------- diff --git a/labelbox/__init__.py b/labelbox/__init__.py index 0efb083de..56b992c11 100644 --- a/labelbox/__init__.py +++ b/labelbox/__init__.py @@ -1,5 +1,5 @@ name = "labelbox" -__version__ = "3.26.0" +__version__ = "3.26.1" from labelbox.client import Client from labelbox.schema.project import Project diff --git a/labelbox/data/serialization/labelbox_v1/label.py b/labelbox/data/serialization/labelbox_v1/label.py index 1e976deae..b143c3636 100644 --- a/labelbox/data/serialization/labelbox_v1/label.py +++ b/labelbox/data/serialization/labelbox_v1/label.py @@ -146,6 +146,7 @@ class LBV1Label(BaseModel): skipped: Optional[bool] = Extra('Skipped') media_type: Optional[str] = Extra('media_type') data_split: Optional[str] = Extra('Data Split') + global_key: Optional[str] = Extra('Global Key') def to_common(self) -> Label: if isinstance(self.label, list): diff --git a/labelbox/data/serialization/labelbox_v1/objects.py b/labelbox/data/serialization/labelbox_v1/objects.py index 77fed2cd6..62a5bd485 100644 --- a/labelbox/data/serialization/labelbox_v1/objects.py +++ b/labelbox/data/serialization/labelbox_v1/objects.py @@ -22,6 +22,8 @@ class LBV1ObjectBase(LBV1Feature): instanceURI: Optional[str] = None classifications: List[Union[LBV1Text, LBV1Radio, LBV1Dropdown, LBV1Checklist]] = [] + page: Optional[int] = None + unit: Optional[str] = None def dict(self, *args, **kwargs) -> Dict[str, Any]: res = super().dict(*args, **kwargs) @@ -262,7 +264,7 @@ def from_common(cls, text_entity: TextEntity, class LBV1Objects(BaseModel): objects: List[Union[LBV1Line, LBV1Point, LBV1Polygon, LBV1Rectangle, LBV1TextEntity, LBV1Mask, LBV1TIPoint, LBV1TILine, - LBV1TIPolygon, LBV1TIRectangle]] + LBV1TIPolygon, LBV1TIRectangle,]] def to_common(self) -> List[ObjectAnnotation]: objects = [ @@ -285,6 +287,8 @@ def to_common(self) -> List[ObjectAnnotation]: 'color': obj.color, 'feature_id': obj.feature_id, 'value': obj.value, + 'page': obj.page, + 'unit': obj.unit, }) for obj in self.objects ] return objects diff --git a/labelbox/schema/data_row.py b/labelbox/schema/data_row.py index fa5ace8f9..4c7bb8287 100644 --- a/labelbox/schema/data_row.py +++ b/labelbox/schema/data_row.py @@ -17,6 +17,7 @@ class DataRow(DbObject, Updateable, BulkDeletable): Attributes: external_id (str): User-generated file name or identifier + global_key (str): User-generated globally unique identifier row_data (str): Paths to local files are uploaded to Labelbox's server. Otherwise, it's treated as an external URL. updated_at (datetime) @@ -33,6 +34,7 @@ class DataRow(DbObject, Updateable, BulkDeletable): attachments (Relationship) `ToMany` relationship with AssetAttachment """ external_id = Field.String("external_id") + global_key = Field.String("global_key") row_data = Field.String("row_data") updated_at = Field.DateTime("updated_at") created_at = Field.DateTime("created_at") diff --git a/labelbox/schema/model_run.py b/labelbox/schema/model_run.py index 39e22548b..62b29c796 100644 --- a/labelbox/schema/model_run.py +++ b/labelbox/schema/model_run.py @@ -42,7 +42,7 @@ class Status(Enum): FAILED = "FAILED" def upsert_labels(self, label_ids, timeout_seconds=60): - """ Adds data rows and labels to a model run + """ Adds data rows and labels to a Model Run Args: label_ids (list): label ids to insert timeout_seconds (float): Max waiting time, in seconds. @@ -75,7 +75,7 @@ def upsert_labels(self, label_ids, timeout_seconds=60): timeout_seconds=timeout_seconds) def upsert_data_rows(self, data_row_ids, timeout_seconds=60): - """ Adds data rows to a model run without any associated labels + """ Adds data rows to a Model Run without any associated labels Args: data_row_ids (list): data row ids to add to mea timeout_seconds (float): Max waiting time, in seconds. @@ -167,7 +167,7 @@ def model_run_data_rows(self): ['annotationGroups', 'pageInfo', 'endCursor']) def delete(self): - """ Deletes specified model run. + """ Deletes specified Model Run. Returns: Query execution success. @@ -178,10 +178,10 @@ def delete(self): self.client.execute(query_str, {ids_param: str(self.uid)}) def delete_model_run_data_rows(self, data_row_ids: List[str]): - """ Deletes data rows from model runs. + """ Deletes data rows from Model Runs. Args: - data_row_ids (list): List of data row ids to delete from the model run. + data_row_ids (list): List of data row ids to delete from the Model Run. Returns: Query execution success. """ @@ -262,6 +262,56 @@ def update_status(self, }, experimental=True) + @experimental + def update_config(self, config: Dict[str, Any]) -> Dict[str, Any]: + """ + Updates the Model Run's training metadata config + Args: + config (dict): A dictionary of keys and values + Returns: + Model Run id and updated training metadata + """ + data: Dict[str, Any] = {'config': config} + res = self.client.execute( + """mutation updateModelRunConfigPyApi($modelRunId: ID!, $data: UpdateModelRunConfigInput!){ + updateModelRunConfig(modelRun: {id : $modelRunId}, data: $data){trainingMetadata} + } + """, { + 'modelRunId': self.uid, + 'data': data + }, + experimental=True) + return res["updateModelRunConfig"] + + @experimental + def reset_config(self) -> Dict[str, Any]: + """ + Resets Model Run's training metadata config + Returns: + Model Run id and reset training metadata + """ + res = self.client.execute( + """mutation resetModelRunConfigPyApi($modelRunId: ID!){ + resetModelRunConfig(modelRun: {id : $modelRunId}){trainingMetadata} + } + """, {'modelRunId': self.uid}, + experimental=True) + return res["resetModelRunConfig"] + + @experimental + def get_config(self) -> Dict[str, Any]: + """ + Gets Model Run's training metadata + Returns: + training metadata as a dictionary + """ + res = self.client.execute("""query ModelRunPyApi($modelRunId: ID!){ + modelRun(where: {id : $modelRunId}){trainingMetadata} + } + """, {'modelRunId': self.uid}, + experimental=True) + return res["modelRun"]["trainingMetadata"] + @experimental def export_labels( self, diff --git a/labelbox/schema/project.py b/labelbox/schema/project.py index 7ab503ec9..27e43f537 100644 --- a/labelbox/schema/project.py +++ b/labelbox/schema/project.py @@ -93,6 +93,22 @@ class QueueMode(Enum): Dataset = "Dataset" def update(self, **kwargs): + """ Updates this project with the specified attributes + + Args: + kwargs: a dictionary containing attributes to be upserted + + Note that the quality setting cannot be changed after a project has been created. The quality mode + for a project is inferred through the following attributes: + Benchmark: + auto_audit_number_of_labels = 1 + auto_audit_percentage = 1.0 + Consensus: + auto_audit_number_of_labels > 1 + auto_audit_percentage <= 1.0 + Attempting to switch between benchmark and consensus modes is an invalid operation and will result + in an error. + """ mode: Optional[Project.QueueMode] = kwargs.pop("queue_mode", None) if mode: self._update_queue_mode(mode) diff --git a/tests/data/assets/labelbox_v1/pdf_export.json b/tests/data/assets/labelbox_v1/pdf_export.json new file mode 100644 index 000000000..0fc253e63 --- /dev/null +++ b/tests/data/assets/labelbox_v1/pdf_export.json @@ -0,0 +1,130 @@ +[{ + "ID": "cl6xnzi4a7ldn0729381g7104", + "DataRow ID": "cl6xnv9h61fv0085yhtoq06ht", + "Labeled Data": "https://storage.labelbox.com/ckcz6bubudyfi0855o1dt1g9s%2F4cef4e08-e13d-8a5e-fbbf-c7624babb490-Airbnb_%20Labelbox%20-%20Focus%20on%20Workforce%20-%20Labelbox%20Labeling%20Operations%20(1).pdf?Expires=1661971050348&KeyName=labelbox-assets-key-3&Signature=JK6ral5CXF7T9Q5LaQqKvJy5A2A", + "Label": { + "objects": [{ + "featureId": "cl6xnzjpq0dmr07yocs2vfot8", + "schemaId": "cl6xnuwt95lqq07330tbb3mfd", + "color": "#1CE6FF", + "title": "boxy", + "value": "boxy", + "bbox": { + "top": 144.68, + "left": 107.84, + "height": 441.6, + "width": 9.48 + }, + "page": 0, + "unit": "POINTS", + "instanceURI": "https://api.labelbox.com/masks/feature/cl6xnzjpq0dmr07yocs2vfot8?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2NjOWZtbXc0aGNkMDczOHFpeWM2YW54Iiwib3JnYW5pemF0aW9uSWQiOiJja2N6NmJ1YnVkeWZpMDg1NW8xZHQxZzlzIiwiaWF0IjoxNjYwNzYxNDUwLCJleHAiOjE2NjMzNTM0NTB9.X4-j6zee8o685PUrL9C6oC2m6TayKuJQHhN8iLgG8kI" + }, { + "featureId": "cl6xnzjpq0dms07yobwv68gxf", + "schemaId": "cl6xnuwt95lqq07330tbb3mfd", + "color": "#1CE6FF", + "title": "boxy", + "value": "boxy", + "bbox": { + "top": 162.73, + "left": 32.45, + "height": 388.17, + "width": 101.66 + }, + "page": 4, + "unit": "POINTS", + "instanceURI": "https://api.labelbox.com/masks/feature/cl6xnzjpq0dms07yobwv68gxf?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2NjOWZtbXc0aGNkMDczOHFpeWM2YW54Iiwib3JnYW5pemF0aW9uSWQiOiJja2N6NmJ1YnVkeWZpMDg1NW8xZHQxZzlzIiwiaWF0IjoxNjYwNzYxNDUwLCJleHAiOjE2NjMzNTM0NTB9.X4-j6zee8o685PUrL9C6oC2m6TayKuJQHhN8iLgG8kI" + }, { + "featureId": "cl6xnzjpq0dmt07yo8pp45gru", + "schemaId": "cl6xnuwt95lqq07330tbb3mfd", + "color": "#1CE6FF", + "title": "boxy", + "value": "boxy", + "bbox": { + "top": 223.26, + "left": 251.42, + "height": 457.04, + "width": 186.78 + }, + "page": 7, + "unit": "POINTS", + "instanceURI": "https://api.labelbox.com/masks/feature/cl6xnzjpq0dmt07yo8pp45gru?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2NjOWZtbXc0aGNkMDczOHFpeWM2YW54Iiwib3JnYW5pemF0aW9uSWQiOiJja2N6NmJ1YnVkeWZpMDg1NW8xZHQxZzlzIiwiaWF0IjoxNjYwNzYxNDUwLCJleHAiOjE2NjMzNTM0NTB9.X4-j6zee8o685PUrL9C6oC2m6TayKuJQHhN8iLgG8kI" + }, { + "featureId": "cl6xnzjpq0dmu07yo2qik0en4", + "schemaId": "cl6xnuwt95lqq07330tbb3mfd", + "color": "#1CE6FF", + "title": "boxy", + "value": "boxy", + "bbox": { + "top": 32.52, + "left": 218.17, + "height": 231.73, + "width": 110.56 + }, + "page": 6, + "unit": "POINTS", + "instanceURI": "https://api.labelbox.com/masks/feature/cl6xnzjpq0dmu07yo2qik0en4?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2NjOWZtbXc0aGNkMDczOHFpeWM2YW54Iiwib3JnYW5pemF0aW9uSWQiOiJja2N6NmJ1YnVkeWZpMDg1NW8xZHQxZzlzIiwiaWF0IjoxNjYwNzYxNDUwLCJleHAiOjE2NjMzNTM0NTB9.X4-j6zee8o685PUrL9C6oC2m6TayKuJQHhN8iLgG8kI" + }, { + "featureId": "cl6xnzjpq0dmv07yo7phz7ofz", + "schemaId": "cl6xnuwt95lqq07330tbb3mfd", + "color": "#1CE6FF", + "title": "boxy", + "value": "boxy", + "bbox": { + "top": 117.39, + "left": 4.25, + "height": 456.92, + "width": 164.83 + }, + "page": 7, + "unit": "POINTS", + "instanceURI": "https://api.labelbox.com/masks/feature/cl6xnzjpq0dmv07yo7phz7ofz?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2NjOWZtbXc0aGNkMDczOHFpeWM2YW54Iiwib3JnYW5pemF0aW9uSWQiOiJja2N6NmJ1YnVkeWZpMDg1NW8xZHQxZzlzIiwiaWF0IjoxNjYwNzYxNDUwLCJleHAiOjE2NjMzNTM0NTB9.X4-j6zee8o685PUrL9C6oC2m6TayKuJQHhN8iLgG8kI" + }, { + "featureId": "cl6xnzjpq0dmw07yofocp6uf6", + "schemaId": "cl6xnuwt95lqq07330tbb3mfd", + "color": "#1CE6FF", + "title": "boxy", + "value": "boxy", + "bbox": { + "top": 82.13, + "left": 217.28, + "height": 279.76, + "width": 82.43 + }, + "page": 8, + "unit": "POINTS", + "instanceURI": "https://api.labelbox.com/masks/feature/cl6xnzjpq0dmw07yofocp6uf6?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2NjOWZtbXc0aGNkMDczOHFpeWM2YW54Iiwib3JnYW5pemF0aW9uSWQiOiJja2N6NmJ1YnVkeWZpMDg1NW8xZHQxZzlzIiwiaWF0IjoxNjYwNzYxNDUwLCJleHAiOjE2NjMzNTM0NTB9.X4-j6zee8o685PUrL9C6oC2m6TayKuJQHhN8iLgG8kI" + }, { + "featureId": "cl6xnzjpq0dmx07yo0qh40z0n", + "schemaId": "cl6xnuwt95lqq07330tbb3mfd", + "color": "#1CE6FF", + "title": "boxy", + "value": "boxy", + "bbox": { + "top": 298.12, + "left": 83.34, + "height": 203.83, + "width": 0.38 + }, + "page": 3, + "unit": "POINTS", + "instanceURI": "https://api.labelbox.com/masks/feature/cl6xnzjpq0dmx07yo0qh40z0n?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2NjOWZtbXc0aGNkMDczOHFpeWM2YW54Iiwib3JnYW5pemF0aW9uSWQiOiJja2N6NmJ1YnVkeWZpMDg1NW8xZHQxZzlzIiwiaWF0IjoxNjYwNzYxNDUwLCJleHAiOjE2NjMzNTM0NTB9.X4-j6zee8o685PUrL9C6oC2m6TayKuJQHhN8iLgG8kI" + }], + "classifications": [], + "relationships": [] + }, + "Created By": "jtso@labelbox.com", + "Project Name": "PDF MAL Test", + "Created At": "2022-08-17T18:37:18.000Z", + "Updated At": "2022-08-17T18:37:20.073Z", + "Seconds to Label": 15.003, + "External ID": "Airbnb_ Labelbox - Focus on Workforce - Labelbox Labeling Operations (1).pdf", + "Global Key": null, + "Agreement": -1, + "Benchmark Agreement": -1, + "Benchmark ID": null, + "Dataset Name": "PDF ", + "Reviews": [], + "View Label": "https://editor.labelbox.com?project=cl6xntneb7t28072bggdydv7a&label=cl6xnzi4a7ldn0729381g7104", + "Has Open Issues": 0, + "Skipped": false +}] \ No newline at end of file diff --git a/tests/data/serialization/labelbox_v1/test_document.py b/tests/data/serialization/labelbox_v1/test_document.py new file mode 100644 index 000000000..a5a0f611e --- /dev/null +++ b/tests/data/serialization/labelbox_v1/test_document.py @@ -0,0 +1,45 @@ +import json +from typing import Dict, Any + +from labelbox.data.serialization.labelbox_v1.converter import LBV1Converter + +IGNORE_KEYS = [ + "Data Split", "media_type", "DataRow Metadata", "Media Attributes" +] + + +def round_dict(data: Dict[str, Any]) -> Dict[str, Any]: + for key in data: + if isinstance(data[key], float): + data[key] = int(data[key]) + elif isinstance(data[key], dict): + data[key] = round_dict(data[key]) + return data + + +def test_pdf(): + """ + Tests an export from a pdf document with only bounding boxes + """ + payload = json.load( + open('tests/data/assets/labelbox_v1/pdf_export.json', 'r')) + collection = LBV1Converter.deserialize(payload) + serialized = next(LBV1Converter.serialize(collection)) + + payload = payload[0] # only one document in the export + + serialized = {k: v for k, v in serialized.items() if k not in IGNORE_KEYS} + + assert serialized.keys() == payload.keys() + for key in payload.keys(): + if key == 'Label': + serialized_no_classes = [{ + k: v for k, v in dic.items() if k != 'classifications' + } for dic in serialized[key]['objects']] + serialized_round = [ + round_dict(dic) for dic in serialized_no_classes + ] + payload_round = [round_dict(dic) for dic in payload[key]['objects']] + assert payload_round == serialized_round + else: + assert serialized[key] == payload[key] diff --git a/tests/data/serialization/labelbox_v1/test_image.py b/tests/data/serialization/labelbox_v1/test_image.py index f6da8b729..546c97f64 100644 --- a/tests/data/serialization/labelbox_v1/test_image.py +++ b/tests/data/serialization/labelbox_v1/test_image.py @@ -22,6 +22,7 @@ def test_image(file_path): # We are storing the media types now. payload['media_type'] = 'image' + payload['Global Key'] = None assert serialized.keys() == payload.keys() @@ -31,6 +32,8 @@ def test_image(file_path): elif key == 'Label': for annotation_a, annotation_b in zip(serialized[key]['objects'], payload[key]['objects']): + annotation_b['page'] = None + annotation_b['unit'] = None if not len(annotation_a['classifications']): # We don't add a classification key to the payload if there is no classifications. annotation_a.pop('classifications') diff --git a/tests/data/serialization/labelbox_v1/test_text.py b/tests/data/serialization/labelbox_v1/test_text.py index 1c6dfa479..bd28a6c04 100644 --- a/tests/data/serialization/labelbox_v1/test_text.py +++ b/tests/data/serialization/labelbox_v1/test_text.py @@ -10,6 +10,7 @@ def test_text(): serialized = next(LBV1Converter.serialize(collection)) payload['media_type'] = 'text' + payload['Global Key'] = None assert serialized.keys() == payload.keys() for key in serialized: @@ -18,6 +19,8 @@ def test_text(): elif key == 'Label': for annotation_a, annotation_b in zip(serialized[key]['objects'], payload[key]['objects']): + annotation_b['page'] = None + annotation_b['unit'] = None if not len(annotation_a['classifications']): # We don't add a classification key to the payload if there is no classifications. annotation_a.pop('classifications') diff --git a/tests/data/serialization/labelbox_v1/test_unknown_media.py b/tests/data/serialization/labelbox_v1/test_unknown_media.py index bd5cddd64..4607d7be3 100644 --- a/tests/data/serialization/labelbox_v1/test_unknown_media.py +++ b/tests/data/serialization/labelbox_v1/test_unknown_media.py @@ -16,6 +16,7 @@ def test_image(): for row in payload: row['media_type'] = 'image' + row['Global Key'] = None collection = LBV1Converter.deserialize(payload) for idx, serialized in enumerate(LBV1Converter.serialize(collection)): @@ -30,6 +31,8 @@ def test_image(): if not len(annotation_a['classifications']): # We don't add a classification key to the payload if there is no classifications. annotation_a.pop('classifications') + annotation_b['page'] = None + annotation_b['unit'] = None if isinstance(annotation_b.get('classifications'), list) and len( diff --git a/tests/data/serialization/labelbox_v1/test_video.py b/tests/data/serialization/labelbox_v1/test_video.py index 880e0be5f..169bef918 100644 --- a/tests/data/serialization/labelbox_v1/test_video.py +++ b/tests/data/serialization/labelbox_v1/test_video.py @@ -18,6 +18,7 @@ def test_video(): collection = LBV1Converter.deserialize([payload]) serialized = next(LBV1Converter.serialize(collection)) payload['media_type'] = 'video' + payload['Global Key'] = None assert serialized.keys() == payload.keys() for key in serialized: if key != 'Label': @@ -32,6 +33,8 @@ def test_video(): for obj_a, obj_b in zip(annotation_a['objects'], annotation_b['objects']): + obj_b['page'] = None + obj_b['unit'] = None obj_a = round_dict(obj_a) obj_b = round_dict(obj_b) assert obj_a == obj_b diff --git a/tests/integration/annotation_import/conftest.py b/tests/integration/annotation_import/conftest.py index c00df1ce3..108c55910 100644 --- a/tests/integration/annotation_import/conftest.py +++ b/tests/integration/annotation_import/conftest.py @@ -360,6 +360,19 @@ def model_run(rand_gen, model): pass +@pytest.fixture +def model_run_with_training_metadata(rand_gen, model): + name = rand_gen(str) + training_metadata = {"batch_size": 1000} + model_run = model.create_model_run(name, training_metadata) + yield model_run + try: + model_run.delete() + except: + # Already was deleted by the test + pass + + @pytest.fixture def model_run_with_model_run_data_rows(client, configured_project, model_run_predictions, model_run): diff --git a/tests/integration/annotation_import/test_model_run.py b/tests/integration/annotation_import/test_model_run.py index e99490f0d..5b170f4aa 100644 --- a/tests/integration/annotation_import/test_model_run.py +++ b/tests/integration/annotation_import/test_model_run.py @@ -56,6 +56,24 @@ def test_model_run_delete(client, model_run): assert len(before) == len(after) + 1 +def test_model_run_update_config(model_run_with_training_metadata): + new_config = {"batch_size": 2000} + res = model_run_with_training_metadata.update_config(new_config) + assert res["trainingMetadata"]["batch_size"] == new_config["batch_size"] + + +def test_model_run_reset_config(model_run_with_training_metadata): + res = model_run_with_training_metadata.reset_config() + assert res["trainingMetadata"] is None + + +def test_model_run_get_config(model_run_with_training_metadata): + new_config = {"batch_size": 2000} + model_run_with_training_metadata.update_config(new_config) + res = model_run_with_training_metadata.get_config() + assert res["batch_size"] == new_config["batch_size"] + + def test_model_run_data_rows_delete(client, model_run_with_model_run_data_rows): models = list(client.get_models()) model = models[0]