micantis 1.2.2__tar.gz → 1.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {micantis-1.2.2/micantis.egg-info → micantis-1.2.3}/PKG-INFO +25 -18
- micantis-1.2.2/PKG-INFO → micantis-1.2.3/README.md +21 -34
- {micantis-1.2.2 → micantis-1.2.3}/micantis/api_wrapper.py +215 -166
- micantis-1.2.2/README.md → micantis-1.2.3/micantis.egg-info/PKG-INFO +41 -17
- {micantis-1.2.2 → micantis-1.2.3}/micantis.egg-info/SOURCES.txt +2 -1
- {micantis-1.2.2 → micantis-1.2.3}/micantis.egg-info/requires.txt +4 -0
- {micantis-1.2.2 → micantis-1.2.3}/pyproject.toml +12 -2
- micantis-1.2.3/tests/test_batch_update_result.py +135 -0
- {micantis-1.2.2 → micantis-1.2.3}/LICENSE +0 -0
- {micantis-1.2.2 → micantis-1.2.3}/micantis/__init__.py +0 -0
- {micantis-1.2.2 → micantis-1.2.3}/micantis/utils.py +0 -0
- {micantis-1.2.2 → micantis-1.2.3}/micantis.egg-info/dependency_links.txt +0 -0
- {micantis-1.2.2 → micantis-1.2.3}/micantis.egg-info/top_level.txt +0 -0
- {micantis-1.2.2 → micantis-1.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: micantis
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Package to simplify Micantis API usage
|
|
5
5
|
Author-email: Mykela DeLuca <mykela.deluca@micantis.io>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,9 @@ Requires-Dist: msal
|
|
|
13
13
|
Requires-Dist: azure-identity
|
|
14
14
|
Provides-Extra: parquet
|
|
15
15
|
Requires-Dist: pyarrow>=16.0.0; extra == "parquet"
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-mock>=3; extra == "test"
|
|
16
19
|
Dynamic: license-file
|
|
17
20
|
|
|
18
21
|
# Micantis API Wrapper
|
|
@@ -499,27 +502,31 @@ combined_df.head()
|
|
|
499
502
|
|
|
500
503
|
## Write Cell Metadata
|
|
501
504
|
Micantis lets you programmatically assign or update metadata for each cell using either:
|
|
502
|
-
- the
|
|
505
|
+
- the built-in field `"name"` (renames the cell) or `"dataSourceKey"`
|
|
506
|
+
- the human-readable field name of a user-defined property (e.g., "Technician", "Weight (g)")
|
|
503
507
|
- or the internal propertyDefinitionId (UUID)
|
|
504
508
|
|
|
509
|
+
**Per-entity atomic:** all changes targeting a single cell commit or rollback together. If any change for a cell fails, none of that cell's changes persist; other cells are unaffected. The call returns a `BatchUpdateResult`:
|
|
510
|
+
|
|
511
|
+
- `result.success` — `True` if every cell committed
|
|
512
|
+
- `result.errors` — list of `{itemId, error}` dicts for the cells whose changes were rolled back
|
|
513
|
+
- `if result:` — truthy iff full success (use `.success` or truthiness, not `result == True`)
|
|
514
|
+
|
|
505
515
|
#### 📘 Examples
|
|
506
516
|
|
|
507
517
|
```python
|
|
508
|
-
# Example 1:
|
|
518
|
+
# Example 1: Rename a cell and update its dataSourceKey + a user property
|
|
509
519
|
changes = [
|
|
510
|
-
{
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
"id": "your-cell-test-guid-here",
|
|
517
|
-
"field": "Weight (g)",
|
|
518
|
-
"value": 98.7
|
|
519
|
-
}
|
|
520
|
+
{"id": "your-cell-test-guid-here", "field": "name", "value": "A-EVE-15P-Cell-04"},
|
|
521
|
+
{"id": "your-cell-test-guid-here", "field": "dataSourceKey", "value": "Formation"},
|
|
522
|
+
{"id": "your-cell-test-guid-here", "field": "Technician", "value": "Mykela"},
|
|
523
|
+
{"id": "your-cell-test-guid-here", "field": "Weight (g)", "value": 98.7},
|
|
520
524
|
]
|
|
521
525
|
|
|
522
|
-
api.write_cell_metadata(changes=changes)
|
|
526
|
+
result = api.write_cell_metadata(changes=changes)
|
|
527
|
+
if not result:
|
|
528
|
+
for e in result.errors:
|
|
529
|
+
print(f"{e['itemId']}: {e['error']}")
|
|
523
530
|
|
|
524
531
|
# Verify the changes
|
|
525
532
|
api.get_cell_metadata(cell_ids=["your-cell-test-guid-here"], metadata=['Weight (g)', 'Technician'])
|
|
@@ -535,12 +542,12 @@ changes = [
|
|
|
535
542
|
}
|
|
536
543
|
]
|
|
537
544
|
|
|
538
|
-
api.write_cell_metadata(changes=changes)
|
|
539
|
-
|
|
540
|
-
# Verify the changes
|
|
541
|
-
api.get_cell_metadata(cell_ids=["your-cell-test-guid-here"], metadata=['Weight (g)', 'Technician'])
|
|
545
|
+
result = api.write_cell_metadata(changes=changes)
|
|
546
|
+
assert result.success, result.errors
|
|
542
547
|
```
|
|
543
548
|
|
|
549
|
+
> **Rename rules:** CellTest names must be unique and may only contain letters, digits, and `_ - . ( ) space`. CellData names must be unique; renaming a CellData also auto-updates the owning CellTest's DataSourceKey.
|
|
550
|
+
|
|
544
551
|
## Stitch Data
|
|
545
552
|
Combine multiple data sets into a single stitched data set. This is useful for creating continuous test data from multiple separate test runs.
|
|
546
553
|
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: micantis
|
|
3
|
-
Version: 1.2.2
|
|
4
|
-
Summary: Package to simplify Micantis API usage
|
|
5
|
-
Author-email: Mykela DeLuca <mykela.deluca@micantis.io>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.7
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: requests
|
|
11
|
-
Requires-Dist: pandas
|
|
12
|
-
Requires-Dist: msal
|
|
13
|
-
Requires-Dist: azure-identity
|
|
14
|
-
Provides-Extra: parquet
|
|
15
|
-
Requires-Dist: pyarrow>=16.0.0; extra == "parquet"
|
|
16
|
-
Dynamic: license-file
|
|
17
|
-
|
|
18
1
|
# Micantis API Wrapper
|
|
19
2
|
|
|
20
3
|
A lightweight Python wrapper for interacting with the Micantis API plus some helpful utilities.
|
|
@@ -499,27 +482,31 @@ combined_df.head()
|
|
|
499
482
|
|
|
500
483
|
## Write Cell Metadata
|
|
501
484
|
Micantis lets you programmatically assign or update metadata for each cell using either:
|
|
502
|
-
- the
|
|
485
|
+
- the built-in field `"name"` (renames the cell) or `"dataSourceKey"`
|
|
486
|
+
- the human-readable field name of a user-defined property (e.g., "Technician", "Weight (g)")
|
|
503
487
|
- or the internal propertyDefinitionId (UUID)
|
|
504
488
|
|
|
489
|
+
**Per-entity atomic:** all changes targeting a single cell commit or rollback together. If any change for a cell fails, none of that cell's changes persist; other cells are unaffected. The call returns a `BatchUpdateResult`:
|
|
490
|
+
|
|
491
|
+
- `result.success` — `True` if every cell committed
|
|
492
|
+
- `result.errors` — list of `{itemId, error}` dicts for the cells whose changes were rolled back
|
|
493
|
+
- `if result:` — truthy iff full success (use `.success` or truthiness, not `result == True`)
|
|
494
|
+
|
|
505
495
|
#### 📘 Examples
|
|
506
496
|
|
|
507
497
|
```python
|
|
508
|
-
# Example 1:
|
|
498
|
+
# Example 1: Rename a cell and update its dataSourceKey + a user property
|
|
509
499
|
changes = [
|
|
510
|
-
{
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
"id": "your-cell-test-guid-here",
|
|
517
|
-
"field": "Weight (g)",
|
|
518
|
-
"value": 98.7
|
|
519
|
-
}
|
|
500
|
+
{"id": "your-cell-test-guid-here", "field": "name", "value": "A-EVE-15P-Cell-04"},
|
|
501
|
+
{"id": "your-cell-test-guid-here", "field": "dataSourceKey", "value": "Formation"},
|
|
502
|
+
{"id": "your-cell-test-guid-here", "field": "Technician", "value": "Mykela"},
|
|
503
|
+
{"id": "your-cell-test-guid-here", "field": "Weight (g)", "value": 98.7},
|
|
520
504
|
]
|
|
521
505
|
|
|
522
|
-
api.write_cell_metadata(changes=changes)
|
|
506
|
+
result = api.write_cell_metadata(changes=changes)
|
|
507
|
+
if not result:
|
|
508
|
+
for e in result.errors:
|
|
509
|
+
print(f"{e['itemId']}: {e['error']}")
|
|
523
510
|
|
|
524
511
|
# Verify the changes
|
|
525
512
|
api.get_cell_metadata(cell_ids=["your-cell-test-guid-here"], metadata=['Weight (g)', 'Technician'])
|
|
@@ -535,12 +522,12 @@ changes = [
|
|
|
535
522
|
}
|
|
536
523
|
]
|
|
537
524
|
|
|
538
|
-
api.write_cell_metadata(changes=changes)
|
|
539
|
-
|
|
540
|
-
# Verify the changes
|
|
541
|
-
api.get_cell_metadata(cell_ids=["your-cell-test-guid-here"], metadata=['Weight (g)', 'Technician'])
|
|
525
|
+
result = api.write_cell_metadata(changes=changes)
|
|
526
|
+
assert result.success, result.errors
|
|
542
527
|
```
|
|
543
528
|
|
|
529
|
+
> **Rename rules:** CellTest names must be unique and may only contain letters, digits, and `_ - . ( ) space`. CellData names must be unique; renaming a CellData also auto-updates the owning CellTest's DataSourceKey.
|
|
530
|
+
|
|
544
531
|
## Stitch Data
|
|
545
532
|
Combine multiple data sets into a single stitched data set. This is useful for creating continuous test data from multiple separate test runs.
|
|
546
533
|
|
|
@@ -43,6 +43,34 @@ def _apply_field_overrides(action, voltage, current, charge_capacity,
|
|
|
43
43
|
action[key] = val
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class BatchUpdateResult:
|
|
47
|
+
"""
|
|
48
|
+
Result of a batch metadata update call (`write_cell_metadata` / `write_data_metadata`).
|
|
49
|
+
|
|
50
|
+
Truthy iff every entity's changes were applied. On partial or full failure, inspect
|
|
51
|
+
`errors` for per-entity details; successful entities' changes are already persisted.
|
|
52
|
+
|
|
53
|
+
Attributes
|
|
54
|
+
----------
|
|
55
|
+
success : bool
|
|
56
|
+
True iff every entity's changes committed (HTTP 204).
|
|
57
|
+
errors : list of dict
|
|
58
|
+
Per-entity errors. Each dict has keys `itemId` (str) and `error` (str).
|
|
59
|
+
Empty list when `success` is True.
|
|
60
|
+
"""
|
|
61
|
+
def __init__(self, success: bool, errors: list = None):
|
|
62
|
+
self.success = success
|
|
63
|
+
self.errors = errors or []
|
|
64
|
+
|
|
65
|
+
def __bool__(self):
|
|
66
|
+
return self.success
|
|
67
|
+
|
|
68
|
+
def __repr__(self):
|
|
69
|
+
if self.success:
|
|
70
|
+
return "BatchUpdateResult(success=True)"
|
|
71
|
+
return f"BatchUpdateResult(success=False, errors={self.errors!r})"
|
|
72
|
+
|
|
73
|
+
|
|
46
74
|
class MicantisAPI:
|
|
47
75
|
"""
|
|
48
76
|
A client for interacting with the Micantis API.
|
|
@@ -967,30 +995,74 @@ class MicantisAPI:
|
|
|
967
995
|
except requests.exceptions.RequestException as e:
|
|
968
996
|
raise self._format_request_error(e)
|
|
969
997
|
|
|
998
|
+
# Built-in fields that the public /cells/updatemetadata endpoint understands directly
|
|
999
|
+
# (i.e. mapped to an OrderByColumn on the server side, not to a UserPropertyDefinition).
|
|
1000
|
+
_BUILTIN_CELL_METADATA_FIELDS = {"dataSourceKey", "name"}
|
|
1001
|
+
|
|
1002
|
+
# Built-in fields that the public /data/updatemetadata endpoint understands directly.
|
|
1003
|
+
_BUILTIN_DATA_METADATA_FIELDS = {"dataSourceKey", "name"}
|
|
1004
|
+
|
|
970
1005
|
def write_cell_metadata(self, changes: list, timeout: int = 30):
|
|
971
1006
|
"""
|
|
972
1007
|
Wrapper for POST /cells/updatemetadata endpoint.
|
|
973
|
-
|
|
1008
|
+
|
|
1009
|
+
Each change targets either a built-in field (supported: "name",
|
|
1010
|
+
"dataSourceKey") or a user-defined metadata property. For user-defined
|
|
1011
|
+
properties, pass either a human-readable 'field' name (resolved via
|
|
1012
|
+
metadata definitions) or a 'propertyDefinitionId' UUID.
|
|
1013
|
+
|
|
1014
|
+
Built-in field names take precedence over user-defined names; if you have a
|
|
1015
|
+
user-defined property literally named "name" or "dataSourceKey", use
|
|
1016
|
+
propertyDefinitionId to disambiguate.
|
|
1017
|
+
|
|
1018
|
+
Renaming a cell ("name" field) enforces name-uniqueness and a character
|
|
1019
|
+
whitelist (letters, digits, `_ - . ( ) space`); renames that fail these
|
|
1020
|
+
checks return an error without applying the change.
|
|
1021
|
+
|
|
1022
|
+
**Per-entity atomicity:** all changes targeting the same cell commit or
|
|
1023
|
+
rollback together. Across cells, each entity is independent — if one
|
|
1024
|
+
cell's changes fail, others still persist. The result indicates which
|
|
1025
|
+
cells failed; successful cells' changes are already saved.
|
|
974
1026
|
|
|
975
1027
|
Automatically reauthenticates if the session is expired or missing.
|
|
976
|
-
Prints
|
|
1028
|
+
Prints a message and returns None if the network request fails entirely.
|
|
977
1029
|
|
|
978
1030
|
Parameters
|
|
979
1031
|
----------
|
|
980
1032
|
changes : list of dict
|
|
981
1033
|
A list of metadata change dictionaries. Each dictionary must contain:
|
|
982
|
-
- "id": str
|
|
983
|
-
-
|
|
984
|
-
|
|
985
|
-
|
|
1034
|
+
- "id": str (cell test GUID)
|
|
1035
|
+
- One of:
|
|
1036
|
+
- "field": "name" or "dataSourceKey" (built-in fields)
|
|
1037
|
+
- "field": "<metadata name>" (user-defined, e.g. "Weight (g)")
|
|
1038
|
+
- "propertyDefinitionId": "<UUID>" (user-defined)
|
|
1039
|
+
- "value": any (will be cast to str). For multi-valued CellTest
|
|
1040
|
+
DataSourceKeys, pass a comma-separated string (e.g. "Formation,CycleLife").
|
|
986
1041
|
|
|
987
1042
|
timeout : int, optional
|
|
988
1043
|
Timeout in seconds for the request. Default is 30.
|
|
989
1044
|
|
|
990
1045
|
Returns
|
|
991
1046
|
-------
|
|
992
|
-
|
|
993
|
-
|
|
1047
|
+
BatchUpdateResult or None
|
|
1048
|
+
A `BatchUpdateResult` whose `.success` is True iff every entity
|
|
1049
|
+
committed (HTTP 204). On partial (207) or full failure (400), the
|
|
1050
|
+
result is falsy and `.errors` lists the per-entity failures as
|
|
1051
|
+
dicts with keys `itemId` and `error`. Returns None on a network-
|
|
1052
|
+
level error.
|
|
1053
|
+
|
|
1054
|
+
Examples
|
|
1055
|
+
--------
|
|
1056
|
+
>>> result = api.write_cell_metadata([
|
|
1057
|
+
... {"id": cell_id, "field": "name", "value": "A-EVE-15P-Cell-04"},
|
|
1058
|
+
... {"id": cell_id, "field": "dataSourceKey", "value": "Formation"},
|
|
1059
|
+
... {"id": cell_id, "field": "Technician", "value": "Alice"},
|
|
1060
|
+
... ])
|
|
1061
|
+
>>> if result:
|
|
1062
|
+
... print("all good")
|
|
1063
|
+
... else:
|
|
1064
|
+
... for e in result.errors:
|
|
1065
|
+
... print(f"{e['itemId']}: {e['error']}")
|
|
994
1066
|
"""
|
|
995
1067
|
try:
|
|
996
1068
|
return self._retry_on_auth_failure(self._write_cell_metadata, changes, timeout)
|
|
@@ -1012,8 +1084,12 @@ class MicantisAPI:
|
|
|
1012
1084
|
|
|
1013
1085
|
self._refresh_token_if_needed()
|
|
1014
1086
|
|
|
1015
|
-
#
|
|
1016
|
-
|
|
1087
|
+
# Only look up user-property definitions if at least one change uses a 'field' that
|
|
1088
|
+
# isn't a built-in (lookup is a separate round-trip and unnecessary otherwise).
|
|
1089
|
+
needs_lookup = any(
|
|
1090
|
+
"field" in change and change["field"] not in self._BUILTIN_CELL_METADATA_FIELDS
|
|
1091
|
+
for change in changes
|
|
1092
|
+
)
|
|
1017
1093
|
if needs_lookup:
|
|
1018
1094
|
metadata_df = self._list_cell_metadata_definitions('df', timeout)
|
|
1019
1095
|
name_to_id = dict(zip(metadata_df['name'], metadata_df['id']))
|
|
@@ -1022,37 +1098,148 @@ class MicantisAPI:
|
|
|
1022
1098
|
|
|
1023
1099
|
formatted_changes = []
|
|
1024
1100
|
for change in changes:
|
|
1025
|
-
|
|
1026
|
-
|
|
1101
|
+
item = {"id": change["id"], "value": str(change["value"])}
|
|
1102
|
+
|
|
1103
|
+
if "field" in change and change["field"] in self._BUILTIN_CELL_METADATA_FIELDS:
|
|
1104
|
+
item["field"] = change["field"]
|
|
1105
|
+
elif "propertyDefinitionId" in change:
|
|
1106
|
+
item["propertyDefinitionId"] = change["propertyDefinitionId"]
|
|
1027
1107
|
elif "field" in change:
|
|
1028
1108
|
field_name = change["field"]
|
|
1029
1109
|
if field_name not in name_to_id:
|
|
1030
1110
|
raise RuntimeError(f"Field name '{field_name}' not found in metadata definitions.")
|
|
1031
|
-
|
|
1111
|
+
item["propertyDefinitionId"] = name_to_id[field_name]
|
|
1032
1112
|
else:
|
|
1033
1113
|
raise RuntimeError("Each change must include either 'field' or 'propertyDefinitionId'.")
|
|
1034
1114
|
|
|
1115
|
+
formatted_changes.append(item)
|
|
1116
|
+
|
|
1117
|
+
payload = {"changes": formatted_changes}
|
|
1118
|
+
|
|
1119
|
+
try:
|
|
1120
|
+
response = self.session.post(
|
|
1121
|
+
f"{self.service_url}/publicapi/v1/cells/updatemetadata",
|
|
1122
|
+
headers=self.headers,
|
|
1123
|
+
json=payload,
|
|
1124
|
+
timeout=timeout
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
if response.status_code == 204:
|
|
1128
|
+
print("✅ Metadata update successful!")
|
|
1129
|
+
return BatchUpdateResult(success=True)
|
|
1130
|
+
if response.status_code in (207, 400):
|
|
1131
|
+
body = response.json()
|
|
1132
|
+
errors = body.get("errors", []) if isinstance(body, dict) else []
|
|
1133
|
+
if response.status_code == 207:
|
|
1134
|
+
print(f"⚠️ Metadata update partial: {len(errors)} of {len(formatted_changes)} entities failed.")
|
|
1135
|
+
else:
|
|
1136
|
+
print(f"⚠️ Metadata update failed for all entities.")
|
|
1137
|
+
return BatchUpdateResult(success=False, errors=errors)
|
|
1138
|
+
raise RuntimeError(f"Metadata update failed: {response.status_code} - {response.text}")
|
|
1139
|
+
|
|
1140
|
+
except requests.exceptions.RequestException as e:
|
|
1141
|
+
raise self._format_request_error(e)
|
|
1142
|
+
|
|
1143
|
+
def write_data_metadata(self, changes: list, timeout: int = 30):
|
|
1144
|
+
"""
|
|
1145
|
+
Wrapper for POST /data/updatemetadata endpoint.
|
|
1146
|
+
|
|
1147
|
+
Updates built-in metadata fields on cell data records. Supported built-in
|
|
1148
|
+
fields are "name" and "dataSourceKey". User-defined metadata properties on
|
|
1149
|
+
cell data are not yet supported via the public API.
|
|
1150
|
+
|
|
1151
|
+
Renaming a data set ("name" field) enforces name-uniqueness; the change is
|
|
1152
|
+
rejected if another data set already has the target name. The owning
|
|
1153
|
+
CellTest's DataSourceKey is updated automatically to match the new name
|
|
1154
|
+
where applicable.
|
|
1155
|
+
|
|
1156
|
+
**Per-entity atomicity:** all changes targeting the same data set commit
|
|
1157
|
+
or rollback together. Across data sets, each entity is independent — if
|
|
1158
|
+
one data set's changes fail, others still persist. The result indicates
|
|
1159
|
+
which data sets failed; successful ones are already saved.
|
|
1160
|
+
|
|
1161
|
+
Automatically reauthenticates if the session is expired or missing.
|
|
1162
|
+
Prints a message and returns None if the network request fails entirely.
|
|
1163
|
+
|
|
1164
|
+
Parameters
|
|
1165
|
+
----------
|
|
1166
|
+
changes : list of dict
|
|
1167
|
+
A list of metadata change dictionaries. Each dictionary must contain:
|
|
1168
|
+
- "id": str (cell data GUID)
|
|
1169
|
+
- "field": "name" or "dataSourceKey"
|
|
1170
|
+
- "value": any (will be cast to str)
|
|
1171
|
+
|
|
1172
|
+
timeout : int, optional
|
|
1173
|
+
Timeout in seconds for the request. Default is 30.
|
|
1174
|
+
|
|
1175
|
+
Returns
|
|
1176
|
+
-------
|
|
1177
|
+
BatchUpdateResult or None
|
|
1178
|
+
A `BatchUpdateResult` whose `.success` is True iff every entity
|
|
1179
|
+
committed (HTTP 204). On partial (207) or full failure (400), the
|
|
1180
|
+
result is falsy and `.errors` lists the per-entity failures as
|
|
1181
|
+
dicts with keys `itemId` and `error`. Returns None on a network-
|
|
1182
|
+
level error.
|
|
1183
|
+
|
|
1184
|
+
Examples
|
|
1185
|
+
--------
|
|
1186
|
+
>>> result = api.write_data_metadata([
|
|
1187
|
+
... {"id": cell_data_id, "field": "name", "value": "Formation_Run_42"},
|
|
1188
|
+
... {"id": cell_data_id, "field": "dataSourceKey", "value": "Formation"},
|
|
1189
|
+
... ])
|
|
1190
|
+
>>> if not result:
|
|
1191
|
+
... for e in result.errors:
|
|
1192
|
+
... print(f"{e['itemId']}: {e['error']}")
|
|
1193
|
+
"""
|
|
1194
|
+
try:
|
|
1195
|
+
return self._retry_on_auth_failure(self._write_data_metadata, changes, timeout)
|
|
1196
|
+
except Exception as e:
|
|
1197
|
+
print(f"⚠️ write_data_metadata failed: {e}")
|
|
1198
|
+
return None
|
|
1199
|
+
|
|
1200
|
+
def _write_data_metadata(self, changes: list, timeout: int):
|
|
1201
|
+
if not self.headers:
|
|
1202
|
+
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
1203
|
+
|
|
1204
|
+
self._refresh_token_if_needed()
|
|
1205
|
+
|
|
1206
|
+
formatted_changes = []
|
|
1207
|
+
for change in changes:
|
|
1208
|
+
field = change.get("field")
|
|
1209
|
+
if field not in self._BUILTIN_DATA_METADATA_FIELDS:
|
|
1210
|
+
supported = ", ".join(f'"{f}"' for f in sorted(self._BUILTIN_DATA_METADATA_FIELDS))
|
|
1211
|
+
raise RuntimeError(
|
|
1212
|
+
f"write_data_metadata only supports built-in fields ({supported}). "
|
|
1213
|
+
f"Got field={field!r}."
|
|
1214
|
+
)
|
|
1035
1215
|
formatted_changes.append({
|
|
1036
1216
|
"id": change["id"],
|
|
1037
|
-
"
|
|
1038
|
-
"value": str(change["value"])
|
|
1217
|
+
"field": field,
|
|
1218
|
+
"value": str(change["value"]),
|
|
1039
1219
|
})
|
|
1040
1220
|
|
|
1041
1221
|
payload = {"changes": formatted_changes}
|
|
1042
1222
|
|
|
1043
1223
|
try:
|
|
1044
1224
|
response = self.session.post(
|
|
1045
|
-
f"{self.service_url}/publicapi/v1/
|
|
1225
|
+
f"{self.service_url}/publicapi/v1/data/updatemetadata",
|
|
1046
1226
|
headers=self.headers,
|
|
1047
1227
|
json=payload,
|
|
1048
1228
|
timeout=timeout
|
|
1049
1229
|
)
|
|
1050
1230
|
|
|
1051
1231
|
if response.status_code == 204:
|
|
1052
|
-
print("✅
|
|
1053
|
-
return True
|
|
1054
|
-
|
|
1055
|
-
|
|
1232
|
+
print("✅ Data metadata update successful!")
|
|
1233
|
+
return BatchUpdateResult(success=True)
|
|
1234
|
+
if response.status_code in (207, 400):
|
|
1235
|
+
body = response.json()
|
|
1236
|
+
errors = body.get("errors", []) if isinstance(body, dict) else []
|
|
1237
|
+
if response.status_code == 207:
|
|
1238
|
+
print(f"⚠️ Data metadata update partial: {len(errors)} of {len(formatted_changes)} entities failed.")
|
|
1239
|
+
else:
|
|
1240
|
+
print(f"⚠️ Data metadata update failed for all entities.")
|
|
1241
|
+
return BatchUpdateResult(success=False, errors=errors)
|
|
1242
|
+
raise RuntimeError(f"Data metadata update failed: {response.status_code} - {response.text}")
|
|
1056
1243
|
|
|
1057
1244
|
except requests.exceptions.RequestException as e:
|
|
1058
1245
|
raise self._format_request_error(e)
|
|
@@ -2026,110 +2213,6 @@ class MicantisAPI:
|
|
|
2026
2213
|
cycle, step_kind, step_index)
|
|
2027
2214
|
return action
|
|
2028
2215
|
|
|
2029
|
-
@staticmethod
|
|
2030
|
-
def rename_column(source_column_index, new_name, name=None):
|
|
2031
|
-
"""Build a rename-column action for clean_data (MetadataEdit mode).
|
|
2032
|
-
|
|
2033
|
-
Rename a column's display Name — what graphs, tables, and reports look up
|
|
2034
|
-
by YAxisName. Targets the ORIGINAL source column index; action order does
|
|
2035
|
-
not shift index meaning.
|
|
2036
|
-
|
|
2037
|
-
Parameters
|
|
2038
|
-
----------
|
|
2039
|
-
source_column_index : int
|
|
2040
|
-
0-based index into the ORIGINAL source column list.
|
|
2041
|
-
new_name : str
|
|
2042
|
-
New display name. Preserve exact spacing / punctuation expected by
|
|
2043
|
-
downstream lookups (e.g. "Temp 1 (C)" with two spaces).
|
|
2044
|
-
name : str, optional
|
|
2045
|
-
Descriptive name for this action.
|
|
2046
|
-
"""
|
|
2047
|
-
action = {"$type": "rename",
|
|
2048
|
-
"sourceColumnIndex": int(source_column_index),
|
|
2049
|
-
"newName": new_name}
|
|
2050
|
-
if name is not None:
|
|
2051
|
-
action["name"] = name
|
|
2052
|
-
return action
|
|
2053
|
-
|
|
2054
|
-
@staticmethod
|
|
2055
|
-
def change_column_kind(source_column_index, new_kind, name=None):
|
|
2056
|
-
"""Build a change-column-kind action for clean_data (MetadataEdit mode).
|
|
2057
|
-
|
|
2058
|
-
Change a column's YAxisKind (cycle-tester aux) or ImportItemKind (logger)
|
|
2059
|
-
— controls which kind-based lookups match the column. Targets the ORIGINAL
|
|
2060
|
-
source column index.
|
|
2061
|
-
|
|
2062
|
-
Parameters
|
|
2063
|
-
----------
|
|
2064
|
-
source_column_index : int
|
|
2065
|
-
0-based index into the ORIGINAL source column list.
|
|
2066
|
-
new_kind : str
|
|
2067
|
-
Exact enum value name (e.g. 'Voltage', 'AuxTemperature').
|
|
2068
|
-
name : str, optional
|
|
2069
|
-
Descriptive name for this action.
|
|
2070
|
-
"""
|
|
2071
|
-
action = {"$type": "kind",
|
|
2072
|
-
"sourceColumnIndex": int(source_column_index),
|
|
2073
|
-
"newKind": new_kind}
|
|
2074
|
-
if name is not None:
|
|
2075
|
-
action["name"] = name
|
|
2076
|
-
return action
|
|
2077
|
-
|
|
2078
|
-
@staticmethod
|
|
2079
|
-
def reorder_columns(source_column_indices, name=None):
|
|
2080
|
-
"""Build a reorder-columns action for clean_data (MetadataEdit mode).
|
|
2081
|
-
|
|
2082
|
-
Declarative final column layout: source columns appear in the cleaned
|
|
2083
|
-
output in the order listed; any source index not listed is dropped.
|
|
2084
|
-
Combines drop + reorder in one action. At most one per definition.
|
|
2085
|
-
|
|
2086
|
-
Parameters
|
|
2087
|
-
----------
|
|
2088
|
-
source_column_indices : list[int]
|
|
2089
|
-
Ordered list of 0-based source column indices to keep. Omit an index
|
|
2090
|
-
to drop that column. Example: [0, 1, 3, 4, 5] drops source column 2.
|
|
2091
|
-
name : str, optional
|
|
2092
|
-
Descriptive name for this action.
|
|
2093
|
-
"""
|
|
2094
|
-
action = {"$type": "reorderColumns",
|
|
2095
|
-
"sourceColumnIndices": [int(i) for i in source_column_indices]}
|
|
2096
|
-
if name is not None:
|
|
2097
|
-
action["name"] = name
|
|
2098
|
-
return action
|
|
2099
|
-
|
|
2100
|
-
@staticmethod
|
|
2101
|
-
def edit_logger_step(line=None, lines=None, columns=None, name=None):
|
|
2102
|
-
"""Build an edit-logger-timestep action for clean_data (logger-data analog of modify_step/bulk_modify).
|
|
2103
|
-
|
|
2104
|
-
Edit Column1..Column10 values by 0-based column index on rows matched by
|
|
2105
|
-
``line`` and/or ``lines`` (union). Use on DataLoggerData sources only; for
|
|
2106
|
-
cycle-tester data use modify_step / bulk_modify.
|
|
2107
|
-
|
|
2108
|
-
Parameters
|
|
2109
|
-
----------
|
|
2110
|
-
line : int, optional
|
|
2111
|
-
Single line number to edit.
|
|
2112
|
-
lines : list of (int, int) tuples, optional
|
|
2113
|
-
Line number ranges to edit, e.g. [(100, 200)]. End may be None for open-ended.
|
|
2114
|
-
columns : dict of {int: float}
|
|
2115
|
-
Column edits as {column_index: value}. Column index is 0-based (0-9).
|
|
2116
|
-
name : str, optional
|
|
2117
|
-
Descriptive name for this action.
|
|
2118
|
-
"""
|
|
2119
|
-
if line is None and not lines:
|
|
2120
|
-
raise ValueError("edit_logger_step requires either line or lines.")
|
|
2121
|
-
if not columns:
|
|
2122
|
-
raise ValueError("edit_logger_step requires columns.")
|
|
2123
|
-
action = {"$type": "editLogger"}
|
|
2124
|
-
if name is not None:
|
|
2125
|
-
action["name"] = name
|
|
2126
|
-
if line is not None:
|
|
2127
|
-
action["lineNumber"] = line
|
|
2128
|
-
if lines:
|
|
2129
|
-
action["lineNumberRanges"] = _ranges(lines)
|
|
2130
|
-
action["columns"] = [{"columnIndex": int(k), "value": float(v)} for k, v in columns.items()]
|
|
2131
|
-
return action
|
|
2132
|
-
|
|
2133
2216
|
def clean_data(self,
|
|
2134
2217
|
source_ids: list,
|
|
2135
2218
|
mode: str = 'CycleAutoFixup',
|
|
@@ -2148,19 +2231,14 @@ class MicantisAPI:
|
|
|
2148
2231
|
source_ids : list of str
|
|
2149
2232
|
List of cell data IDs (GUIDs) to clean.
|
|
2150
2233
|
mode : str, optional
|
|
2151
|
-
Cleaning mode. One of 'CycleAutoFixup', 'FilterCycles', 'Parametric'
|
|
2152
|
-
Default: 'CycleAutoFixup'.
|
|
2153
|
-
it auto-sets to 'Parametric'. For 'MetadataEdit', pass mode explicitly.
|
|
2234
|
+
Cleaning mode. One of 'CycleAutoFixup', 'FilterCycles', 'Parametric'.
|
|
2235
|
+
Default: 'CycleAutoFixup'. Auto-set to 'Parametric' when actions is provided.
|
|
2154
2236
|
filter_cycles_definition : dict, optional
|
|
2155
2237
|
Required when mode is 'FilterCycles'. Definition for filtering cycles.
|
|
2156
2238
|
actions : list, optional
|
|
2157
|
-
|
|
2158
|
-
api.
|
|
2159
|
-
|
|
2160
|
-
api.change_column_kind(), api.reorder_columns().
|
|
2161
|
-
Actions are applied in the order given.
|
|
2162
|
-
|
|
2163
|
-
Parametric example (cycle-tester)::
|
|
2239
|
+
List of action dicts built with api.remove_steps(),
|
|
2240
|
+
api.modify_step(), and/or api.bulk_modify().
|
|
2241
|
+
Auto-sets mode to 'Parametric'. Example::
|
|
2164
2242
|
|
|
2165
2243
|
api.clean_data(
|
|
2166
2244
|
source_ids=[id],
|
|
@@ -2170,29 +2248,6 @@ class MicantisAPI:
|
|
|
2170
2248
|
api.bulk_modify(lines=[(100, 5000)], cycle=1),
|
|
2171
2249
|
]
|
|
2172
2250
|
)
|
|
2173
|
-
|
|
2174
|
-
Parametric example (DataLoggerData)::
|
|
2175
|
-
|
|
2176
|
-
api.clean_data(
|
|
2177
|
-
source_ids=[logger_id],
|
|
2178
|
-
actions=[
|
|
2179
|
-
api.remove_steps(lines=[(1, 3)]),
|
|
2180
|
-
api.edit_logger_step(line=10, columns={0: 1.5}),
|
|
2181
|
-
]
|
|
2182
|
-
)
|
|
2183
|
-
|
|
2184
|
-
MetadataEdit example (rename + drop — works on both CT and logger)::
|
|
2185
|
-
|
|
2186
|
-
api.clean_data(
|
|
2187
|
-
source_ids=[id],
|
|
2188
|
-
mode='MetadataEdit',
|
|
2189
|
-
actions=[
|
|
2190
|
-
api.rename_column(source_column_index=3, new_name='Temp 1 (C)'),
|
|
2191
|
-
api.rename_column(source_column_index=4, new_name='Temp 2 (C)'),
|
|
2192
|
-
api.rename_column(source_column_index=5, new_name='Temp 3 (C)'),
|
|
2193
|
-
api.reorder_columns([0, 1, 3, 4, 5]), # drops source column 2
|
|
2194
|
-
]
|
|
2195
|
-
)
|
|
2196
2251
|
allow_async : bool, optional
|
|
2197
2252
|
If True, runs asynchronously and returns job ID. Default: False.
|
|
2198
2253
|
timeout : int, optional
|
|
@@ -2205,8 +2260,7 @@ class MicantisAPI:
|
|
|
2205
2260
|
If allow_async is True: {'job_id': str}
|
|
2206
2261
|
Returns None if an error occurs.
|
|
2207
2262
|
"""
|
|
2208
|
-
|
|
2209
|
-
if actions is not None and mode == 'CycleAutoFixup':
|
|
2263
|
+
if actions is not None:
|
|
2210
2264
|
mode = 'Parametric'
|
|
2211
2265
|
|
|
2212
2266
|
try:
|
|
@@ -2237,10 +2291,8 @@ class MicantisAPI:
|
|
|
2237
2291
|
|
|
2238
2292
|
if mode == 'Parametric' and not actions:
|
|
2239
2293
|
raise ValueError("actions is required when mode is 'Parametric'.")
|
|
2240
|
-
if mode == 'MetadataEdit' and not actions:
|
|
2241
|
-
raise ValueError("actions is required when mode is 'MetadataEdit'.")
|
|
2242
2294
|
|
|
2243
|
-
_mode_map = {'CycleAutoFixup': 1, 'Parametric': 2, 'FilterCycles': 4
|
|
2295
|
+
_mode_map = {'CycleAutoFixup': 1, 'Parametric': 2, 'FilterCycles': 4}
|
|
2244
2296
|
if mode not in _mode_map:
|
|
2245
2297
|
raise ValueError(f"mode must be one of {list(_mode_map)}, got '{mode}'")
|
|
2246
2298
|
|
|
@@ -2252,10 +2304,7 @@ class MicantisAPI:
|
|
|
2252
2304
|
if filter_cycles_definition is not None:
|
|
2253
2305
|
payload["filterCyclesDefinition"] = filter_cycles_definition
|
|
2254
2306
|
if actions is not None:
|
|
2255
|
-
|
|
2256
|
-
payload["metadataEditDefinition"] = {"actions": actions}
|
|
2257
|
-
else:
|
|
2258
|
-
payload["parametricDefinition"] = {"actions": actions}
|
|
2307
|
+
payload["parametricDefinition"] = {"actions": actions}
|
|
2259
2308
|
|
|
2260
2309
|
url = f"{self.service_url}/publicapi/v1/data/clean"
|
|
2261
2310
|
if allow_async:
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: micantis
|
|
3
|
+
Version: 1.2.3
|
|
4
|
+
Summary: Package to simplify Micantis API usage
|
|
5
|
+
Author-email: Mykela DeLuca <mykela.deluca@micantis.io>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.7
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: requests
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: msal
|
|
13
|
+
Requires-Dist: azure-identity
|
|
14
|
+
Provides-Extra: parquet
|
|
15
|
+
Requires-Dist: pyarrow>=16.0.0; extra == "parquet"
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-mock>=3; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
1
21
|
# Micantis API Wrapper
|
|
2
22
|
|
|
3
23
|
A lightweight Python wrapper for interacting with the Micantis API plus some helpful utilities.
|
|
@@ -482,27 +502,31 @@ combined_df.head()
|
|
|
482
502
|
|
|
483
503
|
## Write Cell Metadata
|
|
484
504
|
Micantis lets you programmatically assign or update metadata for each cell using either:
|
|
485
|
-
- the
|
|
505
|
+
- the built-in field `"name"` (renames the cell) or `"dataSourceKey"`
|
|
506
|
+
- the human-readable field name of a user-defined property (e.g., "Technician", "Weight (g)")
|
|
486
507
|
- or the internal propertyDefinitionId (UUID)
|
|
487
508
|
|
|
509
|
+
**Per-entity atomic:** all changes targeting a single cell commit or rollback together. If any change for a cell fails, none of that cell's changes persist; other cells are unaffected. The call returns a `BatchUpdateResult`:
|
|
510
|
+
|
|
511
|
+
- `result.success` — `True` if every cell committed
|
|
512
|
+
- `result.errors` — list of `{itemId, error}` dicts for the cells whose changes were rolled back
|
|
513
|
+
- `if result:` — truthy iff full success (use `.success` or truthiness, not `result == True`)
|
|
514
|
+
|
|
488
515
|
#### 📘 Examples
|
|
489
516
|
|
|
490
517
|
```python
|
|
491
|
-
# Example 1:
|
|
518
|
+
# Example 1: Rename a cell and update its dataSourceKey + a user property
|
|
492
519
|
changes = [
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
"id": "your-cell-test-guid-here",
|
|
500
|
-
"field": "Weight (g)",
|
|
501
|
-
"value": 98.7
|
|
502
|
-
}
|
|
520
|
+
{"id": "your-cell-test-guid-here", "field": "name", "value": "A-EVE-15P-Cell-04"},
|
|
521
|
+
{"id": "your-cell-test-guid-here", "field": "dataSourceKey", "value": "Formation"},
|
|
522
|
+
{"id": "your-cell-test-guid-here", "field": "Technician", "value": "Mykela"},
|
|
523
|
+
{"id": "your-cell-test-guid-here", "field": "Weight (g)", "value": 98.7},
|
|
503
524
|
]
|
|
504
525
|
|
|
505
|
-
api.write_cell_metadata(changes=changes)
|
|
526
|
+
result = api.write_cell_metadata(changes=changes)
|
|
527
|
+
if not result:
|
|
528
|
+
for e in result.errors:
|
|
529
|
+
print(f"{e['itemId']}: {e['error']}")
|
|
506
530
|
|
|
507
531
|
# Verify the changes
|
|
508
532
|
api.get_cell_metadata(cell_ids=["your-cell-test-guid-here"], metadata=['Weight (g)', 'Technician'])
|
|
@@ -518,12 +542,12 @@ changes = [
|
|
|
518
542
|
}
|
|
519
543
|
]
|
|
520
544
|
|
|
521
|
-
api.write_cell_metadata(changes=changes)
|
|
522
|
-
|
|
523
|
-
# Verify the changes
|
|
524
|
-
api.get_cell_metadata(cell_ids=["your-cell-test-guid-here"], metadata=['Weight (g)', 'Technician'])
|
|
545
|
+
result = api.write_cell_metadata(changes=changes)
|
|
546
|
+
assert result.success, result.errors
|
|
525
547
|
```
|
|
526
548
|
|
|
549
|
+
> **Rename rules:** CellTest names must be unique and may only contain letters, digits, and `_ - . ( ) space`. CellData names must be unique; renaming a CellData also auto-updates the owning CellTest's DataSourceKey.
|
|
550
|
+
|
|
527
551
|
## Stitch Data
|
|
528
552
|
Combine multiple data sets into a single stitched data set. This is useful for creating continuous test data from multiple separate test runs.
|
|
529
553
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "micantis"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.3"
|
|
4
4
|
description = "Package to simplify Micantis API usage"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Mykela DeLuca", email = "mykela.deluca@micantis.io" }
|
|
@@ -18,4 +18,14 @@ dependencies = [
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[project.optional-dependencies]
|
|
21
|
-
parquet = ["pyarrow>=16.0.0"]
|
|
21
|
+
parquet = ["pyarrow>=16.0.0"]
|
|
22
|
+
test = [
|
|
23
|
+
"pytest>=7",
|
|
24
|
+
"pytest-mock>=3",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
python_files = ["test_*.py"]
|
|
30
|
+
python_classes = ["Test*"]
|
|
31
|
+
python_functions = ["test_*"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for BatchUpdateResult parsing in write_cell_metadata / write_data_metadata.
|
|
3
|
+
|
|
4
|
+
Mocks the HTTP layer so it can run anywhere without a live server. Exercises:
|
|
5
|
+
- 204 No Content → BatchUpdateResult(success=True)
|
|
6
|
+
- 207 Multi-Status → BatchUpdateResult(success=False, errors=[...])
|
|
7
|
+
- 400 Bad Request → BatchUpdateResult(success=False, errors=[...])
|
|
8
|
+
- Truthiness (`if result:` works)
|
|
9
|
+
- Rejection of unknown fields (client-side validation)
|
|
10
|
+
|
|
11
|
+
Run: pytest
|
|
12
|
+
"""
|
|
13
|
+
from unittest.mock import MagicMock
|
|
14
|
+
|
|
15
|
+
from micantis.api_wrapper import MicantisAPI, BatchUpdateResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_api():
|
|
19
|
+
api = MicantisAPI.__new__(MicantisAPI)
|
|
20
|
+
api.service_url = "https://fake.test"
|
|
21
|
+
api.headers = {"Authorization": "Bearer fake"}
|
|
22
|
+
api.session = MagicMock()
|
|
23
|
+
api._refresh_token_if_needed = lambda: None
|
|
24
|
+
return api
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _mock_response(status_code, body=None):
|
|
28
|
+
resp = MagicMock()
|
|
29
|
+
resp.status_code = status_code
|
|
30
|
+
resp.json = MagicMock(return_value=body or {})
|
|
31
|
+
resp.text = str(body) if body else ""
|
|
32
|
+
return resp
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_batch_update_result_truthiness():
|
|
36
|
+
r_ok = BatchUpdateResult(success=True)
|
|
37
|
+
assert bool(r_ok) is True
|
|
38
|
+
assert r_ok.errors == []
|
|
39
|
+
assert "success=True" in repr(r_ok)
|
|
40
|
+
|
|
41
|
+
r_fail = BatchUpdateResult(success=False, errors=[{"itemId": "x", "error": "e"}])
|
|
42
|
+
assert bool(r_fail) is False
|
|
43
|
+
assert len(r_fail.errors) == 1
|
|
44
|
+
assert r_fail.errors[0]["itemId"] == "x"
|
|
45
|
+
assert "success=False" in repr(r_fail)
|
|
46
|
+
print("PASS: BatchUpdateResult truthiness + attributes")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_write_cell_204_returns_success():
|
|
50
|
+
api = _make_api()
|
|
51
|
+
api.session.post.return_value = _mock_response(204)
|
|
52
|
+
result = api._write_cell_metadata(
|
|
53
|
+
[{"id": "a", "field": "name", "value": "new"}], timeout=5,
|
|
54
|
+
)
|
|
55
|
+
assert isinstance(result, BatchUpdateResult)
|
|
56
|
+
assert result.success is True
|
|
57
|
+
assert bool(result) is True
|
|
58
|
+
assert result.errors == []
|
|
59
|
+
print("PASS: 204 → BatchUpdateResult(success=True)")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_write_cell_207_returns_partial():
|
|
63
|
+
api = _make_api()
|
|
64
|
+
body = {"errors": [{"itemId": "a", "error": "name exists"}]}
|
|
65
|
+
api.session.post.return_value = _mock_response(207, body)
|
|
66
|
+
result = api._write_cell_metadata(
|
|
67
|
+
[
|
|
68
|
+
{"id": "a", "field": "name", "value": "collision"},
|
|
69
|
+
{"id": "b", "field": "name", "value": "fresh"},
|
|
70
|
+
],
|
|
71
|
+
timeout=5,
|
|
72
|
+
)
|
|
73
|
+
assert isinstance(result, BatchUpdateResult)
|
|
74
|
+
assert result.success is False
|
|
75
|
+
assert bool(result) is False
|
|
76
|
+
assert len(result.errors) == 1
|
|
77
|
+
assert result.errors[0]["itemId"] == "a"
|
|
78
|
+
assert "name exists" in result.errors[0]["error"]
|
|
79
|
+
print("PASS: 207 → BatchUpdateResult(success=False, errors=[...])")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_write_cell_400_returns_failure():
|
|
83
|
+
api = _make_api()
|
|
84
|
+
body = {"errors": [{"itemId": "a", "error": "name exists"}]}
|
|
85
|
+
api.session.post.return_value = _mock_response(400, body)
|
|
86
|
+
result = api._write_cell_metadata(
|
|
87
|
+
[{"id": "a", "field": "name", "value": "collision"}], timeout=5,
|
|
88
|
+
)
|
|
89
|
+
assert isinstance(result, BatchUpdateResult)
|
|
90
|
+
assert result.success is False
|
|
91
|
+
assert len(result.errors) == 1
|
|
92
|
+
print("PASS: 400 → BatchUpdateResult(success=False, errors=[...])")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_write_data_204_returns_success():
|
|
96
|
+
api = _make_api()
|
|
97
|
+
api.session.post.return_value = _mock_response(204)
|
|
98
|
+
result = api._write_data_metadata(
|
|
99
|
+
[{"id": "x", "field": "name", "value": "new"}], timeout=5,
|
|
100
|
+
)
|
|
101
|
+
assert isinstance(result, BatchUpdateResult)
|
|
102
|
+
assert result.success is True
|
|
103
|
+
print("PASS: data/updatemetadata 204 → success")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_write_data_207_returns_partial():
|
|
107
|
+
api = _make_api()
|
|
108
|
+
body = {"errors": [{"itemId": "x", "error": "name exists"}]}
|
|
109
|
+
api.session.post.return_value = _mock_response(207, body)
|
|
110
|
+
result = api._write_data_metadata(
|
|
111
|
+
[{"id": "x", "field": "name", "value": "collision"}], timeout=5,
|
|
112
|
+
)
|
|
113
|
+
assert result.success is False
|
|
114
|
+
assert result.errors[0]["itemId"] == "x"
|
|
115
|
+
print("PASS: data/updatemetadata 207 → partial")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_write_data_rejects_unknown_field():
|
|
119
|
+
api = _make_api()
|
|
120
|
+
try:
|
|
121
|
+
api._write_data_metadata(
|
|
122
|
+
[{"id": "x", "field": "Technician", "value": "foo"}], timeout=5,
|
|
123
|
+
)
|
|
124
|
+
except RuntimeError as e:
|
|
125
|
+
assert "Technician" in str(e)
|
|
126
|
+
assert "name" in str(e) and "dataSourceKey" in str(e)
|
|
127
|
+
print("PASS: data/updatemetadata rejects unknown field client-side")
|
|
128
|
+
return
|
|
129
|
+
raise AssertionError("expected RuntimeError on unknown field")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_name_is_accepted_as_builtin():
|
|
133
|
+
# Ensure the previously-missing client-side allowlist now accepts "name"
|
|
134
|
+
assert "name" in MicantisAPI._BUILTIN_CELL_METADATA_FIELDS
|
|
135
|
+
assert "name" in MicantisAPI._BUILTIN_DATA_METADATA_FIELDS
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|