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.
- onshape_sdk_pr-0.1.2/LICENSE +21 -0
- onshape_sdk_pr-0.1.2/PKG-INFO +106 -0
- onshape_sdk_pr-0.1.2/README.md +84 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/__init__.py +101 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/api_version.py +3 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/assemblies.py +156 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/auth.py +59 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/blob_elements.py +81 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/client.py +225 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/client_routines.py +32 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/documents.py +140 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/drawings.py +63 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/ids.py +20 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/main.py +55 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/metadata.py +68 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/part_studios.py +243 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/parts.py +119 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/py.typed +1 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/settings.py +38 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/thumbnails.py +61 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/translations.py +239 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/types.py +593 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/utils.py +93 -0
- onshape_sdk_pr-0.1.2/onshape_sdk/webhooks.py +100 -0
- onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/PKG-INFO +106 -0
- onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/SOURCES.txt +31 -0
- onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/dependency_links.txt +1 -0
- onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/entry_points.txt +2 -0
- onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/requires.txt +5 -0
- onshape_sdk_pr-0.1.2/onshape_sdk_pr.egg-info/top_level.txt +1 -0
- onshape_sdk_pr-0.1.2/pyproject.toml +41 -0
- onshape_sdk_pr-0.1.2/setup.cfg +4 -0
- 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,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())
|