metafold 0.7.0__tar.gz → 0.8.dev1__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.
- {metafold-0.7.0 → metafold-0.8.dev1}/PKG-INFO +2 -2
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/__init__.py +3 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/api.py +18 -14
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/assets.py +0 -24
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/client.py +29 -1
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/func.py +39 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/jobs.py +71 -39
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/utils.py +1 -4
- metafold-0.8.dev1/metafold/workflows.py +137 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold.egg-info/PKG-INFO +2 -2
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold.egg-info/SOURCES.txt +3 -1
- {metafold-0.7.0 → metafold-0.8.dev1}/pyproject.toml +1 -1
- {metafold-0.7.0 → metafold-0.8.dev1}/tests/test_assets.py +0 -13
- {metafold-0.7.0 → metafold-0.8.dev1}/tests/test_jobs.py +92 -34
- {metafold-0.7.0 → metafold-0.8.dev1}/tests/test_utils.py +4 -25
- metafold-0.8.dev1/tests/test_workflows.py +151 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/LICENSE +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/README.md +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/auth.py +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/exceptions.py +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/func_types.py +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/nx.py +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold/projects.py +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold.egg-info/dependency_links.txt +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold.egg-info/requires.txt +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/metafold.egg-info/top_level.txt +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/setup.cfg +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/tests/test_func.py +0 -0
- {metafold-0.7.0 → metafold-0.8.dev1}/tests/test_projects.py +0 -0
|
@@ -2,6 +2,7 @@ from metafold.client import Client
|
|
|
2
2
|
from metafold.projects import ProjectsEndpoint
|
|
3
3
|
from metafold.assets import AssetsEndpoint
|
|
4
4
|
from metafold.jobs import JobsEndpoint
|
|
5
|
+
from metafold.workflows import WorkflowsEndpoint
|
|
5
6
|
from metafold.auth import AuthProvider
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
@@ -17,6 +18,7 @@ class MetafoldClient(Client):
|
|
|
17
18
|
projects: ProjectsEndpoint
|
|
18
19
|
assets: AssetsEndpoint
|
|
19
20
|
jobs: JobsEndpoint
|
|
21
|
+
workflows: WorkflowsEndpoint
|
|
20
22
|
|
|
21
23
|
def __init__(
|
|
22
24
|
self,
|
|
@@ -48,3 +50,4 @@ class MetafoldClient(Client):
|
|
|
48
50
|
self.projects = ProjectsEndpoint(self)
|
|
49
51
|
self.assets = AssetsEndpoint(self)
|
|
50
52
|
self.jobs = JobsEndpoint(self)
|
|
53
|
+
self.workflows = WorkflowsEndpoint(self)
|
|
@@ -2,20 +2,6 @@ from datetime import datetime, timezone
|
|
|
2
2
|
from functools import wraps
|
|
3
3
|
from typing import Any, Callable, Optional, TypeVar, Union
|
|
4
4
|
|
|
5
|
-
T = TypeVar("T")
|
|
6
|
-
U = TypeVar("U")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def optional(f: Callable[[T], U]) -> Callable[[Optional[T]], Optional[U]]:
|
|
10
|
-
"""Decorator to generate converters that accept optional values."""
|
|
11
|
-
@wraps(f)
|
|
12
|
-
def decorator(v: Optional[T]) -> Optional[U]:
|
|
13
|
-
if v is None:
|
|
14
|
-
return v
|
|
15
|
-
return f(v)
|
|
16
|
-
|
|
17
|
-
return decorator
|
|
18
|
-
|
|
19
5
|
|
|
20
6
|
def asdatetime(s: Union[str, datetime]) -> datetime:
|
|
21
7
|
"""Parse Metafold API datetime.
|
|
@@ -38,3 +24,21 @@ def asdict(**kwargs: Any) -> dict[str, Any]:
|
|
|
38
24
|
if v is not None:
|
|
39
25
|
d[k] = v
|
|
40
26
|
return d
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
U = TypeVar("U")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def optional(f: Callable[[T], U]) -> Callable[[Optional[T]], Optional[U]]:
|
|
34
|
+
"""Decorator to generate converters that accept optional values."""
|
|
35
|
+
@wraps(f)
|
|
36
|
+
def decorator(v: Optional[T]) -> Optional[U]:
|
|
37
|
+
if v is None:
|
|
38
|
+
return None
|
|
39
|
+
return f(v)
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
optional_datetime = optional(asdatetime)
|
|
@@ -115,30 +115,6 @@ class AssetsEndpoint:
|
|
|
115
115
|
fp.close()
|
|
116
116
|
return Asset(**r.json())
|
|
117
117
|
|
|
118
|
-
def update(
|
|
119
|
-
self, asset_id: str,
|
|
120
|
-
f: Union[str, bytes, PathLike, IO[bytes]],
|
|
121
|
-
project_id: Optional[str] = None,
|
|
122
|
-
) -> Asset:
|
|
123
|
-
"""Update an asset.
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
asset_id: ID of asset to update.
|
|
127
|
-
f: File-like object (opened in binary mode) or path to file on disk.
|
|
128
|
-
project_id: Asset project ID.
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
Updated asset resource.
|
|
132
|
-
"""
|
|
133
|
-
project_id = self._client.project_id(project_id)
|
|
134
|
-
fp: IO[bytes] = _open_file(f)
|
|
135
|
-
try:
|
|
136
|
-
url = f"/projects/{project_id}/assets/{asset_id}"
|
|
137
|
-
r: Response = self._client.patch(url, files={"file": fp})
|
|
138
|
-
finally:
|
|
139
|
-
fp.close()
|
|
140
|
-
return Asset(**r.json())
|
|
141
|
-
|
|
142
118
|
def delete(self, asset_id: str, project_id: Optional[str] = None) -> None:
|
|
143
119
|
"""Delete an asset.
|
|
144
120
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from metafold.auth import AuthProvider
|
|
2
|
+
from metafold.exceptions import PollTimeout
|
|
2
3
|
from requests import HTTPError, Response, Session
|
|
3
|
-
from typing import Any, Callable, Optional
|
|
4
|
+
from typing import Any, Callable, Optional, Union
|
|
4
5
|
from urllib.parse import urljoin
|
|
5
6
|
import platform
|
|
7
|
+
import time
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class Client:
|
|
@@ -74,3 +76,29 @@ class Client:
|
|
|
74
76
|
|
|
75
77
|
def delete(self, url: str, *args: Any, **kwargs: Any) -> Response:
|
|
76
78
|
return self._request(self._session.delete, url, *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
def poll(
|
|
81
|
+
self, url: str,
|
|
82
|
+
timeout: Union[int, float] = 120,
|
|
83
|
+
every: Union[int, float] = 1,
|
|
84
|
+
) -> Response:
|
|
85
|
+
"""Poll the given URL in regular intervals.
|
|
86
|
+
|
|
87
|
+
Helpful for waiting on async processes given a status URL.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
timeout: Time in seconds to wait for a result.
|
|
91
|
+
every: Frequency in seconds.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
HTTP response.
|
|
95
|
+
"""
|
|
96
|
+
t0 = time.monotonic()
|
|
97
|
+
r = self.get(url)
|
|
98
|
+
while r.status_code == 202:
|
|
99
|
+
elapsed = time.monotonic() - t0
|
|
100
|
+
if elapsed >= timeout:
|
|
101
|
+
raise PollTimeout(f"Polling timed out: {url}")
|
|
102
|
+
time.sleep(1)
|
|
103
|
+
r = self.get(url)
|
|
104
|
+
return r
|
|
@@ -100,6 +100,44 @@ class CSG(TypedFunc[Literal[FuncType.FLOAT]]):
|
|
|
100
100
|
return cast(TypedResult[Literal[FuncType.FLOAT]], r)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
ComputeCurvatures_Enum_spacing_type: TypeAlias = Literal["Continuous", "Discrete"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ComputeCurvatures_Parameters(TypedDict, total=False):
|
|
107
|
+
spacing_type: ComputeCurvatures_Enum_spacing_type
|
|
108
|
+
step_size: float
|
|
109
|
+
volume_size: Vec3f
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ComputeCurvatures(TypedFunc[Literal[FuncType.VEC3F]]):
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
samples: Func,
|
|
116
|
+
parameters: Optional[ComputeCurvatures_Parameters] = None,
|
|
117
|
+
):
|
|
118
|
+
self.inputs: Optional[dict[str, Func]]
|
|
119
|
+
self.inputs = {
|
|
120
|
+
"Samples": samples,
|
|
121
|
+
}
|
|
122
|
+
self.assets: Optional[Assets]
|
|
123
|
+
self.assets = None
|
|
124
|
+
self.parameters = parameters
|
|
125
|
+
|
|
126
|
+
@cache
|
|
127
|
+
def __call__(self, eval_: Evaluator) -> TypedResult[Literal[FuncType.VEC3F]]:
|
|
128
|
+
inputs: Optional[Inputs] = None
|
|
129
|
+
if self.inputs:
|
|
130
|
+
inputs = dict((k, v(eval_)) for k, v in self.inputs.items())
|
|
131
|
+
r = eval_(
|
|
132
|
+
"ComputeCurvatures",
|
|
133
|
+
inputs=inputs,
|
|
134
|
+
assets=self.assets,
|
|
135
|
+
# https://github.com/python/mypy/issues/4976#issuecomment-460971843
|
|
136
|
+
parameters=cast(Optional[Params], self.parameters),
|
|
137
|
+
)
|
|
138
|
+
return cast(TypedResult[Literal[FuncType.VEC3F]], r)
|
|
139
|
+
|
|
140
|
+
|
|
103
141
|
class ComputeNormals_Parameters(TypedDict, total=False):
|
|
104
142
|
volume_offset: Vec3f
|
|
105
143
|
volume_size: Vec3f
|
|
@@ -608,6 +646,7 @@ class SampleLattice(TypedFunc[Literal[FuncType.FLOAT]]):
|
|
|
608
646
|
class SampleSpinodoid_Parameters(TypedDict, total=False):
|
|
609
647
|
angles: Vec3f
|
|
610
648
|
density: float
|
|
649
|
+
pore_size: float
|
|
611
650
|
wave_count: int
|
|
612
651
|
xform: Mat4f
|
|
613
652
|
|
|
@@ -1,18 +1,50 @@
|
|
|
1
1
|
from attrs import field, frozen
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from metafold.api import asdatetime, asdict, optional
|
|
3
|
+
from metafold.api import asdatetime, asdict, optional, optional_datetime
|
|
4
4
|
from metafold.assets import Asset
|
|
5
5
|
from metafold.client import Client
|
|
6
6
|
from metafold.exceptions import PollTimeout
|
|
7
7
|
from requests import Response
|
|
8
|
-
from typing import Any, Optional, Union
|
|
9
|
-
import
|
|
8
|
+
from typing import Any, Optional, TypedDict, Union
|
|
9
|
+
from typing_extensions import TypeAlias
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _assets(v: list[Union[dict[str, Any], Asset]]) -> list[Asset]:
|
|
13
13
|
return [a if isinstance(a, Asset) else Asset(**a) for a in v]
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
AssetDict: TypeAlias = dict[str, Union[dict[str, Any], Asset]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _assets_dict(v: AssetDict) -> dict[str, Asset]:
|
|
20
|
+
return dict(
|
|
21
|
+
(k, a if isinstance(a, Asset) else Asset(**a))
|
|
22
|
+
for k, a in v.items()
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IODict(TypedDict):
|
|
27
|
+
params: Optional[dict[str, Any]]
|
|
28
|
+
assets: Optional[dict[str, AssetDict]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@frozen(kw_only=True)
|
|
32
|
+
class IO:
|
|
33
|
+
"""Job input/output.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
params: JSON-encoded parameter values.
|
|
37
|
+
assets: Related assets.
|
|
38
|
+
"""
|
|
39
|
+
params: Optional[dict[str, Any]] = None
|
|
40
|
+
assets: Optional[dict[str, Asset]] = field(
|
|
41
|
+
converter=lambda v: optional(_assets_dict)(v), default=None)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def from_dict(d: IODict) -> "IO":
|
|
45
|
+
return IO(params=d.get("params"), assets=d.get("assets"))
|
|
46
|
+
|
|
47
|
+
|
|
16
48
|
@frozen(kw_only=True)
|
|
17
49
|
class Job:
|
|
18
50
|
"""Job resource.
|
|
@@ -21,24 +53,35 @@ class Job:
|
|
|
21
53
|
id: Job ID.
|
|
22
54
|
name: Job name.
|
|
23
55
|
type: Job type.
|
|
24
|
-
|
|
56
|
+
state: Job state. May be one of: pending, started, success, failure, or
|
|
57
|
+
canceled.
|
|
25
58
|
created: Job creation datetime.
|
|
59
|
+
started: Job started datetime.
|
|
26
60
|
finished: Job finished datetime.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
error: Error message for failed jobs.
|
|
62
|
+
inputs: Input assets and parameters.
|
|
63
|
+
outputs: Output assets and parameters.
|
|
64
|
+
assets: (Deprecated) List of generated asset resources.
|
|
65
|
+
parameters: (Deprecated) Job parameters.
|
|
66
|
+
meta: (Deprecated) Additional metadata generated by the job.
|
|
30
67
|
"""
|
|
31
68
|
id: str
|
|
32
69
|
name: Optional[str] = None
|
|
33
70
|
type: str
|
|
34
|
-
|
|
71
|
+
state: str
|
|
35
72
|
created: datetime = field(converter=asdatetime)
|
|
73
|
+
started: Optional[datetime] = field(
|
|
74
|
+
converter=lambda v: optional_datetime(v), default=None)
|
|
36
75
|
finished: Optional[datetime] = field(
|
|
37
|
-
converter=lambda v:
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
76
|
+
converter=lambda v: optional_datetime(v), default=None)
|
|
77
|
+
error: Optional[str] = None
|
|
78
|
+
inputs: IO = field(converter=lambda v: v if isinstance(v, IO) else IO.from_dict(v))
|
|
79
|
+
outputs: IO = field(converter=lambda v: v if isinstance(v, IO) else IO.from_dict(v))
|
|
80
|
+
needs: list[str]
|
|
81
|
+
# NOTE(ryan): Deprecated
|
|
82
|
+
assets: Optional[list[Asset]] = field(
|
|
83
|
+
converter=lambda v: optional(_assets)(v), default=None)
|
|
84
|
+
parameters: dict[str, Any]
|
|
42
85
|
meta: dict[str, Any]
|
|
43
86
|
|
|
44
87
|
|
|
@@ -58,7 +101,8 @@ class JobsEndpoint:
|
|
|
58
101
|
|
|
59
102
|
Args:
|
|
60
103
|
sort: Sort string. For details on syntax see the Metafold API docs.
|
|
61
|
-
Supported sorting fields are: "id", "name",
|
|
104
|
+
Supported sorting fields are: "id", "name", "created", "started", or
|
|
105
|
+
"finished".
|
|
62
106
|
q: Query string. For details on syntax see the Metafold API docs.
|
|
63
107
|
Supported search fields are: "id", "name", "type", and "state".
|
|
64
108
|
project_id: Job project ID.
|
|
@@ -139,31 +183,8 @@ class JobsEndpoint:
|
|
|
139
183
|
r: Response = self._client.post(f"/projects/{project_id}/jobs", json=payload)
|
|
140
184
|
return r.json()["link"]
|
|
141
185
|
|
|
142
|
-
def poll(
|
|
143
|
-
self,
|
|
144
|
-
timeout: Union[int, float] = 120,
|
|
145
|
-
every: Union[int, float] = 1,
|
|
146
|
-
) -> Response:
|
|
147
|
-
"""Poll the given URL in regular intervals.
|
|
148
|
-
|
|
149
|
-
Helpful for waiting on job results given a status URL.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
timeout: Time in seconds to wait for a result.
|
|
153
|
-
every: Frequency in seconds.
|
|
154
|
-
|
|
155
|
-
Returns:
|
|
156
|
-
HTTP response.
|
|
157
|
-
"""
|
|
158
|
-
t0 = time.monotonic()
|
|
159
|
-
r = self._client.get(url)
|
|
160
|
-
while r.status_code == 202:
|
|
161
|
-
elapsed = time.monotonic() - t0
|
|
162
|
-
if elapsed >= timeout:
|
|
163
|
-
raise PollTimeout("Job timed out")
|
|
164
|
-
time.sleep(1)
|
|
165
|
-
r = self._client.get(url)
|
|
166
|
-
return r
|
|
186
|
+
def poll(self, *args, **kwargs):
|
|
187
|
+
return self._client.poll(*args, **kwargs)
|
|
167
188
|
|
|
168
189
|
def update(
|
|
169
190
|
self, job_id: str,
|
|
@@ -185,3 +206,14 @@ class JobsEndpoint:
|
|
|
185
206
|
payload = asdict(name=name)
|
|
186
207
|
r: Response = self._client.patch(url, data=payload)
|
|
187
208
|
return Job(**r.json())
|
|
209
|
+
|
|
210
|
+
def delete(self, job_id: str, project_id: Optional[str] = None):
|
|
211
|
+
"""Delete a job.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
job_id: ID of job to delete.
|
|
215
|
+
project_id: Job project ID.
|
|
216
|
+
"""
|
|
217
|
+
project_id = self._client.project_id(project_id)
|
|
218
|
+
url = f"/projects/{project_id}/jobs/{job_id}"
|
|
219
|
+
self._client.delete(url)
|
|
@@ -7,24 +7,21 @@ import numpy as np
|
|
|
7
7
|
def xform(
|
|
8
8
|
translation: Optional[ArrayLike] = None,
|
|
9
9
|
rotation: Optional[ArrayLike] = None,
|
|
10
|
-
scale: Optional[ArrayLike] = None,
|
|
11
10
|
) -> np.ndarray:
|
|
12
11
|
"""Compose transformation matrix.
|
|
13
12
|
|
|
14
13
|
Args:
|
|
15
14
|
translation: Translation in the x, y, and z directions.
|
|
16
15
|
rotation: Euler angles in degrees. Rotation is applied in the order Rz, Ry, Rx.
|
|
17
|
-
scale: Scale factor applied in the x, y, z directions before rotation.
|
|
18
16
|
|
|
19
17
|
Returns:
|
|
20
18
|
4 x 4 affine transformation matrix.
|
|
21
19
|
"""
|
|
22
20
|
translation = translation or np.array([0.0, 0.0, 0.0])
|
|
23
21
|
rotation = rotation or np.array([0.0, 0.0, 0.0])
|
|
24
|
-
scale = scale or np.array([1.0, 1.0, 1.0])
|
|
25
22
|
|
|
26
23
|
r = R.from_euler("xyz", rotation, degrees=True).as_matrix()
|
|
27
24
|
m = np.eye(4)
|
|
28
|
-
m[:3, :3] =
|
|
25
|
+
m[:3, :3] = r
|
|
29
26
|
m[3, :3] = translation
|
|
30
27
|
return m
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from attrs import field, frozen
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from metafold.api import asdatetime, asdict, optional_datetime
|
|
4
|
+
from metafold.client import Client
|
|
5
|
+
from metafold.exceptions import PollTimeout
|
|
6
|
+
from requests import Response
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@frozen(kw_only=True)
|
|
11
|
+
class Workflow:
|
|
12
|
+
"""Workflow resource.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
id: Workflow ID.
|
|
16
|
+
state: Workflow state. May be one of: pending, started, success, failure, or
|
|
17
|
+
canceled.
|
|
18
|
+
created: Workflow creation datetime.
|
|
19
|
+
started: Workflow started datetime.
|
|
20
|
+
finished: Workflow finished datetime.
|
|
21
|
+
definition: Workflow definition string.
|
|
22
|
+
"""
|
|
23
|
+
id: str
|
|
24
|
+
jobs: list[str] = field(factory=list)
|
|
25
|
+
state: str
|
|
26
|
+
created: datetime = field(converter=asdatetime)
|
|
27
|
+
started: Optional[datetime] = field(
|
|
28
|
+
converter=lambda v: optional_datetime(v), default=None)
|
|
29
|
+
finished: Optional[datetime] = field(
|
|
30
|
+
converter=lambda v: optional_datetime(v), default=None)
|
|
31
|
+
definition: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WorkflowsEndpoint:
|
|
35
|
+
"""Metafold workflows endpoint."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, client: Client) -> None:
|
|
38
|
+
self._client = client
|
|
39
|
+
|
|
40
|
+
def list(
|
|
41
|
+
self,
|
|
42
|
+
sort: Optional[str] = None,
|
|
43
|
+
q: Optional[str] = None,
|
|
44
|
+
project_id: Optional[str] = None,
|
|
45
|
+
) -> list[Workflow]:
|
|
46
|
+
"""List jobs.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
sort: Sort string. For details on syntax see the Metafold API docs.
|
|
50
|
+
Supported sorting fields are: "id", "created", "started", or "finished".
|
|
51
|
+
q: Query string. For details on syntax see the Metafold API docs.
|
|
52
|
+
Supported search fields are: "id" and "state".
|
|
53
|
+
project_id: Workflow project ID.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of job resources.
|
|
57
|
+
"""
|
|
58
|
+
project_id = self._client.project_id(project_id)
|
|
59
|
+
url = f"/projects/{project_id}/workflows"
|
|
60
|
+
payload = asdict(sort=sort, q=q)
|
|
61
|
+
r: Response = self._client.get(url, params=payload)
|
|
62
|
+
return [Workflow(**w) for w in r.json()]
|
|
63
|
+
|
|
64
|
+
def get(self, workflow_id: str, project_id: Optional[str] = None) -> Workflow:
|
|
65
|
+
"""Get a workflow.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
workflow_id: ID of workflow to get.
|
|
69
|
+
project_id: Workflow project ID.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Workflow resource.
|
|
73
|
+
"""
|
|
74
|
+
project_id = self._client.project_id(project_id)
|
|
75
|
+
url = f"/projects/{project_id}/workflows/{workflow_id}"
|
|
76
|
+
r: Response = self._client.get(url)
|
|
77
|
+
return Workflow(**r.json())
|
|
78
|
+
|
|
79
|
+
def run(
|
|
80
|
+
self, definition: str,
|
|
81
|
+
parameters: Optional[dict[str, str]] = None,
|
|
82
|
+
assets: Optional[dict[str, str]] = None,
|
|
83
|
+
timeout: Union[int, float] = 120,
|
|
84
|
+
project_id: Optional[str] = None,
|
|
85
|
+
) -> Workflow:
|
|
86
|
+
"""Dispatch a new workflow and wait for it to complete.
|
|
87
|
+
|
|
88
|
+
Workflow completion does not indicate success. Access the completed workflow's
|
|
89
|
+
state to check for success/failure.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
definition: Workflow definition YAML.
|
|
93
|
+
parameters: Parameter mapping for jobs in the definition.
|
|
94
|
+
assets: Asset mapping for jobs in the definition.
|
|
95
|
+
timeout: Time in seconds to wait for a result.
|
|
96
|
+
project_id: Workflow project ID.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Completed workflow resource.
|
|
100
|
+
"""
|
|
101
|
+
project_id = self._client.project_id(project_id)
|
|
102
|
+
payload = asdict(definition=definition, parameters=parameters, assets=assets)
|
|
103
|
+
r: Response = self._client.post(f"/projects/{project_id}/workflows", json=payload)
|
|
104
|
+
url = r.json()["link"]
|
|
105
|
+
try:
|
|
106
|
+
r = self._client.poll(url, timeout)
|
|
107
|
+
except PollTimeout as e:
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
f"Workflow failed to complete within {timeout} seconds"
|
|
110
|
+
) from e
|
|
111
|
+
return Workflow(**r.json())
|
|
112
|
+
|
|
113
|
+
def cancel(self, workflow_id: str, project_id: Optional[str] = None) -> Workflow:
|
|
114
|
+
"""Cancel a running workflow.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
workflow_id: ID of workflow to cancel.
|
|
118
|
+
project_id: Workflow project ID.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Workflow resource.
|
|
122
|
+
"""
|
|
123
|
+
project_id = self._client.project_id(project_id)
|
|
124
|
+
url = f"/projects/{project_id}/workflows/{workflow_id}/cancel"
|
|
125
|
+
r: Response = self._client.post(url)
|
|
126
|
+
return Workflow(**r.json())
|
|
127
|
+
|
|
128
|
+
def delete(self, workflow_id: str, project_id: Optional[str] = None):
|
|
129
|
+
"""Delete a workflow.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
workflow_id: ID of workflow to delete.
|
|
133
|
+
project_id: Workflow project ID.
|
|
134
|
+
"""
|
|
135
|
+
project_id = self._client.project_id(project_id)
|
|
136
|
+
url = f"/projects/{project_id}/workflows/{workflow_id}"
|
|
137
|
+
self._client.delete(url)
|
|
@@ -13,6 +13,7 @@ metafold/jobs.py
|
|
|
13
13
|
metafold/nx.py
|
|
14
14
|
metafold/projects.py
|
|
15
15
|
metafold/utils.py
|
|
16
|
+
metafold/workflows.py
|
|
16
17
|
metafold.egg-info/PKG-INFO
|
|
17
18
|
metafold.egg-info/SOURCES.txt
|
|
18
19
|
metafold.egg-info/dependency_links.txt
|
|
@@ -22,4 +23,5 @@ tests/test_assets.py
|
|
|
22
23
|
tests/test_func.py
|
|
23
24
|
tests/test_jobs.py
|
|
24
25
|
tests/test_projects.py
|
|
25
|
-
tests/test_utils.py
|
|
26
|
+
tests/test_utils.py
|
|
27
|
+
tests/test_workflows.py
|
|
@@ -171,19 +171,6 @@ def test_create_asset(client):
|
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
def test_update_asset(client):
|
|
175
|
-
with open(test_file, "rb") as f:
|
|
176
|
-
a = client.assets.update("1", f)
|
|
177
|
-
assert a == Asset(
|
|
178
|
-
id="1",
|
|
179
|
-
filename="test.png",
|
|
180
|
-
size=67,
|
|
181
|
-
checksum="sha256:089ad5bf4831b6758e9907db43bc5ebba2e9248a9929dad6132c49932e538278",
|
|
182
|
-
created=default_dt,
|
|
183
|
-
modified=default_dt,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
174
|
def test_delete_asset(client):
|
|
188
175
|
# FIXME: Assert something
|
|
189
176
|
client.assets.delete("1")
|
|
@@ -3,13 +3,18 @@ from datetime import datetime, timezone
|
|
|
3
3
|
from http import HTTPStatus
|
|
4
4
|
from http.server import BaseHTTPRequestHandler
|
|
5
5
|
from metafold.assets import Asset
|
|
6
|
-
from metafold.jobs import Job
|
|
6
|
+
from metafold.jobs import Job, IO
|
|
7
7
|
from urllib.parse import parse_qs, urlparse
|
|
8
8
|
import json
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
11
|
default_dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
|
12
12
|
|
|
13
|
+
default_params = {
|
|
14
|
+
"foo": "...",
|
|
15
|
+
"bar": "...",
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
asset_json = {
|
|
14
19
|
"id": "1",
|
|
15
20
|
"filename": "f763df409e79eb1c.bin",
|
|
@@ -34,36 +39,57 @@ job_list = [
|
|
|
34
39
|
"id": "3",
|
|
35
40
|
"name": "bar",
|
|
36
41
|
"type": "evaluate_graph",
|
|
37
|
-
"parameters": {
|
|
38
|
-
"graph": None,
|
|
39
|
-
},
|
|
40
|
-
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
41
42
|
"state": "success",
|
|
42
|
-
"
|
|
43
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
44
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
45
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
46
|
+
"error": None,
|
|
47
|
+
"inputs": {
|
|
48
|
+
"params": default_params,
|
|
49
|
+
},
|
|
50
|
+
"outputs": {
|
|
51
|
+
"params": None,
|
|
52
|
+
},
|
|
53
|
+
"needs": [],
|
|
54
|
+
"parameters": default_params,
|
|
43
55
|
"meta": None,
|
|
44
56
|
},
|
|
45
57
|
{
|
|
46
58
|
"id": "2",
|
|
47
59
|
"name": "foo",
|
|
48
60
|
"type": "evaluate_graph",
|
|
49
|
-
"parameters": {
|
|
50
|
-
"graph": None,
|
|
51
|
-
},
|
|
52
|
-
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
53
61
|
"state": "success",
|
|
54
|
-
"
|
|
62
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
63
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
64
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
65
|
+
"error": None,
|
|
66
|
+
"inputs": {
|
|
67
|
+
"params": default_params,
|
|
68
|
+
},
|
|
69
|
+
"outputs": {
|
|
70
|
+
"params": None,
|
|
71
|
+
},
|
|
72
|
+
"needs": [],
|
|
73
|
+
"parameters": default_params,
|
|
55
74
|
"meta": None,
|
|
56
75
|
},
|
|
57
76
|
{
|
|
58
77
|
"id": "1",
|
|
59
78
|
"name": "foo",
|
|
60
79
|
"type": "evaluate_graph",
|
|
61
|
-
"parameters": {
|
|
62
|
-
"graph": None,
|
|
63
|
-
},
|
|
64
|
-
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
65
80
|
"state": "success",
|
|
66
|
-
"
|
|
81
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
82
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
83
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
84
|
+
"error": None,
|
|
85
|
+
"inputs": {
|
|
86
|
+
"params": default_params,
|
|
87
|
+
},
|
|
88
|
+
"outputs": {
|
|
89
|
+
"params": None,
|
|
90
|
+
},
|
|
91
|
+
"needs": [],
|
|
92
|
+
"parameters": default_params,
|
|
67
93
|
"meta": None,
|
|
68
94
|
},
|
|
69
95
|
]
|
|
@@ -72,13 +98,27 @@ new_job = {
|
|
|
72
98
|
"id": "1",
|
|
73
99
|
"name": "My Job",
|
|
74
100
|
"type": "test_job",
|
|
101
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
102
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
103
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
104
|
+
"error": None,
|
|
105
|
+
"inputs": {
|
|
106
|
+
"params": {
|
|
107
|
+
"foo": "1",
|
|
108
|
+
"bar": "a",
|
|
109
|
+
"baz": "[2, \"b\"]",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
"outputs": {
|
|
113
|
+
"params": None,
|
|
114
|
+
},
|
|
115
|
+
"needs": [],
|
|
116
|
+
"assets": [],
|
|
75
117
|
"parameters": {
|
|
76
|
-
"foo": 1,
|
|
118
|
+
"foo": "1",
|
|
77
119
|
"bar": "a",
|
|
78
|
-
"baz": [2, "b"],
|
|
120
|
+
"baz": "[2, \"b\"]",
|
|
79
121
|
},
|
|
80
|
-
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
81
|
-
"assets": [],
|
|
82
122
|
"meta": None,
|
|
83
123
|
}
|
|
84
124
|
|
|
@@ -103,7 +143,8 @@ class MockRequestHandler(BaseHTTPRequestHandler):
|
|
|
103
143
|
self.send_response(HTTPStatus.OK)
|
|
104
144
|
self.send_header("Content-Type", "application/json")
|
|
105
145
|
self.end_headers()
|
|
106
|
-
payload = job_list[-1]
|
|
146
|
+
payload = deepcopy(job_list[-1])
|
|
147
|
+
payload["assets"] = [asset_json]
|
|
107
148
|
self.wfile.write(json.dumps(payload).encode())
|
|
108
149
|
elif u.path == "/projects/1/jobs/1/status":
|
|
109
150
|
global poll_count
|
|
@@ -145,7 +186,10 @@ class MockRequestHandler(BaseHTTPRequestHandler):
|
|
|
145
186
|
self.send_header("Content-Type", "application/json")
|
|
146
187
|
self.end_headers()
|
|
147
188
|
payload = deepcopy(job_list[-1])
|
|
148
|
-
payload
|
|
189
|
+
payload.update({
|
|
190
|
+
"name": "baz",
|
|
191
|
+
"assets": [asset_json],
|
|
192
|
+
})
|
|
149
193
|
self.wfile.write(json.dumps(payload).encode())
|
|
150
194
|
else:
|
|
151
195
|
self.send_error(HTTPStatus.NOT_FOUND)
|
|
@@ -177,40 +221,49 @@ def test_get_job(client):
|
|
|
177
221
|
id="1",
|
|
178
222
|
name="foo",
|
|
179
223
|
type="evaluate_graph",
|
|
180
|
-
parameters={
|
|
181
|
-
"graph": None,
|
|
182
|
-
},
|
|
183
|
-
created=default_dt,
|
|
184
224
|
state="success",
|
|
225
|
+
created=default_dt,
|
|
226
|
+
started=default_dt,
|
|
227
|
+
finished=default_dt,
|
|
228
|
+
error=None,
|
|
229
|
+
inputs=IO(params=default_params),
|
|
230
|
+
outputs=IO(),
|
|
231
|
+
needs=[],
|
|
185
232
|
assets=[asset_obj],
|
|
233
|
+
parameters=default_params,
|
|
186
234
|
meta=None,
|
|
187
235
|
)
|
|
188
236
|
|
|
189
237
|
|
|
190
238
|
def test_run_job(client):
|
|
191
239
|
params = {
|
|
192
|
-
"foo": 1,
|
|
240
|
+
"foo": "1",
|
|
193
241
|
"bar": "a",
|
|
194
|
-
"baz": [2, "b"],
|
|
242
|
+
"baz": "[2, \"b\"]",
|
|
195
243
|
}
|
|
196
244
|
j = client.jobs.run("test_job", params, name="My Job")
|
|
197
245
|
assert j == Job(
|
|
198
246
|
id="1",
|
|
199
247
|
name="My Job",
|
|
200
248
|
type="test_job",
|
|
201
|
-
parameters=params,
|
|
202
|
-
created=default_dt,
|
|
203
249
|
state="success",
|
|
250
|
+
created=default_dt,
|
|
251
|
+
started=default_dt,
|
|
252
|
+
finished=default_dt,
|
|
253
|
+
inputs=IO(params=params),
|
|
254
|
+
outputs=IO(),
|
|
255
|
+
needs=[],
|
|
204
256
|
assets=[asset_obj],
|
|
257
|
+
parameters=params,
|
|
205
258
|
meta=None,
|
|
206
259
|
)
|
|
207
260
|
|
|
208
261
|
|
|
209
262
|
def test_poll_job(client):
|
|
210
263
|
params = {
|
|
211
|
-
"foo": 1,
|
|
264
|
+
"foo": "1",
|
|
212
265
|
"bar": "a",
|
|
213
|
-
"baz": [2, "b"],
|
|
266
|
+
"baz": "[2, \"b\"]",
|
|
214
267
|
}
|
|
215
268
|
url = client.jobs.run_status("test_job", params, name="My Job")
|
|
216
269
|
assert url == "http://localhost:8000/projects/1/jobs/1/status"
|
|
@@ -220,10 +273,15 @@ def test_poll_job(client):
|
|
|
220
273
|
id="1",
|
|
221
274
|
name="My Job",
|
|
222
275
|
type="test_job",
|
|
223
|
-
parameters=params,
|
|
224
|
-
created=default_dt,
|
|
225
276
|
state="success",
|
|
277
|
+
created=default_dt,
|
|
278
|
+
started=default_dt,
|
|
279
|
+
finished=default_dt,
|
|
280
|
+
inputs=IO(params=params),
|
|
281
|
+
outputs=IO(),
|
|
282
|
+
needs=[],
|
|
226
283
|
assets=[asset_obj],
|
|
284
|
+
parameters=params,
|
|
227
285
|
meta=None,
|
|
228
286
|
)
|
|
229
287
|
|
|
@@ -12,42 +12,21 @@ def test_xform():
|
|
|
12
12
|
], atol=1.0e-7)
|
|
13
13
|
|
|
14
14
|
m = xform(
|
|
15
|
-
[
|
|
16
|
-
[180.0, -90.0, 0.0]) # R
|
|
15
|
+
rotation=[180.0, -90.0, 0.0]) # R
|
|
17
16
|
assert_allclose(m, [
|
|
18
17
|
[0.0, 0.0, 1.0, 0.0],
|
|
19
18
|
[0.0, -1.0, 0.0, 0.0],
|
|
20
19
|
[1.0, 0.0, 0.0, 0.0],
|
|
21
|
-
[1.0, 2.0, 3.0, 1.0],
|
|
22
|
-
], atol=1.0e-7)
|
|
23
|
-
|
|
24
|
-
m = xform(
|
|
25
|
-
[1.0, 2.0, 3.0], # T
|
|
26
|
-
scale=[2.0, 1.0, 1.0]) # S
|
|
27
|
-
assert_allclose(m, [
|
|
28
|
-
[2.0, 0.0, 0.0, 0.0],
|
|
29
|
-
[0.0, 1.0, 0.0, 0.0],
|
|
30
|
-
[0.0, 0.0, 1.0, 0.0],
|
|
31
|
-
[1.0, 2.0, 3.0, 1.0],
|
|
32
|
-
], atol=1.0e-7)
|
|
33
|
-
|
|
34
|
-
m = xform(
|
|
35
|
-
rotation=[180.0, -90.0, 0.0], # R
|
|
36
|
-
scale=[2.0, 1.0, 1.0]) # S
|
|
37
|
-
assert_allclose(m, [
|
|
38
|
-
[0.0, 0.0, 2.0, 0.0],
|
|
39
|
-
[0.0, -1.0, 0.0, 0.0],
|
|
40
|
-
[1.0, 0.0, 0.0, 0.0],
|
|
41
20
|
[0.0, 0.0, 0.0, 1.0],
|
|
42
21
|
], atol=1.0e-7)
|
|
43
22
|
|
|
44
23
|
m = xform(
|
|
45
24
|
[1.0, 2.0, 3.0], # T
|
|
46
|
-
[180.0, -90.0, 0.0]
|
|
47
|
-
[2.0, 1.0, 1.0]) # S
|
|
25
|
+
[180.0, -90.0, 0.0]) # R
|
|
48
26
|
assert_allclose(m, [
|
|
49
|
-
[0.0, 0.0,
|
|
27
|
+
[0.0, 0.0, 1.0, 0.0],
|
|
50
28
|
[0.0, -1.0, 0.0, 0.0],
|
|
51
29
|
[1.0, 0.0, 0.0, 0.0],
|
|
52
30
|
[1.0, 2.0, 3.0, 1.0],
|
|
53
31
|
], atol=1.0e-7)
|
|
32
|
+
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from http.server import BaseHTTPRequestHandler
|
|
5
|
+
from metafold.workflows import Workflow
|
|
6
|
+
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
import json
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
default_dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
|
|
11
|
+
|
|
12
|
+
# Default sort order is descending by id
|
|
13
|
+
workflow_list = [
|
|
14
|
+
{
|
|
15
|
+
"id": "3",
|
|
16
|
+
"jobs": ["1", "2"],
|
|
17
|
+
"state": "success",
|
|
18
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
19
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
20
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
21
|
+
"definition": "...",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "2",
|
|
25
|
+
"jobs": ["1", "2"],
|
|
26
|
+
"state": "started",
|
|
27
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
28
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
29
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
30
|
+
"definition": "...",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "1",
|
|
34
|
+
"jobs": ["1", "2"],
|
|
35
|
+
"state": "success",
|
|
36
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
37
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
38
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
39
|
+
"definition": "...",
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
new_workflow = {
|
|
44
|
+
"id": "1",
|
|
45
|
+
"jobs": ["1", "2"],
|
|
46
|
+
"created": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
47
|
+
"started": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
48
|
+
"finished": "Mon, 01 Jan 2024 00:00:00 GMT",
|
|
49
|
+
"definition": "foo",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
poll_count: int = 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MockRequestHandler(BaseHTTPRequestHandler):
|
|
56
|
+
def do_GET(self):
|
|
57
|
+
u = urlparse(self.path)
|
|
58
|
+
params = parse_qs(u.query)
|
|
59
|
+
if u.path == "/projects/1/workflows":
|
|
60
|
+
self.send_response(HTTPStatus.OK)
|
|
61
|
+
self.send_header("Content-Type", "application/json")
|
|
62
|
+
self.end_headers()
|
|
63
|
+
payload = workflow_list
|
|
64
|
+
if params.get("sort") == ["id:1"]:
|
|
65
|
+
payload = sorted(workflow_list, key=lambda p: p["id"])
|
|
66
|
+
elif params.get("q") == ["state:started"]:
|
|
67
|
+
payload = [p for p in workflow_list if p["state"] == "started"]
|
|
68
|
+
self.wfile.write(json.dumps(payload).encode())
|
|
69
|
+
elif u.path == "/projects/1/workflows/1":
|
|
70
|
+
self.send_response(HTTPStatus.OK)
|
|
71
|
+
self.send_header("Content-Type", "application/json")
|
|
72
|
+
self.end_headers()
|
|
73
|
+
payload = deepcopy(workflow_list[-1])
|
|
74
|
+
self.wfile.write(json.dumps(payload).encode())
|
|
75
|
+
elif u.path == "/projects/1/workflows/1/status":
|
|
76
|
+
global poll_count
|
|
77
|
+
poll_count += 1
|
|
78
|
+
if poll_count < 3:
|
|
79
|
+
self.send_response(HTTPStatus.ACCEPTED)
|
|
80
|
+
payload = deepcopy(new_workflow)
|
|
81
|
+
payload["state"] = "started"
|
|
82
|
+
else:
|
|
83
|
+
self.send_response(HTTPStatus.CREATED)
|
|
84
|
+
payload = deepcopy(new_workflow)
|
|
85
|
+
payload["state"] = "success"
|
|
86
|
+
self.send_header("Content-Type", "application/json")
|
|
87
|
+
self.end_headers()
|
|
88
|
+
self.wfile.write(json.dumps(payload).encode())
|
|
89
|
+
else:
|
|
90
|
+
self.send_error(HTTPStatus.NOT_FOUND)
|
|
91
|
+
|
|
92
|
+
def do_POST(self):
|
|
93
|
+
if self.path == "/projects/1/workflows":
|
|
94
|
+
self.send_response(HTTPStatus.ACCEPTED)
|
|
95
|
+
self.send_header("Content-Type", "application/json")
|
|
96
|
+
self.end_headers()
|
|
97
|
+
payload = deepcopy(new_workflow)
|
|
98
|
+
payload.update({
|
|
99
|
+
"state": "pending",
|
|
100
|
+
"link": "http://localhost:8000/projects/1/workflows/1/status",
|
|
101
|
+
})
|
|
102
|
+
self.wfile.write(json.dumps(payload).encode())
|
|
103
|
+
else:
|
|
104
|
+
self.send_error(HTTPStatus.NOT_FOUND)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.fixture(scope="module")
|
|
108
|
+
def request_handler():
|
|
109
|
+
return MockRequestHandler
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_list_workflows(client):
|
|
113
|
+
workflows = client.workflows.list()
|
|
114
|
+
assert [w.id for w in workflows] == ["3", "2", "1"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_list_workflows_sorted(client):
|
|
118
|
+
workflows = client.workflows.list(sort="id:1")
|
|
119
|
+
assert [w.id for w in workflows] == ["1", "2", "3"]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_list_workflows_filtered(client):
|
|
123
|
+
workflows = client.workflows.list(q="state:started")
|
|
124
|
+
assert all([w.state == "started" for w in workflows])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_get_workflow(client):
|
|
128
|
+
w = client.workflows.get("1")
|
|
129
|
+
assert w == Workflow(
|
|
130
|
+
id="1",
|
|
131
|
+
jobs=["1", "2"],
|
|
132
|
+
state="success",
|
|
133
|
+
created=default_dt,
|
|
134
|
+
started=default_dt,
|
|
135
|
+
finished=default_dt,
|
|
136
|
+
definition="...",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_run_workflow(client):
|
|
141
|
+
definition = "foo"
|
|
142
|
+
w = client.workflows.run(definition)
|
|
143
|
+
assert w == Workflow(
|
|
144
|
+
id="1",
|
|
145
|
+
jobs=["1", "2"],
|
|
146
|
+
state="success",
|
|
147
|
+
created=default_dt,
|
|
148
|
+
started=default_dt,
|
|
149
|
+
finished=default_dt,
|
|
150
|
+
definition=definition,
|
|
151
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|