micantis 1.2.3__tar.gz → 1.2.5__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.3 → micantis-1.2.5}/PKG-INFO +59 -11
- {micantis-1.2.3 → micantis-1.2.5}/README.md +58 -10
- {micantis-1.2.3 → micantis-1.2.5}/micantis/api_wrapper.py +433 -34
- {micantis-1.2.3 → micantis-1.2.5}/micantis.egg-info/PKG-INFO +59 -11
- {micantis-1.2.3 → micantis-1.2.5}/micantis.egg-info/SOURCES.txt +2 -1
- {micantis-1.2.3 → micantis-1.2.5}/pyproject.toml +1 -1
- {micantis-1.2.3 → micantis-1.2.5}/tests/test_batch_update_result.py +46 -7
- micantis-1.2.5/tests/test_upload_execution_artifact.py +110 -0
- {micantis-1.2.3 → micantis-1.2.5}/LICENSE +0 -0
- {micantis-1.2.3 → micantis-1.2.5}/micantis/__init__.py +0 -0
- {micantis-1.2.3 → micantis-1.2.5}/micantis/utils.py +0 -0
- {micantis-1.2.3 → micantis-1.2.5}/micantis.egg-info/dependency_links.txt +0 -0
- {micantis-1.2.3 → micantis-1.2.5}/micantis.egg-info/requires.txt +0 -0
- {micantis-1.2.3 → micantis-1.2.5}/micantis.egg-info/top_level.txt +0 -0
- {micantis-1.2.3 → micantis-1.2.5}/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.5
|
|
4
4
|
Summary: Package to simplify Micantis API usage
|
|
5
5
|
Author-email: Mykela DeLuca <mykela.deluca@micantis.io>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -548,6 +548,55 @@ assert result.success, result.errors
|
|
|
548
548
|
|
|
549
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
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
|
|
598
|
+
```
|
|
599
|
+
|
|
551
600
|
## Stitch Data
|
|
552
601
|
Combine multiple data sets into a single stitched data set. This is useful for creating continuous test data from multiple separate test runs.
|
|
553
602
|
|
|
@@ -964,14 +1013,16 @@ for group in dupes:
|
|
|
964
1013
|
Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python script execution. Called from within a Python script running in the Micantis environment.
|
|
965
1014
|
|
|
966
1015
|
#### Parameters
|
|
967
|
-
- `execution_id`: **str (
|
|
968
|
-
The execution ID (GUID) of the running or completed execution
|
|
1016
|
+
- `execution_id`: **str (optional inside cloud executions)**
|
|
1017
|
+
The execution ID (GUID) of the running or completed execution. Defaults to the `WORKBOOK_EXECUTION_ID` environment variable, which cloud executions always set; required when running elsewhere. A non-GUID first positional argument is treated as `file_path`
|
|
969
1018
|
- `file_path`: **str (required)**
|
|
970
1019
|
Local path to the file to upload. Maximum size: 50 MB
|
|
971
1020
|
- `title`: **str (required)**
|
|
972
1021
|
Human-readable title for the artifact
|
|
973
1022
|
- `filename`: **str (optional)**
|
|
974
1023
|
Override the filename stored on the server. If not provided, derived from the title and file extension
|
|
1024
|
+
- `mime`: **str (optional)**
|
|
1025
|
+
Content type stored for the artifact (e.g. `image/png`). If not provided, guessed from the file extension, falling back to `application/octet-stream`
|
|
975
1026
|
|
|
976
1027
|
#### Returns
|
|
977
1028
|
- Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
|
|
@@ -979,22 +1030,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
|
|
|
979
1030
|
#### 📘 Examples
|
|
980
1031
|
|
|
981
1032
|
```python
|
|
982
|
-
#
|
|
983
|
-
result = api.upload_execution_artifact(
|
|
984
|
-
execution_id="your-execution-guid",
|
|
985
|
-
file_path="voltage_plot.png",
|
|
986
|
-
title="Voltage vs Time"
|
|
987
|
-
)
|
|
1033
|
+
# Inside a cloud execution — execution_id is resolved from the environment
|
|
1034
|
+
result = api.upload_execution_artifact("voltage_plot.png", title="Voltage vs Time")
|
|
988
1035
|
print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
|
|
989
1036
|
```
|
|
990
1037
|
|
|
991
1038
|
```python
|
|
992
|
-
#
|
|
1039
|
+
# Explicit execution ID with a custom filename and content type
|
|
993
1040
|
result = api.upload_execution_artifact(
|
|
994
1041
|
execution_id="your-execution-guid",
|
|
995
1042
|
file_path="results.csv",
|
|
996
1043
|
title="Cycle Summary Results",
|
|
997
|
-
filename="cycle_summary_2025.csv"
|
|
1044
|
+
filename="cycle_summary_2025.csv",
|
|
1045
|
+
mime="text/csv"
|
|
998
1046
|
)
|
|
999
1047
|
```
|
|
1000
1048
|
|
|
@@ -528,6 +528,55 @@ assert result.success, result.errors
|
|
|
528
528
|
|
|
529
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
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
|
|
578
|
+
```
|
|
579
|
+
|
|
531
580
|
## Stitch Data
|
|
532
581
|
Combine multiple data sets into a single stitched data set. This is useful for creating continuous test data from multiple separate test runs.
|
|
533
582
|
|
|
@@ -944,14 +993,16 @@ for group in dupes:
|
|
|
944
993
|
Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python script execution. Called from within a Python script running in the Micantis environment.
|
|
945
994
|
|
|
946
995
|
#### Parameters
|
|
947
|
-
- `execution_id`: **str (
|
|
948
|
-
The execution ID (GUID) of the running or completed execution
|
|
996
|
+
- `execution_id`: **str (optional inside cloud executions)**
|
|
997
|
+
The execution ID (GUID) of the running or completed execution. Defaults to the `WORKBOOK_EXECUTION_ID` environment variable, which cloud executions always set; required when running elsewhere. A non-GUID first positional argument is treated as `file_path`
|
|
949
998
|
- `file_path`: **str (required)**
|
|
950
999
|
Local path to the file to upload. Maximum size: 50 MB
|
|
951
1000
|
- `title`: **str (required)**
|
|
952
1001
|
Human-readable title for the artifact
|
|
953
1002
|
- `filename`: **str (optional)**
|
|
954
1003
|
Override the filename stored on the server. If not provided, derived from the title and file extension
|
|
1004
|
+
- `mime`: **str (optional)**
|
|
1005
|
+
Content type stored for the artifact (e.g. `image/png`). If not provided, guessed from the file extension, falling back to `application/octet-stream`
|
|
955
1006
|
|
|
956
1007
|
#### Returns
|
|
957
1008
|
- Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
|
|
@@ -959,22 +1010,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
|
|
|
959
1010
|
#### 📘 Examples
|
|
960
1011
|
|
|
961
1012
|
```python
|
|
962
|
-
#
|
|
963
|
-
result = api.upload_execution_artifact(
|
|
964
|
-
execution_id="your-execution-guid",
|
|
965
|
-
file_path="voltage_plot.png",
|
|
966
|
-
title="Voltage vs Time"
|
|
967
|
-
)
|
|
1013
|
+
# Inside a cloud execution — execution_id is resolved from the environment
|
|
1014
|
+
result = api.upload_execution_artifact("voltage_plot.png", title="Voltage vs Time")
|
|
968
1015
|
print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
|
|
969
1016
|
```
|
|
970
1017
|
|
|
971
1018
|
```python
|
|
972
|
-
#
|
|
1019
|
+
# Explicit execution ID with a custom filename and content type
|
|
973
1020
|
result = api.upload_execution_artifact(
|
|
974
1021
|
execution_id="your-execution-guid",
|
|
975
1022
|
file_path="results.csv",
|
|
976
1023
|
title="Cycle Summary Results",
|
|
977
|
-
filename="cycle_summary_2025.csv"
|
|
1024
|
+
filename="cycle_summary_2025.csv",
|
|
1025
|
+
mime="text/csv"
|
|
978
1026
|
)
|
|
979
1027
|
```
|
|
980
1028
|
|
|
@@ -2,11 +2,15 @@ import requests
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import io
|
|
4
4
|
import json
|
|
5
|
+
import mimetypes
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import time
|
|
9
|
+
import tempfile
|
|
10
|
+
import uuid
|
|
8
11
|
from datetime import datetime, timedelta, timezone
|
|
9
12
|
from typing import Optional
|
|
13
|
+
from urllib.parse import quote
|
|
10
14
|
from msal import PublicClientApplication
|
|
11
15
|
from azure.identity import (
|
|
12
16
|
ManagedIdentityCredential,
|
|
@@ -43,6 +47,26 @@ def _apply_field_overrides(action, voltage, current, charge_capacity,
|
|
|
43
47
|
action[key] = val
|
|
44
48
|
|
|
45
49
|
|
|
50
|
+
def _is_guid(value) -> bool:
|
|
51
|
+
"""True if value parses as a GUID — the format of execution IDs."""
|
|
52
|
+
try:
|
|
53
|
+
uuid.UUID(str(value))
|
|
54
|
+
return True
|
|
55
|
+
except ValueError:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Content types that must not depend on the host OS mime registry
|
|
60
|
+
# (Windows maps .csv to application/vnd.ms-excel, stored as an opaque artifact).
|
|
61
|
+
_MIME_OVERRIDES = {
|
|
62
|
+
".csv": "text/csv",
|
|
63
|
+
".json": "application/json",
|
|
64
|
+
".txt": "text/plain",
|
|
65
|
+
".md": "text/markdown",
|
|
66
|
+
".svg": "image/svg+xml",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
46
70
|
class BatchUpdateResult:
|
|
47
71
|
"""
|
|
48
72
|
Result of a batch metadata update call (`write_cell_metadata` / `write_data_metadata`).
|
|
@@ -884,6 +908,59 @@ class MicantisAPI:
|
|
|
884
908
|
except requests.exceptions.RequestException as e:
|
|
885
909
|
raise self._format_request_error(e)
|
|
886
910
|
|
|
911
|
+
def list_data_metadata_definitions(self, return_format='df', timeout: int = 30):
|
|
912
|
+
"""
|
|
913
|
+
Returns a list of cell data metadata types.
|
|
914
|
+
|
|
915
|
+
Parameters
|
|
916
|
+
----------
|
|
917
|
+
return_format : str, optional
|
|
918
|
+
Return format: 'df' for DataFrame (default), anything else for raw list.
|
|
919
|
+
timeout : int, optional
|
|
920
|
+
Request timeout in seconds. Default: 30.
|
|
921
|
+
"""
|
|
922
|
+
try:
|
|
923
|
+
return self._retry_on_auth_failure(
|
|
924
|
+
self._list_data_metadata_definitions, return_format, timeout
|
|
925
|
+
)
|
|
926
|
+
except Exception as e:
|
|
927
|
+
print(f"⚠️ list_data_metadata_definitions failed: {e}")
|
|
928
|
+
return None
|
|
929
|
+
|
|
930
|
+
def _list_data_metadata_definitions(self, return_format: str, timeout: int):
|
|
931
|
+
"""
|
|
932
|
+
Internal helper for list_data_metadata_definitions. Does not handle retries or printing.
|
|
933
|
+
|
|
934
|
+
Raises
|
|
935
|
+
------
|
|
936
|
+
RuntimeError
|
|
937
|
+
If the request fails or authentication is missing.
|
|
938
|
+
"""
|
|
939
|
+
if not self.headers:
|
|
940
|
+
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
941
|
+
|
|
942
|
+
self._refresh_token_if_needed()
|
|
943
|
+
|
|
944
|
+
try:
|
|
945
|
+
# Make the GET request
|
|
946
|
+
response = self.session.get(
|
|
947
|
+
f"{self.service_url}/publicapi/v1/metadata/list/data",
|
|
948
|
+
headers=self.headers,
|
|
949
|
+
timeout=timeout,
|
|
950
|
+
)
|
|
951
|
+
response.raise_for_status()
|
|
952
|
+
|
|
953
|
+
item = response.json()
|
|
954
|
+
if return_format == 'df':
|
|
955
|
+
df = pd.DataFrame(item)
|
|
956
|
+
return df
|
|
957
|
+
|
|
958
|
+
else:
|
|
959
|
+
return item
|
|
960
|
+
|
|
961
|
+
except requests.exceptions.RequestException as e:
|
|
962
|
+
raise self._format_request_error(e)
|
|
963
|
+
|
|
887
964
|
def get_cell_metadata(self, cell_ids: list, metadata: list = None, return_images: bool = False, timeout: int = 30):
|
|
888
965
|
"""
|
|
889
966
|
Fetch metadata for specific cells, using either metadata names or IDs.
|
|
@@ -1144,9 +1221,14 @@ class MicantisAPI:
|
|
|
1144
1221
|
"""
|
|
1145
1222
|
Wrapper for POST /data/updatemetadata endpoint.
|
|
1146
1223
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1224
|
+
Each change targets either a built-in field (supported: "name",
|
|
1225
|
+
"dataSourceKey") or a user-defined metadata property. For user-defined
|
|
1226
|
+
properties, pass either a human-readable 'field' name (resolved via
|
|
1227
|
+
metadata definitions) or a 'propertyDefinitionId' UUID.
|
|
1228
|
+
|
|
1229
|
+
Built-in field names take precedence over user-defined names; if you have a
|
|
1230
|
+
user-defined property literally named "name" or "dataSourceKey", use
|
|
1231
|
+
propertyDefinitionId to disambiguate.
|
|
1150
1232
|
|
|
1151
1233
|
Renaming a data set ("name" field) enforces name-uniqueness; the change is
|
|
1152
1234
|
rejected if another data set already has the target name. The owning
|
|
@@ -1166,7 +1248,10 @@ class MicantisAPI:
|
|
|
1166
1248
|
changes : list of dict
|
|
1167
1249
|
A list of metadata change dictionaries. Each dictionary must contain:
|
|
1168
1250
|
- "id": str (cell data GUID)
|
|
1169
|
-
-
|
|
1251
|
+
- One of:
|
|
1252
|
+
- "field": "name" or "dataSourceKey" (built-in fields)
|
|
1253
|
+
- "field": "<metadata name>" (user-defined, e.g. "Active Cycle Test?")
|
|
1254
|
+
- "propertyDefinitionId": "<UUID>" (user-defined)
|
|
1170
1255
|
- "value": any (will be cast to str)
|
|
1171
1256
|
|
|
1172
1257
|
timeout : int, optional
|
|
@@ -1184,8 +1269,9 @@ class MicantisAPI:
|
|
|
1184
1269
|
Examples
|
|
1185
1270
|
--------
|
|
1186
1271
|
>>> result = api.write_data_metadata([
|
|
1187
|
-
... {"id": cell_data_id, "field": "name",
|
|
1188
|
-
... {"id": cell_data_id, "field": "dataSourceKey",
|
|
1272
|
+
... {"id": cell_data_id, "field": "name", "value": "Formation_Run_42"},
|
|
1273
|
+
... {"id": cell_data_id, "field": "dataSourceKey", "value": "Formation"},
|
|
1274
|
+
... {"id": cell_data_id, "field": "Active Cycle Test?", "value": True},
|
|
1189
1275
|
... ])
|
|
1190
1276
|
>>> if not result:
|
|
1191
1277
|
... for e in result.errors:
|
|
@@ -1203,20 +1289,35 @@ class MicantisAPI:
|
|
|
1203
1289
|
|
|
1204
1290
|
self._refresh_token_if_needed()
|
|
1205
1291
|
|
|
1292
|
+
# Only look up user-property definitions if at least one change uses a 'field' that
|
|
1293
|
+
# isn't a built-in (lookup is a separate round-trip and unnecessary otherwise).
|
|
1294
|
+
needs_lookup = any(
|
|
1295
|
+
"field" in change and change["field"] not in self._BUILTIN_DATA_METADATA_FIELDS
|
|
1296
|
+
for change in changes
|
|
1297
|
+
)
|
|
1298
|
+
if needs_lookup:
|
|
1299
|
+
metadata_df = self._list_data_metadata_definitions('df', timeout)
|
|
1300
|
+
name_to_id = dict(zip(metadata_df['name'], metadata_df['id']))
|
|
1301
|
+
else:
|
|
1302
|
+
name_to_id = {}
|
|
1303
|
+
|
|
1206
1304
|
formatted_changes = []
|
|
1207
1305
|
for change in changes:
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
"
|
|
1219
|
-
|
|
1306
|
+
item = {"id": change["id"], "value": str(change["value"])}
|
|
1307
|
+
|
|
1308
|
+
if "field" in change and change["field"] in self._BUILTIN_DATA_METADATA_FIELDS:
|
|
1309
|
+
item["field"] = change["field"]
|
|
1310
|
+
elif "propertyDefinitionId" in change:
|
|
1311
|
+
item["propertyDefinitionId"] = change["propertyDefinitionId"]
|
|
1312
|
+
elif "field" in change:
|
|
1313
|
+
field_name = change["field"]
|
|
1314
|
+
if field_name not in name_to_id:
|
|
1315
|
+
raise RuntimeError(f"Field name '{field_name}' not found in metadata definitions.")
|
|
1316
|
+
item["propertyDefinitionId"] = name_to_id[field_name]
|
|
1317
|
+
else:
|
|
1318
|
+
raise RuntimeError("Each change must include either 'field' or 'propertyDefinitionId'.")
|
|
1319
|
+
|
|
1320
|
+
formatted_changes.append(item)
|
|
1220
1321
|
|
|
1221
1322
|
payload = {"changes": formatted_changes}
|
|
1222
1323
|
|
|
@@ -2213,6 +2314,111 @@ class MicantisAPI:
|
|
|
2213
2314
|
cycle, step_kind, step_index)
|
|
2214
2315
|
return action
|
|
2215
2316
|
|
|
2317
|
+
@staticmethod
|
|
2318
|
+
def rename_column(source_column_index, new_name, name=None):
|
|
2319
|
+
"""Build a rename-column action for clean_data (MetadataEdit mode).
|
|
2320
|
+
|
|
2321
|
+
Rename a column's display Name — what graphs, tables, and reports look up
|
|
2322
|
+
by YAxisName. Targets the ORIGINAL source column index; action order does
|
|
2323
|
+
not shift index meaning.
|
|
2324
|
+
|
|
2325
|
+
Parameters
|
|
2326
|
+
----------
|
|
2327
|
+
source_column_index : int
|
|
2328
|
+
0-based index into the ORIGINAL source column list.
|
|
2329
|
+
new_name : str
|
|
2330
|
+
New display name. Preserve exact spacing / punctuation expected by
|
|
2331
|
+
downstream lookups (e.g. "Temp 1 (C)" with two spaces).
|
|
2332
|
+
name : str, optional
|
|
2333
|
+
Descriptive name for this action.
|
|
2334
|
+
"""
|
|
2335
|
+
action = {"$type": "rename",
|
|
2336
|
+
"sourceColumnIndex": int(source_column_index),
|
|
2337
|
+
"newName": new_name}
|
|
2338
|
+
if name is not None:
|
|
2339
|
+
action["name"] = name
|
|
2340
|
+
return action
|
|
2341
|
+
|
|
2342
|
+
@staticmethod
|
|
2343
|
+
def change_column_kind(source_column_index, new_kind, name=None):
|
|
2344
|
+
"""Build a change-column-kind action for clean_data (MetadataEdit mode).
|
|
2345
|
+
|
|
2346
|
+
Change a column's YAxisKind (cycle-tester aux) or ImportItemKind (logger)
|
|
2347
|
+
— controls which kind-based lookups match the column. Targets the ORIGINAL
|
|
2348
|
+
source column index.
|
|
2349
|
+
|
|
2350
|
+
Parameters
|
|
2351
|
+
----------
|
|
2352
|
+
source_column_index : int
|
|
2353
|
+
0-based index into the ORIGINAL source column list.
|
|
2354
|
+
new_kind : str
|
|
2355
|
+
Exact enum value name (e.g. 'Voltage', 'AuxTemperature').
|
|
2356
|
+
name : str, optional
|
|
2357
|
+
Descriptive name for this action.
|
|
2358
|
+
"""
|
|
2359
|
+
action = {"$type": "kind",
|
|
2360
|
+
"sourceColumnIndex": int(source_column_index),
|
|
2361
|
+
"newKind": new_kind}
|
|
2362
|
+
if name is not None:
|
|
2363
|
+
action["name"] = name
|
|
2364
|
+
return action
|
|
2365
|
+
|
|
2366
|
+
@staticmethod
|
|
2367
|
+
def reorder_columns(source_column_indices, name=None):
|
|
2368
|
+
"""Build a reorder-columns action for clean_data (MetadataEdit mode).
|
|
2369
|
+
|
|
2370
|
+
Declarative final column layout: source columns appear in the cleaned
|
|
2371
|
+
output in the order listed; any source index not listed is dropped.
|
|
2372
|
+
Combines drop + reorder in one action. At most one per definition.
|
|
2373
|
+
|
|
2374
|
+
Parameters
|
|
2375
|
+
----------
|
|
2376
|
+
source_column_indices : list[int]
|
|
2377
|
+
Ordered list of 0-based source column indices to keep. Omit an index
|
|
2378
|
+
to drop that column. Example: [0, 1, 3, 4, 5] drops source column 2.
|
|
2379
|
+
name : str, optional
|
|
2380
|
+
Descriptive name for this action.
|
|
2381
|
+
"""
|
|
2382
|
+
action = {"$type": "reorderColumns",
|
|
2383
|
+
"sourceColumnIndices": [int(i) for i in source_column_indices]}
|
|
2384
|
+
if name is not None:
|
|
2385
|
+
action["name"] = name
|
|
2386
|
+
return action
|
|
2387
|
+
|
|
2388
|
+
@staticmethod
|
|
2389
|
+
def edit_logger_step(line=None, lines=None, columns=None, name=None):
|
|
2390
|
+
"""Build an edit-logger-timestep action for clean_data (logger-data analog of modify_step/bulk_modify).
|
|
2391
|
+
|
|
2392
|
+
Edit column values by 0-based column index on rows matched by ``line`` and/or
|
|
2393
|
+
``lines`` (union). Use on DataLoggerData sources only; for cycle-tester data
|
|
2394
|
+
use modify_step / bulk_modify.
|
|
2395
|
+
|
|
2396
|
+
Parameters
|
|
2397
|
+
----------
|
|
2398
|
+
line : int, optional
|
|
2399
|
+
Single line number to edit.
|
|
2400
|
+
lines : list of (int, int) tuples, optional
|
|
2401
|
+
Line number ranges to edit, e.g. [(100, 200)]. End may be None for open-ended.
|
|
2402
|
+
columns : dict of {int: float}
|
|
2403
|
+
Column edits as {column_index: value}. Column index is 0-based and
|
|
2404
|
+
must refer to a valid column in the source ImportDefinition.
|
|
2405
|
+
name : str, optional
|
|
2406
|
+
Descriptive name for this action.
|
|
2407
|
+
"""
|
|
2408
|
+
if line is None and not lines:
|
|
2409
|
+
raise ValueError("edit_logger_step requires either line or lines.")
|
|
2410
|
+
if not columns:
|
|
2411
|
+
raise ValueError("edit_logger_step requires columns.")
|
|
2412
|
+
action = {"$type": "editLogger"}
|
|
2413
|
+
if name is not None:
|
|
2414
|
+
action["name"] = name
|
|
2415
|
+
if line is not None:
|
|
2416
|
+
action["lineNumber"] = line
|
|
2417
|
+
if lines:
|
|
2418
|
+
action["lineNumberRanges"] = _ranges(lines)
|
|
2419
|
+
action["columns"] = [{"columnIndex": int(k), "value": float(v)} for k, v in columns.items()]
|
|
2420
|
+
return action
|
|
2421
|
+
|
|
2216
2422
|
def clean_data(self,
|
|
2217
2423
|
source_ids: list,
|
|
2218
2424
|
mode: str = 'CycleAutoFixup',
|
|
@@ -2231,14 +2437,19 @@ class MicantisAPI:
|
|
|
2231
2437
|
source_ids : list of str
|
|
2232
2438
|
List of cell data IDs (GUIDs) to clean.
|
|
2233
2439
|
mode : str, optional
|
|
2234
|
-
Cleaning mode. One of 'CycleAutoFixup', 'FilterCycles', 'Parametric'.
|
|
2235
|
-
Default: 'CycleAutoFixup'.
|
|
2440
|
+
Cleaning mode. One of 'CycleAutoFixup', 'FilterCycles', 'Parametric', 'MetadataEdit'.
|
|
2441
|
+
Default: 'CycleAutoFixup'. When ``actions`` is provided and mode is still the default,
|
|
2442
|
+
it auto-sets to 'Parametric'. For 'MetadataEdit', pass mode explicitly.
|
|
2236
2443
|
filter_cycles_definition : dict, optional
|
|
2237
2444
|
Required when mode is 'FilterCycles'. Definition for filtering cycles.
|
|
2238
2445
|
actions : list, optional
|
|
2239
|
-
|
|
2240
|
-
api.
|
|
2241
|
-
|
|
2446
|
+
For Parametric mode: list of dicts from api.remove_steps(), api.modify_step(),
|
|
2447
|
+
api.bulk_modify(), api.edit_logger_step().
|
|
2448
|
+
For MetadataEdit mode: list of dicts from api.rename_column(),
|
|
2449
|
+
api.change_column_kind(), api.reorder_columns().
|
|
2450
|
+
Actions are applied in the order given.
|
|
2451
|
+
|
|
2452
|
+
Parametric example (cycle-tester)::
|
|
2242
2453
|
|
|
2243
2454
|
api.clean_data(
|
|
2244
2455
|
source_ids=[id],
|
|
@@ -2248,6 +2459,29 @@ class MicantisAPI:
|
|
|
2248
2459
|
api.bulk_modify(lines=[(100, 5000)], cycle=1),
|
|
2249
2460
|
]
|
|
2250
2461
|
)
|
|
2462
|
+
|
|
2463
|
+
Parametric example (DataLoggerData)::
|
|
2464
|
+
|
|
2465
|
+
api.clean_data(
|
|
2466
|
+
source_ids=[logger_id],
|
|
2467
|
+
actions=[
|
|
2468
|
+
api.remove_steps(lines=[(1, 3)]),
|
|
2469
|
+
api.edit_logger_step(line=10, columns={0: 1.5}),
|
|
2470
|
+
]
|
|
2471
|
+
)
|
|
2472
|
+
|
|
2473
|
+
MetadataEdit example (rename + drop — works on both CT and logger)::
|
|
2474
|
+
|
|
2475
|
+
api.clean_data(
|
|
2476
|
+
source_ids=[id],
|
|
2477
|
+
mode='MetadataEdit',
|
|
2478
|
+
actions=[
|
|
2479
|
+
api.rename_column(source_column_index=3, new_name='Temp 1 (C)'),
|
|
2480
|
+
api.rename_column(source_column_index=4, new_name='Temp 2 (C)'),
|
|
2481
|
+
api.rename_column(source_column_index=5, new_name='Temp 3 (C)'),
|
|
2482
|
+
api.reorder_columns([0, 1, 3, 4, 5]), # drops source column 2
|
|
2483
|
+
]
|
|
2484
|
+
)
|
|
2251
2485
|
allow_async : bool, optional
|
|
2252
2486
|
If True, runs asynchronously and returns job ID. Default: False.
|
|
2253
2487
|
timeout : int, optional
|
|
@@ -2260,7 +2494,8 @@ class MicantisAPI:
|
|
|
2260
2494
|
If allow_async is True: {'job_id': str}
|
|
2261
2495
|
Returns None if an error occurs.
|
|
2262
2496
|
"""
|
|
2263
|
-
if
|
|
2497
|
+
# Auto-set mode only if the caller left the default; preserves explicit 'MetadataEdit' etc.
|
|
2498
|
+
if actions is not None and mode == 'CycleAutoFixup':
|
|
2264
2499
|
mode = 'Parametric'
|
|
2265
2500
|
|
|
2266
2501
|
try:
|
|
@@ -2291,8 +2526,10 @@ class MicantisAPI:
|
|
|
2291
2526
|
|
|
2292
2527
|
if mode == 'Parametric' and not actions:
|
|
2293
2528
|
raise ValueError("actions is required when mode is 'Parametric'.")
|
|
2529
|
+
if mode == 'MetadataEdit' and not actions:
|
|
2530
|
+
raise ValueError("actions is required when mode is 'MetadataEdit'.")
|
|
2294
2531
|
|
|
2295
|
-
_mode_map = {'CycleAutoFixup': 1, 'Parametric': 2, 'FilterCycles': 4}
|
|
2532
|
+
_mode_map = {'CycleAutoFixup': 1, 'Parametric': 2, 'FilterCycles': 4, 'MetadataEdit': 5}
|
|
2296
2533
|
if mode not in _mode_map:
|
|
2297
2534
|
raise ValueError(f"mode must be one of {list(_mode_map)}, got '{mode}'")
|
|
2298
2535
|
|
|
@@ -2304,7 +2541,10 @@ class MicantisAPI:
|
|
|
2304
2541
|
if filter_cycles_definition is not None:
|
|
2305
2542
|
payload["filterCyclesDefinition"] = filter_cycles_definition
|
|
2306
2543
|
if actions is not None:
|
|
2307
|
-
|
|
2544
|
+
if mode == 'MetadataEdit':
|
|
2545
|
+
payload["metadataEditDefinition"] = {"actions": actions}
|
|
2546
|
+
else:
|
|
2547
|
+
payload["parametricDefinition"] = {"actions": actions}
|
|
2308
2548
|
|
|
2309
2549
|
url = f"{self.service_url}/publicapi/v1/data/clean"
|
|
2310
2550
|
if allow_async:
|
|
@@ -2578,21 +2818,34 @@ class MicantisAPI:
|
|
|
2578
2818
|
raise self._format_request_error(e)
|
|
2579
2819
|
|
|
2580
2820
|
def upload_execution_artifact(self,
|
|
2581
|
-
execution_id: str,
|
|
2582
|
-
file_path: str,
|
|
2583
|
-
title: str,
|
|
2821
|
+
execution_id: str = None,
|
|
2822
|
+
file_path: str = None,
|
|
2823
|
+
title: str = None,
|
|
2584
2824
|
filename: str = None,
|
|
2585
|
-
timeout: int = 120
|
|
2825
|
+
timeout: int = 120,
|
|
2826
|
+
mime: str = None) -> dict:
|
|
2586
2827
|
"""
|
|
2587
2828
|
Upload an artifact (output file) produced by a running Python execution.
|
|
2588
2829
|
|
|
2830
|
+
Inside a cloud execution, execution_id can be omitted — it is resolved
|
|
2831
|
+
from the WORKBOOK_EXECUTION_ID environment variable, which the runner
|
|
2832
|
+
always sets. The natural call shape there is::
|
|
2833
|
+
|
|
2834
|
+
api.upload_execution_artifact('/tmp/plot.png', title='My Plot')
|
|
2835
|
+
|
|
2836
|
+
A first positional argument that is not a GUID is treated as file_path
|
|
2837
|
+
(when file_path is not also given), so the line above works even though
|
|
2838
|
+
the first parameter is execution_id.
|
|
2839
|
+
|
|
2589
2840
|
Automatically reauthenticates if the session is expired or missing.
|
|
2590
2841
|
Prints an error message and returns None if the request ultimately fails.
|
|
2591
2842
|
|
|
2592
2843
|
Parameters
|
|
2593
2844
|
----------
|
|
2594
|
-
execution_id : str
|
|
2845
|
+
execution_id : str, optional
|
|
2595
2846
|
The execution ID (GUID) of the running or completed Python execution.
|
|
2847
|
+
Defaults to the WORKBOOK_EXECUTION_ID environment variable (always
|
|
2848
|
+
set inside cloud executions). Required when running outside one.
|
|
2596
2849
|
file_path : str
|
|
2597
2850
|
Local path to the file to upload (PNG, CSV, XLSX, JSON, PDF, etc.).
|
|
2598
2851
|
Maximum file size: 50 MB.
|
|
@@ -2603,6 +2856,10 @@ class MicantisAPI:
|
|
|
2603
2856
|
from the title and file extension.
|
|
2604
2857
|
timeout : int, optional
|
|
2605
2858
|
Request timeout in seconds. Default: 120.
|
|
2859
|
+
mime : str, optional
|
|
2860
|
+
Content type stored for the artifact (e.g. 'image/png'). If not
|
|
2861
|
+
provided, guessed from the file_path extension, falling back to
|
|
2862
|
+
'application/octet-stream'.
|
|
2606
2863
|
|
|
2607
2864
|
Returns
|
|
2608
2865
|
-------
|
|
@@ -2618,6 +2875,7 @@ class MicantisAPI:
|
|
|
2618
2875
|
title,
|
|
2619
2876
|
filename,
|
|
2620
2877
|
timeout,
|
|
2878
|
+
mime,
|
|
2621
2879
|
)
|
|
2622
2880
|
except Exception as e:
|
|
2623
2881
|
print(f"⚠️ upload_execution_artifact failed: {e}")
|
|
@@ -2628,28 +2886,61 @@ class MicantisAPI:
|
|
|
2628
2886
|
file_path: str,
|
|
2629
2887
|
title: str,
|
|
2630
2888
|
filename: str,
|
|
2631
|
-
timeout: int
|
|
2889
|
+
timeout: int,
|
|
2890
|
+
mime: str) -> dict:
|
|
2632
2891
|
"""
|
|
2633
2892
|
Internal helper for upload_execution_artifact. Does not handle retries or printing.
|
|
2634
2893
|
|
|
2635
2894
|
Raises
|
|
2636
2895
|
------
|
|
2637
2896
|
RuntimeError
|
|
2638
|
-
If the request fails or
|
|
2897
|
+
If arguments are missing or invalid, the request fails, or
|
|
2898
|
+
authentication is missing.
|
|
2639
2899
|
"""
|
|
2640
2900
|
if not self.headers:
|
|
2641
2901
|
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
2642
2902
|
|
|
2643
2903
|
self._refresh_token_if_needed()
|
|
2644
2904
|
|
|
2905
|
+
# Forgive the natural call shape api.upload_execution_artifact('/tmp/plot.png', title=...):
|
|
2906
|
+
# a non-GUID first argument is the file path, and the execution ID comes from the environment.
|
|
2907
|
+
if execution_id is not None and not _is_guid(execution_id):
|
|
2908
|
+
if file_path is None:
|
|
2909
|
+
file_path = execution_id
|
|
2910
|
+
execution_id = None
|
|
2911
|
+
else:
|
|
2912
|
+
raise RuntimeError(
|
|
2913
|
+
f"execution_id '{execution_id}' is not an execution GUID. Inside a cloud "
|
|
2914
|
+
"execution you can omit execution_id entirely — it is read from the "
|
|
2915
|
+
"WORKBOOK_EXECUTION_ID environment variable automatically."
|
|
2916
|
+
)
|
|
2917
|
+
|
|
2918
|
+
if execution_id is None:
|
|
2919
|
+
execution_id = os.environ.get("WORKBOOK_EXECUTION_ID")
|
|
2920
|
+
if not execution_id:
|
|
2921
|
+
raise RuntimeError(
|
|
2922
|
+
"No execution_id provided and WORKBOOK_EXECUTION_ID is not set. Inside a "
|
|
2923
|
+
"cloud execution the variable is set automatically; outside one, pass "
|
|
2924
|
+
"execution_id explicitly."
|
|
2925
|
+
)
|
|
2926
|
+
|
|
2927
|
+
if not file_path:
|
|
2928
|
+
raise RuntimeError("file_path is required.")
|
|
2929
|
+
if not title:
|
|
2930
|
+
raise RuntimeError("title is required.")
|
|
2931
|
+
|
|
2645
2932
|
url = f"{self.service_url}/publicapi/v1/python/executions/{execution_id}/artifacts"
|
|
2646
2933
|
|
|
2934
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
2935
|
+
content_type = (mime or _MIME_OVERRIDES.get(ext)
|
|
2936
|
+
or mimetypes.guess_type(file_path)[0] or "application/octet-stream")
|
|
2937
|
+
|
|
2647
2938
|
try:
|
|
2648
2939
|
with open(file_path, 'rb') as f:
|
|
2649
2940
|
form_data = {'title': (None, title)}
|
|
2650
2941
|
if filename is not None:
|
|
2651
2942
|
form_data['filename'] = (None, filename)
|
|
2652
|
-
form_data['file'] = (os.path.basename(file_path), f)
|
|
2943
|
+
form_data['file'] = (os.path.basename(file_path), f, content_type)
|
|
2653
2944
|
|
|
2654
2945
|
# Exclude Content-Type so requests sets the multipart boundary correctly
|
|
2655
2946
|
headers = {k: v for k, v in self.headers.items() if k.lower() != 'content-type'}
|
|
@@ -2666,6 +2957,114 @@ class MicantisAPI:
|
|
|
2666
2957
|
except requests.exceptions.RequestException as e:
|
|
2667
2958
|
raise self._format_request_error(e)
|
|
2668
2959
|
|
|
2960
|
+
def list_attachments(self, timeout: int = 30) -> list:
|
|
2961
|
+
"""
|
|
2962
|
+
List the files attached to the Moose conversation that launched this script.
|
|
2963
|
+
|
|
2964
|
+
Only works inside a Moose-triggered cloud execution (uses WORKBOOK_EXECUTION_ID).
|
|
2965
|
+
Automatically reauthenticates if the session is expired or missing. Prints an error
|
|
2966
|
+
and returns None on failure.
|
|
2967
|
+
|
|
2968
|
+
Returns
|
|
2969
|
+
-------
|
|
2970
|
+
list of dict or None
|
|
2971
|
+
One entry per attachment: {"name", "contentType", "sizeBytes"}.
|
|
2972
|
+
"""
|
|
2973
|
+
try:
|
|
2974
|
+
return self._retry_on_auth_failure(self._list_attachments, timeout)
|
|
2975
|
+
except Exception as e:
|
|
2976
|
+
print(f"⚠️ list_attachments failed: {e}")
|
|
2977
|
+
return None
|
|
2978
|
+
|
|
2979
|
+
def _list_attachments(self, timeout: int) -> list:
|
|
2980
|
+
if not self.headers:
|
|
2981
|
+
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
2982
|
+
self._refresh_token_if_needed()
|
|
2983
|
+
|
|
2984
|
+
execution_id = os.environ.get("WORKBOOK_EXECUTION_ID")
|
|
2985
|
+
if not execution_id:
|
|
2986
|
+
raise RuntimeError(
|
|
2987
|
+
"list_attachments is only available inside a cloud execution (WORKBOOK_EXECUTION_ID is not set)."
|
|
2988
|
+
)
|
|
2989
|
+
|
|
2990
|
+
try:
|
|
2991
|
+
r = self.session.get(
|
|
2992
|
+
f"{self.service_url}/publicapi/v1/python/executions/{execution_id}/attachments",
|
|
2993
|
+
headers=self.headers,
|
|
2994
|
+
timeout=timeout,
|
|
2995
|
+
)
|
|
2996
|
+
r.raise_for_status()
|
|
2997
|
+
return r.json()
|
|
2998
|
+
except requests.exceptions.RequestException as e:
|
|
2999
|
+
raise self._format_request_error(e)
|
|
3000
|
+
|
|
3001
|
+
def download_attachment(self, name: str, output_path: str = None,
|
|
3002
|
+
return_type: str = "bytes", timeout: int = 120):
|
|
3003
|
+
"""
|
|
3004
|
+
Download a file attached to the Moose conversation that launched this script, by name,
|
|
3005
|
+
WITHOUT inlining its bytes into your script source. Use this for any conversation
|
|
3006
|
+
attachment too large to paste in — inlining data into the script is bounded by the
|
|
3007
|
+
per-turn output budget (a few KB), but this streams the file into the container.
|
|
3008
|
+
|
|
3009
|
+
Only works inside a Moose-triggered cloud execution (uses WORKBOOK_EXECUTION_ID). Call
|
|
3010
|
+
list_attachments() first to get exact names. Automatically reauthenticates if the
|
|
3011
|
+
session is expired or missing. Prints an error and returns None on failure.
|
|
3012
|
+
|
|
3013
|
+
Parameters
|
|
3014
|
+
----------
|
|
3015
|
+
name : str
|
|
3016
|
+
Exact attachment filename (from list_attachments()).
|
|
3017
|
+
output_path : str, optional
|
|
3018
|
+
Where to save the file when return_type='path'. Defaults to a temp file.
|
|
3019
|
+
return_type : str, optional
|
|
3020
|
+
'bytes' (default) returns the raw bytes; 'path' writes the file to disk and returns
|
|
3021
|
+
the path (best for large files); 'text' returns a UTF-8-decoded str.
|
|
3022
|
+
timeout : int, optional
|
|
3023
|
+
Request timeout in seconds. Default: 120.
|
|
3024
|
+
|
|
3025
|
+
Returns
|
|
3026
|
+
-------
|
|
3027
|
+
bytes | str | None
|
|
3028
|
+
The attachment content per return_type, or None on failure.
|
|
3029
|
+
"""
|
|
3030
|
+
try:
|
|
3031
|
+
return self._retry_on_auth_failure(
|
|
3032
|
+
self._download_attachment, name, output_path, return_type, timeout)
|
|
3033
|
+
except Exception as e:
|
|
3034
|
+
print(f"⚠️ download_attachment failed: {e}")
|
|
3035
|
+
return None
|
|
3036
|
+
|
|
3037
|
+
def _download_attachment(self, name: str, output_path: str, return_type: str, timeout: int):
|
|
3038
|
+
if not self.headers:
|
|
3039
|
+
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
3040
|
+
self._refresh_token_if_needed()
|
|
3041
|
+
|
|
3042
|
+
execution_id = os.environ.get("WORKBOOK_EXECUTION_ID")
|
|
3043
|
+
if not execution_id:
|
|
3044
|
+
raise RuntimeError(
|
|
3045
|
+
"download_attachment is only available inside a cloud execution (WORKBOOK_EXECUTION_ID is not set)."
|
|
3046
|
+
)
|
|
3047
|
+
|
|
3048
|
+
url = (f"{self.service_url}/publicapi/v1/python/executions/{execution_id}"
|
|
3049
|
+
f"/attachments/{quote(name)}")
|
|
3050
|
+
|
|
3051
|
+
try:
|
|
3052
|
+
with self.session.get(url, headers=self.headers, timeout=timeout, stream=True) as r:
|
|
3053
|
+
r.raise_for_status()
|
|
3054
|
+
if return_type == "path":
|
|
3055
|
+
path = output_path or os.path.join(tempfile.gettempdir(), os.path.basename(name))
|
|
3056
|
+
with open(path, "wb") as f:
|
|
3057
|
+
for chunk in r.iter_content(chunk_size=1024 * 1024):
|
|
3058
|
+
if chunk:
|
|
3059
|
+
f.write(chunk)
|
|
3060
|
+
return path
|
|
3061
|
+
content = r.content
|
|
3062
|
+
if return_type == "text":
|
|
3063
|
+
return content.decode("utf-8", errors="replace")
|
|
3064
|
+
return content
|
|
3065
|
+
except requests.exceptions.RequestException as e:
|
|
3066
|
+
raise self._format_request_error(e)
|
|
3067
|
+
|
|
2669
3068
|
# ─────────────────────────────────────────
|
|
2670
3069
|
# Job management (async operation polling)
|
|
2671
3070
|
# ─────────────────────────────────────────
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: micantis
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: Package to simplify Micantis API usage
|
|
5
5
|
Author-email: Mykela DeLuca <mykela.deluca@micantis.io>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -548,6 +548,55 @@ assert result.success, result.errors
|
|
|
548
548
|
|
|
549
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
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
|
|
598
|
+
```
|
|
599
|
+
|
|
551
600
|
## Stitch Data
|
|
552
601
|
Combine multiple data sets into a single stitched data set. This is useful for creating continuous test data from multiple separate test runs.
|
|
553
602
|
|
|
@@ -964,14 +1013,16 @@ for group in dupes:
|
|
|
964
1013
|
Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python script execution. Called from within a Python script running in the Micantis environment.
|
|
965
1014
|
|
|
966
1015
|
#### Parameters
|
|
967
|
-
- `execution_id`: **str (
|
|
968
|
-
The execution ID (GUID) of the running or completed execution
|
|
1016
|
+
- `execution_id`: **str (optional inside cloud executions)**
|
|
1017
|
+
The execution ID (GUID) of the running or completed execution. Defaults to the `WORKBOOK_EXECUTION_ID` environment variable, which cloud executions always set; required when running elsewhere. A non-GUID first positional argument is treated as `file_path`
|
|
969
1018
|
- `file_path`: **str (required)**
|
|
970
1019
|
Local path to the file to upload. Maximum size: 50 MB
|
|
971
1020
|
- `title`: **str (required)**
|
|
972
1021
|
Human-readable title for the artifact
|
|
973
1022
|
- `filename`: **str (optional)**
|
|
974
1023
|
Override the filename stored on the server. If not provided, derived from the title and file extension
|
|
1024
|
+
- `mime`: **str (optional)**
|
|
1025
|
+
Content type stored for the artifact (e.g. `image/png`). If not provided, guessed from the file extension, falling back to `application/octet-stream`
|
|
975
1026
|
|
|
976
1027
|
#### Returns
|
|
977
1028
|
- Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
|
|
@@ -979,22 +1030,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
|
|
|
979
1030
|
#### 📘 Examples
|
|
980
1031
|
|
|
981
1032
|
```python
|
|
982
|
-
#
|
|
983
|
-
result = api.upload_execution_artifact(
|
|
984
|
-
execution_id="your-execution-guid",
|
|
985
|
-
file_path="voltage_plot.png",
|
|
986
|
-
title="Voltage vs Time"
|
|
987
|
-
)
|
|
1033
|
+
# Inside a cloud execution — execution_id is resolved from the environment
|
|
1034
|
+
result = api.upload_execution_artifact("voltage_plot.png", title="Voltage vs Time")
|
|
988
1035
|
print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
|
|
989
1036
|
```
|
|
990
1037
|
|
|
991
1038
|
```python
|
|
992
|
-
#
|
|
1039
|
+
# Explicit execution ID with a custom filename and content type
|
|
993
1040
|
result = api.upload_execution_artifact(
|
|
994
1041
|
execution_id="your-execution-guid",
|
|
995
1042
|
file_path="results.csv",
|
|
996
1043
|
title="Cycle Summary Results",
|
|
997
|
-
filename="cycle_summary_2025.csv"
|
|
1044
|
+
filename="cycle_summary_2025.csv",
|
|
1045
|
+
mime="text/csv"
|
|
998
1046
|
)
|
|
999
1047
|
```
|
|
1000
1048
|
|
|
@@ -6,12 +6,14 @@ Mocks the HTTP layer so it can run anywhere without a live server. Exercises:
|
|
|
6
6
|
- 207 Multi-Status → BatchUpdateResult(success=False, errors=[...])
|
|
7
7
|
- 400 Bad Request → BatchUpdateResult(success=False, errors=[...])
|
|
8
8
|
- Truthiness (`if result:` works)
|
|
9
|
-
-
|
|
9
|
+
- User-property name resolution and propertyDefinitionId pass-through
|
|
10
10
|
|
|
11
11
|
Run: pytest
|
|
12
12
|
"""
|
|
13
13
|
from unittest.mock import MagicMock
|
|
14
14
|
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
15
17
|
from micantis.api_wrapper import MicantisAPI, BatchUpdateResult
|
|
16
18
|
|
|
17
19
|
|
|
@@ -115,18 +117,55 @@ def test_write_data_207_returns_partial():
|
|
|
115
117
|
print("PASS: data/updatemetadata 207 → partial")
|
|
116
118
|
|
|
117
119
|
|
|
118
|
-
def
|
|
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.
|
|
119
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
|
+
)
|
|
120
127
|
try:
|
|
121
128
|
api._write_data_metadata(
|
|
122
|
-
[{"id": "x", "field": "
|
|
129
|
+
[{"id": "x", "field": "Nonexistent Property", "value": "foo"}], timeout=5,
|
|
123
130
|
)
|
|
124
131
|
except RuntimeError as e:
|
|
125
|
-
assert "
|
|
126
|
-
assert "
|
|
127
|
-
print("PASS: data/updatemetadata
|
|
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")
|
|
128
135
|
return
|
|
129
|
-
raise AssertionError("expected RuntimeError on unknown
|
|
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")
|
|
130
169
|
|
|
131
170
|
|
|
132
171
|
def test_name_is_accepted_as_builtin():
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for upload_execution_artifact argument resolution and multipart content type.
|
|
3
|
+
|
|
4
|
+
Mocks the HTTP layer so it can run anywhere without a live server. Exercises the
|
|
5
|
+
four supported call shapes:
|
|
6
|
+
- Path-only positional + title + mime → execution_id resolved from WORKBOOK_EXECUTION_ID
|
|
7
|
+
- Classic positional (guid, path, title) → unchanged, content type guessed from extension
|
|
8
|
+
- Full keyword form → unchanged
|
|
9
|
+
- No execution_id anywhere and no env var → clear printed error, returns None
|
|
10
|
+
|
|
11
|
+
Run: pytest
|
|
12
|
+
"""
|
|
13
|
+
from .test_batch_update_result import _make_api, _mock_response
|
|
14
|
+
|
|
15
|
+
EXEC_ID = "11111111-2222-3333-4444-555555555555"
|
|
16
|
+
OTHER_EXEC_ID = "99999999-8888-7777-6666-555555555555"
|
|
17
|
+
|
|
18
|
+
RESULT_BODY = {
|
|
19
|
+
"name": "plot.png",
|
|
20
|
+
"title": "My Plot",
|
|
21
|
+
"contentType": "image/png",
|
|
22
|
+
"sizeBytes": 9,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_path_first_positional_resolves_execution_id_from_env(monkeypatch, tmp_path):
|
|
27
|
+
# The natural cloud call shape: file path first, no execution_id anywhere.
|
|
28
|
+
api = _make_api()
|
|
29
|
+
api.session.post.return_value = _mock_response(200, RESULT_BODY)
|
|
30
|
+
monkeypatch.setenv("WORKBOOK_EXECUTION_ID", EXEC_ID)
|
|
31
|
+
|
|
32
|
+
png = tmp_path / "plot.png"
|
|
33
|
+
png.write_bytes(b"\x89PNG fake")
|
|
34
|
+
|
|
35
|
+
result = api.upload_execution_artifact(str(png), title="My Plot", mime="image/png")
|
|
36
|
+
|
|
37
|
+
assert result == RESULT_BODY
|
|
38
|
+
url = api.session.post.call_args.args[0]
|
|
39
|
+
assert url == f"https://fake.test/publicapi/v1/python/executions/{EXEC_ID}/artifacts"
|
|
40
|
+
name, fobj, content_type = api.session.post.call_args.kwargs["files"]["file"]
|
|
41
|
+
assert name == "plot.png"
|
|
42
|
+
assert content_type == "image/png"
|
|
43
|
+
print("PASS: path-first positional resolves WORKBOOK_EXECUTION_ID and sends mime")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_classic_positional_shape_unchanged(monkeypatch, tmp_path):
|
|
47
|
+
# (execution_id, file_path, title) keeps working; explicit GUID wins over the env var,
|
|
48
|
+
# and the part content type is guessed from the file extension when mime is omitted.
|
|
49
|
+
api = _make_api()
|
|
50
|
+
api.session.post.return_value = _mock_response(200, RESULT_BODY)
|
|
51
|
+
monkeypatch.setenv("WORKBOOK_EXECUTION_ID", OTHER_EXEC_ID)
|
|
52
|
+
|
|
53
|
+
png = tmp_path / "voltage_profile.png"
|
|
54
|
+
png.write_bytes(b"\x89PNG fake")
|
|
55
|
+
|
|
56
|
+
result = api.upload_execution_artifact(EXEC_ID, str(png), "Voltage Profile")
|
|
57
|
+
|
|
58
|
+
assert result == RESULT_BODY
|
|
59
|
+
url = api.session.post.call_args.args[0]
|
|
60
|
+
assert url == f"https://fake.test/publicapi/v1/python/executions/{EXEC_ID}/artifacts"
|
|
61
|
+
name, fobj, content_type = api.session.post.call_args.kwargs["files"]["file"]
|
|
62
|
+
assert name == "voltage_profile.png"
|
|
63
|
+
assert content_type == "image/png"
|
|
64
|
+
print("PASS: classic positional (guid, path, title) unchanged; mime guessed from extension")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_keyword_shape_unchanged(monkeypatch, tmp_path):
|
|
68
|
+
api = _make_api()
|
|
69
|
+
api.session.post.return_value = _mock_response(200, RESULT_BODY)
|
|
70
|
+
monkeypatch.delenv("WORKBOOK_EXECUTION_ID", raising=False)
|
|
71
|
+
|
|
72
|
+
csv = tmp_path / "results.csv"
|
|
73
|
+
csv.write_text("a,b\n1,2\n")
|
|
74
|
+
|
|
75
|
+
result = api.upload_execution_artifact(
|
|
76
|
+
execution_id=EXEC_ID,
|
|
77
|
+
file_path=str(csv),
|
|
78
|
+
title="Analysis Results",
|
|
79
|
+
filename="cycle_summary.csv",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert result == RESULT_BODY
|
|
83
|
+
url = api.session.post.call_args.args[0]
|
|
84
|
+
assert url == f"https://fake.test/publicapi/v1/python/executions/{EXEC_ID}/artifacts"
|
|
85
|
+
files = api.session.post.call_args.kwargs["files"]
|
|
86
|
+
assert files["filename"] == (None, "cycle_summary.csv")
|
|
87
|
+
name, fobj, content_type = files["file"]
|
|
88
|
+
assert name == "results.csv"
|
|
89
|
+
# Pinned by _MIME_OVERRIDES, independent of the host OS mime registry.
|
|
90
|
+
assert content_type == "text/csv"
|
|
91
|
+
print("PASS: full keyword form unchanged; filename part still sent")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_missing_execution_id_and_env_var_reports_clear_error(monkeypatch, tmp_path, capsys):
|
|
95
|
+
# Outside a cloud execution with no explicit execution_id: P-03 convention —
|
|
96
|
+
# print the real error (naming the env var) and return None, no HTTP call.
|
|
97
|
+
api = _make_api()
|
|
98
|
+
monkeypatch.delenv("WORKBOOK_EXECUTION_ID", raising=False)
|
|
99
|
+
|
|
100
|
+
csv = tmp_path / "out.csv"
|
|
101
|
+
csv.write_text("a,b\n")
|
|
102
|
+
|
|
103
|
+
result = api.upload_execution_artifact(file_path=str(csv), title="Out")
|
|
104
|
+
|
|
105
|
+
assert result is None
|
|
106
|
+
out = capsys.readouterr().out
|
|
107
|
+
assert "upload_execution_artifact failed" in out
|
|
108
|
+
assert "WORKBOOK_EXECUTION_ID" in out
|
|
109
|
+
api.session.post.assert_not_called()
|
|
110
|
+
print("PASS: missing execution_id + env var → clear error naming WORKBOOK_EXECUTION_ID")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|