tofupilot 2.2.2__tar.gz → 2.2.3__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.
Files changed (140) hide show
  1. tofupilot-2.2.3/PKG-INFO +221 -0
  2. tofupilot-2.2.3/README.md +179 -0
  3. {tofupilot-2.2.2 → tofupilot-2.2.3}/pyproject.toml +1 -2
  4. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/__init__.py +1 -1
  5. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/utils/files.py +3 -0
  6. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/__init__.py +9 -13
  7. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/_version.py +2 -2
  8. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/attachments.py +4 -4
  9. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/client_with_error_tracking.py +1 -1
  10. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/__init__.py +15 -21
  11. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/run_createop.py +15 -25
  12. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/pyproject.toml +1 -2
  13. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/sdkconfiguration.py +4 -1
  14. tofupilot-2.2.2/PKG-INFO +0 -633
  15. tofupilot-2.2.2/README.md +0 -591
  16. {tofupilot-2.2.2 → tofupilot-2.2.3}/LICENSE +0 -0
  17. {tofupilot-2.2.2 → tofupilot-2.2.3}/py.typed +0 -0
  18. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/banner.py +0 -0
  19. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/error_tracking.py +0 -0
  20. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/openhtf/__init__.py +0 -0
  21. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/openhtf/tofupilot.py +0 -0
  22. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/openhtf/upload.py +0 -0
  23. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/pytest/__init__.py +0 -0
  24. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/pytest/plugin.py +0 -0
  25. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/pytest/pytest.ini +0 -0
  26. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/telemetry_config.py +0 -0
  27. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/__init__.py +0 -0
  28. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/client.py +0 -0
  29. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/constants/__init__.py +0 -0
  30. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/constants/attachments.py +0 -0
  31. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/constants/requests.py +0 -0
  32. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/models/__init__.py +0 -0
  33. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/models/models.py +0 -0
  34. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/pyproject.toml +0 -0
  35. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/responses/__init__.py +0 -0
  36. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/responses/responses.py +0 -0
  37. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/utils/__init__.py +0 -0
  38. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/utils/dates.py +0 -0
  39. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/utils/logger.py +0 -0
  40. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v1/utils/network.py +0 -0
  41. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/_hooks/__init__.py +0 -0
  42. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/_hooks/sdkhooks.py +0 -0
  43. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/_hooks/types.py +0 -0
  44. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/basesdk.py +0 -0
  45. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/batches.py +0 -0
  46. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/__init__.py +0 -0
  47. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/apierror.py +0 -0
  48. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errorbadgateway.py +0 -0
  49. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errorbadrequest.py +0 -0
  50. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errorconflict.py +0 -0
  51. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errorforbidden.py +0 -0
  52. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errorinternalservererror.py +0 -0
  53. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errornotfound.py +0 -0
  54. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errorunauthorized.py +0 -0
  55. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/errorunprocessablecontent.py +0 -0
  56. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/no_response_error.py +0 -0
  57. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/responsevalidationerror.py +0 -0
  58. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/errors/tofupiloterror.py +0 -0
  59. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/httpclient.py +0 -0
  60. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/attachment_deleteop.py +0 -0
  61. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/attachment_finalizeop.py +0 -0
  62. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/attachment_initializeop.py +0 -0
  63. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/batch_createop.py +0 -0
  64. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/batch_deleteop.py +0 -0
  65. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/batch_getop.py +0 -0
  66. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/batch_listop.py +0 -0
  67. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/batch_updateop.py +0 -0
  68. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errorbadgateway.py +0 -0
  69. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errorbadrequest.py +0 -0
  70. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errorconflict.py +0 -0
  71. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errorforbidden.py +0 -0
  72. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errorinternalservererror.py +0 -0
  73. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errornotfound.py +0 -0
  74. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errorunauthorized.py +0 -0
  75. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/errorunprocessablecontent.py +0 -0
  76. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_createop.py +0 -0
  77. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_createrevisionop.py +0 -0
  78. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_deleteop.py +0 -0
  79. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_deleterevisionop.py +0 -0
  80. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_getop.py +0 -0
  81. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_getrevisionop.py +0 -0
  82. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_listop.py +0 -0
  83. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_updateop.py +0 -0
  84. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/part_updaterevisionop.py +0 -0
  85. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_createop.py +0 -0
  86. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_createversionop.py +0 -0
  87. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_deleteop.py +0 -0
  88. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_deleteversionop.py +0 -0
  89. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_getop.py +0 -0
  90. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_getversionop.py +0 -0
  91. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_listop.py +0 -0
  92. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/procedure_updateop.py +0 -0
  93. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/run_deleteop.py +0 -0
  94. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/run_getop.py +0 -0
  95. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/run_listop.py +0 -0
  96. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/run_updateop.py +0 -0
  97. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/security.py +0 -0
  98. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/station_createop.py +0 -0
  99. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/station_getcurrentop.py +0 -0
  100. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/station_getop.py +0 -0
  101. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/station_listop.py +0 -0
  102. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/station_removeop.py +0 -0
  103. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/station_updateop.py +0 -0
  104. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/unit_addchildop.py +0 -0
  105. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/unit_createop.py +0 -0
  106. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/unit_deleteop.py +0 -0
  107. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/unit_getop.py +0 -0
  108. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/unit_listop.py +0 -0
  109. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/unit_removechildop.py +0 -0
  110. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/unit_updateop.py +0 -0
  111. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/models/user_listop.py +0 -0
  112. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/parts.py +0 -0
  113. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/procedures.py +0 -0
  114. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/py.typed +0 -0
  115. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/revisions.py +0 -0
  116. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/runs.py +0 -0
  117. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/sdk.py +0 -0
  118. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/stations.py +0 -0
  119. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/types/__init__.py +0 -0
  120. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/types/basemodel.py +0 -0
  121. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/units_sdk.py +0 -0
  122. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/user.py +0 -0
  123. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/__init__.py +0 -0
  124. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/annotations.py +0 -0
  125. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/datetimes.py +0 -0
  126. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/enums.py +0 -0
  127. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/eventstreaming.py +0 -0
  128. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/forms.py +0 -0
  129. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/headers.py +0 -0
  130. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/logger.py +0 -0
  131. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/metadata.py +0 -0
  132. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/queryparams.py +0 -0
  133. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/requestbodies.py +0 -0
  134. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/retries.py +0 -0
  135. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/security.py +0 -0
  136. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/serializers.py +0 -0
  137. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/unmarshal_json_response.py +0 -0
  138. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/url.py +0 -0
  139. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/utils/values.py +0 -0
  140. {tofupilot-2.2.2 → tofupilot-2.2.3}/tofupilot/v2/versions.py +0 -0
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: tofupilot
3
+ Version: 2.2.3
4
+ Summary: Official Python client for TofuPilot with OpenHTF integration, real-time streaming and file attachment support
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: automatic,hardware,testing,tofupilot,openhtf,sdk
8
+ Author: TofuPilot Team
9
+ Author-email: hello@tofupilot.com
10
+ Requires-Python: >=3.9.2
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Manufacturing
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Provides-Extra: dev
20
+ Requires-Dist: build ; extra == "dev"
21
+ Requires-Dist: certifi (>=2023.7.22)
22
+ Requires-Dist: httpcore (>=1.0.9)
23
+ Requires-Dist: httpx (>=0.28.1)
24
+ Requires-Dist: mypy (==1.15.0) ; extra == "dev"
25
+ Requires-Dist: openhtf
26
+ Requires-Dist: packaging (>=20.0)
27
+ Requires-Dist: paho-mqtt (>=2.0.0)
28
+ Requires-Dist: posthog (>=3.0.0)
29
+ Requires-Dist: pydantic (>=2.11.2)
30
+ Requires-Dist: pylint (==3.2.3) ; extra == "dev"
31
+ Requires-Dist: pytest (>=6.0.0)
32
+ Requires-Dist: python-dotenv ; extra == "dev"
33
+ Requires-Dist: requests (>=2.25.0)
34
+ Requires-Dist: trycast ; extra == "dev"
35
+ Requires-Dist: twine ; extra == "dev"
36
+ Project-URL: Documentation, https://tofupilot.com/docs
37
+ Project-URL: Homepage, https://github.com/tofupilot/python-client
38
+ Project-URL: Issues, https://github.com/tofupilot/python-client/issues
39
+ Project-URL: Repository, https://github.com/tofupilot/python-client
40
+ Description-Content-Type: text/markdown
41
+
42
+ # TofuPilot Python Client
43
+
44
+ [![PyPI version](https://badge.fury.io/py/tofupilot.svg)](https://badge.fury.io/py/tofupilot)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
46
+ [![Tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/upview/8e092a304bae7d91aa154e19bdf694a4/raw/python-client-tests.json)](https://github.com/tofupilot/python)
47
+
48
+ The official Python client for [TofuPilot](https://tofupilot.com). Integrate your hardware test runs into one app with just a few lines of Python.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install tofupilot
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```python
59
+ import os
60
+ from tofupilot.v2 import TofuPilot
61
+
62
+ with TofuPilot(api_key=os.getenv("TOFUPILOT_API_KEY")) as client:
63
+ run = client.runs.create(
64
+ procedure_id="your-procedure-id",
65
+ serial_number="SN001",
66
+ part_number="PN001",
67
+ outcome="PASS",
68
+ )
69
+ print(f"Run created: {run.id}")
70
+ ```
71
+
72
+ ## Authentication
73
+
74
+ Set your API key as an environment variable:
75
+
76
+ ```bash
77
+ export TOFUPILOT_API_KEY="your-api-key"
78
+ ```
79
+
80
+ Or pass it directly:
81
+
82
+ ```python
83
+ client = TofuPilot(api_key="your-api-key")
84
+ ```
85
+
86
+ To point to a different server (e.g. self-hosted):
87
+
88
+ ```python
89
+ client = TofuPilot(api_key="your-api-key", server_url="https://your-instance.com/api")
90
+ ```
91
+
92
+ ## Available Resources
93
+
94
+ | Resource | Operations |
95
+ |----------|-----------|
96
+ | `client.runs` | list, create, get, delete, update |
97
+ | `client.units` | list, create, get, delete, update, add_child, remove_child |
98
+ | `client.parts` | list, create, get, delete, update |
99
+ | `client.parts.revisions` | create, get, delete, update |
100
+ | `client.procedures` | list, create, get, delete, update |
101
+ | `client.procedures.versions` | create, get, delete |
102
+ | `client.batches` | list, create, get, delete, update |
103
+ | `client.stations` | list, create, get, get_current, remove, update |
104
+ | `client.attachments` | initialize, finalize, delete, upload, download |
105
+ | `client.user` | list |
106
+
107
+ ## Usage Examples
108
+
109
+ ### Create a run with measurements
110
+
111
+ ```python
112
+ from datetime import datetime, timedelta, timezone
113
+
114
+ run = client.runs.create(
115
+ procedure_id=procedure_id,
116
+ serial_number="SN-001",
117
+ part_number="PCB-V1",
118
+ outcome="PASS",
119
+ started_at=datetime.now(timezone.utc) - timedelta(minutes=5),
120
+ ended_at=datetime.now(timezone.utc),
121
+ phases=[{
122
+ "name": "Voltage Test",
123
+ "outcome": "PASS",
124
+ "started_at": datetime.now(timezone.utc) - timedelta(minutes=5),
125
+ "ended_at": datetime.now(timezone.utc),
126
+ "measurements": [{
127
+ "name": "Output Voltage",
128
+ "outcome": "PASS",
129
+ "measured_value": 3.3,
130
+ "units": "V",
131
+ "validators": [
132
+ {"operator": ">=", "expected_value": 3.0},
133
+ {"operator": "<=", "expected_value": 3.6},
134
+ ],
135
+ }],
136
+ }],
137
+ )
138
+ ```
139
+
140
+ ### List and filter runs
141
+
142
+ ```python
143
+ result = client.runs.list(
144
+ part_numbers=["PCB-V1"],
145
+ outcomes=["PASS"],
146
+ limit=10,
147
+ )
148
+
149
+ for run in result.data:
150
+ print(f"{run.id} — {run.unit.serial_number}")
151
+ ```
152
+
153
+ ### Manage units and sub-units
154
+
155
+ ```python
156
+ # Create part and revision
157
+ client.parts.create(number="PCB-V1", name="Main Board")
158
+ client.parts.revisions.create(part_number="PCB-V1", number="REV-A")
159
+
160
+ # Create units
161
+ client.units.create(serial_number="PARENT-001", part_number="PCB-V1", revision_number="REV-A")
162
+ client.units.create(serial_number="CHILD-001", part_number="PCB-V1", revision_number="REV-A")
163
+
164
+ # Link parent-child
165
+ client.units.add_child(serial_number="PARENT-001", child_serial_number="CHILD-001")
166
+ ```
167
+
168
+ ### Upload and download attachments
169
+
170
+ ```python
171
+ # Upload a file (one line)
172
+ attachment_id = client.attachments.upload("report.pdf")
173
+
174
+ # Link to a run
175
+ client.runs.update(id=run_id, attachments=[attachment_id])
176
+
177
+ # Download an attachment
178
+ client.attachments.download(attachment, dest="local-copy.pdf")
179
+ ```
180
+
181
+ ## Error Handling
182
+
183
+ ```python
184
+ from tofupilot.v2.models.errors import ErrorNOTFOUND, ErrorBADREQUEST
185
+
186
+ try:
187
+ client.runs.get(id="nonexistent-id")
188
+ except ErrorNOTFOUND as e:
189
+ print(f"Not found: {e.message}")
190
+ except ErrorBADREQUEST as e:
191
+ print(f"Bad request: {e.message}")
192
+ ```
193
+
194
+ | Exception | Status Code |
195
+ |-----------|------------|
196
+ | `ErrorBADREQUEST` | 400 |
197
+ | `ErrorUNAUTHORIZED` | 401 |
198
+ | `ErrorFORBIDDEN` | 403 |
199
+ | `ErrorNOTFOUND` | 404 |
200
+ | `ErrorCONFLICT` | 409 |
201
+ | `ErrorUNPROCESSABLECONTENT` | 422 |
202
+ | `ErrorINTERNALSERVERERROR` | 500 |
203
+
204
+ ## Running Tests
205
+
206
+ ```bash
207
+ cd clients/python-speakeasy
208
+ cp tests/.env.local.example tests/.env.local # Set your API key and URL
209
+ python -m pytest tests/v2/
210
+ ```
211
+
212
+ ## Documentation
213
+
214
+ - [Getting Started](https://tofupilot.com/docs/dashboard)
215
+ - [API Reference](https://tofupilot.com/docs/dashboard/api/v2)
216
+ - [Changelog](https://tofupilot.com/changelog)
217
+
218
+ ## License
219
+
220
+ MIT
221
+
@@ -0,0 +1,179 @@
1
+ # TofuPilot Python Client
2
+
3
+ [![PyPI version](https://badge.fury.io/py/tofupilot.svg)](https://badge.fury.io/py/tofupilot)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/upview/8e092a304bae7d91aa154e19bdf694a4/raw/python-client-tests.json)](https://github.com/tofupilot/python)
6
+
7
+ The official Python client for [TofuPilot](https://tofupilot.com). Integrate your hardware test runs into one app with just a few lines of Python.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install tofupilot
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ import os
19
+ from tofupilot.v2 import TofuPilot
20
+
21
+ with TofuPilot(api_key=os.getenv("TOFUPILOT_API_KEY")) as client:
22
+ run = client.runs.create(
23
+ procedure_id="your-procedure-id",
24
+ serial_number="SN001",
25
+ part_number="PN001",
26
+ outcome="PASS",
27
+ )
28
+ print(f"Run created: {run.id}")
29
+ ```
30
+
31
+ ## Authentication
32
+
33
+ Set your API key as an environment variable:
34
+
35
+ ```bash
36
+ export TOFUPILOT_API_KEY="your-api-key"
37
+ ```
38
+
39
+ Or pass it directly:
40
+
41
+ ```python
42
+ client = TofuPilot(api_key="your-api-key")
43
+ ```
44
+
45
+ To point to a different server (e.g. self-hosted):
46
+
47
+ ```python
48
+ client = TofuPilot(api_key="your-api-key", server_url="https://your-instance.com/api")
49
+ ```
50
+
51
+ ## Available Resources
52
+
53
+ | Resource | Operations |
54
+ |----------|-----------|
55
+ | `client.runs` | list, create, get, delete, update |
56
+ | `client.units` | list, create, get, delete, update, add_child, remove_child |
57
+ | `client.parts` | list, create, get, delete, update |
58
+ | `client.parts.revisions` | create, get, delete, update |
59
+ | `client.procedures` | list, create, get, delete, update |
60
+ | `client.procedures.versions` | create, get, delete |
61
+ | `client.batches` | list, create, get, delete, update |
62
+ | `client.stations` | list, create, get, get_current, remove, update |
63
+ | `client.attachments` | initialize, finalize, delete, upload, download |
64
+ | `client.user` | list |
65
+
66
+ ## Usage Examples
67
+
68
+ ### Create a run with measurements
69
+
70
+ ```python
71
+ from datetime import datetime, timedelta, timezone
72
+
73
+ run = client.runs.create(
74
+ procedure_id=procedure_id,
75
+ serial_number="SN-001",
76
+ part_number="PCB-V1",
77
+ outcome="PASS",
78
+ started_at=datetime.now(timezone.utc) - timedelta(minutes=5),
79
+ ended_at=datetime.now(timezone.utc),
80
+ phases=[{
81
+ "name": "Voltage Test",
82
+ "outcome": "PASS",
83
+ "started_at": datetime.now(timezone.utc) - timedelta(minutes=5),
84
+ "ended_at": datetime.now(timezone.utc),
85
+ "measurements": [{
86
+ "name": "Output Voltage",
87
+ "outcome": "PASS",
88
+ "measured_value": 3.3,
89
+ "units": "V",
90
+ "validators": [
91
+ {"operator": ">=", "expected_value": 3.0},
92
+ {"operator": "<=", "expected_value": 3.6},
93
+ ],
94
+ }],
95
+ }],
96
+ )
97
+ ```
98
+
99
+ ### List and filter runs
100
+
101
+ ```python
102
+ result = client.runs.list(
103
+ part_numbers=["PCB-V1"],
104
+ outcomes=["PASS"],
105
+ limit=10,
106
+ )
107
+
108
+ for run in result.data:
109
+ print(f"{run.id} — {run.unit.serial_number}")
110
+ ```
111
+
112
+ ### Manage units and sub-units
113
+
114
+ ```python
115
+ # Create part and revision
116
+ client.parts.create(number="PCB-V1", name="Main Board")
117
+ client.parts.revisions.create(part_number="PCB-V1", number="REV-A")
118
+
119
+ # Create units
120
+ client.units.create(serial_number="PARENT-001", part_number="PCB-V1", revision_number="REV-A")
121
+ client.units.create(serial_number="CHILD-001", part_number="PCB-V1", revision_number="REV-A")
122
+
123
+ # Link parent-child
124
+ client.units.add_child(serial_number="PARENT-001", child_serial_number="CHILD-001")
125
+ ```
126
+
127
+ ### Upload and download attachments
128
+
129
+ ```python
130
+ # Upload a file (one line)
131
+ attachment_id = client.attachments.upload("report.pdf")
132
+
133
+ # Link to a run
134
+ client.runs.update(id=run_id, attachments=[attachment_id])
135
+
136
+ # Download an attachment
137
+ client.attachments.download(attachment, dest="local-copy.pdf")
138
+ ```
139
+
140
+ ## Error Handling
141
+
142
+ ```python
143
+ from tofupilot.v2.models.errors import ErrorNOTFOUND, ErrorBADREQUEST
144
+
145
+ try:
146
+ client.runs.get(id="nonexistent-id")
147
+ except ErrorNOTFOUND as e:
148
+ print(f"Not found: {e.message}")
149
+ except ErrorBADREQUEST as e:
150
+ print(f"Bad request: {e.message}")
151
+ ```
152
+
153
+ | Exception | Status Code |
154
+ |-----------|------------|
155
+ | `ErrorBADREQUEST` | 400 |
156
+ | `ErrorUNAUTHORIZED` | 401 |
157
+ | `ErrorFORBIDDEN` | 403 |
158
+ | `ErrorNOTFOUND` | 404 |
159
+ | `ErrorCONFLICT` | 409 |
160
+ | `ErrorUNPROCESSABLECONTENT` | 422 |
161
+ | `ErrorINTERNALSERVERERROR` | 500 |
162
+
163
+ ## Running Tests
164
+
165
+ ```bash
166
+ cd clients/python-speakeasy
167
+ cp tests/.env.local.example tests/.env.local # Set your API key and URL
168
+ python -m pytest tests/v2/
169
+ ```
170
+
171
+ ## Documentation
172
+
173
+ - [Getting Started](https://tofupilot.com/docs/dashboard)
174
+ - [API Reference](https://tofupilot.com/docs/dashboard/api/v2)
175
+ - [Changelog](https://tofupilot.com/changelog)
176
+
177
+ ## License
178
+
179
+ MIT
@@ -1,10 +1,9 @@
1
1
  [project]
2
2
  name = "tofupilot"
3
- version = "2.2.2"
3
+ version = "2.2.3"
4
4
  description = "Official Python client for TofuPilot with OpenHTF integration, real-time streaming and file attachment support"
5
5
  authors = [
6
6
  { name = "TofuPilot Team", email = "hello@tofupilot.com" },
7
- { name = "Speakeasy" }
8
7
  ]
9
8
  readme = "README.md"
10
9
  license = { text = "MIT" }
@@ -2,7 +2,7 @@
2
2
 
3
3
  This package provides both v1 and v2 API clients:
4
4
  - v1: Legacy client for backward compatibility
5
- - v2: New Speakeasy-generated SDK with enhanced features and error tracking
5
+ - v2: Current SDK with enhanced features and error tracking
6
6
 
7
7
  For new projects, we recommend using the v2 client.
8
8
 
@@ -96,6 +96,9 @@ def upload_file(
96
96
  upload_url = response_json.get("uploadUrl")
97
97
  upload_id = response_json.get("id")
98
98
 
99
+ if not upload_id or not upload_url:
100
+ raise ValueError(f"Upload initialization failed: missing 'id' or 'uploadUrl' in response: {response_json}")
101
+
99
102
  # File storing
100
103
  with open(file_path, "rb") as file:
101
104
  content_type, _ = mimetypes.guess_type(file_path) or "application/octet-stream"
@@ -1,4 +1,8 @@
1
- """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
1
+ # Import the error tracking enhanced client by default
2
+ from .client_with_error_tracking import TofuPilotWithErrorTracking as TofuPilot
3
+ # Still export the base SDK for those who want it
4
+ from .sdk import TofuPilot as TofuPilotBase
5
+ from .sdkconfiguration import *
2
6
 
3
7
  from ._version import (
4
8
  __title__,
@@ -7,25 +11,17 @@ from ._version import (
7
11
  __gen_version__,
8
12
  __user_agent__,
9
13
  )
10
- # Import the error tracking enhanced client by default
11
- from .client_with_error_tracking import TofuPilotWithErrorTracking as TofuPilot
12
- # Still export the base SDK for those who want it
13
- from .sdk import TofuPilot as TofuPilotBase
14
- from .sdkconfiguration import *
15
-
16
14
 
17
15
  VERSION: str = __version__
18
16
  OPENAPI_DOC_VERSION = __openapi_doc_version__
19
- SPEAKEASY_GENERATOR_VERSION = __gen_version__
17
+ GENERATOR_VERSION = __gen_version__
20
18
  USER_AGENT = __user_agent__
21
19
 
22
- # Export both versions
23
20
  __all__ = [
24
- "TofuPilot", # Default with error tracking
25
- "TofuPilotBase", # Base without error tracking
26
- # Version info
21
+ "TofuPilot",
22
+ "TofuPilotBase",
27
23
  "VERSION",
28
24
  "OPENAPI_DOC_VERSION",
29
- "SPEAKEASY_GENERATOR_VERSION",
25
+ "GENERATOR_VERSION",
30
26
  "USER_AGENT",
31
27
  ]
@@ -3,10 +3,10 @@
3
3
  import importlib.metadata
4
4
 
5
5
  __title__: str = "tofupilot.v2"
6
- __version__: str = "0.7.12"
6
+ __version__: str = "2.2.3"
7
7
  __openapi_doc_version__: str = "2.0.0"
8
8
  __gen_version__: str = "2.657.1"
9
- __user_agent__: str = "speakeasy-sdk/python 0.7.12 2.657.1 2.0.0 tofupilot.v2"
9
+ __user_agent__: str = "speakeasy-sdk/python 2.2.3 2.657.1 2.0.0 tofupilot.v2"
10
10
 
11
11
  try:
12
12
  if __package__ is not None:
@@ -251,7 +251,7 @@ class Attachments(BaseSDK):
251
251
  ) -> models.AttachmentDeleteResponse:
252
252
  r"""Delete attachments
253
253
 
254
- Permanently delete attachments by their IDs and unlink them from any associated runs or units.
254
+ Permanently delete attachments by their IDs and unlink them from any associated runs or units. Removes files from storage and clears all references.
255
255
 
256
256
  :param ids: Upload IDs to delete
257
257
  :param retries: Override the default retry configuration for this method
@@ -353,7 +353,7 @@ class Attachments(BaseSDK):
353
353
  ) -> models.AttachmentDeleteResponse:
354
354
  r"""Delete attachments
355
355
 
356
- Permanently delete attachments by their IDs and unlink them from any associated runs or units.
356
+ Permanently delete attachments by their IDs and unlink them from any associated runs or units. Removes files from storage and clears all references.
357
357
 
358
358
  :param ids: Upload IDs to delete
359
359
  :param retries: Override the default retry configuration for this method
@@ -455,7 +455,7 @@ class Attachments(BaseSDK):
455
455
  ) -> models.AttachmentFinalizeResponse:
456
456
  r"""Finalize upload
457
457
 
458
- Finalize a file upload after uploading to the pre-signed URL. Validates the file and records its metadata. Link the attachment to a run or unit using Update Run or Update Unit.
458
+ Finalize a file upload after uploading to the pre-signed URL. Validates the file and records its metadata.
459
459
 
460
460
  :param id: ID of the upload to finalize
461
461
  :param retries: Override the default retry configuration for this method
@@ -552,7 +552,7 @@ class Attachments(BaseSDK):
552
552
  ) -> models.AttachmentFinalizeResponse:
553
553
  r"""Finalize upload
554
554
 
555
- Finalize a file upload after uploading to the pre-signed URL. Validates the file and records its metadata. Link the attachment to a run or unit using Update Run or Update Unit.
555
+ Finalize a file upload after uploading to the pre-signed URL. Validates the file and records its metadata.
556
556
 
557
557
  :param id: ID of the upload to finalize
558
558
  :param retries: Override the default retry configuration for this method
@@ -99,7 +99,7 @@ class _AttachmentsWithUpload(_ResourceWithBetterErrors):
99
99
  resp = httpx.put(init.upload_url, content=f.read(), headers={"Content-Type": content_type})
100
100
  if resp.status_code != 200:
101
101
  raise RuntimeError(f"File upload failed with status {resp.status_code}")
102
- self._resource.finalize(id=init.id, request_body={})
102
+ self._resource.finalize(id=init.id)
103
103
  return init.id
104
104
 
105
105
  def download(self, attachment, dest: Union[str, Path, None] = None) -> Path:
@@ -263,6 +263,8 @@ if TYPE_CHECKING:
263
263
  RunCreateAggregationValidator,
264
264
  RunCreateAggregationValidatorOutcome,
265
265
  RunCreateAggregationValidatorTypedDict,
266
+ RunCreateExpectedValue,
267
+ RunCreateExpectedValueTypedDict,
266
268
  RunCreateLevel,
267
269
  RunCreateLog,
268
270
  RunCreateLogTypedDict,
@@ -283,15 +285,11 @@ if TYPE_CHECKING:
283
285
  RunCreateResponseTypedDict,
284
286
  RunCreateUnits,
285
287
  RunCreateUnitsTypedDict,
288
+ RunCreateValidator,
289
+ RunCreateValidatorOutcome,
290
+ RunCreateValidatorTypedDict,
286
291
  RunCreateValue,
287
292
  RunCreateValueTypedDict,
288
- Validators,
289
- ValidatorsExpectedValue,
290
- ValidatorsExpectedValueTypedDict,
291
- ValidatorsOutcome,
292
- ValidatorsTypedDict,
293
- ValidatorsUnion,
294
- ValidatorsUnionTypedDict,
295
293
  XAxis,
296
294
  XAxisAggregation,
297
295
  XAxisAggregationExpectedValue,
@@ -857,6 +855,8 @@ __all__ = [
857
855
  "RunCreateAggregationValidator",
858
856
  "RunCreateAggregationValidatorOutcome",
859
857
  "RunCreateAggregationValidatorTypedDict",
858
+ "RunCreateExpectedValue",
859
+ "RunCreateExpectedValueTypedDict",
860
860
  "RunCreateLevel",
861
861
  "RunCreateLog",
862
862
  "RunCreateLogTypedDict",
@@ -877,6 +877,9 @@ __all__ = [
877
877
  "RunCreateResponseTypedDict",
878
878
  "RunCreateUnits",
879
879
  "RunCreateUnitsTypedDict",
880
+ "RunCreateValidator",
881
+ "RunCreateValidatorOutcome",
882
+ "RunCreateValidatorTypedDict",
880
883
  "RunCreateValue",
881
884
  "RunCreateValueTypedDict",
882
885
  "RunDeleteRequest",
@@ -1111,13 +1114,6 @@ __all__ = [
1111
1114
  "UserListRequestTypedDict",
1112
1115
  "UserListResponse",
1113
1116
  "UserListResponseTypedDict",
1114
- "Validators",
1115
- "ValidatorsExpectedValue",
1116
- "ValidatorsExpectedValueTypedDict",
1117
- "ValidatorsOutcome",
1118
- "ValidatorsTypedDict",
1119
- "ValidatorsUnion",
1120
- "ValidatorsUnionTypedDict",
1121
1117
  "XAxis",
1122
1118
  "XAxisAggregation",
1123
1119
  "XAxisAggregationExpectedValue",
@@ -1362,6 +1358,8 @@ _dynamic_imports: dict[str, str] = {
1362
1358
  "RunCreateAggregationValidator": ".run_createop",
1363
1359
  "RunCreateAggregationValidatorOutcome": ".run_createop",
1364
1360
  "RunCreateAggregationValidatorTypedDict": ".run_createop",
1361
+ "RunCreateExpectedValue": ".run_createop",
1362
+ "RunCreateExpectedValueTypedDict": ".run_createop",
1365
1363
  "RunCreateLevel": ".run_createop",
1366
1364
  "RunCreateLog": ".run_createop",
1367
1365
  "RunCreateLogTypedDict": ".run_createop",
@@ -1382,15 +1380,11 @@ _dynamic_imports: dict[str, str] = {
1382
1380
  "RunCreateResponseTypedDict": ".run_createop",
1383
1381
  "RunCreateUnits": ".run_createop",
1384
1382
  "RunCreateUnitsTypedDict": ".run_createop",
1383
+ "RunCreateValidator": ".run_createop",
1384
+ "RunCreateValidatorOutcome": ".run_createop",
1385
+ "RunCreateValidatorTypedDict": ".run_createop",
1385
1386
  "RunCreateValue": ".run_createop",
1386
1387
  "RunCreateValueTypedDict": ".run_createop",
1387
- "Validators": ".run_createop",
1388
- "ValidatorsExpectedValue": ".run_createop",
1389
- "ValidatorsExpectedValueTypedDict": ".run_createop",
1390
- "ValidatorsOutcome": ".run_createop",
1391
- "ValidatorsTypedDict": ".run_createop",
1392
- "ValidatorsUnion": ".run_createop",
1393
- "ValidatorsUnionTypedDict": ".run_createop",
1394
1388
  "XAxis": ".run_createop",
1395
1389
  "XAxisAggregation": ".run_createop",
1396
1390
  "XAxisAggregationExpectedValue": ".run_createop",