micantis 1.2.4__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.4 → micantis-1.2.5}/PKG-INFO +10 -11
- {micantis-1.2.4 → micantis-1.2.5}/README.md +9 -10
- {micantis-1.2.4 → micantis-1.2.5}/micantis/api_wrapper.py +191 -8
- {micantis-1.2.4 → micantis-1.2.5}/micantis.egg-info/PKG-INFO +10 -11
- {micantis-1.2.4 → micantis-1.2.5}/micantis.egg-info/SOURCES.txt +2 -1
- {micantis-1.2.4 → micantis-1.2.5}/pyproject.toml +1 -1
- micantis-1.2.5/tests/test_upload_execution_artifact.py +110 -0
- {micantis-1.2.4 → micantis-1.2.5}/LICENSE +0 -0
- {micantis-1.2.4 → micantis-1.2.5}/micantis/__init__.py +0 -0
- {micantis-1.2.4 → micantis-1.2.5}/micantis/utils.py +0 -0
- {micantis-1.2.4 → micantis-1.2.5}/micantis.egg-info/dependency_links.txt +0 -0
- {micantis-1.2.4 → micantis-1.2.5}/micantis.egg-info/requires.txt +0 -0
- {micantis-1.2.4 → micantis-1.2.5}/micantis.egg-info/top_level.txt +0 -0
- {micantis-1.2.4 → micantis-1.2.5}/setup.cfg +0 -0
- {micantis-1.2.4 → micantis-1.2.5}/tests/test_batch_update_result.py +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
|
|
@@ -1013,14 +1013,16 @@ for group in dupes:
|
|
|
1013
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.
|
|
1014
1014
|
|
|
1015
1015
|
#### Parameters
|
|
1016
|
-
- `execution_id`: **str (
|
|
1017
|
-
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`
|
|
1018
1018
|
- `file_path`: **str (required)**
|
|
1019
1019
|
Local path to the file to upload. Maximum size: 50 MB
|
|
1020
1020
|
- `title`: **str (required)**
|
|
1021
1021
|
Human-readable title for the artifact
|
|
1022
1022
|
- `filename`: **str (optional)**
|
|
1023
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`
|
|
1024
1026
|
|
|
1025
1027
|
#### Returns
|
|
1026
1028
|
- Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
|
|
@@ -1028,22 +1030,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
|
|
|
1028
1030
|
#### 📘 Examples
|
|
1029
1031
|
|
|
1030
1032
|
```python
|
|
1031
|
-
#
|
|
1032
|
-
result = api.upload_execution_artifact(
|
|
1033
|
-
execution_id="your-execution-guid",
|
|
1034
|
-
file_path="voltage_plot.png",
|
|
1035
|
-
title="Voltage vs Time"
|
|
1036
|
-
)
|
|
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")
|
|
1037
1035
|
print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
|
|
1038
1036
|
```
|
|
1039
1037
|
|
|
1040
1038
|
```python
|
|
1041
|
-
#
|
|
1039
|
+
# Explicit execution ID with a custom filename and content type
|
|
1042
1040
|
result = api.upload_execution_artifact(
|
|
1043
1041
|
execution_id="your-execution-guid",
|
|
1044
1042
|
file_path="results.csv",
|
|
1045
1043
|
title="Cycle Summary Results",
|
|
1046
|
-
filename="cycle_summary_2025.csv"
|
|
1044
|
+
filename="cycle_summary_2025.csv",
|
|
1045
|
+
mime="text/csv"
|
|
1047
1046
|
)
|
|
1048
1047
|
```
|
|
1049
1048
|
|
|
@@ -993,14 +993,16 @@ for group in dupes:
|
|
|
993
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.
|
|
994
994
|
|
|
995
995
|
#### Parameters
|
|
996
|
-
- `execution_id`: **str (
|
|
997
|
-
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`
|
|
998
998
|
- `file_path`: **str (required)**
|
|
999
999
|
Local path to the file to upload. Maximum size: 50 MB
|
|
1000
1000
|
- `title`: **str (required)**
|
|
1001
1001
|
Human-readable title for the artifact
|
|
1002
1002
|
- `filename`: **str (optional)**
|
|
1003
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`
|
|
1004
1006
|
|
|
1005
1007
|
#### Returns
|
|
1006
1008
|
- Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
|
|
@@ -1008,22 +1010,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
|
|
|
1008
1010
|
#### 📘 Examples
|
|
1009
1011
|
|
|
1010
1012
|
```python
|
|
1011
|
-
#
|
|
1012
|
-
result = api.upload_execution_artifact(
|
|
1013
|
-
execution_id="your-execution-guid",
|
|
1014
|
-
file_path="voltage_plot.png",
|
|
1015
|
-
title="Voltage vs Time"
|
|
1016
|
-
)
|
|
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")
|
|
1017
1015
|
print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
|
|
1018
1016
|
```
|
|
1019
1017
|
|
|
1020
1018
|
```python
|
|
1021
|
-
#
|
|
1019
|
+
# Explicit execution ID with a custom filename and content type
|
|
1022
1020
|
result = api.upload_execution_artifact(
|
|
1023
1021
|
execution_id="your-execution-guid",
|
|
1024
1022
|
file_path="results.csv",
|
|
1025
1023
|
title="Cycle Summary Results",
|
|
1026
|
-
filename="cycle_summary_2025.csv"
|
|
1024
|
+
filename="cycle_summary_2025.csv",
|
|
1025
|
+
mime="text/csv"
|
|
1027
1026
|
)
|
|
1028
1027
|
```
|
|
1029
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`).
|
|
@@ -2794,21 +2818,34 @@ class MicantisAPI:
|
|
|
2794
2818
|
raise self._format_request_error(e)
|
|
2795
2819
|
|
|
2796
2820
|
def upload_execution_artifact(self,
|
|
2797
|
-
execution_id: str,
|
|
2798
|
-
file_path: str,
|
|
2799
|
-
title: str,
|
|
2821
|
+
execution_id: str = None,
|
|
2822
|
+
file_path: str = None,
|
|
2823
|
+
title: str = None,
|
|
2800
2824
|
filename: str = None,
|
|
2801
|
-
timeout: int = 120
|
|
2825
|
+
timeout: int = 120,
|
|
2826
|
+
mime: str = None) -> dict:
|
|
2802
2827
|
"""
|
|
2803
2828
|
Upload an artifact (output file) produced by a running Python execution.
|
|
2804
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
|
+
|
|
2805
2840
|
Automatically reauthenticates if the session is expired or missing.
|
|
2806
2841
|
Prints an error message and returns None if the request ultimately fails.
|
|
2807
2842
|
|
|
2808
2843
|
Parameters
|
|
2809
2844
|
----------
|
|
2810
|
-
execution_id : str
|
|
2845
|
+
execution_id : str, optional
|
|
2811
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.
|
|
2812
2849
|
file_path : str
|
|
2813
2850
|
Local path to the file to upload (PNG, CSV, XLSX, JSON, PDF, etc.).
|
|
2814
2851
|
Maximum file size: 50 MB.
|
|
@@ -2819,6 +2856,10 @@ class MicantisAPI:
|
|
|
2819
2856
|
from the title and file extension.
|
|
2820
2857
|
timeout : int, optional
|
|
2821
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'.
|
|
2822
2863
|
|
|
2823
2864
|
Returns
|
|
2824
2865
|
-------
|
|
@@ -2834,6 +2875,7 @@ class MicantisAPI:
|
|
|
2834
2875
|
title,
|
|
2835
2876
|
filename,
|
|
2836
2877
|
timeout,
|
|
2878
|
+
mime,
|
|
2837
2879
|
)
|
|
2838
2880
|
except Exception as e:
|
|
2839
2881
|
print(f"⚠️ upload_execution_artifact failed: {e}")
|
|
@@ -2844,28 +2886,61 @@ class MicantisAPI:
|
|
|
2844
2886
|
file_path: str,
|
|
2845
2887
|
title: str,
|
|
2846
2888
|
filename: str,
|
|
2847
|
-
timeout: int
|
|
2889
|
+
timeout: int,
|
|
2890
|
+
mime: str) -> dict:
|
|
2848
2891
|
"""
|
|
2849
2892
|
Internal helper for upload_execution_artifact. Does not handle retries or printing.
|
|
2850
2893
|
|
|
2851
2894
|
Raises
|
|
2852
2895
|
------
|
|
2853
2896
|
RuntimeError
|
|
2854
|
-
If the request fails or
|
|
2897
|
+
If arguments are missing or invalid, the request fails, or
|
|
2898
|
+
authentication is missing.
|
|
2855
2899
|
"""
|
|
2856
2900
|
if not self.headers:
|
|
2857
2901
|
raise RuntimeError("Authentication required. Call authenticate() first.")
|
|
2858
2902
|
|
|
2859
2903
|
self._refresh_token_if_needed()
|
|
2860
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
|
+
|
|
2861
2932
|
url = f"{self.service_url}/publicapi/v1/python/executions/{execution_id}/artifacts"
|
|
2862
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
|
+
|
|
2863
2938
|
try:
|
|
2864
2939
|
with open(file_path, 'rb') as f:
|
|
2865
2940
|
form_data = {'title': (None, title)}
|
|
2866
2941
|
if filename is not None:
|
|
2867
2942
|
form_data['filename'] = (None, filename)
|
|
2868
|
-
form_data['file'] = (os.path.basename(file_path), f)
|
|
2943
|
+
form_data['file'] = (os.path.basename(file_path), f, content_type)
|
|
2869
2944
|
|
|
2870
2945
|
# Exclude Content-Type so requests sets the multipart boundary correctly
|
|
2871
2946
|
headers = {k: v for k, v in self.headers.items() if k.lower() != 'content-type'}
|
|
@@ -2882,6 +2957,114 @@ class MicantisAPI:
|
|
|
2882
2957
|
except requests.exceptions.RequestException as e:
|
|
2883
2958
|
raise self._format_request_error(e)
|
|
2884
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
|
+
|
|
2885
3068
|
# ─────────────────────────────────────────
|
|
2886
3069
|
# Job management (async operation polling)
|
|
2887
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
|
|
@@ -1013,14 +1013,16 @@ for group in dupes:
|
|
|
1013
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.
|
|
1014
1014
|
|
|
1015
1015
|
#### Parameters
|
|
1016
|
-
- `execution_id`: **str (
|
|
1017
|
-
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`
|
|
1018
1018
|
- `file_path`: **str (required)**
|
|
1019
1019
|
Local path to the file to upload. Maximum size: 50 MB
|
|
1020
1020
|
- `title`: **str (required)**
|
|
1021
1021
|
Human-readable title for the artifact
|
|
1022
1022
|
- `filename`: **str (optional)**
|
|
1023
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`
|
|
1024
1026
|
|
|
1025
1027
|
#### Returns
|
|
1026
1028
|
- Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
|
|
@@ -1028,22 +1030,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
|
|
|
1028
1030
|
#### 📘 Examples
|
|
1029
1031
|
|
|
1030
1032
|
```python
|
|
1031
|
-
#
|
|
1032
|
-
result = api.upload_execution_artifact(
|
|
1033
|
-
execution_id="your-execution-guid",
|
|
1034
|
-
file_path="voltage_plot.png",
|
|
1035
|
-
title="Voltage vs Time"
|
|
1036
|
-
)
|
|
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")
|
|
1037
1035
|
print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
|
|
1038
1036
|
```
|
|
1039
1037
|
|
|
1040
1038
|
```python
|
|
1041
|
-
#
|
|
1039
|
+
# Explicit execution ID with a custom filename and content type
|
|
1042
1040
|
result = api.upload_execution_artifact(
|
|
1043
1041
|
execution_id="your-execution-guid",
|
|
1044
1042
|
file_path="results.csv",
|
|
1045
1043
|
title="Cycle Summary Results",
|
|
1046
|
-
filename="cycle_summary_2025.csv"
|
|
1044
|
+
filename="cycle_summary_2025.csv",
|
|
1045
|
+
mime="text/csv"
|
|
1047
1046
|
)
|
|
1048
1047
|
```
|
|
1049
1048
|
|
|
@@ -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
|
|
File without changes
|