micantis 1.2.2__tar.gz → 1.2.4__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.4}/PKG-INFO +73 -17
- micantis-1.2.2/PKG-INFO → micantis-1.2.4/README.md +69 -33
- {micantis-1.2.2 → micantis-1.2.4}/micantis/api_wrapper.py +289 -24
- micantis-1.2.2/README.md → micantis-1.2.4/micantis.egg-info/PKG-INFO +89 -16
- {micantis-1.2.2 → micantis-1.2.4}/micantis.egg-info/SOURCES.txt +2 -1
- {micantis-1.2.2 → micantis-1.2.4}/micantis.egg-info/requires.txt +4 -0
- {micantis-1.2.2 → micantis-1.2.4}/pyproject.toml +12 -2
- micantis-1.2.4/tests/test_batch_update_result.py +174 -0
- {micantis-1.2.2 → micantis-1.2.4}/LICENSE +0 -0
- {micantis-1.2.2 → micantis-1.2.4}/micantis/__init__.py +0 -0
- {micantis-1.2.2 → micantis-1.2.4}/micantis/utils.py +0 -0
- {micantis-1.2.2 → micantis-1.2.4}/micantis.egg-info/dependency_links.txt +0 -0
- {micantis-1.2.2 → micantis-1.2.4}/micantis.egg-info/top_level.txt +0 -0
- {micantis-1.2.2 → micantis-1.2.4}/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.4
|
|
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,10 +542,59 @@ changes = [
|
|
|
535
542
|
}
|
|
536
543
|
]
|
|
537
544
|
|
|
538
|
-
api.write_cell_metadata(changes=changes)
|
|
545
|
+
result = api.write_cell_metadata(changes=changes)
|
|
546
|
+
assert result.success, result.errors
|
|
547
|
+
```
|
|
539
548
|
|
|
540
|
-
|
|
541
|
-
|
|
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
|
+
|
|
551
|
+
## Write Data Metadata
|
|
552
|
+
Assign or update metadata on a **data set** (CellData record), the same way as cells, using either:
|
|
553
|
+
- the built-in field `"name"` (renames the data set) or `"dataSourceKey"`
|
|
554
|
+
- the human-readable field name of a user-defined property (e.g., "Active Cycle Test?")
|
|
555
|
+
- or the internal propertyDefinitionId (UUID)
|
|
556
|
+
|
|
557
|
+
User-defined property names are resolved against the data-set metadata definitions, which you can list with `api.list_data_metadata_definitions()`.
|
|
558
|
+
|
|
559
|
+
**Per-entity atomic:** all changes targeting a single data set commit or rollback together; other data sets are unaffected. Returns a `BatchUpdateResult` with `.success` and `.errors`, identical to `write_cell_metadata`.
|
|
560
|
+
|
|
561
|
+
#### 📘 Examples
|
|
562
|
+
|
|
563
|
+
```python
|
|
564
|
+
# Example 1: Set a user-defined property on specific data sets
|
|
565
|
+
changes = [
|
|
566
|
+
{"id": "your-cell-data-guid-here", "field": "Active Cycle Test?", "value": True},
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
result = api.write_data_metadata(changes=changes)
|
|
570
|
+
if not result:
|
|
571
|
+
for e in result.errors:
|
|
572
|
+
print(f"{e['itemId']}: {e['error']}")
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
# Example 2: Rename a data set and update its dataSourceKey
|
|
577
|
+
changes = [
|
|
578
|
+
{"id": "your-cell-data-guid-here", "field": "name", "value": "Formation_Run_42"},
|
|
579
|
+
{"id": "your-cell-data-guid-here", "field": "dataSourceKey", "value": "Formation"},
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
result = api.write_data_metadata(changes=changes)
|
|
583
|
+
assert result.success, result.errors
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```python
|
|
587
|
+
# Example 3: Update using propertyDefinitionId (advanced)
|
|
588
|
+
changes = [
|
|
589
|
+
{
|
|
590
|
+
"id": "your-cell-data-guid-here",
|
|
591
|
+
"propertyDefinitionId": "your-property-definition-guid",
|
|
592
|
+
"value": "true"
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
result = api.write_data_metadata(changes=changes)
|
|
597
|
+
assert result.success, result.errors
|
|
542
598
|
```
|
|
543
599
|
|
|
544
600
|
## Stitch Data
|
|
@@ -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,10 +522,59 @@ changes = [
|
|
|
535
522
|
}
|
|
536
523
|
]
|
|
537
524
|
|
|
538
|
-
api.write_cell_metadata(changes=changes)
|
|
525
|
+
result = api.write_cell_metadata(changes=changes)
|
|
526
|
+
assert result.success, result.errors
|
|
527
|
+
```
|
|
539
528
|
|
|
540
|
-
|
|
541
|
-
|
|
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
|
+
|
|
531
|
+
## Write Data Metadata
|
|
532
|
+
Assign or update metadata on a **data set** (CellData record), the same way as cells, using either:
|
|
533
|
+
- the built-in field `"name"` (renames the data set) or `"dataSourceKey"`
|
|
534
|
+
- the human-readable field name of a user-defined property (e.g., "Active Cycle Test?")
|
|
535
|
+
- or the internal propertyDefinitionId (UUID)
|
|
536
|
+
|
|
537
|
+
User-defined property names are resolved against the data-set metadata definitions, which you can list with `api.list_data_metadata_definitions()`.
|
|
538
|
+
|
|
539
|
+
**Per-entity atomic:** all changes targeting a single data set commit or rollback together; other data sets are unaffected. Returns a `BatchUpdateResult` with `.success` and `.errors`, identical to `write_cell_metadata`.
|
|
540
|
+
|
|
541
|
+
#### 📘 Examples
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
# Example 1: Set a user-defined property on specific data sets
|
|
545
|
+
changes = [
|
|
546
|
+
{"id": "your-cell-data-guid-here", "field": "Active Cycle Test?", "value": True},
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
result = api.write_data_metadata(changes=changes)
|
|
550
|
+
if not result:
|
|
551
|
+
for e in result.errors:
|
|
552
|
+
print(f"{e['itemId']}: {e['error']}")
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
# Example 2: Rename a data set and update its dataSourceKey
|
|
557
|
+
changes = [
|
|
558
|
+
{"id": "your-cell-data-guid-here", "field": "name", "value": "Formation_Run_42"},
|
|
559
|
+
{"id": "your-cell-data-guid-here", "field": "dataSourceKey", "value": "Formation"},
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
result = api.write_data_metadata(changes=changes)
|
|
563
|
+
assert result.success, result.errors
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
```python
|
|
567
|
+
# Example 3: Update using propertyDefinitionId (advanced)
|
|
568
|
+
changes = [
|
|
569
|
+
{
|
|
570
|
+
"id": "your-cell-data-guid-here",
|
|
571
|
+
"propertyDefinitionId": "your-property-definition-guid",
|
|
572
|
+
"value": "true"
|
|
573
|
+
}
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
result = api.write_data_metadata(changes=changes)
|
|
577
|
+
assert result.success, result.errors
|
|
542
578
|
```
|
|
543
579
|
|
|
544
580
|
## Stitch Data
|
|
@@ -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.
|
|
@@ -856,6 +884,59 @@ class MicantisAPI:
|
|
|
856
884
|
except requests.exceptions.RequestException as e:
|
|
857
885
|
raise self._format_request_error(e)
|
|
858
886
|
|
|
887
|
+
def list_data_metadata_definitions(self, return_format='df', timeout: int = 30):
|
|
888
|
+
"""
|
|
889
|
+
Returns a list of cell data metadata types.
|
|
890
|
+
|
|
891
|
+
Parameters
|
|
892
|
+
----------
|
|
893
|
+
return_format : str, optional
|
|
894
|
+
Return format: 'df' for DataFrame (default), anything else for raw list.
|
|
895
|
+
timeout : int, optional
|
|
896
|
+
Request timeout in seconds. Default: 30.
|
|
897
|
+
"""
|
|
898
|
+
try:
|
|
899
|
+
return self._retry_on_auth_failure(
|
|
900
|
+
self._list_data_metadata_definitions, return_format, timeout
|
|
901
|
+
)
|
|
902
|
+
except Exception as e:
|
|
903
|
+
print(f"⚠️ list_data_metadata_definitions failed: {e}")
|
|
904
|
+
return None
|
|
905
|
+
|
|
906
|
+
def _list_data_metadata_definitions(self, return_format: str, timeout: int):
|
|
907
|
+
"""
|
|
908
|
+
Internal helper for list_data_metadata_definitions. Does not handle retries or printing.
|
|
909
|
+
|
|
910
|
+
Raises
|
|
911
|
+
------
|
|
912
|
+
RuntimeError
|
|
913
|
+
If the request fails or authentication is missing.
|
|
914
|
+
"""
|
|
915
|
+
if not self.headers:
|
|
916
|
+
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
917
|
+
|
|
918
|
+
self._refresh_token_if_needed()
|
|
919
|
+
|
|
920
|
+
try:
|
|
921
|
+
# Make the GET request
|
|
922
|
+
response = self.session.get(
|
|
923
|
+
f"{self.service_url}/publicapi/v1/metadata/list/data",
|
|
924
|
+
headers=self.headers,
|
|
925
|
+
timeout=timeout,
|
|
926
|
+
)
|
|
927
|
+
response.raise_for_status()
|
|
928
|
+
|
|
929
|
+
item = response.json()
|
|
930
|
+
if return_format == 'df':
|
|
931
|
+
df = pd.DataFrame(item)
|
|
932
|
+
return df
|
|
933
|
+
|
|
934
|
+
else:
|
|
935
|
+
return item
|
|
936
|
+
|
|
937
|
+
except requests.exceptions.RequestException as e:
|
|
938
|
+
raise self._format_request_error(e)
|
|
939
|
+
|
|
859
940
|
def get_cell_metadata(self, cell_ids: list, metadata: list = None, return_images: bool = False, timeout: int = 30):
|
|
860
941
|
"""
|
|
861
942
|
Fetch metadata for specific cells, using either metadata names or IDs.
|
|
@@ -967,30 +1048,74 @@ class MicantisAPI:
|
|
|
967
1048
|
except requests.exceptions.RequestException as e:
|
|
968
1049
|
raise self._format_request_error(e)
|
|
969
1050
|
|
|
1051
|
+
# Built-in fields that the public /cells/updatemetadata endpoint understands directly
|
|
1052
|
+
# (i.e. mapped to an OrderByColumn on the server side, not to a UserPropertyDefinition).
|
|
1053
|
+
_BUILTIN_CELL_METADATA_FIELDS = {"dataSourceKey", "name"}
|
|
1054
|
+
|
|
1055
|
+
# Built-in fields that the public /data/updatemetadata endpoint understands directly.
|
|
1056
|
+
_BUILTIN_DATA_METADATA_FIELDS = {"dataSourceKey", "name"}
|
|
1057
|
+
|
|
970
1058
|
def write_cell_metadata(self, changes: list, timeout: int = 30):
|
|
971
1059
|
"""
|
|
972
1060
|
Wrapper for POST /cells/updatemetadata endpoint.
|
|
973
|
-
|
|
1061
|
+
|
|
1062
|
+
Each change targets either a built-in field (supported: "name",
|
|
1063
|
+
"dataSourceKey") or a user-defined metadata property. For user-defined
|
|
1064
|
+
properties, pass either a human-readable 'field' name (resolved via
|
|
1065
|
+
metadata definitions) or a 'propertyDefinitionId' UUID.
|
|
1066
|
+
|
|
1067
|
+
Built-in field names take precedence over user-defined names; if you have a
|
|
1068
|
+
user-defined property literally named "name" or "dataSourceKey", use
|
|
1069
|
+
propertyDefinitionId to disambiguate.
|
|
1070
|
+
|
|
1071
|
+
Renaming a cell ("name" field) enforces name-uniqueness and a character
|
|
1072
|
+
whitelist (letters, digits, `_ - . ( ) space`); renames that fail these
|
|
1073
|
+
checks return an error without applying the change.
|
|
1074
|
+
|
|
1075
|
+
**Per-entity atomicity:** all changes targeting the same cell commit or
|
|
1076
|
+
rollback together. Across cells, each entity is independent — if one
|
|
1077
|
+
cell's changes fail, others still persist. The result indicates which
|
|
1078
|
+
cells failed; successful cells' changes are already saved.
|
|
974
1079
|
|
|
975
1080
|
Automatically reauthenticates if the session is expired or missing.
|
|
976
|
-
Prints
|
|
1081
|
+
Prints a message and returns None if the network request fails entirely.
|
|
977
1082
|
|
|
978
1083
|
Parameters
|
|
979
1084
|
----------
|
|
980
1085
|
changes : list of dict
|
|
981
1086
|
A list of metadata change dictionaries. Each dictionary must contain:
|
|
982
|
-
- "id": str
|
|
983
|
-
-
|
|
984
|
-
|
|
985
|
-
|
|
1087
|
+
- "id": str (cell test GUID)
|
|
1088
|
+
- One of:
|
|
1089
|
+
- "field": "name" or "dataSourceKey" (built-in fields)
|
|
1090
|
+
- "field": "<metadata name>" (user-defined, e.g. "Weight (g)")
|
|
1091
|
+
- "propertyDefinitionId": "<UUID>" (user-defined)
|
|
1092
|
+
- "value": any (will be cast to str). For multi-valued CellTest
|
|
1093
|
+
DataSourceKeys, pass a comma-separated string (e.g. "Formation,CycleLife").
|
|
986
1094
|
|
|
987
1095
|
timeout : int, optional
|
|
988
1096
|
Timeout in seconds for the request. Default is 30.
|
|
989
1097
|
|
|
990
1098
|
Returns
|
|
991
1099
|
-------
|
|
992
|
-
|
|
993
|
-
|
|
1100
|
+
BatchUpdateResult or None
|
|
1101
|
+
A `BatchUpdateResult` whose `.success` is True iff every entity
|
|
1102
|
+
committed (HTTP 204). On partial (207) or full failure (400), the
|
|
1103
|
+
result is falsy and `.errors` lists the per-entity failures as
|
|
1104
|
+
dicts with keys `itemId` and `error`. Returns None on a network-
|
|
1105
|
+
level error.
|
|
1106
|
+
|
|
1107
|
+
Examples
|
|
1108
|
+
--------
|
|
1109
|
+
>>> result = api.write_cell_metadata([
|
|
1110
|
+
... {"id": cell_id, "field": "name", "value": "A-EVE-15P-Cell-04"},
|
|
1111
|
+
... {"id": cell_id, "field": "dataSourceKey", "value": "Formation"},
|
|
1112
|
+
... {"id": cell_id, "field": "Technician", "value": "Alice"},
|
|
1113
|
+
... ])
|
|
1114
|
+
>>> if result:
|
|
1115
|
+
... print("all good")
|
|
1116
|
+
... else:
|
|
1117
|
+
... for e in result.errors:
|
|
1118
|
+
... print(f"{e['itemId']}: {e['error']}")
|
|
994
1119
|
"""
|
|
995
1120
|
try:
|
|
996
1121
|
return self._retry_on_auth_failure(self._write_cell_metadata, changes, timeout)
|
|
@@ -1012,8 +1137,12 @@ class MicantisAPI:
|
|
|
1012
1137
|
|
|
1013
1138
|
self._refresh_token_if_needed()
|
|
1014
1139
|
|
|
1015
|
-
#
|
|
1016
|
-
|
|
1140
|
+
# Only look up user-property definitions if at least one change uses a 'field' that
|
|
1141
|
+
# isn't a built-in (lookup is a separate round-trip and unnecessary otherwise).
|
|
1142
|
+
needs_lookup = any(
|
|
1143
|
+
"field" in change and change["field"] not in self._BUILTIN_CELL_METADATA_FIELDS
|
|
1144
|
+
for change in changes
|
|
1145
|
+
)
|
|
1017
1146
|
if needs_lookup:
|
|
1018
1147
|
metadata_df = self._list_cell_metadata_definitions('df', timeout)
|
|
1019
1148
|
name_to_id = dict(zip(metadata_df['name'], metadata_df['id']))
|
|
@@ -1022,21 +1151,21 @@ class MicantisAPI:
|
|
|
1022
1151
|
|
|
1023
1152
|
formatted_changes = []
|
|
1024
1153
|
for change in changes:
|
|
1025
|
-
|
|
1026
|
-
|
|
1154
|
+
item = {"id": change["id"], "value": str(change["value"])}
|
|
1155
|
+
|
|
1156
|
+
if "field" in change and change["field"] in self._BUILTIN_CELL_METADATA_FIELDS:
|
|
1157
|
+
item["field"] = change["field"]
|
|
1158
|
+
elif "propertyDefinitionId" in change:
|
|
1159
|
+
item["propertyDefinitionId"] = change["propertyDefinitionId"]
|
|
1027
1160
|
elif "field" in change:
|
|
1028
1161
|
field_name = change["field"]
|
|
1029
1162
|
if field_name not in name_to_id:
|
|
1030
1163
|
raise RuntimeError(f"Field name '{field_name}' not found in metadata definitions.")
|
|
1031
|
-
|
|
1164
|
+
item["propertyDefinitionId"] = name_to_id[field_name]
|
|
1032
1165
|
else:
|
|
1033
1166
|
raise RuntimeError("Each change must include either 'field' or 'propertyDefinitionId'.")
|
|
1034
1167
|
|
|
1035
|
-
formatted_changes.append(
|
|
1036
|
-
"id": change["id"],
|
|
1037
|
-
"propertyDefinitionId": property_id,
|
|
1038
|
-
"value": str(change["value"])
|
|
1039
|
-
})
|
|
1168
|
+
formatted_changes.append(item)
|
|
1040
1169
|
|
|
1041
1170
|
payload = {"changes": formatted_changes}
|
|
1042
1171
|
|
|
@@ -1050,9 +1179,144 @@ class MicantisAPI:
|
|
|
1050
1179
|
|
|
1051
1180
|
if response.status_code == 204:
|
|
1052
1181
|
print("✅ Metadata update successful!")
|
|
1053
|
-
return True
|
|
1182
|
+
return BatchUpdateResult(success=True)
|
|
1183
|
+
if response.status_code in (207, 400):
|
|
1184
|
+
body = response.json()
|
|
1185
|
+
errors = body.get("errors", []) if isinstance(body, dict) else []
|
|
1186
|
+
if response.status_code == 207:
|
|
1187
|
+
print(f"⚠️ Metadata update partial: {len(errors)} of {len(formatted_changes)} entities failed.")
|
|
1188
|
+
else:
|
|
1189
|
+
print(f"⚠️ Metadata update failed for all entities.")
|
|
1190
|
+
return BatchUpdateResult(success=False, errors=errors)
|
|
1191
|
+
raise RuntimeError(f"Metadata update failed: {response.status_code} - {response.text}")
|
|
1192
|
+
|
|
1193
|
+
except requests.exceptions.RequestException as e:
|
|
1194
|
+
raise self._format_request_error(e)
|
|
1195
|
+
|
|
1196
|
+
def write_data_metadata(self, changes: list, timeout: int = 30):
|
|
1197
|
+
"""
|
|
1198
|
+
Wrapper for POST /data/updatemetadata endpoint.
|
|
1199
|
+
|
|
1200
|
+
Each change targets either a built-in field (supported: "name",
|
|
1201
|
+
"dataSourceKey") or a user-defined metadata property. For user-defined
|
|
1202
|
+
properties, pass either a human-readable 'field' name (resolved via
|
|
1203
|
+
metadata definitions) or a 'propertyDefinitionId' UUID.
|
|
1204
|
+
|
|
1205
|
+
Built-in field names take precedence over user-defined names; if you have a
|
|
1206
|
+
user-defined property literally named "name" or "dataSourceKey", use
|
|
1207
|
+
propertyDefinitionId to disambiguate.
|
|
1208
|
+
|
|
1209
|
+
Renaming a data set ("name" field) enforces name-uniqueness; the change is
|
|
1210
|
+
rejected if another data set already has the target name. The owning
|
|
1211
|
+
CellTest's DataSourceKey is updated automatically to match the new name
|
|
1212
|
+
where applicable.
|
|
1213
|
+
|
|
1214
|
+
**Per-entity atomicity:** all changes targeting the same data set commit
|
|
1215
|
+
or rollback together. Across data sets, each entity is independent — if
|
|
1216
|
+
one data set's changes fail, others still persist. The result indicates
|
|
1217
|
+
which data sets failed; successful ones are already saved.
|
|
1218
|
+
|
|
1219
|
+
Automatically reauthenticates if the session is expired or missing.
|
|
1220
|
+
Prints a message and returns None if the network request fails entirely.
|
|
1221
|
+
|
|
1222
|
+
Parameters
|
|
1223
|
+
----------
|
|
1224
|
+
changes : list of dict
|
|
1225
|
+
A list of metadata change dictionaries. Each dictionary must contain:
|
|
1226
|
+
- "id": str (cell data GUID)
|
|
1227
|
+
- One of:
|
|
1228
|
+
- "field": "name" or "dataSourceKey" (built-in fields)
|
|
1229
|
+
- "field": "<metadata name>" (user-defined, e.g. "Active Cycle Test?")
|
|
1230
|
+
- "propertyDefinitionId": "<UUID>" (user-defined)
|
|
1231
|
+
- "value": any (will be cast to str)
|
|
1232
|
+
|
|
1233
|
+
timeout : int, optional
|
|
1234
|
+
Timeout in seconds for the request. Default is 30.
|
|
1235
|
+
|
|
1236
|
+
Returns
|
|
1237
|
+
-------
|
|
1238
|
+
BatchUpdateResult or None
|
|
1239
|
+
A `BatchUpdateResult` whose `.success` is True iff every entity
|
|
1240
|
+
committed (HTTP 204). On partial (207) or full failure (400), the
|
|
1241
|
+
result is falsy and `.errors` lists the per-entity failures as
|
|
1242
|
+
dicts with keys `itemId` and `error`. Returns None on a network-
|
|
1243
|
+
level error.
|
|
1244
|
+
|
|
1245
|
+
Examples
|
|
1246
|
+
--------
|
|
1247
|
+
>>> result = api.write_data_metadata([
|
|
1248
|
+
... {"id": cell_data_id, "field": "name", "value": "Formation_Run_42"},
|
|
1249
|
+
... {"id": cell_data_id, "field": "dataSourceKey", "value": "Formation"},
|
|
1250
|
+
... {"id": cell_data_id, "field": "Active Cycle Test?", "value": True},
|
|
1251
|
+
... ])
|
|
1252
|
+
>>> if not result:
|
|
1253
|
+
... for e in result.errors:
|
|
1254
|
+
... print(f"{e['itemId']}: {e['error']}")
|
|
1255
|
+
"""
|
|
1256
|
+
try:
|
|
1257
|
+
return self._retry_on_auth_failure(self._write_data_metadata, changes, timeout)
|
|
1258
|
+
except Exception as e:
|
|
1259
|
+
print(f"⚠️ write_data_metadata failed: {e}")
|
|
1260
|
+
return None
|
|
1261
|
+
|
|
1262
|
+
def _write_data_metadata(self, changes: list, timeout: int):
|
|
1263
|
+
if not self.headers:
|
|
1264
|
+
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
1265
|
+
|
|
1266
|
+
self._refresh_token_if_needed()
|
|
1267
|
+
|
|
1268
|
+
# Only look up user-property definitions if at least one change uses a 'field' that
|
|
1269
|
+
# isn't a built-in (lookup is a separate round-trip and unnecessary otherwise).
|
|
1270
|
+
needs_lookup = any(
|
|
1271
|
+
"field" in change and change["field"] not in self._BUILTIN_DATA_METADATA_FIELDS
|
|
1272
|
+
for change in changes
|
|
1273
|
+
)
|
|
1274
|
+
if needs_lookup:
|
|
1275
|
+
metadata_df = self._list_data_metadata_definitions('df', timeout)
|
|
1276
|
+
name_to_id = dict(zip(metadata_df['name'], metadata_df['id']))
|
|
1277
|
+
else:
|
|
1278
|
+
name_to_id = {}
|
|
1279
|
+
|
|
1280
|
+
formatted_changes = []
|
|
1281
|
+
for change in changes:
|
|
1282
|
+
item = {"id": change["id"], "value": str(change["value"])}
|
|
1283
|
+
|
|
1284
|
+
if "field" in change and change["field"] in self._BUILTIN_DATA_METADATA_FIELDS:
|
|
1285
|
+
item["field"] = change["field"]
|
|
1286
|
+
elif "propertyDefinitionId" in change:
|
|
1287
|
+
item["propertyDefinitionId"] = change["propertyDefinitionId"]
|
|
1288
|
+
elif "field" in change:
|
|
1289
|
+
field_name = change["field"]
|
|
1290
|
+
if field_name not in name_to_id:
|
|
1291
|
+
raise RuntimeError(f"Field name '{field_name}' not found in metadata definitions.")
|
|
1292
|
+
item["propertyDefinitionId"] = name_to_id[field_name]
|
|
1054
1293
|
else:
|
|
1055
|
-
raise RuntimeError(
|
|
1294
|
+
raise RuntimeError("Each change must include either 'field' or 'propertyDefinitionId'.")
|
|
1295
|
+
|
|
1296
|
+
formatted_changes.append(item)
|
|
1297
|
+
|
|
1298
|
+
payload = {"changes": formatted_changes}
|
|
1299
|
+
|
|
1300
|
+
try:
|
|
1301
|
+
response = self.session.post(
|
|
1302
|
+
f"{self.service_url}/publicapi/v1/data/updatemetadata",
|
|
1303
|
+
headers=self.headers,
|
|
1304
|
+
json=payload,
|
|
1305
|
+
timeout=timeout
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
if response.status_code == 204:
|
|
1309
|
+
print("✅ Data metadata update successful!")
|
|
1310
|
+
return BatchUpdateResult(success=True)
|
|
1311
|
+
if response.status_code in (207, 400):
|
|
1312
|
+
body = response.json()
|
|
1313
|
+
errors = body.get("errors", []) if isinstance(body, dict) else []
|
|
1314
|
+
if response.status_code == 207:
|
|
1315
|
+
print(f"⚠️ Data metadata update partial: {len(errors)} of {len(formatted_changes)} entities failed.")
|
|
1316
|
+
else:
|
|
1317
|
+
print(f"⚠️ Data metadata update failed for all entities.")
|
|
1318
|
+
return BatchUpdateResult(success=False, errors=errors)
|
|
1319
|
+
raise RuntimeError(f"Data metadata update failed: {response.status_code} - {response.text}")
|
|
1056
1320
|
|
|
1057
1321
|
except requests.exceptions.RequestException as e:
|
|
1058
1322
|
raise self._format_request_error(e)
|
|
@@ -2101,9 +2365,9 @@ class MicantisAPI:
|
|
|
2101
2365
|
def edit_logger_step(line=None, lines=None, columns=None, name=None):
|
|
2102
2366
|
"""Build an edit-logger-timestep action for clean_data (logger-data analog of modify_step/bulk_modify).
|
|
2103
2367
|
|
|
2104
|
-
Edit
|
|
2105
|
-
``
|
|
2106
|
-
|
|
2368
|
+
Edit column values by 0-based column index on rows matched by ``line`` and/or
|
|
2369
|
+
``lines`` (union). Use on DataLoggerData sources only; for cycle-tester data
|
|
2370
|
+
use modify_step / bulk_modify.
|
|
2107
2371
|
|
|
2108
2372
|
Parameters
|
|
2109
2373
|
----------
|
|
@@ -2112,7 +2376,8 @@ class MicantisAPI:
|
|
|
2112
2376
|
lines : list of (int, int) tuples, optional
|
|
2113
2377
|
Line number ranges to edit, e.g. [(100, 200)]. End may be None for open-ended.
|
|
2114
2378
|
columns : dict of {int: float}
|
|
2115
|
-
Column edits as {column_index: value}. Column index is 0-based
|
|
2379
|
+
Column edits as {column_index: value}. Column index is 0-based and
|
|
2380
|
+
must refer to a valid column in the source ImportDefinition.
|
|
2116
2381
|
name : str, optional
|
|
2117
2382
|
Descriptive name for this action.
|
|
2118
2383
|
"""
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: micantis
|
|
3
|
+
Version: 1.2.4
|
|
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,10 +542,59 @@ changes = [
|
|
|
518
542
|
}
|
|
519
543
|
]
|
|
520
544
|
|
|
521
|
-
api.write_cell_metadata(changes=changes)
|
|
545
|
+
result = api.write_cell_metadata(changes=changes)
|
|
546
|
+
assert result.success, result.errors
|
|
547
|
+
```
|
|
522
548
|
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
|
|
551
|
+
## Write Data Metadata
|
|
552
|
+
Assign or update metadata on a **data set** (CellData record), the same way as cells, using either:
|
|
553
|
+
- the built-in field `"name"` (renames the data set) or `"dataSourceKey"`
|
|
554
|
+
- the human-readable field name of a user-defined property (e.g., "Active Cycle Test?")
|
|
555
|
+
- or the internal propertyDefinitionId (UUID)
|
|
556
|
+
|
|
557
|
+
User-defined property names are resolved against the data-set metadata definitions, which you can list with `api.list_data_metadata_definitions()`.
|
|
558
|
+
|
|
559
|
+
**Per-entity atomic:** all changes targeting a single data set commit or rollback together; other data sets are unaffected. Returns a `BatchUpdateResult` with `.success` and `.errors`, identical to `write_cell_metadata`.
|
|
560
|
+
|
|
561
|
+
#### 📘 Examples
|
|
562
|
+
|
|
563
|
+
```python
|
|
564
|
+
# Example 1: Set a user-defined property on specific data sets
|
|
565
|
+
changes = [
|
|
566
|
+
{"id": "your-cell-data-guid-here", "field": "Active Cycle Test?", "value": True},
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
result = api.write_data_metadata(changes=changes)
|
|
570
|
+
if not result:
|
|
571
|
+
for e in result.errors:
|
|
572
|
+
print(f"{e['itemId']}: {e['error']}")
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
```python
|
|
576
|
+
# Example 2: Rename a data set and update its dataSourceKey
|
|
577
|
+
changes = [
|
|
578
|
+
{"id": "your-cell-data-guid-here", "field": "name", "value": "Formation_Run_42"},
|
|
579
|
+
{"id": "your-cell-data-guid-here", "field": "dataSourceKey", "value": "Formation"},
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
result = api.write_data_metadata(changes=changes)
|
|
583
|
+
assert result.success, result.errors
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```python
|
|
587
|
+
# Example 3: Update using propertyDefinitionId (advanced)
|
|
588
|
+
changes = [
|
|
589
|
+
{
|
|
590
|
+
"id": "your-cell-data-guid-here",
|
|
591
|
+
"propertyDefinitionId": "your-property-definition-guid",
|
|
592
|
+
"value": "true"
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
result = api.write_data_metadata(changes=changes)
|
|
597
|
+
assert result.success, result.errors
|
|
525
598
|
```
|
|
526
599
|
|
|
527
600
|
## Stitch Data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "micantis"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.4"
|
|
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,174 @@
|
|
|
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
|
+
- User-property name resolution and propertyDefinitionId pass-through
|
|
10
|
+
|
|
11
|
+
Run: pytest
|
|
12
|
+
"""
|
|
13
|
+
from unittest.mock import MagicMock
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from micantis.api_wrapper import MicantisAPI, BatchUpdateResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_api():
|
|
21
|
+
api = MicantisAPI.__new__(MicantisAPI)
|
|
22
|
+
api.service_url = "https://fake.test"
|
|
23
|
+
api.headers = {"Authorization": "Bearer fake"}
|
|
24
|
+
api.session = MagicMock()
|
|
25
|
+
api._refresh_token_if_needed = lambda: None
|
|
26
|
+
return api
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _mock_response(status_code, body=None):
|
|
30
|
+
resp = MagicMock()
|
|
31
|
+
resp.status_code = status_code
|
|
32
|
+
resp.json = MagicMock(return_value=body or {})
|
|
33
|
+
resp.text = str(body) if body else ""
|
|
34
|
+
return resp
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_batch_update_result_truthiness():
|
|
38
|
+
r_ok = BatchUpdateResult(success=True)
|
|
39
|
+
assert bool(r_ok) is True
|
|
40
|
+
assert r_ok.errors == []
|
|
41
|
+
assert "success=True" in repr(r_ok)
|
|
42
|
+
|
|
43
|
+
r_fail = BatchUpdateResult(success=False, errors=[{"itemId": "x", "error": "e"}])
|
|
44
|
+
assert bool(r_fail) is False
|
|
45
|
+
assert len(r_fail.errors) == 1
|
|
46
|
+
assert r_fail.errors[0]["itemId"] == "x"
|
|
47
|
+
assert "success=False" in repr(r_fail)
|
|
48
|
+
print("PASS: BatchUpdateResult truthiness + attributes")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_write_cell_204_returns_success():
|
|
52
|
+
api = _make_api()
|
|
53
|
+
api.session.post.return_value = _mock_response(204)
|
|
54
|
+
result = api._write_cell_metadata(
|
|
55
|
+
[{"id": "a", "field": "name", "value": "new"}], timeout=5,
|
|
56
|
+
)
|
|
57
|
+
assert isinstance(result, BatchUpdateResult)
|
|
58
|
+
assert result.success is True
|
|
59
|
+
assert bool(result) is True
|
|
60
|
+
assert result.errors == []
|
|
61
|
+
print("PASS: 204 → BatchUpdateResult(success=True)")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_write_cell_207_returns_partial():
|
|
65
|
+
api = _make_api()
|
|
66
|
+
body = {"errors": [{"itemId": "a", "error": "name exists"}]}
|
|
67
|
+
api.session.post.return_value = _mock_response(207, body)
|
|
68
|
+
result = api._write_cell_metadata(
|
|
69
|
+
[
|
|
70
|
+
{"id": "a", "field": "name", "value": "collision"},
|
|
71
|
+
{"id": "b", "field": "name", "value": "fresh"},
|
|
72
|
+
],
|
|
73
|
+
timeout=5,
|
|
74
|
+
)
|
|
75
|
+
assert isinstance(result, BatchUpdateResult)
|
|
76
|
+
assert result.success is False
|
|
77
|
+
assert bool(result) is False
|
|
78
|
+
assert len(result.errors) == 1
|
|
79
|
+
assert result.errors[0]["itemId"] == "a"
|
|
80
|
+
assert "name exists" in result.errors[0]["error"]
|
|
81
|
+
print("PASS: 207 → BatchUpdateResult(success=False, errors=[...])")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_write_cell_400_returns_failure():
|
|
85
|
+
api = _make_api()
|
|
86
|
+
body = {"errors": [{"itemId": "a", "error": "name exists"}]}
|
|
87
|
+
api.session.post.return_value = _mock_response(400, body)
|
|
88
|
+
result = api._write_cell_metadata(
|
|
89
|
+
[{"id": "a", "field": "name", "value": "collision"}], timeout=5,
|
|
90
|
+
)
|
|
91
|
+
assert isinstance(result, BatchUpdateResult)
|
|
92
|
+
assert result.success is False
|
|
93
|
+
assert len(result.errors) == 1
|
|
94
|
+
print("PASS: 400 → BatchUpdateResult(success=False, errors=[...])")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_write_data_204_returns_success():
|
|
98
|
+
api = _make_api()
|
|
99
|
+
api.session.post.return_value = _mock_response(204)
|
|
100
|
+
result = api._write_data_metadata(
|
|
101
|
+
[{"id": "x", "field": "name", "value": "new"}], timeout=5,
|
|
102
|
+
)
|
|
103
|
+
assert isinstance(result, BatchUpdateResult)
|
|
104
|
+
assert result.success is True
|
|
105
|
+
print("PASS: data/updatemetadata 204 → success")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_write_data_207_returns_partial():
|
|
109
|
+
api = _make_api()
|
|
110
|
+
body = {"errors": [{"itemId": "x", "error": "name exists"}]}
|
|
111
|
+
api.session.post.return_value = _mock_response(207, body)
|
|
112
|
+
result = api._write_data_metadata(
|
|
113
|
+
[{"id": "x", "field": "name", "value": "collision"}], timeout=5,
|
|
114
|
+
)
|
|
115
|
+
assert result.success is False
|
|
116
|
+
assert result.errors[0]["itemId"] == "x"
|
|
117
|
+
print("PASS: data/updatemetadata 207 → partial")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_write_data_unknown_property_name_raises():
|
|
121
|
+
# A non-built-in field is treated as a user-property name and resolved against the
|
|
122
|
+
# CellData metadata definitions; a name with no matching definition raises.
|
|
123
|
+
api = _make_api()
|
|
124
|
+
api._list_data_metadata_definitions = lambda return_format, timeout: pd.DataFrame(
|
|
125
|
+
{"name": ["Active Cycle Test?"], "id": ["def-1"]}
|
|
126
|
+
)
|
|
127
|
+
try:
|
|
128
|
+
api._write_data_metadata(
|
|
129
|
+
[{"id": "x", "field": "Nonexistent Property", "value": "foo"}], timeout=5,
|
|
130
|
+
)
|
|
131
|
+
except RuntimeError as e:
|
|
132
|
+
assert "Nonexistent Property" in str(e)
|
|
133
|
+
assert "not found in metadata definitions" in str(e)
|
|
134
|
+
print("PASS: data/updatemetadata raises on unknown user-property name")
|
|
135
|
+
return
|
|
136
|
+
raise AssertionError("expected RuntimeError on unknown property name")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_write_data_resolves_property_name():
|
|
140
|
+
# A user-property name is resolved to its definition ID and sent as propertyDefinitionId.
|
|
141
|
+
api = _make_api()
|
|
142
|
+
api._list_data_metadata_definitions = lambda return_format, timeout: pd.DataFrame(
|
|
143
|
+
{"name": ["Active Cycle Test?"], "id": ["def-1"]}
|
|
144
|
+
)
|
|
145
|
+
api.session.post.return_value = _mock_response(204)
|
|
146
|
+
result = api._write_data_metadata(
|
|
147
|
+
[{"id": "x", "field": "Active Cycle Test?", "value": True}], timeout=5,
|
|
148
|
+
)
|
|
149
|
+
assert result.success is True
|
|
150
|
+
sent = api.session.post.call_args.kwargs["json"]["changes"][0]
|
|
151
|
+
assert sent == {"id": "x", "value": "True", "propertyDefinitionId": "def-1"}
|
|
152
|
+
print("PASS: data/updatemetadata resolves user-property name → propertyDefinitionId")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_write_data_accepts_property_definition_id():
|
|
156
|
+
# A propertyDefinitionId is forwarded directly, with no definitions lookup.
|
|
157
|
+
api = _make_api()
|
|
158
|
+
api._list_data_metadata_definitions = MagicMock(
|
|
159
|
+
side_effect=AssertionError("should not look up definitions when an id is supplied")
|
|
160
|
+
)
|
|
161
|
+
api.session.post.return_value = _mock_response(204)
|
|
162
|
+
result = api._write_data_metadata(
|
|
163
|
+
[{"id": "x", "propertyDefinitionId": "def-9", "value": "true"}], timeout=5,
|
|
164
|
+
)
|
|
165
|
+
assert result.success is True
|
|
166
|
+
sent = api.session.post.call_args.kwargs["json"]["changes"][0]
|
|
167
|
+
assert sent == {"id": "x", "value": "true", "propertyDefinitionId": "def-9"}
|
|
168
|
+
print("PASS: data/updatemetadata forwards propertyDefinitionId directly")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_name_is_accepted_as_builtin():
|
|
172
|
+
# Ensure the previously-missing client-side allowlist now accepts "name"
|
|
173
|
+
assert "name" in MicantisAPI._BUILTIN_CELL_METADATA_FIELDS
|
|
174
|
+
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
|