micantis 1.2.1__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.1/micantis.egg-info → micantis-1.2.3}/PKG-INFO +25 -18
- micantis-1.2.1/PKG-INFO → micantis-1.2.3/README.md +21 -34
- {micantis-1.2.1 → micantis-1.2.3}/micantis/api_wrapper.py +207 -20
- micantis-1.2.1/README.md → micantis-1.2.3/micantis.egg-info/PKG-INFO +41 -17
- {micantis-1.2.1 → micantis-1.2.3}/micantis.egg-info/SOURCES.txt +2 -1
- {micantis-1.2.1 → micantis-1.2.3}/micantis.egg-info/requires.txt +4 -0
- {micantis-1.2.1 → micantis-1.2.3}/pyproject.toml +12 -2
- micantis-1.2.3/tests/test_batch_update_result.py +135 -0
- {micantis-1.2.1 → micantis-1.2.3}/LICENSE +0 -0
- {micantis-1.2.1 → micantis-1.2.3}/micantis/__init__.py +0 -0
- {micantis-1.2.1 → micantis-1.2.3}/micantis/utils.py +0 -0
- {micantis-1.2.1 → micantis-1.2.3}/micantis.egg-info/dependency_links.txt +0 -0
- {micantis-1.2.1 → micantis-1.2.3}/micantis.egg-info/top_level.txt +0 -0
- {micantis-1.2.1 → 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.1
|
|
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)
|
|
@@ -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
|