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.
Files changed (31) hide show
  1. {stackmachine-0.3.1 → stackmachine-0.3.3}/PKG-INFO +26 -21
  2. {stackmachine-0.3.1 → stackmachine-0.3.3}/README.md +25 -20
  3. {stackmachine-0.3.1 → stackmachine-0.3.3}/pyproject.toml +1 -1
  4. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/__init__.py +3 -1
  5. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_async_client.py +20 -8
  6. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_client.py +20 -8
  7. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_types.py +4 -2
  8. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/apps.py +23 -3
  9. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/deployments.py +59 -4
  10. {stackmachine-0.3.1 → stackmachine-0.3.3}/tests/test_package.py +150 -3
  11. {stackmachine-0.3.1 → stackmachine-0.3.3}/.gitignore +0 -0
  12. {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/async_usage.py +0 -0
  13. {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/basic_usage.py +0 -0
  14. {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/deploy_app.py +0 -0
  15. {stackmachine-0.3.1 → stackmachine-0.3.3}/examples/list_apps.py +0 -0
  16. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_config.py +0 -0
  17. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_errors.py +0 -0
  18. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_graphql/__init__.py +0 -0
  19. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_graphql/operations.py +0 -0
  20. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_models.py +0 -0
  21. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_pagination.py +0 -0
  22. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_transport.py +0 -0
  23. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_uploads.py +0 -0
  24. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/_utils.py +0 -0
  25. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/py.typed +0 -0
  26. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/__init__.py +0 -0
  27. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/_shared.py +0 -0
  28. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/domains.py +0 -0
  29. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/files.py +0 -0
  30. {stackmachine-0.3.1 → stackmachine-0.3.3}/src/stackmachine/resources/ssh.py +0 -0
  31. {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.1
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 JavaScript-style aliases during initialization:
64
+ Both clients accept configuration options during initialization:
65
65
 
66
66
  ```python
67
- stackmachine = StackMachine.init(
68
- {
69
- "token": "sk_stackmachine_...",
70
- "apiUrl": "https://api.stackmachine.com/graphql",
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
- "file": "https://example.com/app.zip",
106
- "name": "my-app",
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
- "file": "https://example.com/app.zip",
117
- "name": "my-app",
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
- "api_key": "sk_stackmachine_other",
169
- "timeout": 30,
170
- "idempotency_key": "deploy-123",
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 JavaScript-style aliases during initialization:
42
+ Both clients accept configuration options during initialization:
43
43
 
44
44
  ```python
45
- stackmachine = StackMachine.init(
46
- {
47
- "token": "sk_stackmachine_...",
48
- "apiUrl": "https://api.stackmachine.com/graphql",
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
- "file": "https://example.com/app.zip",
84
- "name": "my-app",
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
- "file": "https://example.com/app.zip",
95
- "name": "my-app",
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
- "api_key": "sk_stackmachine_other",
147
- "timeout": 30,
148
- "idempotency_key": "deploy-123",
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "stackmachine"
7
- version = "0.3.1"
7
+ version = "0.3.3"
8
8
  description = "Python SDK for StackMachine."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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.1"
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 = DEFAULT_API_URL,
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 = DEFAULT_MAX_NETWORK_RETRIES,
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 = api_url
37
- self.apiUrl = api_url
48
+ self.api_url = resolved_api_url
49
+ self.apiUrl = resolved_api_url
38
50
  self.timeout = timeout
39
- self.max_network_retries = max_network_retries
40
- self.maxNetworkRetries = max_network_retries
51
+ self.max_network_retries = resolved_max_retries
52
+ self.maxNetworkRetries = resolved_max_retries
41
53
  self._config = ClientConfig(
42
- api_url=api_url,
54
+ api_url=resolved_api_url,
43
55
  headers=headers,
44
56
  timeout=timeout,
45
- max_network_retries=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 = DEFAULT_API_URL,
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 = DEFAULT_MAX_NETWORK_RETRIES,
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 = api_url
37
- self.apiUrl = api_url
48
+ self.api_url = resolved_api_url
49
+ self.apiUrl = resolved_api_url
38
50
  self.timeout = timeout
39
- self.max_network_retries = max_network_retries
40
- self.maxNetworkRetries = max_network_retries
51
+ self.max_network_retries = resolved_max_retries
52
+ self.maxNetworkRetries = resolved_max_retries
41
53
  self._config = ClientConfig(
42
- api_url=api_url,
54
+ api_url=resolved_api_url,
43
55
  headers=headers,
44
56
  timeout=timeout,
45
- max_network_retries=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
- camelize(merge_input(input, **kwargs)), request_options=request_options
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
- camelize(merge_input(input, **kwargs)), request_options=request_options
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(merge_input(input, **kwargs))},
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(merge_input(input, **kwargs))},
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 test_client_init_accepts_js_style_aliases() -> None:
69
- client = StackMachine.init(
70
- {"token": "token-1", "apiUrl": "https://api.example/graphql"},
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