stackmachine 0.3.1__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stackmachine-0.3.1 → stackmachine-0.3.3}/PKG-INFO +26 -21
- {stackmachine-0.3.1 → stackmachine-0.3.3}/README.md +25 -20
- {stackmachine-0.3.1 → stackmachine-0.3.3}/pyproject.toml +1 -1
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/__init__.py +3 -1
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_async_client.py +20 -8
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_client.py +20 -8
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_types.py +4 -2
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/apps.py +23 -3
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/deployments.py +59 -4
- {stackmachine-0.3.1 → stackmachine-0.3.3}/tests/test_package.py +150 -3
- {stackmachine-0.3.1 → stackmachine-0.3.3}/.gitignore +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/async_usage.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/basic_usage.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/deploy_app.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/list_apps.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_config.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_errors.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_graphql/__init__.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_graphql/operations.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_models.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_pagination.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_transport.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_uploads.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_utils.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/py.typed +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/__init__.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/_shared.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/domains.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/files.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/ssh.py +0 -0
- {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackmachine
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Python SDK for StackMachine.
|
|
5
5
|
Project-URL: Homepage, https://github.com/stackmachine/sdks/tree/main/python
|
|
6
6
|
Project-URL: Repository, https://github.com/stackmachine/sdks
|
|
@@ -61,15 +61,13 @@ stackmachine = StackMachine("sk_stackmachine_...")
|
|
|
61
61
|
async_stackmachine = AsyncStackMachine("sk_stackmachine_...")
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Both clients accept
|
|
64
|
+
Both clients accept configuration options during initialization:
|
|
65
65
|
|
|
66
66
|
```python
|
|
67
|
-
stackmachine = StackMachine
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"maxNetworkRetries": 2,
|
|
72
|
-
}
|
|
67
|
+
stackmachine = StackMachine(
|
|
68
|
+
"sk_stackmachine_...",
|
|
69
|
+
apiUrl="https://api.stackmachine.com/graphql",
|
|
70
|
+
maxNetworkRetries=2,
|
|
73
71
|
)
|
|
74
72
|
```
|
|
75
73
|
|
|
@@ -101,10 +99,12 @@ async for app in apps:
|
|
|
101
99
|
|
|
102
100
|
```python
|
|
103
101
|
deployment = stackmachine.deployments.create(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
app_name="hello-stackmachine",
|
|
103
|
+
owner="stackmachine",
|
|
104
|
+
files={
|
|
105
|
+
"index.html": "<html><body><h1>Hello StackMachine</h1></body></html>",
|
|
106
|
+
},
|
|
107
|
+
on_upload_progress=lambda progress: print("Uploading", progress.percent * 100),
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
version = deployment.wait()
|
|
@@ -112,15 +112,18 @@ version = deployment.wait()
|
|
|
112
112
|
|
|
113
113
|
```python
|
|
114
114
|
deployment = stackmachine.apps.autobuild(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
app_name="hello-stackmachine",
|
|
116
|
+
owner="stackmachine",
|
|
117
|
+
files={
|
|
118
|
+
"index.html": "<html><body><h1>Hello StackMachine</h1></body></html>",
|
|
119
|
+
},
|
|
119
120
|
)
|
|
120
121
|
```
|
|
121
122
|
|
|
122
123
|
## Files
|
|
123
124
|
|
|
125
|
+
Use this path only for manual package uploads:
|
|
126
|
+
|
|
124
127
|
```python
|
|
125
128
|
from stackmachine import create_zip
|
|
126
129
|
|
|
@@ -162,13 +165,15 @@ key = stackmachine.apps.ssh.users.authorized_keys.create(
|
|
|
162
165
|
Most methods accept `request_options` for per-request configuration:
|
|
163
166
|
|
|
164
167
|
```python
|
|
168
|
+
from stackmachine import RequestOptions
|
|
169
|
+
|
|
165
170
|
app = stackmachine.apps.retrieve(
|
|
166
171
|
"app_id",
|
|
167
|
-
request_options=
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
request_options=RequestOptions(
|
|
173
|
+
api_key="sk_stackmachine_other",
|
|
174
|
+
timeout=30,
|
|
175
|
+
idempotency_key="deploy-123",
|
|
176
|
+
),
|
|
172
177
|
)
|
|
173
178
|
```
|
|
174
179
|
|
|
@@ -39,15 +39,13 @@ stackmachine = StackMachine("sk_stackmachine_...")
|
|
|
39
39
|
async_stackmachine = AsyncStackMachine("sk_stackmachine_...")
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Both clients accept
|
|
42
|
+
Both clients accept configuration options during initialization:
|
|
43
43
|
|
|
44
44
|
```python
|
|
45
|
-
stackmachine = StackMachine
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"maxNetworkRetries": 2,
|
|
50
|
-
}
|
|
45
|
+
stackmachine = StackMachine(
|
|
46
|
+
"sk_stackmachine_...",
|
|
47
|
+
apiUrl="https://api.stackmachine.com/graphql",
|
|
48
|
+
maxNetworkRetries=2,
|
|
51
49
|
)
|
|
52
50
|
```
|
|
53
51
|
|
|
@@ -79,10 +77,12 @@ async for app in apps:
|
|
|
79
77
|
|
|
80
78
|
```python
|
|
81
79
|
deployment = stackmachine.deployments.create(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
app_name="hello-stackmachine",
|
|
81
|
+
owner="stackmachine",
|
|
82
|
+
files={
|
|
83
|
+
"index.html": "<html><body><h1>Hello StackMachine</h1></body></html>",
|
|
84
|
+
},
|
|
85
|
+
on_upload_progress=lambda progress: print("Uploading", progress.percent * 100),
|
|
86
86
|
)
|
|
87
87
|
|
|
88
88
|
version = deployment.wait()
|
|
@@ -90,15 +90,18 @@ version = deployment.wait()
|
|
|
90
90
|
|
|
91
91
|
```python
|
|
92
92
|
deployment = stackmachine.apps.autobuild(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
app_name="hello-stackmachine",
|
|
94
|
+
owner="stackmachine",
|
|
95
|
+
files={
|
|
96
|
+
"index.html": "<html><body><h1>Hello StackMachine</h1></body></html>",
|
|
97
|
+
},
|
|
97
98
|
)
|
|
98
99
|
```
|
|
99
100
|
|
|
100
101
|
## Files
|
|
101
102
|
|
|
103
|
+
Use this path only for manual package uploads:
|
|
104
|
+
|
|
102
105
|
```python
|
|
103
106
|
from stackmachine import create_zip
|
|
104
107
|
|
|
@@ -140,13 +143,15 @@ key = stackmachine.apps.ssh.users.authorized_keys.create(
|
|
|
140
143
|
Most methods accept `request_options` for per-request configuration:
|
|
141
144
|
|
|
142
145
|
```python
|
|
146
|
+
from stackmachine import RequestOptions
|
|
147
|
+
|
|
143
148
|
app = stackmachine.apps.retrieve(
|
|
144
149
|
"app_id",
|
|
145
|
-
request_options=
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
request_options=RequestOptions(
|
|
151
|
+
api_key="sk_stackmachine_other",
|
|
152
|
+
timeout=30,
|
|
153
|
+
idempotency_key="deploy-123",
|
|
154
|
+
),
|
|
150
155
|
)
|
|
151
156
|
```
|
|
152
157
|
|
|
@@ -54,6 +54,7 @@ from ._types import (
|
|
|
54
54
|
DeployAppsSortBy,
|
|
55
55
|
DeployAppVersionsSortBy,
|
|
56
56
|
DeployAppWordPressExtraData,
|
|
57
|
+
DeploymentFilesInput,
|
|
57
58
|
DeploymentProgressCallback,
|
|
58
59
|
FileInput,
|
|
59
60
|
Headers,
|
|
@@ -74,7 +75,7 @@ from ._uploads import create_zip
|
|
|
74
75
|
try:
|
|
75
76
|
__version__ = version("stackmachine")
|
|
76
77
|
except PackageNotFoundError:
|
|
77
|
-
__version__ = "0.3.
|
|
78
|
+
__version__ = "0.3.3"
|
|
78
79
|
|
|
79
80
|
__all__ = [
|
|
80
81
|
"AppAlias",
|
|
@@ -105,6 +106,7 @@ __all__ = [
|
|
|
105
106
|
"DeployAppWordPressExtraData",
|
|
106
107
|
"DeployAppsListInput",
|
|
107
108
|
"DeployAppsSortBy",
|
|
109
|
+
"DeploymentFilesInput",
|
|
108
110
|
"DeploymentProgress",
|
|
109
111
|
"DeploymentProgressCallback",
|
|
110
112
|
"ExpectedDNSRecord",
|
|
@@ -25,24 +25,36 @@ class AsyncStackMachine:
|
|
|
25
25
|
self,
|
|
26
26
|
api_key: str,
|
|
27
27
|
*,
|
|
28
|
-
api_url: str =
|
|
28
|
+
api_url: Optional[str] = None,
|
|
29
|
+
apiUrl: Optional[str] = None,
|
|
29
30
|
headers: Optional[Headers] = None,
|
|
30
31
|
timeout: float = DEFAULT_TIMEOUT,
|
|
31
|
-
max_network_retries: int =
|
|
32
|
+
max_network_retries: Optional[int] = None,
|
|
33
|
+
maxNetworkRetries: Optional[int] = None,
|
|
32
34
|
http_client: Optional[httpx.AsyncClient] = None,
|
|
33
35
|
http_transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
34
36
|
) -> None:
|
|
37
|
+
resolved_api_url = (
|
|
38
|
+
api_url if api_url is not None else apiUrl or DEFAULT_API_URL
|
|
39
|
+
)
|
|
40
|
+
resolved_max_retries = (
|
|
41
|
+
max_network_retries
|
|
42
|
+
if max_network_retries is not None
|
|
43
|
+
else maxNetworkRetries
|
|
44
|
+
if maxNetworkRetries is not None
|
|
45
|
+
else DEFAULT_MAX_NETWORK_RETRIES
|
|
46
|
+
)
|
|
35
47
|
self.api_key = api_key
|
|
36
|
-
self.api_url =
|
|
37
|
-
self.apiUrl =
|
|
48
|
+
self.api_url = resolved_api_url
|
|
49
|
+
self.apiUrl = resolved_api_url
|
|
38
50
|
self.timeout = timeout
|
|
39
|
-
self.max_network_retries =
|
|
40
|
-
self.maxNetworkRetries =
|
|
51
|
+
self.max_network_retries = resolved_max_retries
|
|
52
|
+
self.maxNetworkRetries = resolved_max_retries
|
|
41
53
|
self._config = ClientConfig(
|
|
42
|
-
api_url=
|
|
54
|
+
api_url=resolved_api_url,
|
|
43
55
|
headers=headers,
|
|
44
56
|
timeout=timeout,
|
|
45
|
-
max_network_retries=
|
|
57
|
+
max_network_retries=resolved_max_retries,
|
|
46
58
|
)
|
|
47
59
|
self._transport = AsyncTransport(
|
|
48
60
|
api_key,
|
|
@@ -25,24 +25,36 @@ class StackMachine:
|
|
|
25
25
|
self,
|
|
26
26
|
api_key: str,
|
|
27
27
|
*,
|
|
28
|
-
api_url: str =
|
|
28
|
+
api_url: Optional[str] = None,
|
|
29
|
+
apiUrl: Optional[str] = None,
|
|
29
30
|
headers: Optional[Headers] = None,
|
|
30
31
|
timeout: float = DEFAULT_TIMEOUT,
|
|
31
|
-
max_network_retries: int =
|
|
32
|
+
max_network_retries: Optional[int] = None,
|
|
33
|
+
maxNetworkRetries: Optional[int] = None,
|
|
32
34
|
http_client: Optional[httpx.Client] = None,
|
|
33
35
|
http_transport: Optional[httpx.BaseTransport] = None,
|
|
34
36
|
) -> None:
|
|
37
|
+
resolved_api_url = (
|
|
38
|
+
api_url if api_url is not None else apiUrl or DEFAULT_API_URL
|
|
39
|
+
)
|
|
40
|
+
resolved_max_retries = (
|
|
41
|
+
max_network_retries
|
|
42
|
+
if max_network_retries is not None
|
|
43
|
+
else maxNetworkRetries
|
|
44
|
+
if maxNetworkRetries is not None
|
|
45
|
+
else DEFAULT_MAX_NETWORK_RETRIES
|
|
46
|
+
)
|
|
35
47
|
self.api_key = api_key
|
|
36
|
-
self.api_url =
|
|
37
|
-
self.apiUrl =
|
|
48
|
+
self.api_url = resolved_api_url
|
|
49
|
+
self.apiUrl = resolved_api_url
|
|
38
50
|
self.timeout = timeout
|
|
39
|
-
self.max_network_retries =
|
|
40
|
-
self.maxNetworkRetries =
|
|
51
|
+
self.max_network_retries = resolved_max_retries
|
|
52
|
+
self.maxNetworkRetries = resolved_max_retries
|
|
41
53
|
self._config = ClientConfig(
|
|
42
|
-
api_url=
|
|
54
|
+
api_url=resolved_api_url,
|
|
43
55
|
headers=headers,
|
|
44
56
|
timeout=timeout,
|
|
45
|
-
max_network_retries=
|
|
57
|
+
max_network_retries=resolved_max_retries,
|
|
46
58
|
)
|
|
47
59
|
self._transport = SyncTransport(
|
|
48
60
|
api_key,
|
|
@@ -178,6 +178,7 @@ class DeployAppAutobuildInput(TypedDict, total=False):
|
|
|
178
178
|
envVars: Optional[Sequence[Optional[DeployAppEnvVarInput]]]
|
|
179
179
|
extra_data: Optional[DeployAppAutobuildExtraData]
|
|
180
180
|
extraData: Optional[DeployAppAutobuildExtraData]
|
|
181
|
+
files: Optional[DeploymentFilesInput]
|
|
181
182
|
install_cmd: Optional[str]
|
|
182
183
|
installCmd: Optional[str]
|
|
183
184
|
jobs: Optional[Sequence[Optional[DeployAppJobDefinitionInput]]]
|
|
@@ -240,15 +241,16 @@ class Readable(Protocol):
|
|
|
240
241
|
|
|
241
242
|
FileInput = Union[str, bytes, bytearray, memoryview, Path, Readable]
|
|
242
243
|
CreateZipFiles = Mapping[str, FileInput]
|
|
244
|
+
DeploymentFilesInput = CreateZipFiles
|
|
243
245
|
|
|
244
246
|
|
|
245
247
|
class UploadProgressCallback(Protocol):
|
|
246
|
-
def __call__(self, progress: "UploadProgress") -> None:
|
|
248
|
+
def __call__(self, progress: "UploadProgress", /) -> None:
|
|
247
249
|
...
|
|
248
250
|
|
|
249
251
|
|
|
250
252
|
class DeploymentProgressCallback(Protocol):
|
|
251
|
-
def __call__(self, progress: "DeploymentProgress") -> None:
|
|
253
|
+
def __call__(self, progress: "DeploymentProgress", /) -> None:
|
|
252
254
|
...
|
|
253
255
|
|
|
254
256
|
|
|
@@ -20,8 +20,8 @@ from .._types import (
|
|
|
20
20
|
DeployAppsSortBy,
|
|
21
21
|
PaginationOptions,
|
|
22
22
|
RequestOptionsLike,
|
|
23
|
+
UploadProgressCallback,
|
|
23
24
|
)
|
|
24
|
-
from .._utils import camelize, merge_input
|
|
25
25
|
from ._shared import page_variables, resource_missing_error
|
|
26
26
|
from .deployments import (
|
|
27
27
|
AsyncDeployment,
|
|
@@ -148,10 +148,20 @@ class DeployAppsResource:
|
|
|
148
148
|
input: Optional[DeployAppAutobuildInput] = None,
|
|
149
149
|
*,
|
|
150
150
|
request_options: Optional[RequestOptionsLike] = None,
|
|
151
|
+
chunk_size: Optional[int] = None,
|
|
152
|
+
on_upload_progress: Optional[UploadProgressCallback] = None,
|
|
153
|
+
timeout: Optional[float] = None,
|
|
154
|
+
max_network_retries: Optional[int] = None,
|
|
151
155
|
**kwargs: Unpack[DeployAppAutobuildInput],
|
|
152
156
|
) -> Deployment:
|
|
153
157
|
return self._deployments.create(
|
|
154
|
-
|
|
158
|
+
input,
|
|
159
|
+
request_options=request_options,
|
|
160
|
+
chunk_size=chunk_size,
|
|
161
|
+
on_upload_progress=on_upload_progress,
|
|
162
|
+
timeout=timeout,
|
|
163
|
+
max_network_retries=max_network_retries,
|
|
164
|
+
**kwargs,
|
|
155
165
|
)
|
|
156
166
|
|
|
157
167
|
del_ = delete
|
|
@@ -273,10 +283,20 @@ class AsyncDeployAppsResource:
|
|
|
273
283
|
input: Optional[DeployAppAutobuildInput] = None,
|
|
274
284
|
*,
|
|
275
285
|
request_options: Optional[RequestOptionsLike] = None,
|
|
286
|
+
chunk_size: Optional[int] = None,
|
|
287
|
+
on_upload_progress: Optional[UploadProgressCallback] = None,
|
|
288
|
+
timeout: Optional[float] = None,
|
|
289
|
+
max_network_retries: Optional[int] = None,
|
|
276
290
|
**kwargs: Unpack[DeployAppAutobuildInput],
|
|
277
291
|
) -> AsyncDeployment:
|
|
278
292
|
return await self._deployments.create(
|
|
279
|
-
|
|
293
|
+
input,
|
|
294
|
+
request_options=request_options,
|
|
295
|
+
chunk_size=chunk_size,
|
|
296
|
+
on_upload_progress=on_upload_progress,
|
|
297
|
+
timeout=timeout,
|
|
298
|
+
max_network_retries=max_network_retries,
|
|
299
|
+
**kwargs,
|
|
280
300
|
)
|
|
281
301
|
|
|
282
302
|
del_ = delete
|
|
@@ -3,24 +3,47 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import threading
|
|
5
5
|
import warnings
|
|
6
|
-
from typing import Any, Callable, Mapping, Optional
|
|
6
|
+
from typing import Any, Callable, Mapping, Optional, cast
|
|
7
7
|
|
|
8
8
|
from typing_extensions import Unpack
|
|
9
9
|
|
|
10
|
-
from .._errors import StackMachineAPIError
|
|
10
|
+
from .._errors import StackMachineAPIError, StackMachineValidationError
|
|
11
11
|
from .._graphql import operations as gql
|
|
12
12
|
from .._models import DeployAppVersion, DeploymentProgress
|
|
13
13
|
from .._types import (
|
|
14
|
+
CreateZipFiles,
|
|
14
15
|
DeployAppAutobuildInput,
|
|
15
16
|
DeploymentProgressCallback,
|
|
16
17
|
RequestOptionsLike,
|
|
18
|
+
UploadProgressCallback,
|
|
17
19
|
)
|
|
20
|
+
from .._uploads import create_zip
|
|
18
21
|
from .._utils import camelize, merge_input
|
|
19
22
|
from ._shared import required_payload, resource_missing_error
|
|
20
23
|
|
|
21
24
|
FAILED_STATUSES = {"CANCELLED", "FAILED", "INTERNAL_ERROR", "TIMEOUT"}
|
|
22
25
|
|
|
23
26
|
|
|
27
|
+
def _merge_deployment_create_input(
|
|
28
|
+
input: Optional[DeployAppAutobuildInput],
|
|
29
|
+
kwargs: DeployAppAutobuildInput,
|
|
30
|
+
) -> tuple[dict[str, object], Optional[CreateZipFiles]]:
|
|
31
|
+
deployment_input = merge_input(input, **kwargs)
|
|
32
|
+
files = deployment_input.pop("files", None)
|
|
33
|
+
has_upload_url = (
|
|
34
|
+
deployment_input.get("upload_url") is not None
|
|
35
|
+
or deployment_input.get("uploadUrl") is not None
|
|
36
|
+
)
|
|
37
|
+
if files is not None and has_upload_url:
|
|
38
|
+
raise StackMachineValidationError(
|
|
39
|
+
"`files` cannot be passed together with `upload_url`; "
|
|
40
|
+
"pass one deployment source.",
|
|
41
|
+
code="invalid_deployment_source",
|
|
42
|
+
param="files",
|
|
43
|
+
)
|
|
44
|
+
return deployment_input, cast(Optional[CreateZipFiles], files)
|
|
45
|
+
|
|
46
|
+
|
|
24
47
|
class Deployment:
|
|
25
48
|
def __init__(
|
|
26
49
|
self,
|
|
@@ -275,11 +298,27 @@ class DeploymentsResource:
|
|
|
275
298
|
input: Optional[DeployAppAutobuildInput] = None,
|
|
276
299
|
*,
|
|
277
300
|
request_options: Optional[RequestOptionsLike] = None,
|
|
301
|
+
chunk_size: Optional[int] = None,
|
|
302
|
+
on_upload_progress: Optional[UploadProgressCallback] = None,
|
|
303
|
+
timeout: Optional[float] = None,
|
|
304
|
+
max_network_retries: Optional[int] = None,
|
|
278
305
|
**kwargs: Unpack[DeployAppAutobuildInput],
|
|
279
306
|
) -> Deployment:
|
|
307
|
+
deployment_input, files = _merge_deployment_create_input(input, kwargs)
|
|
308
|
+
if files is not None:
|
|
309
|
+
upload_url = self._client.files.upload(
|
|
310
|
+
create_zip(files),
|
|
311
|
+
chunk_size=chunk_size,
|
|
312
|
+
on_progress=on_upload_progress,
|
|
313
|
+
timeout=timeout,
|
|
314
|
+
max_network_retries=max_network_retries,
|
|
315
|
+
request_options=request_options,
|
|
316
|
+
)
|
|
317
|
+
deployment_input["upload_url"] = upload_url
|
|
318
|
+
|
|
280
319
|
response = self._client._mutation(
|
|
281
320
|
gql.AUTOBUILD_MUTATION,
|
|
282
|
-
{"input": camelize(
|
|
321
|
+
{"input": camelize(deployment_input)},
|
|
283
322
|
request_options=request_options,
|
|
284
323
|
)
|
|
285
324
|
payload = required_payload(
|
|
@@ -358,11 +397,27 @@ class AsyncDeploymentsResource:
|
|
|
358
397
|
input: Optional[DeployAppAutobuildInput] = None,
|
|
359
398
|
*,
|
|
360
399
|
request_options: Optional[RequestOptionsLike] = None,
|
|
400
|
+
chunk_size: Optional[int] = None,
|
|
401
|
+
on_upload_progress: Optional[UploadProgressCallback] = None,
|
|
402
|
+
timeout: Optional[float] = None,
|
|
403
|
+
max_network_retries: Optional[int] = None,
|
|
361
404
|
**kwargs: Unpack[DeployAppAutobuildInput],
|
|
362
405
|
) -> AsyncDeployment:
|
|
406
|
+
deployment_input, files = _merge_deployment_create_input(input, kwargs)
|
|
407
|
+
if files is not None:
|
|
408
|
+
upload_url = await self._client.files.upload(
|
|
409
|
+
create_zip(files),
|
|
410
|
+
chunk_size=chunk_size,
|
|
411
|
+
on_progress=on_upload_progress,
|
|
412
|
+
timeout=timeout,
|
|
413
|
+
max_network_retries=max_network_retries,
|
|
414
|
+
request_options=request_options,
|
|
415
|
+
)
|
|
416
|
+
deployment_input["upload_url"] = upload_url
|
|
417
|
+
|
|
363
418
|
response = await self._client._mutation(
|
|
364
419
|
gql.AUTOBUILD_MUTATION,
|
|
365
|
-
{"input": camelize(
|
|
420
|
+
{"input": camelize(deployment_input)},
|
|
366
421
|
request_options=request_options,
|
|
367
422
|
)
|
|
368
423
|
payload = required_payload(
|
|
@@ -18,6 +18,7 @@ from stackmachine import (
|
|
|
18
18
|
StackMachineValidationError,
|
|
19
19
|
create_zip,
|
|
20
20
|
)
|
|
21
|
+
from stackmachine.resources.deployments import DeploymentsResource
|
|
21
22
|
from stackmachine.resources.files import FilesResource
|
|
22
23
|
|
|
23
24
|
|
|
@@ -51,9 +52,11 @@ def test_exports_clients_and_models() -> None:
|
|
|
51
52
|
|
|
52
53
|
def test_exports_public_input_types() -> None:
|
|
53
54
|
assert "DeployAppAutobuildInput" in stackmachine.__all__
|
|
55
|
+
assert "DeploymentFilesInput" in stackmachine.__all__
|
|
54
56
|
assert "RequestOptionsInput" in stackmachine.__all__
|
|
55
57
|
assert "FileInput" in stackmachine.__all__
|
|
56
58
|
assert stackmachine.DeployAppAutobuildInput.__name__ == "DeployAppAutobuildInput"
|
|
59
|
+
assert stackmachine.DeploymentFilesInput == stackmachine.CreateZipFiles
|
|
57
60
|
assert stackmachine.RequestOptionsInput.__name__ == "RequestOptionsInput"
|
|
58
61
|
|
|
59
62
|
|
|
@@ -65,9 +68,18 @@ def test_file_upload_signature_uses_public_types() -> None:
|
|
|
65
68
|
assert hints["request_options"] == Optional[stackmachine.RequestOptionsLike]
|
|
66
69
|
|
|
67
70
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
def test_deployment_create_signature_uses_upload_progress_type() -> None:
|
|
72
|
+
hints = get_type_hints(DeploymentsResource.create)
|
|
73
|
+
|
|
74
|
+
assert hints["on_upload_progress"] == Optional[
|
|
75
|
+
stackmachine.UploadProgressCallback
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_client_constructor_accepts_configuration_aliases() -> None:
|
|
80
|
+
client = StackMachine(
|
|
81
|
+
"token-1",
|
|
82
|
+
apiUrl="https://api.example/graphql",
|
|
71
83
|
maxNetworkRetries=3,
|
|
72
84
|
http_transport=httpx.MockTransport(lambda _: graphql_response({})),
|
|
73
85
|
)
|
|
@@ -81,6 +93,20 @@ def test_client_init_accepts_js_style_aliases() -> None:
|
|
|
81
93
|
client.close()
|
|
82
94
|
|
|
83
95
|
|
|
96
|
+
def test_client_init_accepts_mapping_settings() -> None:
|
|
97
|
+
client = StackMachine.init(
|
|
98
|
+
{"token": "token-1", "apiUrl": "https://api.example/graphql"},
|
|
99
|
+
maxNetworkRetries=3,
|
|
100
|
+
http_transport=httpx.MockTransport(lambda _: graphql_response({})),
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
assert client.api_key == "token-1"
|
|
104
|
+
assert client.api_url == "https://api.example/graphql"
|
|
105
|
+
assert client.max_network_retries == 3
|
|
106
|
+
finally:
|
|
107
|
+
client.close()
|
|
108
|
+
|
|
109
|
+
|
|
84
110
|
def test_sync_viewer_sends_auth_header_and_returns_model() -> None:
|
|
85
111
|
seen: dict[str, Any] = {}
|
|
86
112
|
|
|
@@ -123,6 +149,127 @@ def test_request_options_override_auth_and_mutation_id() -> None:
|
|
|
123
149
|
assert seen["body"]["variables"]["input"]["clientMutationId"] == "mutation-1"
|
|
124
150
|
|
|
125
151
|
|
|
152
|
+
def test_deployments_create_accepts_files_and_upload_progress() -> None:
|
|
153
|
+
seen: dict[str, Any] = {}
|
|
154
|
+
upload_progress: list[stackmachine.UploadProgress] = []
|
|
155
|
+
|
|
156
|
+
def on_upload_progress(progress: stackmachine.UploadProgress) -> None:
|
|
157
|
+
upload_progress.append(progress)
|
|
158
|
+
|
|
159
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
160
|
+
if str(request.url) == "https://storage.example.test/app.zip":
|
|
161
|
+
assert request.method == "POST"
|
|
162
|
+
return httpx.Response(
|
|
163
|
+
200, headers={"Location": "https://storage.example.test/session"}
|
|
164
|
+
)
|
|
165
|
+
if str(request.url) == "https://storage.example.test/session":
|
|
166
|
+
assert request.method == "PUT"
|
|
167
|
+
seen["archive"] = request.content
|
|
168
|
+
return httpx.Response(200)
|
|
169
|
+
|
|
170
|
+
body = json.loads(request.content)
|
|
171
|
+
if body["operationName"] == "uploadQuery":
|
|
172
|
+
return graphql_response(
|
|
173
|
+
{"getSignedUrl": {"url": "https://storage.example.test/app.zip"}}
|
|
174
|
+
)
|
|
175
|
+
if body["operationName"] == "srcAutobuildMutation":
|
|
176
|
+
seen["mutation"] = body
|
|
177
|
+
return graphql_response(
|
|
178
|
+
{"deployViaAutobuild": {"success": True, "buildId": "build-files"}}
|
|
179
|
+
)
|
|
180
|
+
raise AssertionError(f"Unexpected operation {body['operationName']}")
|
|
181
|
+
|
|
182
|
+
with StackMachine("secret", http_transport=httpx.MockTransport(handler)) as client:
|
|
183
|
+
deployment = client.deployments.create(
|
|
184
|
+
app_name="hello-stackmachine",
|
|
185
|
+
owner="tester",
|
|
186
|
+
files={"index.html": "<html><body><h1>Hello</h1></body></html>"},
|
|
187
|
+
chunk_size=10_000_000,
|
|
188
|
+
on_upload_progress=on_upload_progress,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
assert deployment.build_id == "build-files"
|
|
192
|
+
assert seen["mutation"]["variables"]["input"] == {
|
|
193
|
+
"appName": "hello-stackmachine",
|
|
194
|
+
"owner": "tester",
|
|
195
|
+
"uploadUrl": "https://storage.example.test/app.zip",
|
|
196
|
+
}
|
|
197
|
+
assert "files" not in seen["mutation"]["variables"]["input"]
|
|
198
|
+
assert upload_progress[0].loaded == 0
|
|
199
|
+
assert upload_progress[-1].percent == 1
|
|
200
|
+
with zipfile.ZipFile(BytesIO(seen["archive"])) as archive:
|
|
201
|
+
assert archive.read("index.html") == b"<html><body><h1>Hello</h1></body></html>"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def test_async_deployments_create_accepts_files() -> None:
|
|
205
|
+
seen: dict[str, Any] = {}
|
|
206
|
+
|
|
207
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
208
|
+
if str(request.url) == "https://storage.example.test/app.zip":
|
|
209
|
+
return httpx.Response(
|
|
210
|
+
200, headers={"Location": "https://storage.example.test/session"}
|
|
211
|
+
)
|
|
212
|
+
if str(request.url) == "https://storage.example.test/session":
|
|
213
|
+
return httpx.Response(200)
|
|
214
|
+
|
|
215
|
+
body = json.loads(request.content)
|
|
216
|
+
if body["operationName"] == "uploadQuery":
|
|
217
|
+
return graphql_response(
|
|
218
|
+
{"getSignedUrl": {"url": "https://storage.example.test/app.zip"}}
|
|
219
|
+
)
|
|
220
|
+
if body["operationName"] == "srcAutobuildMutation":
|
|
221
|
+
seen["mutation"] = body
|
|
222
|
+
return graphql_response(
|
|
223
|
+
{
|
|
224
|
+
"deployViaAutobuild": {
|
|
225
|
+
"success": True,
|
|
226
|
+
"buildId": "build-files-async",
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
raise AssertionError(f"Unexpected operation {body['operationName']}")
|
|
231
|
+
|
|
232
|
+
client = AsyncStackMachine("secret", http_transport=httpx.MockTransport(handler))
|
|
233
|
+
try:
|
|
234
|
+
deployment = await client.deployments.create(
|
|
235
|
+
app_name="hello-stackmachine",
|
|
236
|
+
owner="tester",
|
|
237
|
+
files={"index.html": "<h1>Hello</h1>"},
|
|
238
|
+
chunk_size=10_000_000,
|
|
239
|
+
)
|
|
240
|
+
finally:
|
|
241
|
+
await client.close()
|
|
242
|
+
|
|
243
|
+
assert deployment.build_id == "build-files-async"
|
|
244
|
+
assert seen["mutation"]["variables"]["input"] == {
|
|
245
|
+
"appName": "hello-stackmachine",
|
|
246
|
+
"owner": "tester",
|
|
247
|
+
"uploadUrl": "https://storage.example.test/app.zip",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_deployments_create_rejects_files_with_upload_url() -> None:
|
|
252
|
+
calls = 0
|
|
253
|
+
|
|
254
|
+
def handler(_: httpx.Request) -> httpx.Response:
|
|
255
|
+
nonlocal calls
|
|
256
|
+
calls += 1
|
|
257
|
+
return graphql_response({})
|
|
258
|
+
|
|
259
|
+
with StackMachine("secret", http_transport=httpx.MockTransport(handler)) as client:
|
|
260
|
+
with pytest.raises(StackMachineValidationError) as exc_info:
|
|
261
|
+
client.deployments.create(
|
|
262
|
+
app_name="ambiguous",
|
|
263
|
+
owner="tester",
|
|
264
|
+
upload_url="https://storage.example.test/app.zip",
|
|
265
|
+
files={"index.html": "<h1>Hello</h1>"},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
assert exc_info.value.code == "invalid_deployment_source"
|
|
269
|
+
assert exc_info.value.param == "files"
|
|
270
|
+
assert calls == 0
|
|
271
|
+
|
|
272
|
+
|
|
126
273
|
def test_graphql_errors_are_mapped() -> None:
|
|
127
274
|
def handler(_: httpx.Request) -> httpx.Response:
|
|
128
275
|
return httpx.Response(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|