metafold 0.1.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.
- metafold/__init__.py +27 -0
- metafold/api.py +25 -0
- metafold/assets.py +130 -0
- metafold/client.py +52 -0
- metafold/exceptions.py +2 -0
- metafold/jobs.py +123 -0
- metafold-0.1.0.dist-info/LICENSE +7 -0
- metafold-0.1.0.dist-info/METADATA +44 -0
- metafold-0.1.0.dist-info/RECORD +11 -0
- metafold-0.1.0.dist-info/WHEEL +5 -0
- metafold-0.1.0.dist-info/top_level.txt +1 -0
metafold/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from metafold.client import Client
|
|
2
|
+
from metafold.assets import AssetsEndpoint
|
|
3
|
+
from metafold.jobs import JobsEndpoint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MetafoldClient(Client):
|
|
7
|
+
"""Metafold REST API client.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
assets: Sub-client for assets endpoint.
|
|
11
|
+
jobs: Sub-client for jobs endpoint.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, access_token: str, project_id: str,
|
|
16
|
+
base_url: str = "https://api.metafold3d.com",
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Initialize Metafold API client.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
access_token: Metafold API secret key.
|
|
22
|
+
project_id: ID of the project to make API calls against.
|
|
23
|
+
base_url: Metafold API URL. Used for internal testing.
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(access_token, project_id, base_url=base_url)
|
|
26
|
+
self.assets = AssetsEndpoint(self)
|
|
27
|
+
self.jobs = JobsEndpoint(self)
|
metafold/api.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def asdatetime(s: str | datetime) -> datetime:
|
|
6
|
+
"""Parse Metafold API datetime.
|
|
7
|
+
|
|
8
|
+
Note datetime strings returned by the Metafold API are RFC 1123 formatted,
|
|
9
|
+
times are always in GMT.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
UTC datetime.
|
|
13
|
+
"""
|
|
14
|
+
if isinstance(s, datetime):
|
|
15
|
+
return s
|
|
16
|
+
return datetime.strptime(s, "%a, %d %b %Y %H:%M:%S %Z").replace(tzinfo=timezone.utc)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def asdict(**kwargs: Any) -> dict[str, Any]:
|
|
20
|
+
"""Convert present kwargs to dictionary."""
|
|
21
|
+
d = {}
|
|
22
|
+
for k, v in kwargs.items():
|
|
23
|
+
if v is not None:
|
|
24
|
+
d[k] = v
|
|
25
|
+
return d
|
metafold/assets.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from attrs import field, frozen
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from metafold.api import asdatetime, asdict
|
|
4
|
+
from metafold.client import Client
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from requests import Response
|
|
7
|
+
from typing import IO, Optional
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@frozen(kw_only=True)
|
|
12
|
+
class Asset:
|
|
13
|
+
"""Asset resource.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
id: Asset ID.
|
|
17
|
+
filename: Asset filename.
|
|
18
|
+
size: File size in bytes.
|
|
19
|
+
checksum: File checksum.
|
|
20
|
+
created: Asset creation datetime.
|
|
21
|
+
modified: Asset last modified datetime.
|
|
22
|
+
"""
|
|
23
|
+
id: str
|
|
24
|
+
filename: str
|
|
25
|
+
size: int
|
|
26
|
+
checksum: str
|
|
27
|
+
created: datetime = field(converter=asdatetime)
|
|
28
|
+
modified: datetime = field(converter=asdatetime)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AssetsEndpoint:
|
|
32
|
+
"""Metafold assets endpoint."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, client: Client) -> None:
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
def list(self, sort: Optional[str] = None, q: Optional[str] = None) -> list[Asset]:
|
|
38
|
+
"""List assets.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
sort: Sort string. For details on syntax see the Metafold API docs.
|
|
42
|
+
Supported sorting fields are: "id", "filename", "size", "created", or
|
|
43
|
+
"modified".
|
|
44
|
+
q: Query string. For details on syntax see the Metafold API docs.
|
|
45
|
+
Supported search fields are: "id" and "filename".
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of asset resources.
|
|
49
|
+
"""
|
|
50
|
+
url = f"/projects/{self._client.project_id}/assets"
|
|
51
|
+
payload = asdict(sort=sort, q=q)
|
|
52
|
+
r: Response = self._client.get(url, params=payload)
|
|
53
|
+
return [Asset(**a) for a in r.json()]
|
|
54
|
+
|
|
55
|
+
def get(self, id: str) -> Asset:
|
|
56
|
+
"""Get an asset.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
id: ID of asset to get.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Asset resource.
|
|
63
|
+
"""
|
|
64
|
+
url = f"/projects/{self._client.project_id}/assets/{id}"
|
|
65
|
+
r: Response = self._client.get(url)
|
|
66
|
+
return Asset(**r.json())
|
|
67
|
+
|
|
68
|
+
def download_file(self, id: str, path: str | PathLike):
|
|
69
|
+
"""Download an asset.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
id: ID of asset to download.
|
|
73
|
+
path: Path to downloaded file.
|
|
74
|
+
"""
|
|
75
|
+
url = f"/projects/{self._client.project_id}/assets/{id}"
|
|
76
|
+
r: Response = self._client.get(url, params={"download": "true"})
|
|
77
|
+
r = requests.get(r.json()["link"], stream=True)
|
|
78
|
+
with open(path, "wb") as f:
|
|
79
|
+
for chunk in r.iter_content(chunk_size=65536): # 64 KiB
|
|
80
|
+
f.write(chunk)
|
|
81
|
+
|
|
82
|
+
def create(self, f: str | bytes | PathLike | IO[bytes]) -> Asset:
|
|
83
|
+
"""Upload an asset.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
f: File-like object (opened in binary mode) or path to file on disk.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Asset resource.
|
|
90
|
+
"""
|
|
91
|
+
fp: IO[bytes] = _open_file(f)
|
|
92
|
+
try:
|
|
93
|
+
url = f"/projects/{self._client.project_id}/assets"
|
|
94
|
+
r: Response = self._client.post(url, files={"file": fp})
|
|
95
|
+
finally:
|
|
96
|
+
fp.close()
|
|
97
|
+
return Asset(**r.json())
|
|
98
|
+
|
|
99
|
+
def update(self, id: str, f: str | bytes | PathLike | IO[bytes]) -> Asset:
|
|
100
|
+
"""Update an asset.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
id: ID of asset to update.
|
|
104
|
+
f: File-like object (opened in binary mode) or path to file on disk.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Updated asset resource.
|
|
108
|
+
"""
|
|
109
|
+
fp: IO[bytes] = _open_file(f)
|
|
110
|
+
try:
|
|
111
|
+
url = f"/projects/{self._client.project_id}/assets/{id}"
|
|
112
|
+
r: Response = self._client.patch(url, files={"file": fp})
|
|
113
|
+
finally:
|
|
114
|
+
fp.close()
|
|
115
|
+
return Asset(**r.json())
|
|
116
|
+
|
|
117
|
+
def delete(self, id: str) -> None:
|
|
118
|
+
"""Delete an asset.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
id: ID of asset to delete.
|
|
122
|
+
"""
|
|
123
|
+
url = f"/projects/{self._client.project_id}/assets/{id}"
|
|
124
|
+
self._client.delete(url)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _open_file(f: str | bytes | PathLike | IO[bytes]) -> IO[bytes]:
|
|
128
|
+
if isinstance(f, str | bytes | PathLike):
|
|
129
|
+
return open(f, "rb")
|
|
130
|
+
return f
|
metafold/client.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from requests import HTTPError, Response, Session
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
import platform
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Client:
|
|
8
|
+
"""Base client."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, access_token: str, project_id: str, base_url: str) -> None:
|
|
11
|
+
# TODO(ryan): Validate project id
|
|
12
|
+
self._project_id = project_id
|
|
13
|
+
self._base_url = base_url
|
|
14
|
+
self._session = Session()
|
|
15
|
+
self._session.headers.update({
|
|
16
|
+
"Accept": "application/json",
|
|
17
|
+
"Authorization": f"Bearer {access_token}",
|
|
18
|
+
"User-Agent": f"Python/{platform.python_version()}",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def project_id(self) -> str:
|
|
23
|
+
return self._project_id
|
|
24
|
+
|
|
25
|
+
def _request(
|
|
26
|
+
self, request: Callable[..., Response], url: str,
|
|
27
|
+
*args: Any, **kwargs: Any,
|
|
28
|
+
) -> Response:
|
|
29
|
+
url = urljoin(self._base_url, url)
|
|
30
|
+
r: Response = request(url, *args, **kwargs)
|
|
31
|
+
if not r.ok:
|
|
32
|
+
r: dict[str, Any] = r.json()
|
|
33
|
+
# Error responses aren't entirely consistent in the Metafold API,
|
|
34
|
+
# for now we check for a handful of possible fields.
|
|
35
|
+
reason = r.get("errors") or r.get("msg") or r.get("description")
|
|
36
|
+
raise HTTPError(f"HTTP error occurred: {reason or r.reason}")
|
|
37
|
+
return r
|
|
38
|
+
|
|
39
|
+
def get(self, url: str, *args: Any, **kwargs: Any) -> Response:
|
|
40
|
+
return self._request(self._session.get, url, *args, **kwargs)
|
|
41
|
+
|
|
42
|
+
def post(self, url: str, *args: Any, **kwargs: Any) -> Response:
|
|
43
|
+
return self._request(self._session.post, url, *args, **kwargs)
|
|
44
|
+
|
|
45
|
+
def put(self, url: str, *args: Any, **kwargs: Any) -> Response:
|
|
46
|
+
return self._request(self._session.put, url, *args, **kwargs)
|
|
47
|
+
|
|
48
|
+
def patch(self, url: str, *args: Any, **kwargs: Any) -> Response:
|
|
49
|
+
return self._request(self._session.patch, url, *args, **kwargs)
|
|
50
|
+
|
|
51
|
+
def delete(self, url: str, *args: Any, **kwargs: Any) -> Response:
|
|
52
|
+
return self._request(self._session.delete, url, *args, **kwargs)
|
metafold/exceptions.py
ADDED
metafold/jobs.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from attrs import field, frozen
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from metafold.api import asdatetime, asdict
|
|
4
|
+
from metafold.assets import Asset
|
|
5
|
+
from metafold.client import Client
|
|
6
|
+
from metafold.exceptions import PollTimeout
|
|
7
|
+
from requests import Response
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _assets(v: list[dict[str, Any] | Asset]) -> list[Asset]:
|
|
13
|
+
return [a if isinstance(a, Asset) else Asset(**a) for a in v]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@frozen(kw_only=True)
|
|
17
|
+
class Job:
|
|
18
|
+
"""Job resource.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
id: Job ID.
|
|
22
|
+
name: Job name.
|
|
23
|
+
type: Job type.
|
|
24
|
+
parameters: Job parameters.
|
|
25
|
+
created: Job creation datetime.
|
|
26
|
+
state: Job state. May be one of: pending, started, success, or failure.
|
|
27
|
+
assets: List of generated asset resources.
|
|
28
|
+
meta: Additional metadata generated by the job.
|
|
29
|
+
"""
|
|
30
|
+
id: str
|
|
31
|
+
name: Optional[str] = None
|
|
32
|
+
type: str
|
|
33
|
+
parameters: dict[str, Any]
|
|
34
|
+
created: datetime = field(converter=asdatetime)
|
|
35
|
+
state: str
|
|
36
|
+
assets: list[Asset] = field(converter=_assets)
|
|
37
|
+
meta: dict[str, Any]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class JobsEndpoint:
|
|
41
|
+
"""Metafold jobs endpoint."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, client: Client) -> None:
|
|
44
|
+
self._client = client
|
|
45
|
+
|
|
46
|
+
def list(self, sort: Optional[str] = None, q: Optional[str] = None) -> list[Job]:
|
|
47
|
+
"""List jobs.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
sort: Sort string. For details on syntax see the Metafold API docs.
|
|
51
|
+
Supported sorting fields are: "id", "name", or "created".
|
|
52
|
+
q: Query string. For details on syntax see the Metafold API docs.
|
|
53
|
+
Supported search fields are: "id", "name", "type", and "state".
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of job resources.
|
|
57
|
+
"""
|
|
58
|
+
url = f"/projects/{self._client.project_id}/jobs"
|
|
59
|
+
payload = asdict(sort=sort, q=q)
|
|
60
|
+
r: Response = self._client.get(url, params=payload)
|
|
61
|
+
return [Job(**j) for j in r.json()]
|
|
62
|
+
|
|
63
|
+
def get(self, id: str) -> Job:
|
|
64
|
+
"""Get a job.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
id: ID of job to get.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Job resource.
|
|
71
|
+
"""
|
|
72
|
+
url = f"/projects/{self._client.project_id}/jobs/{id}"
|
|
73
|
+
r: Response = self._client.get(url)
|
|
74
|
+
return Job(**r.json())
|
|
75
|
+
|
|
76
|
+
def run(
|
|
77
|
+
self, type: str, params: dict[str, Any],
|
|
78
|
+
name: Optional[str] = None,
|
|
79
|
+
timeout: int | float = 120,
|
|
80
|
+
) -> Job:
|
|
81
|
+
"""Dispatch a new job and wait for a result.
|
|
82
|
+
|
|
83
|
+
See Metafold API docs for the full list of jobs.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
type: Job type.
|
|
87
|
+
params: Job parameters.
|
|
88
|
+
name: Optional job name.
|
|
89
|
+
timeout: Time in seconds to wait for a result.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Completed job resource.
|
|
93
|
+
"""
|
|
94
|
+
payload = asdict(type=type, parameters=params, name=name)
|
|
95
|
+
r: Response = self._client.post(f"/projects/{self._client.project_id}/jobs", json=payload)
|
|
96
|
+
r = self._poll(r.json()["link"], timeout)
|
|
97
|
+
return Job(**r.json())
|
|
98
|
+
|
|
99
|
+
def update(self, id: str, name: Optional[str] = None) -> Job:
|
|
100
|
+
"""Update a job.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
id: ID of job to update.
|
|
104
|
+
name: New job name. The existing name remains unchanged if None.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Updated job resource.
|
|
108
|
+
"""
|
|
109
|
+
url = f"/projects/{self._client.project_id}/jobs/{id}"
|
|
110
|
+
payload = asdict(name=name)
|
|
111
|
+
r: Response = self._client.patch(url, data=payload)
|
|
112
|
+
return Job(**r.json())
|
|
113
|
+
|
|
114
|
+
def _poll(self, url: str, timeout: int | float) -> Response:
|
|
115
|
+
t0 = time.monotonic()
|
|
116
|
+
r = self._client.get(url)
|
|
117
|
+
while r.status_code == 202:
|
|
118
|
+
elapsed = time.monotonic() - t0
|
|
119
|
+
if elapsed >= timeout:
|
|
120
|
+
raise PollTimeout("Job timed out")
|
|
121
|
+
time.sleep(1)
|
|
122
|
+
r = self._client.get(url)
|
|
123
|
+
return r
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2024 Metafold 3D
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: metafold
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Metafold SDK for Python
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: attrs >=23.2
|
|
9
|
+
Requires-Dist: requests >=2.31
|
|
10
|
+
Requires-Dist: sphinx >=7.2
|
|
11
|
+
Requires-Dist: sphinx-rtd-theme >=2.0
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest >=7.3 ; extra == 'test'
|
|
14
|
+
Requires-Dist: requests-toolbelt >=1.0 ; extra == 'test'
|
|
15
|
+
|
|
16
|
+
# Metafold SDK for Python
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
pip install metafold
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
from metafold import MetafoldClient
|
|
28
|
+
|
|
29
|
+
access_token = "..."
|
|
30
|
+
project_id = "123"
|
|
31
|
+
|
|
32
|
+
metafold = MetafoldClient(access_token, project_id)
|
|
33
|
+
|
|
34
|
+
assets = metafold.assets.list()
|
|
35
|
+
print(assets[0].name)
|
|
36
|
+
|
|
37
|
+
asset = metafold.assets.get("123")
|
|
38
|
+
print(asset.name)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Read the [documentation][] for more info or play around with one of the
|
|
42
|
+
[examples](examples).
|
|
43
|
+
|
|
44
|
+
[documentation]: https://Metafold3d.github.io/metafold-python/
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
metafold/__init__.py,sha256=-xgonly8A0YEzh3uMkDA_vfRD0Ms9rgrMOkAoy-2F_A,846
|
|
2
|
+
metafold/api.py,sha256=QPnsf_ntNsAN8uY_pUHB2R2AyOl5qsYhNpcBGAPmPgo,637
|
|
3
|
+
metafold/assets.py,sha256=GVvdP0AaGqgyu73asDxILLy-ZuIP93yFq-B0yye1emc,3905
|
|
4
|
+
metafold/client.py,sha256=0glZICHjNyVUvxdKNDaHNRwkOW81xtJsOLn1kbYt9oc,2006
|
|
5
|
+
metafold/exceptions.py,sha256=dJhBgbryBhJUf20FT3eU1rjQJQ-aDEZ25g-YCn_TwQk,110
|
|
6
|
+
metafold/jobs.py,sha256=mGFdA2NpQGxwTM7SV6xcPcl3cG2gyqlvAuRkCr4B7Ag,3805
|
|
7
|
+
metafold-0.1.0.dist-info/LICENSE,sha256=LejZXzGwe9t0Ezk6g0bRmWUuFSI9vQSk1wJAc1NrrhE,1059
|
|
8
|
+
metafold-0.1.0.dist-info/METADATA,sha256=p_ACUtmteEhITfEKZCKoYpJJSr2ZdPWbILRUNZNmjqs,911
|
|
9
|
+
metafold-0.1.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
10
|
+
metafold-0.1.0.dist-info/top_level.txt,sha256=0dvwa2N6gvl2x4T9c62V4MbYD2soFedI6hm3-lWgyew,9
|
|
11
|
+
metafold-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
metafold
|