onshape-sdk-pr 0.1.2__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 (33) hide show
  1. onshape_sdk_pr-0.1.2/LICENSE +21 -0
  2. onshape_sdk_pr-0.1.2/PKG-INFO +106 -0
  3. onshape_sdk_pr-0.1.2/README.md +84 -0
  4. onshape_sdk_pr-0.1.2/onshape_sdk/__init__.py +101 -0
  5. onshape_sdk_pr-0.1.2/onshape_sdk/api_version.py +3 -0
  6. onshape_sdk_pr-0.1.2/onshape_sdk/assemblies.py +156 -0
  7. onshape_sdk_pr-0.1.2/onshape_sdk/auth.py +59 -0
  8. onshape_sdk_pr-0.1.2/onshape_sdk/blob_elements.py +81 -0
  9. onshape_sdk_pr-0.1.2/onshape_sdk/client.py +225 -0
  10. onshape_sdk_pr-0.1.2/onshape_sdk/client_routines.py +32 -0
  11. onshape_sdk_pr-0.1.2/onshape_sdk/documents.py +140 -0
  12. onshape_sdk_pr-0.1.2/onshape_sdk/drawings.py +63 -0
  13. onshape_sdk_pr-0.1.2/onshape_sdk/ids.py +20 -0
  14. onshape_sdk_pr-0.1.2/onshape_sdk/main.py +55 -0
  15. onshape_sdk_pr-0.1.2/onshape_sdk/metadata.py +68 -0
  16. onshape_sdk_pr-0.1.2/onshape_sdk/part_studios.py +243 -0
  17. onshape_sdk_pr-0.1.2/onshape_sdk/parts.py +119 -0
  18. onshape_sdk_pr-0.1.2/onshape_sdk/py.typed +1 -0
  19. onshape_sdk_pr-0.1.2/onshape_sdk/settings.py +38 -0
  20. onshape_sdk_pr-0.1.2/onshape_sdk/thumbnails.py +61 -0
  21. onshape_sdk_pr-0.1.2/onshape_sdk/translations.py +239 -0
  22. onshape_sdk_pr-0.1.2/onshape_sdk/types.py +593 -0
  23. onshape_sdk_pr-0.1.2/onshape_sdk/utils.py +93 -0
  24. onshape_sdk_pr-0.1.2/onshape_sdk/webhooks.py +100 -0
  25. onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/PKG-INFO +106 -0
  26. onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/SOURCES.txt +31 -0
  27. onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/dependency_links.txt +1 -0
  28. onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/entry_points.txt +2 -0
  29. onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/requires.txt +5 -0
  30. onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/top_level.txt +1 -0
  31. onshape_sdk_pr-0.1.2/pyproject.toml +41 -0
  32. onshape_sdk_pr-0.1.2/setup.cfg +4 -0
  33. onshape_sdk_pr-0.1.2/tests/test_onshape.py +74 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zach Sharma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: onshape-sdk-pr
3
+ Version: 0.1.2
4
+ Summary: A lightweight, typed Python SDK for the Onshape REST API
5
+ Author-email: Zach Sharma <zachariah.sharma@gmail.com>, PawRequest <nomail@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pawrequest/onshape-sdk
8
+ Keywords: onshape,cad,api,sdk,rest
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Requires-Python: >=3.13
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: httpx>=0.28.1
17
+ Requires-Dist: pydantic>=2.14.2
18
+ Requires-Dist: pydantic-settings>=2.16.2
19
+ Requires-Dist: python-dotenv>=1.0
20
+ Requires-Dist: requests>=2.28
21
+ Dynamic: license-file
22
+
23
+ # onshape-sdk-pawrequest
24
+
25
+ A lightweight, typed Python SDK for the [Onshape REST API](https://onshape-public.github.io/docs/).
26
+
27
+ Upstream Documentation: [https://zachariahsharma.github.io/onshape-sdk/](https://zachariahsharma.github.io/onshape-sdk/)
28
+
29
+ Upstream didn't work for some routes and it's api_version is old, so i forked.
30
+
31
+ I have not tested all routes, ymmv.
32
+
33
+ Major Fork changes:
34
+
35
+ - Updated API version from 6 to 10
36
+ - Fixed some broken routes
37
+ - Added pydantic-settings
38
+ - Changed the auth setup. (`onshape_sdk.auth.OnshapeHmacAuth`)
39
+
40
+ ## Install
41
+
42
+ ```
43
+ git clone <rep-url>
44
+ ```
45
+
46
+ or don't and use uv:
47
+
48
+ ```shell
49
+ uv tool run --from git+https://github.com/pawrequest/onshape-sdk onshape-sdk export
50
+ ```
51
+
52
+ ### Configuration
53
+
54
+ Create Onshape API access and secret keys via the developer portal on OnShape's website.
55
+
56
+ PydanticSettings are built from .env file or environment variables.
57
+ Place a .env with required inputs in default location "<project_root>/.env" (sibling of this readme)
58
+ Or do one of:
59
+
60
+ - Pass custom .env locations to `OnshapeSettings(_env_file=<path_to_env_file>)`,
61
+ - Set the required inputs in your environment,
62
+ - Instantiate `OnshapeSettings` with the required inputs.
63
+
64
+ #### required inputs
65
+
66
+ ONSHAPE_ACCESS_KEY=your_access_key
67
+ ONSHAPE_SECRET_KEY=your_secret_key
68
+
69
+ ## Usage
70
+
71
+ ### cli
72
+
73
+ cli entry is via uv from `src/main.py` - see there fore details.
74
+ uv sets `onshape-sdk` as the program name
75
+ `onshape-sdk <command> <command options>`
76
+ atm i mostly use this project as an exporter to speed up iterating from onshape to slicer.
77
+
78
+ Commands:
79
+
80
+ - Export
81
+
82
+ ```
83
+ onshape-sdk export /
84
+ -u <url-to-partstudio> /
85
+ -o <optional output file or dir> / # save exported stl bytes
86
+ -pf <optional part-name filter>
87
+
88
+ ```
89
+
90
+ ### python
91
+
92
+ ```python
93
+ from onshape_sdk import OnshapeClient, OnshapeIdsTup, OnshapeSettings
94
+
95
+ settings = OnshapeSettings() # optional - otherwise read default .env location or env_vars
96
+ client = OnshapeClient(settings) # or just `OnshapeClient() for defaults`
97
+ url = '<ONSHAPE_URL>'
98
+ ids = OnshapeIdsTup.from_url(url)
99
+ bom = client.assemblies.get_bom(*ids)
100
+ for row in bom.rows:
101
+ print(row.part_number, row.name, row.quantity, row.material_name)
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,84 @@
1
+ # onshape-sdk-pawrequest
2
+
3
+ A lightweight, typed Python SDK for the [Onshape REST API](https://onshape-public.github.io/docs/).
4
+
5
+ Upstream Documentation: [https://zachariahsharma.github.io/onshape-sdk/](https://zachariahsharma.github.io/onshape-sdk/)
6
+
7
+ Upstream didn't work for some routes and it's api_version is old, so i forked.
8
+
9
+ I have not tested all routes, ymmv.
10
+
11
+ Major Fork changes:
12
+
13
+ - Updated API version from 6 to 10
14
+ - Fixed some broken routes
15
+ - Added pydantic-settings
16
+ - Changed the auth setup. (`onshape_sdk.auth.OnshapeHmacAuth`)
17
+
18
+ ## Install
19
+
20
+ ```
21
+ git clone <rep-url>
22
+ ```
23
+
24
+ or don't and use uv:
25
+
26
+ ```shell
27
+ uv tool run --from git+https://github.com/pawrequest/onshape-sdk onshape-sdk export
28
+ ```
29
+
30
+ ### Configuration
31
+
32
+ Create Onshape API access and secret keys via the developer portal on OnShape's website.
33
+
34
+ PydanticSettings are built from .env file or environment variables.
35
+ Place a .env with required inputs in default location "<project_root>/.env" (sibling of this readme)
36
+ Or do one of:
37
+
38
+ - Pass custom .env locations to `OnshapeSettings(_env_file=<path_to_env_file>)`,
39
+ - Set the required inputs in your environment,
40
+ - Instantiate `OnshapeSettings` with the required inputs.
41
+
42
+ #### required inputs
43
+
44
+ ONSHAPE_ACCESS_KEY=your_access_key
45
+ ONSHAPE_SECRET_KEY=your_secret_key
46
+
47
+ ## Usage
48
+
49
+ ### cli
50
+
51
+ cli entry is via uv from `src/main.py` - see there fore details.
52
+ uv sets `onshape-sdk` as the program name
53
+ `onshape-sdk <command> <command options>`
54
+ atm i mostly use this project as an exporter to speed up iterating from onshape to slicer.
55
+
56
+ Commands:
57
+
58
+ - Export
59
+
60
+ ```
61
+ onshape-sdk export /
62
+ -u <url-to-partstudio> /
63
+ -o <optional output file or dir> / # save exported stl bytes
64
+ -pf <optional part-name filter>
65
+
66
+ ```
67
+
68
+ ### python
69
+
70
+ ```python
71
+ from onshape_sdk import OnshapeClient, OnshapeIdsTup, OnshapeSettings
72
+
73
+ settings = OnshapeSettings() # optional - otherwise read default .env location or env_vars
74
+ client = OnshapeClient(settings) # or just `OnshapeClient() for defaults`
75
+ url = '<ONSHAPE_URL>'
76
+ ids = OnshapeIdsTup.from_url(url)
77
+ bom = client.assemblies.get_bom(*ids)
78
+ for row in bom.rows:
79
+ print(row.part_number, row.name, row.quantity, row.material_name)
80
+ ```
81
+
82
+ ## License
83
+
84
+ MIT
@@ -0,0 +1,101 @@
1
+ """Onshape Python SDK — typed client for the Onshape REST API.
2
+
3
+ Quick start
4
+ -----------
5
+ >>> from onshape_sdk import OnshapeClient, parse_onshape_url
6
+ >>> client = OnshapeClient() # reads .env
7
+ >>> client.test_auth()
8
+ >>> ids = parse_onshape_url("https://cad.onshape.com/documents/…/w/…/e/…")
9
+ >>> parts = client.parts.list(ids["did"], ids["wvm_type"], ids["wvm_id"], ids["eid"])
10
+ """
11
+
12
+ from .client import OnshapeClient
13
+ from .ids import OnshapeIdsTup
14
+ from .settings import OnshapeSettings
15
+ from .utils import OnshapeIds, build_wvm_path, parse_onshape_url
16
+
17
+ # Types — re-exported so users can do ``from onshape_sdk import Document, Part, …``
18
+ from .types import (
19
+ AssemblyDefinition,
20
+ BomHeader,
21
+ BomRow,
22
+ BomTable,
23
+ Document,
24
+ DocumentVersion,
25
+ DrawingView,
26
+ Element,
27
+ Feature,
28
+ FeatureParameter,
29
+ MateFeature,
30
+ MetadataProperty,
31
+ Occurrence,
32
+ Owner,
33
+ PaginatedList,
34
+ Part,
35
+ ThumbnailInfo,
36
+ ThumbnailSize,
37
+ TranslationStatus,
38
+ UserInfo,
39
+ Variable,
40
+ Webhook,
41
+ Workspace,
42
+ )
43
+
44
+ # Resource APIs — importable for type annotations
45
+ from .assemblies import AssembliesApi
46
+ from .blob_elements import BlobElementsApi
47
+ from .documents import DocumentsApi
48
+ from .drawings import DrawingsApi
49
+ from .metadata import MetadataApi
50
+ from .parts import PartsApi
51
+ from .part_studios import PartStudiosApi
52
+ from .thumbnails import ThumbnailsApi
53
+ from .translations import TranslationError, TranslationsApi
54
+ from .webhooks import WebhooksApi
55
+
56
+ __all__ = [
57
+ # Client
58
+ 'OnshapeClient',
59
+ 'OnshapeSettings',
60
+ 'OnshapeIdsTup',
61
+ # Utils
62
+ 'parse_onshape_url',
63
+ 'build_wvm_path',
64
+ 'OnshapeIds',
65
+ # Types
66
+ 'AssemblyDefinition',
67
+ 'BomHeader',
68
+ 'BomRow',
69
+ 'BomTable',
70
+ 'Document',
71
+ 'DocumentVersion',
72
+ 'DrawingView',
73
+ 'Element',
74
+ 'Feature',
75
+ 'FeatureParameter',
76
+ 'MateFeature',
77
+ 'MetadataProperty',
78
+ 'Occurrence',
79
+ 'Owner',
80
+ 'PaginatedList',
81
+ 'Part',
82
+ 'ThumbnailInfo',
83
+ 'ThumbnailSize',
84
+ 'TranslationError',
85
+ 'TranslationStatus',
86
+ 'UserInfo',
87
+ 'Variable',
88
+ 'Webhook',
89
+ 'Workspace',
90
+ # Resource APIs
91
+ 'AssembliesApi',
92
+ 'BlobElementsApi',
93
+ 'DocumentsApi',
94
+ 'DrawingsApi',
95
+ 'MetadataApi',
96
+ 'PartsApi',
97
+ 'PartStudiosApi',
98
+ 'ThumbnailsApi',
99
+ 'TranslationsApi',
100
+ 'WebhooksApi',
101
+ ]
@@ -0,0 +1,3 @@
1
+ API_VERSION = 'v10'
2
+ API_VERSION_PATH = f'/api/{API_VERSION}'
3
+ BASE_API_PATH = f'https://cad.onshape.com{API_VERSION_PATH}'
@@ -0,0 +1,156 @@
1
+ """Assemblies API — definitions, BOM, occurrences, mates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Optional
6
+
7
+ from .api_version import API_VERSION_PATH
8
+ from .types import AssemblyDefinition, BomTable, MateFeature, Occurrence
9
+ from .utils import build_wvm_path
10
+
11
+ if TYPE_CHECKING:
12
+ from .client import OnshapeClient
13
+
14
+
15
+ class AssembliesApi:
16
+ """Endpoints under ``/{API_VERSION_PATH}/assemblies``."""
17
+
18
+ _BASE = f'/assemblies'
19
+
20
+ def __init__(self, client: 'OnshapeClient') -> None:
21
+ self._c = client
22
+
23
+ def _path(self, did: str, wvm: str, wvm_id: str, eid: str) -> str:
24
+ return f'{self._BASE}{build_wvm_path(did, wvm, wvm_id, eid)}'
25
+
26
+ # ── Definition ────────────────────────────────────────────────────────────
27
+
28
+ def get_definition(
29
+ self,
30
+ did: str,
31
+ wvm: str,
32
+ wvm_id: str,
33
+ eid: str,
34
+ *,
35
+ include_mate_features: bool = False,
36
+ include_non_solids: bool = False,
37
+ ) -> AssemblyDefinition:
38
+ """Get the full assembly definition (parts, sub-assemblies, instances)."""
39
+ params: dict = {}
40
+ if include_mate_features:
41
+ params['includeMateFeatures'] = 'true'
42
+ if include_non_solids:
43
+ params['includeNonSolids'] = 'true'
44
+ r = self._c.get(
45
+ f'{self._path(did, wvm, wvm_id, eid)}',
46
+ query_params=params or None,
47
+ )
48
+ r.raise_for_status()
49
+ return AssemblyDefinition.from_dict(r.json())
50
+
51
+ # ── BOM ───────────────────────────────────────────────────────────────────
52
+
53
+ def get_bom(
54
+ self,
55
+ did: str,
56
+ wvm: str,
57
+ wvm_id: str,
58
+ eid: str,
59
+ *,
60
+ indented: bool = True,
61
+ multi_level: bool = True,
62
+ ) -> BomTable:
63
+ """Get the Bill of Materials for the assembly."""
64
+ params: dict = {
65
+ 'indented': str(indented).lower(),
66
+ 'multiLevel': str(multi_level).lower(),
67
+ }
68
+ r = self._c.get(
69
+ f'{self._path(did, wvm, wvm_id, eid)}/bom',
70
+ query_params=params,
71
+ )
72
+ r.raise_for_status()
73
+ return BomTable.from_dict(r.json())
74
+
75
+ # ── Features (mates) ─────────────────────────────────────────────────────
76
+
77
+ def get_features(
78
+ self,
79
+ did: str,
80
+ wvm: str,
81
+ wvm_id: str,
82
+ eid: str,
83
+ ) -> list[MateFeature]:
84
+ """Get mate features in the assembly."""
85
+ r = self._c.get(f'{self._path(did, wvm, wvm_id, eid)}/features')
86
+ r.raise_for_status()
87
+ data = r.json()
88
+ raw = data.get('features', data if isinstance(data, list) else [])
89
+ return [MateFeature.from_dict(f) for f in raw if isinstance(f, dict)]
90
+
91
+ # ── Occurrences ──────────────────────────────────────────────────────────
92
+
93
+ def get_occurrences(
94
+ self,
95
+ did: str,
96
+ wvm: str,
97
+ wvm_id: str,
98
+ eid: str,
99
+ ) -> list[Occurrence]:
100
+ """List all part occurrences with their transforms."""
101
+ r = self._c.get(f'{self._path(did, wvm, wvm_id, eid)}/occurrences')
102
+ r.raise_for_status()
103
+ return [Occurrence.from_dict(o) for o in r.json()]
104
+
105
+ # ── Write operations ─────────────────────────────────────────────────────
106
+
107
+ def insert_instance(
108
+ self,
109
+ did: str,
110
+ wid: str,
111
+ eid: str,
112
+ *,
113
+ document_id: str,
114
+ element_id: str,
115
+ part_id: Optional[str] = None,
116
+ version_id: Optional[str] = None,
117
+ is_assembly: bool = False,
118
+ ) -> dict[str, Any]:
119
+ """Insert a part or sub-assembly instance into the assembly."""
120
+ body: dict = {
121
+ 'documentId': document_id,
122
+ 'elementId': element_id,
123
+ 'isAssembly': is_assembly,
124
+ }
125
+ if part_id:
126
+ body['partId'] = part_id
127
+ if version_id:
128
+ body['versionId'] = version_id
129
+ r = self._c.post(
130
+ f'{self._path(did, "w", wid, eid)}/instances',
131
+ body=body,
132
+ )
133
+ r.raise_for_status()
134
+ return r.json()
135
+
136
+ def transform_occurrences(
137
+ self,
138
+ did: str,
139
+ wid: str,
140
+ eid: str,
141
+ transforms: list[dict],
142
+ ) -> dict[str, Any]:
143
+ """Apply transforms to occurrences.
144
+
145
+ Parameters
146
+ ----------
147
+ transforms : list[dict]
148
+ Each dict should contain ``path`` (list[str]) and ``transform``
149
+ (list[float] — 16-element row-major 4×4 matrix).
150
+ """
151
+ r = self._c.post(
152
+ f'{self._path(did, "w", wid, eid)}/occurrencetransforms',
153
+ body={'occurrences': transforms, 'isRelative': True},
154
+ )
155
+ r.raise_for_status()
156
+ return r.json()
@@ -0,0 +1,59 @@
1
+ import base64
2
+ import hmac
3
+ import secrets
4
+ import string
5
+ from collections.abc import Generator
6
+ from email.utils import formatdate
7
+
8
+ import httpx
9
+ from pydantic import SecretStr
10
+
11
+ _NONCE_CHARS = string.ascii_letters + string.digits
12
+
13
+
14
+ class SecretBasicAuth(httpx.Auth):
15
+ def __init__(self, username: SecretStr, password: SecretStr) -> None:
16
+ self._username = username
17
+ self._password = password
18
+
19
+ def auth_flow(self, request: httpx.Request):
20
+ # unwrapped per request, never stored as plain str on self
21
+ basic = httpx.BasicAuth(
22
+ self._username.get_secret_value(),
23
+ self._password.get_secret_value(),
24
+ )
25
+ yield from basic.auth_flow(request)
26
+
27
+ def __repr__(self) -> str:
28
+ return f'{type(self).__name__}(<redacted>)'
29
+
30
+
31
+ class OnshapeHmacAuth(httpx.Auth):
32
+ def __init__(self, access_key: SecretStr, secret_key: SecretStr) -> None:
33
+ self._access = access_key
34
+ self._secret = secret_key
35
+
36
+ def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
37
+ nonce = ''.join(secrets.choice(_NONCE_CHARS) for _ in range(25))
38
+ date = formatdate(usegmt=True)
39
+ content_type = request.headers.get('content-type', 'application/json')
40
+ path = request.url.raw_path.split(b'?', 1)[0].decode()
41
+ query = request.url.query.decode()
42
+
43
+ signing_string = (
44
+ f'{request.method}\n{nonce}\n{date}\n{content_type}\n{path}\n{query}\n'
45
+ ).lower()
46
+
47
+ access = self._access.get_secret_value()
48
+ digest = hmac.digest(
49
+ self._secret.get_secret_value().encode(), signing_string.encode(), 'sha256'
50
+ )
51
+ sig = base64.b64encode(digest).decode()
52
+
53
+ request.headers['Date'] = date
54
+ request.headers['On-Nonce'] = nonce
55
+ request.headers['Authorization'] = f'On {access}:HmacSHA256:{sig}'
56
+ yield request
57
+
58
+ def __repr__(self) -> str:
59
+ return f'{type(self).__name__}(<redacted>)'
@@ -0,0 +1,81 @@
1
+ """Blob Elements API — upload and download binary files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING, Optional
7
+
8
+ import requests as _requests
9
+
10
+ from .api_version import API_VERSION_PATH
11
+ from .types import Element
12
+ from .utils import build_wvm_path
13
+
14
+ if TYPE_CHECKING:
15
+ from .client import OnshapeClient
16
+
17
+
18
+ class BlobElementsApi:
19
+ """Endpoints under ``/{API_VERSION_PATH}/blobelements``."""
20
+
21
+ _BASE = f'/blobelements'
22
+
23
+ def __init__(self, client: 'OnshapeClient') -> None:
24
+ self._c = client
25
+
26
+ def download(
27
+ self,
28
+ did: str,
29
+ wvm: str,
30
+ wvm_id: str,
31
+ eid: str,
32
+ ) -> bytes:
33
+ """Download the raw contents of a blob element.
34
+
35
+ Handles the S3 redirect that Onshape typically returns.
36
+ """
37
+ path = f'{self._BASE}{build_wvm_path(did, wvm, wvm_id, eid)}'
38
+ r = self._c.get(path, accept='application/octet-stream')
39
+ if r.status_code in (301, 302, 303, 307, 308):
40
+ r = _requests.get(r.headers['Location'])
41
+ r.raise_for_status()
42
+ return r.content
43
+
44
+ def upload(
45
+ self,
46
+ did: str,
47
+ wid: str,
48
+ file_path: str,
49
+ *,
50
+ content_type: Optional[str] = None,
51
+ ) -> Element:
52
+ """Upload a file as a new blob element in the document.
53
+
54
+ Parameters
55
+ ----------
56
+ file_path : str
57
+ Local file to upload.
58
+ content_type : str, optional
59
+ MIME type. Auto-detected from extension if not given.
60
+ """
61
+ if not content_type:
62
+ import mimetypes
63
+
64
+ content_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
65
+
66
+ filename = os.path.basename(file_path)
67
+ path = f'{self._BASE}/d/{did}/w/{wid}'
68
+
69
+ # Build multipart headers manually since Onshape expects
70
+ # the HMAC to be signed with application/json content type
71
+ # but the actual upload is multipart
72
+ # headers = self._c._sign('POST', path, content_type='application/json')
73
+ # del headers['Content-Type'] # let requests set the multipart boundary
74
+
75
+ with open(file_path, 'rb') as f:
76
+ files = {'file': (filename, f, content_type)}
77
+ url = self._c.base_url + path
78
+ r = _requests.post(url, files=files)
79
+
80
+ r.raise_for_status()
81
+ return Element.from_dict(r.json())