nominal 1.96.0__py3-none-any.whl → 1.97.0__py3-none-any.whl

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.
CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.97.0](https://github.com/nominal-io/nominal-client/compare/v1.96.0...v1.97.0) (2025-12-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * add create_workbook_template ([#535](https://github.com/nominal-io/nominal-client/issues/535)) ([9c98975](https://github.com/nominal-io/nominal-client/commit/9c989753828c39e417bafed3db2981444132aeac))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add per-part retries during multipart uploads ([#537](https://github.com/nominal-io/nominal-client/issues/537)) ([ffdbc4a](https://github.com/nominal-io/nominal-client/commit/ffdbc4ac6f82562fb9eff3fed4b254b784aa89bc))
14
+
3
15
  ## [1.96.0](https://github.com/nominal-io/nominal-client/compare/v1.95.0...v1.96.0) (2025-12-03)
4
16
 
5
17
 
@@ -27,29 +27,53 @@ def _sign_and_upload_part_job(
27
27
  upload_id: str,
28
28
  q: Queue[bytes],
29
29
  part: int,
30
+ num_retries: int = 3,
30
31
  ) -> requests.Response:
31
32
  data = q.get()
33
+
32
34
  try:
33
- response = upload_client.sign_part(auth_header, key, part, upload_id)
34
- logger.debug(
35
- "successfully signed multipart upload part",
36
- extra={"key": key, "part": part, "upload_id": upload_id, "response.url": response.url},
37
- )
38
- put_response = requests.put(
39
- response.url,
40
- data=data,
41
- headers=response.headers,
42
- verify=upload_client._verify,
43
- )
44
- logger.debug(
45
- "put multipart upload part",
46
- extra={"url": response.url, "size": len(data), "status_code": put_response.status_code},
35
+ last_ex: Exception | None = None
36
+ for attempt in range(num_retries):
37
+ try:
38
+ log_extras = {"key": key, "part": part, "upload_id": upload_id, "attempt": attempt + 1}
39
+
40
+ logger.debug("Signing part %d for upload", part, extra=log_extras)
41
+ sign_response = upload_client.sign_part(auth_header, key, part, upload_id)
42
+ logger.debug(
43
+ "Successfully signed part %d for upload",
44
+ part,
45
+ extra={"response.url": sign_response.url, **log_extras},
46
+ )
47
+
48
+ logger.debug("Pushing part %d for multipart upload", extra=log_extras)
49
+ put_response = requests.put(
50
+ sign_response.url,
51
+ data=data,
52
+ headers=sign_response.headers,
53
+ verify=upload_client._verify,
54
+ )
55
+ logger.debug(
56
+ "Finished pushing part %d for multipart upload with status %d",
57
+ part,
58
+ put_response.status_code,
59
+ extra={"response.url": put_response.url, **log_extras},
60
+ )
61
+ put_response.raise_for_status()
62
+ return put_response
63
+ except Exception as ex:
64
+ logger.warning(
65
+ "Failed to upload part %d: %s",
66
+ part,
67
+ ex,
68
+ extra=log_extras,
69
+ )
70
+ last_ex = ex
71
+
72
+ raise (
73
+ last_ex
74
+ if last_ex
75
+ else RuntimeError(f"Unknown error uploading part {part} for upload_id={upload_id} and key={key}")
47
76
  )
48
- put_response.raise_for_status()
49
- return put_response
50
- except Exception as e:
51
- logger.exception("error uploading part", exc_info=e, extra={"key": key, "upload_id": upload_id, "part": part})
52
- raise e
53
77
  finally:
54
78
  q.task_done()
55
79
 
nominal/core/client.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import enum
4
4
  import logging
5
+ import uuid
5
6
  from dataclasses import dataclass, field
6
7
  from datetime import datetime, timedelta
7
8
  from io import TextIOBase
@@ -21,10 +22,12 @@ from nominal_api import (
21
22
  scout_catalog,
22
23
  scout_checks_api,
23
24
  scout_datasource_connection_api,
25
+ scout_layout_api,
24
26
  scout_notebook_api,
25
27
  scout_run_api,
26
28
  scout_template_api,
27
29
  scout_video_api,
30
+ scout_workbookcommon_api,
28
31
  secrets_api,
29
32
  storage_datasource_api,
30
33
  )
@@ -1478,3 +1481,52 @@ class NominalClient:
1478
1481
  "nominal.core.NominalClient.create_workbook_from_template",
1479
1482
  "use 'nominal.core.WorkbookTemplate.create_workbook' instead",
1480
1483
  )
1484
+
1485
+ def create_workbook_template(
1486
+ self,
1487
+ title: str,
1488
+ *,
1489
+ description: str | None = None,
1490
+ labels: list[str] | None = None,
1491
+ properties: dict[str, str] | None = None,
1492
+ commit_message: str | None = None,
1493
+ workspace: Workspace | str | None = None,
1494
+ ) -> WorkbookTemplate:
1495
+ """Create an empty workbook template.
1496
+
1497
+ Args:
1498
+ title: Title of the workbook template
1499
+ description: Description of the workbook template
1500
+ labels: Labels to attach to the workbook template
1501
+ properties: Properties to attach to the workbook template
1502
+ commit_message: An optional message to include with the creation of the template
1503
+ workspace: Workspace to create the workbook template in.
1504
+
1505
+ Returns:
1506
+ The created WorkbookTemplate
1507
+ """
1508
+ request = scout_template_api.CreateTemplateRequest(
1509
+ title=title,
1510
+ description=description if description is not None else "",
1511
+ labels=labels if labels is not None else [],
1512
+ properties=properties if properties is not None else {},
1513
+ is_published=False,
1514
+ layout=scout_layout_api.WorkbookLayout(
1515
+ v1=scout_layout_api.WorkbookLayoutV1(
1516
+ root_panel=scout_layout_api.Panel(
1517
+ tabbed=scout_layout_api.TabbedPanel(
1518
+ v1=scout_layout_api.TabbedPanelV1(
1519
+ id=str(uuid.uuid4()),
1520
+ tabs=[],
1521
+ )
1522
+ )
1523
+ )
1524
+ )
1525
+ ),
1526
+ content=scout_workbookcommon_api.WorkbookContent(channel_variables={}, charts={}),
1527
+ message=commit_message if commit_message is not None else "Initial blank workbook template",
1528
+ workspace=self._workspace_rid_for_search(workspace or WorkspaceSearchType.ALL),
1529
+ )
1530
+
1531
+ template = self._clients.template.create(self._clients.auth_header, request)
1532
+ return WorkbookTemplate._from_conjure(self._clients, template)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nominal
3
- Version: 1.96.0
3
+ Version: 1.97.0
4
4
  Summary: Automate Nominal workflows in Python
5
5
  Project-URL: Homepage, https://nominal.io
6
6
  Project-URL: Documentation, https://docs.nominal.io
@@ -1,4 +1,4 @@
1
- CHANGELOG.md,sha256=NZ7acl4CWsmucCYmnmZR2SPA6dRL2FekSVJtIWfAkEY,79763
1
+ CHANGELOG.md,sha256=6LrMT1H8kFV_1cH8Bu_s8Itk_yzsIcAd0UfnMWuHdAU,80317
2
2
  LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
3
3
  README.md,sha256=KKe0dxh_pHXCtB7I9G4qWGQYvot_BZU8yW6MJyuyUHM,311
4
4
  nominal/__init__.py,sha256=rbraORnXUrNn1hywLXM0XwSQCd9UmQt20PDYlsBalfE,2167
@@ -35,7 +35,7 @@ nominal/core/attachment.py,sha256=iJaDyF6JXsKxxBLA03I0WMmQF8U0bA-wRwvXMEhfWLU,42
35
35
  nominal/core/bounds.py,sha256=742BWmGL3FBryRAjoiJRg2N6aVinjYkQLxN7kfnJ40Q,581
36
36
  nominal/core/channel.py,sha256=dbe8wpfMiWqHu98x66w6GOmC9Ro33Wv9AhBVx2DvtVk,18970
37
37
  nominal/core/checklist.py,sha256=rO1RPDYV3o2miPKF7DcCiYpj6bUN-sdtZNhJkXzkfYE,7110
38
- nominal/core/client.py,sha256=5raS_n__qbUjpCqqxAovd6LAAxRoSkpL87GhoSCPCj0,65205
38
+ nominal/core/client.py,sha256=L6IQVEPTiKbOjtbn4G0_R90jbVeOOxpHBHmminGQ3FE,67403
39
39
  nominal/core/connection.py,sha256=ySbPN_a2takVa8wIU9mK4fB6vYLyZnN-qSmXVkLUxAY,5157
40
40
  nominal/core/containerized_extractors.py,sha256=HrcMJzdE-hH66AgYIA0LTeFELsBHa0Sm0vlsKMiIzDU,9501
41
41
  nominal/core/data_review.py,sha256=bEnRsd8LI4x9YOBPcF2H3h5-e12A7Gh8gQfsNUAZmPQ,7922
@@ -63,7 +63,7 @@ nominal/core/_stream/write_stream_base.py,sha256=AxK3fAq3IBjNXZkxYFVXu3dGNWLCBhg
63
63
  nominal/core/_utils/README.md,sha256=kWPQDc6kn-PjXFUsIH9u2nOA3RdGSXCOlxqeJSmUsPA,160
64
64
  nominal/core/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  nominal/core/_utils/api_tools.py,sha256=Z--Et7NjpCH4if72WwGm45EyqyeqK2ambcBrtOSDMrY,2949
66
- nominal/core/_utils/multipart.py,sha256=6bce0kZB510TovD71HnjP-EJr6nas9vfO0VPZB4LVY0,9104
66
+ nominal/core/_utils/multipart.py,sha256=YZb6SDJG9eM9AmSHikMbkEOtxy1DTOi9ggVUVzO0Ur4,9998
67
67
  nominal/core/_utils/multipart_downloader.py,sha256=zS6wxecAZYeWBfiOjtscoUVPaSiUYTI_Ckn4sknSxMM,14275
68
68
  nominal/core/_utils/networking.py,sha256=0cted8IF52WdYAPtiAy9IeosmQkoytXOnazJkdYRkk8,5862
69
69
  nominal/core/_utils/pagination_tools.py,sha256=cEBY1WiA1d3cWJEM0myYF_pX8JdQ_e-5asngVXrUc_Y,12152
@@ -102,8 +102,8 @@ nominal/thirdparty/polars/polars_export_handler.py,sha256=hGCSwXX9dC4MG01CmmjlTb
102
102
  nominal/thirdparty/tdms/__init__.py,sha256=6n2ImFr2Wiil6JM1P5Q7Mpr0VzLcnDkmup_ftNpPq-s,142
103
103
  nominal/thirdparty/tdms/_tdms.py,sha256=eiHFTUviyDPDClckNldjs_jTTSH_sdmboKDq0oIGChQ,8711
104
104
  nominal/ts/__init__.py,sha256=hmd0ENvDhxRnzDKGLxIub6QG8LpcxCgcyAct029CaEs,21442
105
- nominal-1.96.0.dist-info/METADATA,sha256=jq3jplT23Y5wxFaRyMjVL0znHbpFavQSqG6yt00xhyE,1946
106
- nominal-1.96.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
107
- nominal-1.96.0.dist-info/entry_points.txt,sha256=-mCLhxgg9R_lm5efT7vW9wuBH12izvY322R0a3TYxbE,66
108
- nominal-1.96.0.dist-info/licenses/LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
109
- nominal-1.96.0.dist-info/RECORD,,
105
+ nominal-1.97.0.dist-info/METADATA,sha256=f_-Nkovbw63L8TqEce4QVrxF6xRqpH8lOJwSBt97-9g,1946
106
+ nominal-1.97.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
107
+ nominal-1.97.0.dist-info/entry_points.txt,sha256=-mCLhxgg9R_lm5efT7vW9wuBH12izvY322R0a3TYxbE,66
108
+ nominal-1.97.0.dist-info/licenses/LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
109
+ nominal-1.97.0.dist-info/RECORD,,