micantis 1.2.4__tar.gz → 1.2.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: micantis
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Package to simplify Micantis API usage
5
5
  Author-email: Mykela DeLuca <mykela.deluca@micantis.io>
6
6
  License-Expression: MIT
@@ -500,6 +500,24 @@ print(f"Retrieved {len(combined_df)} test requests")
500
500
  combined_df.head()
501
501
  ```
502
502
 
503
+ ### Get Test Request Member Cells
504
+ Retrieve the actual cells linked to a test request. Note that the `tests` array from `get_test_request()` is the test plan (names and quantities) — it contains no cell IDs.
505
+
506
+ ```python
507
+ # DataFrame with one row per member cell (default)
508
+ cells_df = api.get_test_request_cells(request_id)
509
+ cells_df.head() # columns: id, name, updated, cycleCount, hasGraphData
510
+
511
+ # Raw list of dicts
512
+ cells = api.get_test_request_cells(request_id, return_format='list')
513
+ ```
514
+
515
+ ```python
516
+ # Use the cell IDs directly — e.g. fetch metadata for every cell in the request
517
+ cell_ids = cells_df['id'].to_list()
518
+ metadata_df = api.get_cell_metadata(cell_ids=cell_ids, metadata=["Cell height"])
519
+ ```
520
+
503
521
  ## Write Cell Metadata
504
522
  Micantis lets you programmatically assign or update metadata for each cell using either:
505
523
  - the built-in field `"name"` (renames the cell) or `"dataSourceKey"`
@@ -1013,14 +1031,16 @@ for group in dupes:
1013
1031
  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
1032
 
1015
1033
  #### Parameters
1016
- - `execution_id`: **str (required)**
1017
- The execution ID (GUID) of the running or completed execution
1034
+ - `execution_id`: **str (optional inside cloud executions)**
1035
+ 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
1036
  - `file_path`: **str (required)**
1019
1037
  Local path to the file to upload. Maximum size: 50 MB
1020
1038
  - `title`: **str (required)**
1021
1039
  Human-readable title for the artifact
1022
1040
  - `filename`: **str (optional)**
1023
1041
  Override the filename stored on the server. If not provided, derived from the title and file extension
1042
+ - `mime`: **str (optional)**
1043
+ 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
1044
 
1025
1045
  #### Returns
1026
1046
  - Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
@@ -1028,22 +1048,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
1028
1048
  #### 📘 Examples
1029
1049
 
1030
1050
  ```python
1031
- # Upload a plot generated during a Python execution
1032
- result = api.upload_execution_artifact(
1033
- execution_id="your-execution-guid",
1034
- file_path="voltage_plot.png",
1035
- title="Voltage vs Time"
1036
- )
1051
+ # Inside a cloud execution execution_id is resolved from the environment
1052
+ result = api.upload_execution_artifact("voltage_plot.png", title="Voltage vs Time")
1037
1053
  print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
1038
1054
  ```
1039
1055
 
1040
1056
  ```python
1041
- # Upload a CSV results file with a custom filename
1057
+ # Explicit execution ID with a custom filename and content type
1042
1058
  result = api.upload_execution_artifact(
1043
1059
  execution_id="your-execution-guid",
1044
1060
  file_path="results.csv",
1045
1061
  title="Cycle Summary Results",
1046
- filename="cycle_summary_2025.csv"
1062
+ filename="cycle_summary_2025.csv",
1063
+ mime="text/csv"
1047
1064
  )
1048
1065
  ```
1049
1066
 
@@ -480,6 +480,24 @@ print(f"Retrieved {len(combined_df)} test requests")
480
480
  combined_df.head()
481
481
  ```
482
482
 
483
+ ### Get Test Request Member Cells
484
+ Retrieve the actual cells linked to a test request. Note that the `tests` array from `get_test_request()` is the test plan (names and quantities) — it contains no cell IDs.
485
+
486
+ ```python
487
+ # DataFrame with one row per member cell (default)
488
+ cells_df = api.get_test_request_cells(request_id)
489
+ cells_df.head() # columns: id, name, updated, cycleCount, hasGraphData
490
+
491
+ # Raw list of dicts
492
+ cells = api.get_test_request_cells(request_id, return_format='list')
493
+ ```
494
+
495
+ ```python
496
+ # Use the cell IDs directly — e.g. fetch metadata for every cell in the request
497
+ cell_ids = cells_df['id'].to_list()
498
+ metadata_df = api.get_cell_metadata(cell_ids=cell_ids, metadata=["Cell height"])
499
+ ```
500
+
483
501
  ## Write Cell Metadata
484
502
  Micantis lets you programmatically assign or update metadata for each cell using either:
485
503
  - the built-in field `"name"` (renames the cell) or `"dataSourceKey"`
@@ -993,14 +1011,16 @@ for group in dupes:
993
1011
  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
1012
 
995
1013
  #### Parameters
996
- - `execution_id`: **str (required)**
997
- The execution ID (GUID) of the running or completed execution
1014
+ - `execution_id`: **str (optional inside cloud executions)**
1015
+ 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
1016
  - `file_path`: **str (required)**
999
1017
  Local path to the file to upload. Maximum size: 50 MB
1000
1018
  - `title`: **str (required)**
1001
1019
  Human-readable title for the artifact
1002
1020
  - `filename`: **str (optional)**
1003
1021
  Override the filename stored on the server. If not provided, derived from the title and file extension
1022
+ - `mime`: **str (optional)**
1023
+ 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
1024
 
1005
1025
  #### Returns
1006
1026
  - Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
@@ -1008,22 +1028,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
1008
1028
  #### 📘 Examples
1009
1029
 
1010
1030
  ```python
1011
- # Upload a plot generated during a Python execution
1012
- result = api.upload_execution_artifact(
1013
- execution_id="your-execution-guid",
1014
- file_path="voltage_plot.png",
1015
- title="Voltage vs Time"
1016
- )
1031
+ # Inside a cloud execution execution_id is resolved from the environment
1032
+ result = api.upload_execution_artifact("voltage_plot.png", title="Voltage vs Time")
1017
1033
  print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
1018
1034
  ```
1019
1035
 
1020
1036
  ```python
1021
- # Upload a CSV results file with a custom filename
1037
+ # Explicit execution ID with a custom filename and content type
1022
1038
  result = api.upload_execution_artifact(
1023
1039
  execution_id="your-execution-guid",
1024
1040
  file_path="results.csv",
1025
1041
  title="Cycle Summary Results",
1026
- filename="cycle_summary_2025.csv"
1042
+ filename="cycle_summary_2025.csv",
1043
+ mime="text/csv"
1027
1044
  )
1028
1045
  ```
1029
1046
 
@@ -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`).
@@ -1690,6 +1714,72 @@ class MicantisAPI:
1690
1714
  raise RuntimeError(f"Test request {request_id} not found.")
1691
1715
  raise self._format_request_error(e)
1692
1716
 
1717
+ def get_test_request_cells(self, request_id, return_format='df', timeout: int = 30):
1718
+ """
1719
+ Get the member cells of a test request.
1720
+
1721
+ These are the actual cells linked to the request. The `tests` array returned by
1722
+ get_test_request() is the test plan (names and quantities) and contains no cell IDs.
1723
+
1724
+ Automatically reauthenticates if the session is expired or missing.
1725
+ Prints an error message and returns None if the request ultimately fails.
1726
+
1727
+ Parameters
1728
+ ----------
1729
+ request_id : str
1730
+ GUID of the test request.
1731
+ return_format : str, optional
1732
+ Return format: 'df' for DataFrame (default), anything else for the raw item list.
1733
+ timeout : int, optional
1734
+ Request timeout in seconds. Default: 30.
1735
+
1736
+ Returns
1737
+ -------
1738
+ pd.DataFrame, list, or None
1739
+ One row/dict per member cell with `id`, `name`, `updated`, `cycleCount`,
1740
+ and `hasGraphData`. Returns None if an error occurs.
1741
+ """
1742
+ try:
1743
+ return self._retry_on_auth_failure(
1744
+ self._get_test_request_cells, request_id, return_format, timeout
1745
+ )
1746
+ except Exception as e:
1747
+ print(f"⚠️ get_test_request_cells failed: {e}")
1748
+ return None
1749
+
1750
+ def _get_test_request_cells(self, request_id, return_format: str, timeout: int):
1751
+ """
1752
+ Internal helper for get_test_request_cells. Does not handle retries or printing.
1753
+
1754
+ Raises
1755
+ ------
1756
+ RuntimeError
1757
+ If the request fails or authentication is missing.
1758
+ """
1759
+ if not self.headers:
1760
+ raise RuntimeError("Authentication required. Call authenticate() first.")
1761
+
1762
+ self._refresh_token_if_needed()
1763
+
1764
+ try:
1765
+ response = self.session.get(
1766
+ f"{self.service_url}/publicapi/v1/testmgt/request/get/{request_id}/cells",
1767
+ headers=self.headers,
1768
+ timeout=timeout,
1769
+ )
1770
+ response.raise_for_status()
1771
+
1772
+ items = response.json().get('items', [])
1773
+ if return_format == 'df':
1774
+ return pd.DataFrame(items)
1775
+
1776
+ return items
1777
+
1778
+ except requests.exceptions.RequestException as e:
1779
+ if hasattr(e, 'response') and e.response is not None and e.response.status_code == 404:
1780
+ raise RuntimeError(f"Test request {request_id} not found.")
1781
+ raise self._format_request_error(e)
1782
+
1693
1783
  def download_parquet_file(self,
1694
1784
  cell_data_id: str,
1695
1785
  cycle_ranges: list = None,
@@ -2794,21 +2884,34 @@ class MicantisAPI:
2794
2884
  raise self._format_request_error(e)
2795
2885
 
2796
2886
  def upload_execution_artifact(self,
2797
- execution_id: str,
2798
- file_path: str,
2799
- title: str,
2887
+ execution_id: str = None,
2888
+ file_path: str = None,
2889
+ title: str = None,
2800
2890
  filename: str = None,
2801
- timeout: int = 120) -> dict:
2891
+ timeout: int = 120,
2892
+ mime: str = None) -> dict:
2802
2893
  """
2803
2894
  Upload an artifact (output file) produced by a running Python execution.
2804
2895
 
2896
+ Inside a cloud execution, execution_id can be omitted — it is resolved
2897
+ from the WORKBOOK_EXECUTION_ID environment variable, which the runner
2898
+ always sets. The natural call shape there is::
2899
+
2900
+ api.upload_execution_artifact('/tmp/plot.png', title='My Plot')
2901
+
2902
+ A first positional argument that is not a GUID is treated as file_path
2903
+ (when file_path is not also given), so the line above works even though
2904
+ the first parameter is execution_id.
2905
+
2805
2906
  Automatically reauthenticates if the session is expired or missing.
2806
2907
  Prints an error message and returns None if the request ultimately fails.
2807
2908
 
2808
2909
  Parameters
2809
2910
  ----------
2810
- execution_id : str
2911
+ execution_id : str, optional
2811
2912
  The execution ID (GUID) of the running or completed Python execution.
2913
+ Defaults to the WORKBOOK_EXECUTION_ID environment variable (always
2914
+ set inside cloud executions). Required when running outside one.
2812
2915
  file_path : str
2813
2916
  Local path to the file to upload (PNG, CSV, XLSX, JSON, PDF, etc.).
2814
2917
  Maximum file size: 50 MB.
@@ -2819,6 +2922,10 @@ class MicantisAPI:
2819
2922
  from the title and file extension.
2820
2923
  timeout : int, optional
2821
2924
  Request timeout in seconds. Default: 120.
2925
+ mime : str, optional
2926
+ Content type stored for the artifact (e.g. 'image/png'). If not
2927
+ provided, guessed from the file_path extension, falling back to
2928
+ 'application/octet-stream'.
2822
2929
 
2823
2930
  Returns
2824
2931
  -------
@@ -2834,6 +2941,7 @@ class MicantisAPI:
2834
2941
  title,
2835
2942
  filename,
2836
2943
  timeout,
2944
+ mime,
2837
2945
  )
2838
2946
  except Exception as e:
2839
2947
  print(f"⚠️ upload_execution_artifact failed: {e}")
@@ -2844,28 +2952,61 @@ class MicantisAPI:
2844
2952
  file_path: str,
2845
2953
  title: str,
2846
2954
  filename: str,
2847
- timeout: int) -> dict:
2955
+ timeout: int,
2956
+ mime: str) -> dict:
2848
2957
  """
2849
2958
  Internal helper for upload_execution_artifact. Does not handle retries or printing.
2850
2959
 
2851
2960
  Raises
2852
2961
  ------
2853
2962
  RuntimeError
2854
- If the request fails or authentication is missing.
2963
+ If arguments are missing or invalid, the request fails, or
2964
+ authentication is missing.
2855
2965
  """
2856
2966
  if not self.headers:
2857
2967
  raise RuntimeError("Authentication required. Call authenticate() first.")
2858
2968
 
2859
2969
  self._refresh_token_if_needed()
2860
2970
 
2971
+ # Forgive the natural call shape api.upload_execution_artifact('/tmp/plot.png', title=...):
2972
+ # a non-GUID first argument is the file path, and the execution ID comes from the environment.
2973
+ if execution_id is not None and not _is_guid(execution_id):
2974
+ if file_path is None:
2975
+ file_path = execution_id
2976
+ execution_id = None
2977
+ else:
2978
+ raise RuntimeError(
2979
+ f"execution_id '{execution_id}' is not an execution GUID. Inside a cloud "
2980
+ "execution you can omit execution_id entirely — it is read from the "
2981
+ "WORKBOOK_EXECUTION_ID environment variable automatically."
2982
+ )
2983
+
2984
+ if execution_id is None:
2985
+ execution_id = os.environ.get("WORKBOOK_EXECUTION_ID")
2986
+ if not execution_id:
2987
+ raise RuntimeError(
2988
+ "No execution_id provided and WORKBOOK_EXECUTION_ID is not set. Inside a "
2989
+ "cloud execution the variable is set automatically; outside one, pass "
2990
+ "execution_id explicitly."
2991
+ )
2992
+
2993
+ if not file_path:
2994
+ raise RuntimeError("file_path is required.")
2995
+ if not title:
2996
+ raise RuntimeError("title is required.")
2997
+
2861
2998
  url = f"{self.service_url}/publicapi/v1/python/executions/{execution_id}/artifacts"
2862
2999
 
3000
+ ext = os.path.splitext(file_path)[1].lower()
3001
+ content_type = (mime or _MIME_OVERRIDES.get(ext)
3002
+ or mimetypes.guess_type(file_path)[0] or "application/octet-stream")
3003
+
2863
3004
  try:
2864
3005
  with open(file_path, 'rb') as f:
2865
3006
  form_data = {'title': (None, title)}
2866
3007
  if filename is not None:
2867
3008
  form_data['filename'] = (None, filename)
2868
- form_data['file'] = (os.path.basename(file_path), f)
3009
+ form_data['file'] = (os.path.basename(file_path), f, content_type)
2869
3010
 
2870
3011
  # Exclude Content-Type so requests sets the multipart boundary correctly
2871
3012
  headers = {k: v for k, v in self.headers.items() if k.lower() != 'content-type'}
@@ -2882,6 +3023,114 @@ class MicantisAPI:
2882
3023
  except requests.exceptions.RequestException as e:
2883
3024
  raise self._format_request_error(e)
2884
3025
 
3026
+ def list_attachments(self, timeout: int = 30) -> list:
3027
+ """
3028
+ List the files attached to the Moose conversation that launched this script.
3029
+
3030
+ Only works inside a Moose-triggered cloud execution (uses WORKBOOK_EXECUTION_ID).
3031
+ Automatically reauthenticates if the session is expired or missing. Prints an error
3032
+ and returns None on failure.
3033
+
3034
+ Returns
3035
+ -------
3036
+ list of dict or None
3037
+ One entry per attachment: {"name", "contentType", "sizeBytes"}.
3038
+ """
3039
+ try:
3040
+ return self._retry_on_auth_failure(self._list_attachments, timeout)
3041
+ except Exception as e:
3042
+ print(f"⚠️ list_attachments failed: {e}")
3043
+ return None
3044
+
3045
+ def _list_attachments(self, timeout: int) -> list:
3046
+ if not self.headers:
3047
+ raise RuntimeError("Authentication required. Call authenticate() first.")
3048
+ self._refresh_token_if_needed()
3049
+
3050
+ execution_id = os.environ.get("WORKBOOK_EXECUTION_ID")
3051
+ if not execution_id:
3052
+ raise RuntimeError(
3053
+ "list_attachments is only available inside a cloud execution (WORKBOOK_EXECUTION_ID is not set)."
3054
+ )
3055
+
3056
+ try:
3057
+ r = self.session.get(
3058
+ f"{self.service_url}/publicapi/v1/python/executions/{execution_id}/attachments",
3059
+ headers=self.headers,
3060
+ timeout=timeout,
3061
+ )
3062
+ r.raise_for_status()
3063
+ return r.json()
3064
+ except requests.exceptions.RequestException as e:
3065
+ raise self._format_request_error(e)
3066
+
3067
+ def download_attachment(self, name: str, output_path: str = None,
3068
+ return_type: str = "bytes", timeout: int = 120):
3069
+ """
3070
+ Download a file attached to the Moose conversation that launched this script, by name,
3071
+ WITHOUT inlining its bytes into your script source. Use this for any conversation
3072
+ attachment too large to paste in — inlining data into the script is bounded by the
3073
+ per-turn output budget (a few KB), but this streams the file into the container.
3074
+
3075
+ Only works inside a Moose-triggered cloud execution (uses WORKBOOK_EXECUTION_ID). Call
3076
+ list_attachments() first to get exact names. Automatically reauthenticates if the
3077
+ session is expired or missing. Prints an error and returns None on failure.
3078
+
3079
+ Parameters
3080
+ ----------
3081
+ name : str
3082
+ Exact attachment filename (from list_attachments()).
3083
+ output_path : str, optional
3084
+ Where to save the file when return_type='path'. Defaults to a temp file.
3085
+ return_type : str, optional
3086
+ 'bytes' (default) returns the raw bytes; 'path' writes the file to disk and returns
3087
+ the path (best for large files); 'text' returns a UTF-8-decoded str.
3088
+ timeout : int, optional
3089
+ Request timeout in seconds. Default: 120.
3090
+
3091
+ Returns
3092
+ -------
3093
+ bytes | str | None
3094
+ The attachment content per return_type, or None on failure.
3095
+ """
3096
+ try:
3097
+ return self._retry_on_auth_failure(
3098
+ self._download_attachment, name, output_path, return_type, timeout)
3099
+ except Exception as e:
3100
+ print(f"⚠️ download_attachment failed: {e}")
3101
+ return None
3102
+
3103
+ def _download_attachment(self, name: str, output_path: str, return_type: str, timeout: int):
3104
+ if not self.headers:
3105
+ raise RuntimeError("Authentication required. Call authenticate() first.")
3106
+ self._refresh_token_if_needed()
3107
+
3108
+ execution_id = os.environ.get("WORKBOOK_EXECUTION_ID")
3109
+ if not execution_id:
3110
+ raise RuntimeError(
3111
+ "download_attachment is only available inside a cloud execution (WORKBOOK_EXECUTION_ID is not set)."
3112
+ )
3113
+
3114
+ url = (f"{self.service_url}/publicapi/v1/python/executions/{execution_id}"
3115
+ f"/attachments/{quote(name)}")
3116
+
3117
+ try:
3118
+ with self.session.get(url, headers=self.headers, timeout=timeout, stream=True) as r:
3119
+ r.raise_for_status()
3120
+ if return_type == "path":
3121
+ path = output_path or os.path.join(tempfile.gettempdir(), os.path.basename(name))
3122
+ with open(path, "wb") as f:
3123
+ for chunk in r.iter_content(chunk_size=1024 * 1024):
3124
+ if chunk:
3125
+ f.write(chunk)
3126
+ return path
3127
+ content = r.content
3128
+ if return_type == "text":
3129
+ return content.decode("utf-8", errors="replace")
3130
+ return content
3131
+ except requests.exceptions.RequestException as e:
3132
+ raise self._format_request_error(e)
3133
+
2885
3134
  # ─────────────────────────────────────────
2886
3135
  # Job management (async operation polling)
2887
3136
  # ─────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: micantis
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Package to simplify Micantis API usage
5
5
  Author-email: Mykela DeLuca <mykela.deluca@micantis.io>
6
6
  License-Expression: MIT
@@ -500,6 +500,24 @@ print(f"Retrieved {len(combined_df)} test requests")
500
500
  combined_df.head()
501
501
  ```
502
502
 
503
+ ### Get Test Request Member Cells
504
+ Retrieve the actual cells linked to a test request. Note that the `tests` array from `get_test_request()` is the test plan (names and quantities) — it contains no cell IDs.
505
+
506
+ ```python
507
+ # DataFrame with one row per member cell (default)
508
+ cells_df = api.get_test_request_cells(request_id)
509
+ cells_df.head() # columns: id, name, updated, cycleCount, hasGraphData
510
+
511
+ # Raw list of dicts
512
+ cells = api.get_test_request_cells(request_id, return_format='list')
513
+ ```
514
+
515
+ ```python
516
+ # Use the cell IDs directly — e.g. fetch metadata for every cell in the request
517
+ cell_ids = cells_df['id'].to_list()
518
+ metadata_df = api.get_cell_metadata(cell_ids=cell_ids, metadata=["Cell height"])
519
+ ```
520
+
503
521
  ## Write Cell Metadata
504
522
  Micantis lets you programmatically assign or update metadata for each cell using either:
505
523
  - the built-in field `"name"` (renames the cell) or `"dataSourceKey"`
@@ -1013,14 +1031,16 @@ for group in dupes:
1013
1031
  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
1032
 
1015
1033
  #### Parameters
1016
- - `execution_id`: **str (required)**
1017
- The execution ID (GUID) of the running or completed execution
1034
+ - `execution_id`: **str (optional inside cloud executions)**
1035
+ 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
1036
  - `file_path`: **str (required)**
1019
1037
  Local path to the file to upload. Maximum size: 50 MB
1020
1038
  - `title`: **str (required)**
1021
1039
  Human-readable title for the artifact
1022
1040
  - `filename`: **str (optional)**
1023
1041
  Override the filename stored on the server. If not provided, derived from the title and file extension
1042
+ - `mime`: **str (optional)**
1043
+ 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
1044
 
1025
1045
  #### Returns
1026
1046
  - Dictionary with `'name'`, `'title'`, `'contentType'`, and `'sizeBytes'`
@@ -1028,22 +1048,19 @@ Attach an output file (PNG, CSV, XLSX, etc.) to a running or completed Python sc
1028
1048
  #### 📘 Examples
1029
1049
 
1030
1050
  ```python
1031
- # Upload a plot generated during a Python execution
1032
- result = api.upload_execution_artifact(
1033
- execution_id="your-execution-guid",
1034
- file_path="voltage_plot.png",
1035
- title="Voltage vs Time"
1036
- )
1051
+ # Inside a cloud execution execution_id is resolved from the environment
1052
+ result = api.upload_execution_artifact("voltage_plot.png", title="Voltage vs Time")
1037
1053
  print(f"Uploaded: {result['name']} ({result['sizeBytes']:,} bytes)")
1038
1054
  ```
1039
1055
 
1040
1056
  ```python
1041
- # Upload a CSV results file with a custom filename
1057
+ # Explicit execution ID with a custom filename and content type
1042
1058
  result = api.upload_execution_artifact(
1043
1059
  execution_id="your-execution-guid",
1044
1060
  file_path="results.csv",
1045
1061
  title="Cycle Summary Results",
1046
- filename="cycle_summary_2025.csv"
1062
+ filename="cycle_summary_2025.csv",
1063
+ mime="text/csv"
1047
1064
  )
1048
1065
  ```
1049
1066
 
@@ -9,4 +9,6 @@ micantis.egg-info/SOURCES.txt
9
9
  micantis.egg-info/dependency_links.txt
10
10
  micantis.egg-info/requires.txt
11
11
  micantis.egg-info/top_level.txt
12
- tests/test_batch_update_result.py
12
+ tests/test_batch_update_result.py
13
+ tests/test_get_test_request_cells.py
14
+ tests/test_upload_execution_artifact.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "micantis"
3
- version = "1.2.4"
3
+ version = "1.2.6"
4
4
  description = "Package to simplify Micantis API usage"
5
5
  authors = [
6
6
  { name = "Mykela DeLuca", email = "mykela.deluca@micantis.io" }
@@ -0,0 +1,88 @@
1
+ """
2
+ Unit tests for get_test_request_cells.
3
+
4
+ Mocks the HTTP layer so it can run anywhere without a live server. Exercises:
5
+ - Default 'df' return format → DataFrame with one row per member cell
6
+ - Non-'df' return format → raw item list
7
+ - Empty membership → empty DataFrame
8
+ - 404 → P-03 convention: printed "not found" error, returns None
9
+
10
+ Run: pytest
11
+ """
12
+ import pandas as pd
13
+ import requests
14
+
15
+ from .test_batch_update_result import _make_api, _mock_response
16
+
17
+ REQUEST_ID = "11111111-2222-3333-4444-555555555555"
18
+
19
+ CELLS_BODY = {
20
+ "count": 2,
21
+ "items": [
22
+ {
23
+ "id": "aaaaaaaa-0000-0000-0000-000000000001",
24
+ "name": "IQC-EVE-20RM-HLM7-0001",
25
+ "updated": "2026-05-01T00:00:00Z",
26
+ "cycleCount": 12,
27
+ "hasGraphData": True,
28
+ },
29
+ {
30
+ "id": "aaaaaaaa-0000-0000-0000-000000000002",
31
+ "name": "IQC-EVE-20RM-HLM7-0002",
32
+ "updated": "2026-05-02T00:00:00Z",
33
+ "cycleCount": 0,
34
+ "hasGraphData": False,
35
+ },
36
+ ],
37
+ }
38
+
39
+
40
+ def test_default_df_returns_one_row_per_cell():
41
+ api = _make_api()
42
+ api.session.get.return_value = _mock_response(200, CELLS_BODY)
43
+
44
+ result = api.get_test_request_cells(REQUEST_ID)
45
+
46
+ assert isinstance(result, pd.DataFrame)
47
+ assert len(result) == 2
48
+ assert list(result.columns) == ["id", "name", "updated", "cycleCount", "hasGraphData"]
49
+ assert result["name"].tolist() == ["IQC-EVE-20RM-HLM7-0001", "IQC-EVE-20RM-HLM7-0002"]
50
+ url = api.session.get.call_args.args[0]
51
+ assert url == f"https://fake.test/publicapi/v1/testmgt/request/get/{REQUEST_ID}/cells"
52
+ print("PASS: default 'df' format → DataFrame with one row per member cell")
53
+
54
+
55
+ def test_raw_list_format_returns_items():
56
+ api = _make_api()
57
+ api.session.get.return_value = _mock_response(200, CELLS_BODY)
58
+
59
+ result = api.get_test_request_cells(REQUEST_ID, return_format="list")
60
+
61
+ assert result == CELLS_BODY["items"]
62
+ print("PASS: non-'df' format → raw item list")
63
+
64
+
65
+ def test_empty_membership_returns_empty_dataframe():
66
+ api = _make_api()
67
+ api.session.get.return_value = _mock_response(200, {"count": 0, "items": []})
68
+
69
+ result = api.get_test_request_cells(REQUEST_ID)
70
+
71
+ assert isinstance(result, pd.DataFrame)
72
+ assert result.empty
73
+ print("PASS: empty membership → empty DataFrame")
74
+
75
+
76
+ def test_404_prints_not_found_and_returns_none(capsys):
77
+ api = _make_api()
78
+ resp = _mock_response(404)
79
+ resp.raise_for_status.side_effect = requests.exceptions.HTTPError(response=resp)
80
+ api.session.get.return_value = resp
81
+
82
+ result = api.get_test_request_cells(REQUEST_ID)
83
+
84
+ assert result is None
85
+ out = capsys.readouterr().out
86
+ assert "get_test_request_cells failed" in out
87
+ assert f"Test request {REQUEST_ID} not found" in out
88
+ print("PASS: 404 → printed not-found error, returns None")
@@ -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