phala-cloud 0.2.0__tar.gz → 0.3.0__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.
- phala_cloud-0.3.0/CHANGELOG.md +33 -0
- phala_cloud-0.3.0/PKG-INFO +232 -0
- phala_cloud-0.3.0/README.md +200 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/pyproject.toml +4 -2
- phala_cloud-0.3.0/src/phala_cloud/__init__.py +142 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/action_responses.py +73 -4
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/actions.py +226 -21
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/client.py +60 -6
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/full_client.py +793 -222
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/models/__init__.py +20 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/models/auth.py +10 -0
- phala_cloud-0.3.0/src/phala_cloud/models/base.py +35 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/models/cvms.py +115 -7
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/models/nodes.py +17 -1
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/result.py +3 -3
- phala_cloud-0.3.0/src/phala_cloud/utils.py +443 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_action_matrix_async.py +6 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_action_matrix_sync.py +5 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_actions_module.py +0 -4
- phala_cloud-0.3.0/tests/test_auth_config.py +151 -0
- phala_cloud-0.3.0/tests/test_commit_cvm_update.py +61 -0
- phala_cloud-0.3.0/tests/test_compose_hash_dump_dict.py +129 -0
- phala_cloud-0.3.0/tests/test_compose_hash_vectors.py +22 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_create_app_instance.py +4 -3
- phala_cloud-0.3.0/tests/test_cvm_config_endpoints.py +157 -0
- phala_cloud-0.3.0/tests/test_cvm_create_config_fields.py +95 -0
- phala_cloud-0.3.0/tests/test_cvm_creation_node.py +113 -0
- phala_cloud-0.3.0/tests/test_cvm_ssh_keys.py +90 -0
- phala_cloud-0.3.0/tests/test_cvm_update_contract.py +275 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_e2e_all_interfaces.py +6 -2
- phala_cloud-0.3.0/tests/test_encrypt_env_vars.py +42 -0
- phala_cloud-0.3.0/tests/test_js_parity_schemas.py +173 -0
- phala_cloud-0.3.0/tests/test_mr_config_id.py +377 -0
- phala_cloud-0.3.0/tests/test_node_placement.py +118 -0
- phala_cloud-0.3.0/tests/test_prelaunch_script_upgrade.py +145 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_provision_compose_update.py +12 -0
- phala_cloud-0.3.0/tests/test_redirect_security.py +216 -0
- phala_cloud-0.3.0/tests/test_replicate_cvm.py +56 -0
- phala_cloud-0.3.0/tests/test_revoke_current_api_token.py +33 -0
- phala_cloud-0.3.0/tests/test_safe_result.py +14 -0
- phala_cloud-0.3.0/tests/test_unsupported_blockchain_actions.py +13 -0
- phala_cloud-0.3.0/tests/test_update_cvm_resources.py +49 -0
- phala_cloud-0.3.0/tests/test_watch_delay.py +41 -0
- phala_cloud-0.3.0/tests/test_watch_errors.py +206 -0
- phala_cloud-0.3.0/tests/test_watch_reconnect.py +201 -0
- phala_cloud-0.3.0/tests/test_watch_retry_options.py +47 -0
- phala_cloud-0.3.0/tests/test_with_version.py +49 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/uv.lock +133 -79
- phala_cloud-0.2.0/CHANGELOG.md +0 -2
- phala_cloud-0.2.0/PKG-INFO +0 -165
- phala_cloud-0.2.0/README.md +0 -134
- phala_cloud-0.2.0/pdm.lock +0 -1093
- phala_cloud-0.2.0/src/phala_cloud/__init__.py +0 -65
- phala_cloud-0.2.0/src/phala_cloud/blockchains.py +0 -32
- phala_cloud-0.2.0/src/phala_cloud/models/base.py +0 -18
- phala_cloud-0.2.0/src/phala_cloud/utils.py +0 -133
- phala_cloud-0.2.0/tests/test_mr_config_id.py +0 -125
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/.gitignore +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/.pdm-python +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/.python-version +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/Makefile +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/examples/repl.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/error_codes.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/errors.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/models/apps.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/models/kms.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/models/os_images.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/src/phala_cloud/webhook.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_465_structured_error.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_client_async.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_client_sync.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_cvm_hashid_version_contract.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_cvm_kms_info_chain.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_kms_contract.py +0 -0
- {phala_cloud-0.2.0 → phala_cloud-0.3.0}/tests/test_webhook.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## [](https://github.com/Phala-Network/phala-cloud/compare/python-v0.1.3...python-v) (2026-07-26)
|
|
2
|
+
|
|
3
|
+
### ⚠ BREAKING CHANGE
|
|
4
|
+
|
|
5
|
+
* **sdk:** reject deprecated user_id/teepod_id/node_id CVM list filters
|
|
6
|
+
|
|
7
|
+
### feat
|
|
8
|
+
|
|
9
|
+
* add algorithm_version to DeviceIdEntry ([701ac00](https://github.com/Phala-Network/phala-cloud/commit/701ac0017acc5fb7851e285e27cc1e9ba7af05cd))
|
|
10
|
+
* add device_ids matrix to node schemas ([de36a3a](https://github.com/Phala-Network/phala-cloud/commit/de36a3af84b1f16abdadeada607d00843817e244))
|
|
11
|
+
* add kms_contract_id to CVM SDK schemas, deprecate kms_id ([16734b9](https://github.com/Phala-Network/phala-cloud/commit/16734b9d22c2e6584c3c320b64d2bd48cdcb5a2b))
|
|
12
|
+
* **go,python:** add mr_config_id computation ([d558b09](https://github.com/Phala-Network/phala-cloud/commit/d558b09cb114300c861ac99d76915779e3363993))
|
|
13
|
+
* **python:** add typed CvmStatus response to get_cvm_status_batch ([01ee3fd](https://github.com/Phala-Network/phala-cloud/commit/01ee3fd2e460700f518278f41cb2ea6b701a61c5))
|
|
14
|
+
* **python:** type app attestation response ([058197b](https://github.com/Phala-Network/phala-cloud/commit/058197ba6940c3fcc0de2d857b4fef3ff562cedc))
|
|
15
|
+
* **sdk/cli:** surface KMS contract verification anchors in on-chain detail ([35a48a3](https://github.com/Phala-Network/phala-cloud/commit/35a48a3a81a4d762cb3a7b49bdcddfb196fd6db1))
|
|
16
|
+
* **sdk:** add hourly_rate, billing_interval, gpu_rental_order to CVM v20260121 ([0f665eb](https://github.com/Phala-Network/phala-cloud/commit/0f665ebaa68158804a835488be4dd76cf1708895))
|
|
17
|
+
* **sdk:** contract-centric KMS API, default to version 2026-06-23 ([0fb89e2](https://github.com/Phala-Network/phala-cloud/commit/0fb89e23c2c7a62bca6ef7f32649d06c9eabf2e4))
|
|
18
|
+
|
|
19
|
+
### fix
|
|
20
|
+
|
|
21
|
+
* **ci:** scope changelog generation to each package's subdirectory ([e2364a8](https://github.com/Phala-Network/phala-cloud/commit/e2364a87872665e5cbc9f62ba0e5ffb15dc04e9b))
|
|
22
|
+
* **cli:** surface request details in API errors ([c53a329](https://github.com/Phala-Network/phala-cloud/commit/c53a329b4cfa3027e05d27c7e3ed5bb8e0fbbfbb))
|
|
23
|
+
* **js:** add version-based schema selection for versioned action responses ([d96f402](https://github.com/Phala-Network/phala-cloud/commit/d96f402353cbbd6557c14908c6d2ba795dab28b7))
|
|
24
|
+
* **python:** lock pysha3 dependency ([e13cc57](https://github.com/Phala-Network/phala-cloud/commit/e13cc57a71699001b0f423361795c0b5084fd00b))
|
|
25
|
+
* **python:** resolve ruff E402 and E501 lint errors ([2b493ce](https://github.com/Phala-Network/phala-cloud/commit/2b493ce2332faeeb9777d57528bf4239ec7d6a8a))
|
|
26
|
+
* **sdk:** expose k256_pubkey on Go and Python CVM KMS info ([b9721b6](https://github.com/Phala-Network/phala-cloud/commit/b9721b6702c93da00370e99b66aa46463a108e53))
|
|
27
|
+
* **sdk:** reject deprecated user_id/teepod_id/node_id CVM list filters ([399cea5](https://github.com/Phala-Network/phala-cloud/commit/399cea5a56054f7566b36ba6f950e4e8eb4bfeef))
|
|
28
|
+
|
|
29
|
+
### refactor
|
|
30
|
+
|
|
31
|
+
* migrate resize endpoint across all SDKs ([56638f1](https://github.com/Phala-Network/phala-cloud/commit/56638f1c1592a1a48dab7fedf9152b60c8e52a01))
|
|
32
|
+
# Changelog
|
|
33
|
+
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: phala-cloud
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for Phala Cloud API (sync/async, pydantic, REPL-friendly)
|
|
5
|
+
Project-URL: Homepage, https://github.com/Phala-Network/phala-cloud
|
|
6
|
+
Project-URL: Repository, https://github.com/Phala-Network/phala-cloud
|
|
7
|
+
Project-URL: Issues, https://github.com/Phala-Network/phala-cloud/issues
|
|
8
|
+
Author: Phala Network
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: cloud,cvm,phala,sdk,tee
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: certifi>=2026.10.19
|
|
22
|
+
Requires-Dist: dstack-sdk>=0.5.4
|
|
23
|
+
Requires-Dist: eth-hash[pycryptodome]>=0.8.0
|
|
24
|
+
Requires-Dist: httpx[socks]>=0.27.0
|
|
25
|
+
Requires-Dist: pydantic>=2.7.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Phala Cloud Python SDK
|
|
34
|
+
|
|
35
|
+
Python SDK for Phala Cloud API, aligned with `@phala/cloud` action surface.
|
|
36
|
+
|
|
37
|
+
## Goals
|
|
38
|
+
|
|
39
|
+
- Pythonic API (`snake_case`, sync-first REPL experience)
|
|
40
|
+
- Sync + Async parity
|
|
41
|
+
- Pydantic request/response validation
|
|
42
|
+
- Safe calls (`safe_*`) without exceptions
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python >= 3.10
|
|
47
|
+
- [uv](https://docs.astral.sh/uv/) (for development)
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install phala-cloud
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
### Sync
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from phala_cloud import create_client
|
|
61
|
+
|
|
62
|
+
client = create_client(api_key="<api-key>")
|
|
63
|
+
me = client.get_current_user()
|
|
64
|
+
print(me.model_dump())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Async
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
from phala_cloud import create_async_client
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def main() -> None:
|
|
75
|
+
client = create_async_client(api_key="<api-key>")
|
|
76
|
+
me = await client.get_current_user()
|
|
77
|
+
print(me.model_dump())
|
|
78
|
+
await client.aclose()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
### Environment variables
|
|
87
|
+
|
|
88
|
+
- `PHALA_CLOUD_API_KEY`
|
|
89
|
+
- `PHALA_OIDC_TOKEN`
|
|
90
|
+
- `PHALA_CLOUD_WORKSPACE`
|
|
91
|
+
- `PHALA_CLOUD_API_PREFIX` (e.g. `https://cloud-api.phala.com/api/v1`)
|
|
92
|
+
|
|
93
|
+
### Client options
|
|
94
|
+
|
|
95
|
+
- `api_key`
|
|
96
|
+
- `bearer_token`
|
|
97
|
+
- `workspace`
|
|
98
|
+
- `base_url`
|
|
99
|
+
- `version` (`2025-10-28` / `2026-01-21` / `2026-05-22`)
|
|
100
|
+
- `timeout`
|
|
101
|
+
- `headers`
|
|
102
|
+
- `use_cookie_auth`
|
|
103
|
+
|
|
104
|
+
A non-empty client option takes precedence over its corresponding environment variable. After resolving both credentials, the SDK uses the API key if both are set. This includes an API key from `PHALA_CLOUD_API_KEY` when `bearer_token` is set explicitly. Cookie auth suppresses both generated authentication headers; it does not remove headers supplied by the caller. The SDK removes surrounding whitespace from the workspace value.
|
|
105
|
+
|
|
106
|
+
When you supply `http_client`, its base URL and timeout control requests; configure them on that client. Watch streams use their own read timeout, as described below. The SDK does not close the supplied client.
|
|
107
|
+
|
|
108
|
+
## Does the SDK follow redirects?
|
|
109
|
+
|
|
110
|
+
No. Redirects can send your API key to another server, so the SDK stops at the first response. This applies to API calls and SSE streams. Set `base_url` to the final API URL instead of relying on a redirect.
|
|
111
|
+
|
|
112
|
+
Direct calls raise `ApiError` for 3xx responses. `request_full` returns the original status, headers, and body with `ok=False`, so you can inspect the response without following it.
|
|
113
|
+
|
|
114
|
+
The SDK ignores `follow_redirects=True` on SDK requests, including requests made with a supplied `http_client`. It does not change that client's settings or close the supplied client. Custom transports must not follow redirects internally.
|
|
115
|
+
|
|
116
|
+
## API Style
|
|
117
|
+
|
|
118
|
+
- Direct methods: raise on HTTP/validation errors
|
|
119
|
+
- Safe methods (`safe_*`): return `SafeResult`
|
|
120
|
+
- Function-style wrappers available in `phala_cloud.actions`
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
result = client.safe_get_kms_list()
|
|
126
|
+
if result.ok:
|
|
127
|
+
print(result.data.items)
|
|
128
|
+
else:
|
|
129
|
+
print(result.error)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## How do I commit a prepared CVM update?
|
|
133
|
+
|
|
134
|
+
Pass the CVM ID and the one-time token to `commit_cvm_update`. `compose_hash` and `transaction_hash` are optional. If you omit the compose hash, the server reads it from the token. If you omit the transaction hash, the server checks whether the compose hash is already allowed on-chain. The server still validates the token and the update.
|
|
135
|
+
|
|
136
|
+
## How do I change a CVM's app compose settings?
|
|
137
|
+
|
|
138
|
+
Send `compose` to `patch_cvm` as a JSON Merge Patch: a set field sets the value, a field set to `None` removes the key, and a field you leave out is unchanged. `requirements` merges per key and cannot change `gpu_policy`. `init_scripts` replaces the CVM's platform init scripts with the given slugs (`docker_cleanup`, `env_merge`); `get_cvm_init_scripts` shows which ones the CVM runs and which it can select. Turning on `docker_cleanup` or `requirements.ephemeral_data_disk` erases data on every boot, so it also needs `confirm_data_wipe=True`.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
client.patch_cvm({
|
|
142
|
+
"id": "my-cvm",
|
|
143
|
+
"compose": {"secure_time": True, "swap_size": None, "requirements": {"health_check": True}},
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## How do I gate a CVM with a launch token?
|
|
148
|
+
|
|
149
|
+
On create, send `launch_token` (at least 32 bytes) or `generate_launch_token=True`; the commit response carries a newly issued token once. To rotate, call `set_app_launch_token` (omit `launch_token` to have Phala Cloud generate one), then set each CVM's `compose.requirements.launch_token_hash` to the returned hash before it restarts. `clear_app_launch_token` removes the token after every CVM has dropped the hash. `get_cvm_user_config` returns the token to members who can read the CVM. Phala Cloud and the node operator can read the token, so it protects only against parties who know the public compose.
|
|
150
|
+
|
|
151
|
+
## How do I compute a compose hash locally?
|
|
152
|
+
|
|
153
|
+
`get_compose_hash(compose, os_image_version="0.6.0")` hashes the compose the way Phala Cloud does for that OS image: below 0.6.0 `manifest_version` is 2 and `init_script` is a single string, from 0.6.0 they are `"3"` and a list. Without `os_image_version` both fields are hashed as given.
|
|
154
|
+
|
|
155
|
+
## How does watching a CVM retry?
|
|
156
|
+
|
|
157
|
+
`watch_cvm_state` reconnects after a server timeout event, an incomplete stream, or a transient network failure. Set `max_retries` to limit reconnects; the default is unlimited. Set `retry_delay` in seconds (default 5). The aliases `maxRetries` and `retryDelay` also work. The stream read timeout is the requested `timeout` plus `interval` plus 5 seconds.
|
|
158
|
+
|
|
159
|
+
Only a complete event finishes the watch. The server may send an error event and then continue streaming. If retries are exhausted, the SDK raises `RequestError` with the last available server and network error details. HTTP errors use the same SDK exception types as other requests and are not retried. In async code, cancelling the task closes the stream and propagates cancellation. Previous versions raised `TimeoutError` on retry exhaustion; catch `RequestError` instead.
|
|
160
|
+
|
|
161
|
+
## Are blockchain actions supported?
|
|
162
|
+
|
|
163
|
+
The Python SDK no longer exposes `add_compose_hash`, `deploy_app_auth`, or their safe wrappers. These wrappers did not implement the blockchain operations. Use the JavaScript SDK for these operations instead.
|
|
164
|
+
|
|
165
|
+
## How do I encrypt environment variables?
|
|
166
|
+
|
|
167
|
+
Pass the output of `parse_env_vars` or `dstack_sdk.EnvVar` objects to `encrypt_env_vars(envs, public_key_hex)`. It returns the encrypted string synchronously. In async code, use `await async_encrypt_env_vars(envs, public_key_hex)` instead.
|
|
168
|
+
|
|
169
|
+
This changes the previous `encrypt_env_vars` behavior: code that awaited it must switch to `async_encrypt_env_vars`.
|
|
170
|
+
|
|
171
|
+
## How do I upgrade to these SDK changes?
|
|
172
|
+
|
|
173
|
+
- Replace `await encrypt_env_vars(...)` with `await async_encrypt_env_vars(...)`. The `encrypt_env_vars` function now returns a string synchronously.
|
|
174
|
+
- Use the JavaScript SDK for `add_compose_hash` and `deploy_app_auth`. Their Python wrappers have been removed.
|
|
175
|
+
- Catch `RequestError` instead of `TimeoutError` when watch retries are exhausted. Watch HTTP errors now use SDK exceptions instead of `httpx.HTTPStatusError`.
|
|
176
|
+
- Do not rely on a raw state event followed by a closed stream as a successful watch result. Only a complete event finishes the watch. Timeouts, incomplete streams, and transient network failures use the reconnect budget.
|
|
177
|
+
- `patch_cvm` validates `gpus` as `{"gpus": [{"slot": ..., "product_id": ...}], "attach_mode": ...}`, the shape the API accepts. Other shapes now fail validation instead of being sent and ignored. `patch_cvm` also accepts `instance_type`, and `prepare_only=True` sends `X-Prepare-Only: true`; the 465 result then includes `commit_token`, `commit_url`, `api_commit_url` and `onchain_status`.
|
|
178
|
+
- `update_cvm_visibility` now calls `PATCH /cvms/{id}` instead of the deprecated `PATCH /cvms/{id}/visibility`. It returns `message`, `correlation_id` and `status` instead of the CVM record; call `get_cvm_info` if you need the record.
|
|
179
|
+
- `update_cvm_resources` no longer sends `allow_restart`, which the API ignores. The field is deprecated.
|
|
180
|
+
- `get_compose_hash` now drops null `storage_discard`, `event_log_version`, `snapshotter` and `verity_volumes` and always emits `tproxy_enabled` (defaulting to `gateway_enabled`), as the API does. A compose without `tproxy_enabled` hashes differently than before.
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
cd python
|
|
186
|
+
|
|
187
|
+
# Install dependencies
|
|
188
|
+
uv sync
|
|
189
|
+
|
|
190
|
+
# Run tests
|
|
191
|
+
make test
|
|
192
|
+
|
|
193
|
+
# Lint & format
|
|
194
|
+
make check
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## E2E Test
|
|
198
|
+
|
|
199
|
+
E2E tests cover sync + async, direct + safe styles, and full interface paths.
|
|
200
|
+
|
|
201
|
+
Required:
|
|
202
|
+
|
|
203
|
+
- `PHALA_CLOUD_E2E_API_KEY` — your Phala Cloud API key
|
|
204
|
+
|
|
205
|
+
Optional:
|
|
206
|
+
|
|
207
|
+
- `PHALA_CLOUD_E2E_BASE_URL` — defaults to `https://cloud-api.phala.com/api/v1`
|
|
208
|
+
|
|
209
|
+
### Run
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
cd python
|
|
213
|
+
PHALA_CLOUD_E2E_API_KEY="<api-key>" make e2e
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Or put them in `python/.env.test`:
|
|
217
|
+
|
|
218
|
+
```dotenv
|
|
219
|
+
PHALA_CLOUD_E2E_API_KEY=<api-key>
|
|
220
|
+
# PHALA_CLOUD_E2E_BASE_URL=https://cloud-api.phala.com/api/v1
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Then simply:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
cd python
|
|
227
|
+
make e2e
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
Apache-2.0
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Phala Cloud Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for Phala Cloud API, aligned with `@phala/cloud` action surface.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- Pythonic API (`snake_case`, sync-first REPL experience)
|
|
8
|
+
- Sync + Async parity
|
|
9
|
+
- Pydantic request/response validation
|
|
10
|
+
- Safe calls (`safe_*`) without exceptions
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Python >= 3.10
|
|
15
|
+
- [uv](https://docs.astral.sh/uv/) (for development)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install phala-cloud
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
### Sync
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from phala_cloud import create_client
|
|
29
|
+
|
|
30
|
+
client = create_client(api_key="<api-key>")
|
|
31
|
+
me = client.get_current_user()
|
|
32
|
+
print(me.model_dump())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Async
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from phala_cloud import create_async_client
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def main() -> None:
|
|
43
|
+
client = create_async_client(api_key="<api-key>")
|
|
44
|
+
me = await client.get_current_user()
|
|
45
|
+
print(me.model_dump())
|
|
46
|
+
await client.aclose()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
### Environment variables
|
|
55
|
+
|
|
56
|
+
- `PHALA_CLOUD_API_KEY`
|
|
57
|
+
- `PHALA_OIDC_TOKEN`
|
|
58
|
+
- `PHALA_CLOUD_WORKSPACE`
|
|
59
|
+
- `PHALA_CLOUD_API_PREFIX` (e.g. `https://cloud-api.phala.com/api/v1`)
|
|
60
|
+
|
|
61
|
+
### Client options
|
|
62
|
+
|
|
63
|
+
- `api_key`
|
|
64
|
+
- `bearer_token`
|
|
65
|
+
- `workspace`
|
|
66
|
+
- `base_url`
|
|
67
|
+
- `version` (`2025-10-28` / `2026-01-21` / `2026-05-22`)
|
|
68
|
+
- `timeout`
|
|
69
|
+
- `headers`
|
|
70
|
+
- `use_cookie_auth`
|
|
71
|
+
|
|
72
|
+
A non-empty client option takes precedence over its corresponding environment variable. After resolving both credentials, the SDK uses the API key if both are set. This includes an API key from `PHALA_CLOUD_API_KEY` when `bearer_token` is set explicitly. Cookie auth suppresses both generated authentication headers; it does not remove headers supplied by the caller. The SDK removes surrounding whitespace from the workspace value.
|
|
73
|
+
|
|
74
|
+
When you supply `http_client`, its base URL and timeout control requests; configure them on that client. Watch streams use their own read timeout, as described below. The SDK does not close the supplied client.
|
|
75
|
+
|
|
76
|
+
## Does the SDK follow redirects?
|
|
77
|
+
|
|
78
|
+
No. Redirects can send your API key to another server, so the SDK stops at the first response. This applies to API calls and SSE streams. Set `base_url` to the final API URL instead of relying on a redirect.
|
|
79
|
+
|
|
80
|
+
Direct calls raise `ApiError` for 3xx responses. `request_full` returns the original status, headers, and body with `ok=False`, so you can inspect the response without following it.
|
|
81
|
+
|
|
82
|
+
The SDK ignores `follow_redirects=True` on SDK requests, including requests made with a supplied `http_client`. It does not change that client's settings or close the supplied client. Custom transports must not follow redirects internally.
|
|
83
|
+
|
|
84
|
+
## API Style
|
|
85
|
+
|
|
86
|
+
- Direct methods: raise on HTTP/validation errors
|
|
87
|
+
- Safe methods (`safe_*`): return `SafeResult`
|
|
88
|
+
- Function-style wrappers available in `phala_cloud.actions`
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
result = client.safe_get_kms_list()
|
|
94
|
+
if result.ok:
|
|
95
|
+
print(result.data.items)
|
|
96
|
+
else:
|
|
97
|
+
print(result.error)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## How do I commit a prepared CVM update?
|
|
101
|
+
|
|
102
|
+
Pass the CVM ID and the one-time token to `commit_cvm_update`. `compose_hash` and `transaction_hash` are optional. If you omit the compose hash, the server reads it from the token. If you omit the transaction hash, the server checks whether the compose hash is already allowed on-chain. The server still validates the token and the update.
|
|
103
|
+
|
|
104
|
+
## How do I change a CVM's app compose settings?
|
|
105
|
+
|
|
106
|
+
Send `compose` to `patch_cvm` as a JSON Merge Patch: a set field sets the value, a field set to `None` removes the key, and a field you leave out is unchanged. `requirements` merges per key and cannot change `gpu_policy`. `init_scripts` replaces the CVM's platform init scripts with the given slugs (`docker_cleanup`, `env_merge`); `get_cvm_init_scripts` shows which ones the CVM runs and which it can select. Turning on `docker_cleanup` or `requirements.ephemeral_data_disk` erases data on every boot, so it also needs `confirm_data_wipe=True`.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
client.patch_cvm({
|
|
110
|
+
"id": "my-cvm",
|
|
111
|
+
"compose": {"secure_time": True, "swap_size": None, "requirements": {"health_check": True}},
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## How do I gate a CVM with a launch token?
|
|
116
|
+
|
|
117
|
+
On create, send `launch_token` (at least 32 bytes) or `generate_launch_token=True`; the commit response carries a newly issued token once. To rotate, call `set_app_launch_token` (omit `launch_token` to have Phala Cloud generate one), then set each CVM's `compose.requirements.launch_token_hash` to the returned hash before it restarts. `clear_app_launch_token` removes the token after every CVM has dropped the hash. `get_cvm_user_config` returns the token to members who can read the CVM. Phala Cloud and the node operator can read the token, so it protects only against parties who know the public compose.
|
|
118
|
+
|
|
119
|
+
## How do I compute a compose hash locally?
|
|
120
|
+
|
|
121
|
+
`get_compose_hash(compose, os_image_version="0.6.0")` hashes the compose the way Phala Cloud does for that OS image: below 0.6.0 `manifest_version` is 2 and `init_script` is a single string, from 0.6.0 they are `"3"` and a list. Without `os_image_version` both fields are hashed as given.
|
|
122
|
+
|
|
123
|
+
## How does watching a CVM retry?
|
|
124
|
+
|
|
125
|
+
`watch_cvm_state` reconnects after a server timeout event, an incomplete stream, or a transient network failure. Set `max_retries` to limit reconnects; the default is unlimited. Set `retry_delay` in seconds (default 5). The aliases `maxRetries` and `retryDelay` also work. The stream read timeout is the requested `timeout` plus `interval` plus 5 seconds.
|
|
126
|
+
|
|
127
|
+
Only a complete event finishes the watch. The server may send an error event and then continue streaming. If retries are exhausted, the SDK raises `RequestError` with the last available server and network error details. HTTP errors use the same SDK exception types as other requests and are not retried. In async code, cancelling the task closes the stream and propagates cancellation. Previous versions raised `TimeoutError` on retry exhaustion; catch `RequestError` instead.
|
|
128
|
+
|
|
129
|
+
## Are blockchain actions supported?
|
|
130
|
+
|
|
131
|
+
The Python SDK no longer exposes `add_compose_hash`, `deploy_app_auth`, or their safe wrappers. These wrappers did not implement the blockchain operations. Use the JavaScript SDK for these operations instead.
|
|
132
|
+
|
|
133
|
+
## How do I encrypt environment variables?
|
|
134
|
+
|
|
135
|
+
Pass the output of `parse_env_vars` or `dstack_sdk.EnvVar` objects to `encrypt_env_vars(envs, public_key_hex)`. It returns the encrypted string synchronously. In async code, use `await async_encrypt_env_vars(envs, public_key_hex)` instead.
|
|
136
|
+
|
|
137
|
+
This changes the previous `encrypt_env_vars` behavior: code that awaited it must switch to `async_encrypt_env_vars`.
|
|
138
|
+
|
|
139
|
+
## How do I upgrade to these SDK changes?
|
|
140
|
+
|
|
141
|
+
- Replace `await encrypt_env_vars(...)` with `await async_encrypt_env_vars(...)`. The `encrypt_env_vars` function now returns a string synchronously.
|
|
142
|
+
- Use the JavaScript SDK for `add_compose_hash` and `deploy_app_auth`. Their Python wrappers have been removed.
|
|
143
|
+
- Catch `RequestError` instead of `TimeoutError` when watch retries are exhausted. Watch HTTP errors now use SDK exceptions instead of `httpx.HTTPStatusError`.
|
|
144
|
+
- Do not rely on a raw state event followed by a closed stream as a successful watch result. Only a complete event finishes the watch. Timeouts, incomplete streams, and transient network failures use the reconnect budget.
|
|
145
|
+
- `patch_cvm` validates `gpus` as `{"gpus": [{"slot": ..., "product_id": ...}], "attach_mode": ...}`, the shape the API accepts. Other shapes now fail validation instead of being sent and ignored. `patch_cvm` also accepts `instance_type`, and `prepare_only=True` sends `X-Prepare-Only: true`; the 465 result then includes `commit_token`, `commit_url`, `api_commit_url` and `onchain_status`.
|
|
146
|
+
- `update_cvm_visibility` now calls `PATCH /cvms/{id}` instead of the deprecated `PATCH /cvms/{id}/visibility`. It returns `message`, `correlation_id` and `status` instead of the CVM record; call `get_cvm_info` if you need the record.
|
|
147
|
+
- `update_cvm_resources` no longer sends `allow_restart`, which the API ignores. The field is deprecated.
|
|
148
|
+
- `get_compose_hash` now drops null `storage_discard`, `event_log_version`, `snapshotter` and `verity_volumes` and always emits `tproxy_enabled` (defaulting to `gateway_enabled`), as the API does. A compose without `tproxy_enabled` hashes differently than before.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
cd python
|
|
154
|
+
|
|
155
|
+
# Install dependencies
|
|
156
|
+
uv sync
|
|
157
|
+
|
|
158
|
+
# Run tests
|
|
159
|
+
make test
|
|
160
|
+
|
|
161
|
+
# Lint & format
|
|
162
|
+
make check
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## E2E Test
|
|
166
|
+
|
|
167
|
+
E2E tests cover sync + async, direct + safe styles, and full interface paths.
|
|
168
|
+
|
|
169
|
+
Required:
|
|
170
|
+
|
|
171
|
+
- `PHALA_CLOUD_E2E_API_KEY` — your Phala Cloud API key
|
|
172
|
+
|
|
173
|
+
Optional:
|
|
174
|
+
|
|
175
|
+
- `PHALA_CLOUD_E2E_BASE_URL` — defaults to `https://cloud-api.phala.com/api/v1`
|
|
176
|
+
|
|
177
|
+
### Run
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
cd python
|
|
181
|
+
PHALA_CLOUD_E2E_API_KEY="<api-key>" make e2e
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Or put them in `python/.env.test`:
|
|
185
|
+
|
|
186
|
+
```dotenv
|
|
187
|
+
PHALA_CLOUD_E2E_API_KEY=<api-key>
|
|
188
|
+
# PHALA_CLOUD_E2E_BASE_URL=https://cloud-api.phala.com/api/v1
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Then simply:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
cd python
|
|
195
|
+
make e2e
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "phala-cloud"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python SDK for Phala Cloud API (sync/async, pydantic, REPL-friendly)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -24,9 +24,11 @@ classifiers = [
|
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
26
|
"httpx[socks]>=0.27.0",
|
|
27
|
+
# 2026.10.19 removes emSign ECC Root CA - G3 (compromised eMudhra intermediates).
|
|
28
|
+
"certifi>=2026.10.19",
|
|
27
29
|
"pydantic>=2.7.0",
|
|
28
30
|
"dstack-sdk>=0.5.4",
|
|
29
|
-
"
|
|
31
|
+
"eth-hash[pycryptodome]>=0.8.0",
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from . import error_codes
|
|
2
|
+
from .action_responses import (
|
|
3
|
+
AppLaunchTokenResponse,
|
|
4
|
+
CvmAnnotationsResponse,
|
|
5
|
+
CvmInitScript,
|
|
6
|
+
CvmInitScriptsResponse,
|
|
7
|
+
CvmUserConfigResponse,
|
|
8
|
+
)
|
|
9
|
+
from .actions import * # noqa: F403
|
|
10
|
+
from .actions import (
|
|
11
|
+
commit_cvm_update,
|
|
12
|
+
get_cvm_ssh_keys,
|
|
13
|
+
revoke_current_api_token,
|
|
14
|
+
safe_commit_cvm_update,
|
|
15
|
+
safe_get_cvm_ssh_keys,
|
|
16
|
+
safe_revoke_current_api_token,
|
|
17
|
+
safe_update_cvm_ssh_keys,
|
|
18
|
+
update_cvm_ssh_keys,
|
|
19
|
+
)
|
|
20
|
+
from .client import (
|
|
21
|
+
DEFAULT_API_VERSION,
|
|
22
|
+
DEFAULT_TIMEOUT,
|
|
23
|
+
SUPPORTED_API_VERSIONS,
|
|
24
|
+
ApiVersion,
|
|
25
|
+
)
|
|
26
|
+
from .errors import (
|
|
27
|
+
ApiError,
|
|
28
|
+
AuthError,
|
|
29
|
+
BusinessError,
|
|
30
|
+
ConflictError,
|
|
31
|
+
PhalaCloudError,
|
|
32
|
+
RequestError,
|
|
33
|
+
ResourceError,
|
|
34
|
+
ServerError,
|
|
35
|
+
ValidationError,
|
|
36
|
+
)
|
|
37
|
+
from .full_client import (
|
|
38
|
+
AsyncPhalaCloud,
|
|
39
|
+
CommitCvmUpdateRequest,
|
|
40
|
+
CommitCvmUpdateResult,
|
|
41
|
+
CreateAppInstanceRequest,
|
|
42
|
+
CvmIdRequest,
|
|
43
|
+
CvmSshKey,
|
|
44
|
+
CvmSshKeysResponse,
|
|
45
|
+
PhalaCloud,
|
|
46
|
+
ReplicateCvmRequest,
|
|
47
|
+
SetAppLaunchTokenRequest,
|
|
48
|
+
UpdateCvmAnnotationsRequest,
|
|
49
|
+
UpdateCvmSshKeysRequest,
|
|
50
|
+
create_async_client,
|
|
51
|
+
create_client,
|
|
52
|
+
)
|
|
53
|
+
from .models import * # noqa: F403
|
|
54
|
+
from .models import CheckAppIsAllowedRequest, CheckCvmIsAllowedRequest
|
|
55
|
+
from .result import SafeResult
|
|
56
|
+
from .utils import (
|
|
57
|
+
MAX_INIT_SCRIPTS,
|
|
58
|
+
MrConfigV3Document,
|
|
59
|
+
async_encrypt_env_vars,
|
|
60
|
+
build_mr_config_v3_document,
|
|
61
|
+
canonicalize_mr_config_v3_document,
|
|
62
|
+
encrypt_env_vars,
|
|
63
|
+
get_compose_hash,
|
|
64
|
+
get_mr_config_id,
|
|
65
|
+
get_mr_config_id_hex,
|
|
66
|
+
get_mr_config_id_v1,
|
|
67
|
+
get_mr_config_id_v3,
|
|
68
|
+
get_mr_config_id_v3_hex,
|
|
69
|
+
get_snp_host_data_v3,
|
|
70
|
+
parse_env,
|
|
71
|
+
parse_env_vars,
|
|
72
|
+
verify_env_encrypt_public_key,
|
|
73
|
+
verify_mr_config_id,
|
|
74
|
+
)
|
|
75
|
+
from .webhook import WebhookEvent, parse_webhook_event, verify_webhook_signature
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"ApiError",
|
|
79
|
+
"ApiVersion",
|
|
80
|
+
"AsyncPhalaCloud",
|
|
81
|
+
"AuthError",
|
|
82
|
+
"BusinessError",
|
|
83
|
+
"ConflictError",
|
|
84
|
+
"CheckAppIsAllowedRequest",
|
|
85
|
+
"CheckCvmIsAllowedRequest",
|
|
86
|
+
"CommitCvmUpdateRequest",
|
|
87
|
+
"CommitCvmUpdateResult",
|
|
88
|
+
"commit_cvm_update",
|
|
89
|
+
"safe_commit_cvm_update",
|
|
90
|
+
"CreateAppInstanceRequest",
|
|
91
|
+
"CvmIdRequest",
|
|
92
|
+
"CvmSshKey",
|
|
93
|
+
"CvmSshKeysResponse",
|
|
94
|
+
"UpdateCvmSshKeysRequest",
|
|
95
|
+
"AppLaunchTokenResponse",
|
|
96
|
+
"CvmAnnotationsResponse",
|
|
97
|
+
"CvmInitScript",
|
|
98
|
+
"CvmInitScriptsResponse",
|
|
99
|
+
"CvmUserConfigResponse",
|
|
100
|
+
"SetAppLaunchTokenRequest",
|
|
101
|
+
"UpdateCvmAnnotationsRequest",
|
|
102
|
+
"get_cvm_ssh_keys",
|
|
103
|
+
"safe_get_cvm_ssh_keys",
|
|
104
|
+
"update_cvm_ssh_keys",
|
|
105
|
+
"safe_update_cvm_ssh_keys",
|
|
106
|
+
"DEFAULT_API_VERSION",
|
|
107
|
+
"DEFAULT_TIMEOUT",
|
|
108
|
+
"PhalaCloud",
|
|
109
|
+
"PhalaCloudError",
|
|
110
|
+
"ReplicateCvmRequest",
|
|
111
|
+
"RequestError",
|
|
112
|
+
"revoke_current_api_token",
|
|
113
|
+
"safe_revoke_current_api_token",
|
|
114
|
+
"ResourceError",
|
|
115
|
+
"SUPPORTED_API_VERSIONS",
|
|
116
|
+
"SafeResult",
|
|
117
|
+
"ServerError",
|
|
118
|
+
"ValidationError",
|
|
119
|
+
"async_encrypt_env_vars",
|
|
120
|
+
"encrypt_env_vars",
|
|
121
|
+
"error_codes",
|
|
122
|
+
"get_compose_hash",
|
|
123
|
+
"MAX_INIT_SCRIPTS",
|
|
124
|
+
"MrConfigV3Document",
|
|
125
|
+
"build_mr_config_v3_document",
|
|
126
|
+
"canonicalize_mr_config_v3_document",
|
|
127
|
+
"get_mr_config_id",
|
|
128
|
+
"get_mr_config_id_hex",
|
|
129
|
+
"get_mr_config_id_v1",
|
|
130
|
+
"get_mr_config_id_v3",
|
|
131
|
+
"get_mr_config_id_v3_hex",
|
|
132
|
+
"get_snp_host_data_v3",
|
|
133
|
+
"parse_env",
|
|
134
|
+
"parse_env_vars",
|
|
135
|
+
"verify_env_encrypt_public_key",
|
|
136
|
+
"verify_mr_config_id",
|
|
137
|
+
"verify_webhook_signature",
|
|
138
|
+
"parse_webhook_event",
|
|
139
|
+
"WebhookEvent",
|
|
140
|
+
"create_async_client",
|
|
141
|
+
"create_client",
|
|
142
|
+
]
|