morphcloud 0.1.111__tar.gz → 0.1.113__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.
- {morphcloud-0.1.111 → morphcloud-0.1.113}/.gitignore +8 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/PKG-INFO +99 -4
- {morphcloud-0.1.111 → morphcloud-0.1.113}/README.md +98 -3
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/api.py +233 -3
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/cli.py +270 -8
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/config.py +12 -0
- morphcloud-0.1.113/morphcloud/plugins/__init__.py +1 -0
- morphcloud-0.1.113/morphcloud/plugins/cli.py +1164 -0
- morphcloud-0.1.113/morphcloud/plugins/installer.py +569 -0
- morphcloud-0.1.113/morphcloud/simple_index/__init__.py +6 -0
- morphcloud-0.1.113/morphcloud/simple_index/client.py +415 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/pyproject.toml +1 -1
- morphcloud-0.1.113/tests/integration/test_managed_ssh_keys.py +146 -0
- morphcloud-0.1.113/tests/unit/test_cli_managed_ssh_keys.py +249 -0
- morphcloud-0.1.113/tests/unit/test_cli_plugins.py +1764 -0
- morphcloud-0.1.113/tests/unit/test_managed_ssh_keys_api.py +233 -0
- morphcloud-0.1.113/tests/unit/test_plugin_installer.py +431 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_profiles.py +8 -0
- morphcloud-0.1.113/tests/unit/test_simple_index_client.py +392 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/.github/workflows/publish.yaml +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/LICENSE +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/Makefile +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/conf.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/examples/browser_example.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/examples/devbox_example.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/index.rst +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/make.bat +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/_asyncify.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/_bash_interpreter.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/_http_transport.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/_llm.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/_scramble.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/_ssh.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/_utils.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/cli_helpers.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/cli.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/config.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/admin/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/admin/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/admin/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/aliases/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/aliases/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/aliases/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/api_error.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/client_wrapper.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/datetime_utils.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/file.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/force_multipart.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/http_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/http_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/jsonable_encoder.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/pydantic_utilities.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/query_encoder.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/remove_none_from_dict.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/request_options.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/core/serialization.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_actions/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_actions/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_actions/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_core/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_core/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_core/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_lifecycle/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_lifecycle/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/devboxes_lifecycle/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/errors/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/errors/unprocessable_entity_error.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/templates/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/templates/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/templates/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/tmux/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/tmux/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/tmux/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/alias_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/branch_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/delete_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/devbox_detail_list_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/devbox_list_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/devbox_refs.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/devbox_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/devbox_spec.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/devbox_ssh_credentials_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/devbox_summary_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/http_service.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/http_service_auth_mode.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/http_validation_error.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/networking.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/secret_value_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/snapshot_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/success_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_cache_request.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_cache_start_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_detail.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_list_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_share_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_status.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_step_model.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_step_status.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/template_summary.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/timeline_event.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_debug_info_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_install_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_new_session_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_send_keys_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_session.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_sessions_list.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_snapshot_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/tmux_target.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/ttl.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/upload_response.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/validation_error.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/validation_error_loc_item.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/types/wake_on.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/uploads/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/uploads/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/uploads/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/user_secrets/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/user_secrets/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/gen/user_secrets/raw_client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/openapi.json +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/template_local_runner.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/template_runner.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/devbox/terminals.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/experimental/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/experimental/browser.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/py.typed +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/volumes/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/volumes/cli.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/morphcloud/volumes/client.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/requirements.txt +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/scripts/abuild_smoke.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/scripts/increment_version.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/__init__.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/conftest.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/pytest.ini +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_as_container.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_branching.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_command_execution.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_file_operations.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_from_tag_multiple.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_http_service.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_metadata.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_simple.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_snapshot_operations.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_ssh_key_rotation.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_ttl.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/integration/test_user.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_cli_devbox.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_cli_devbox_terminal.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_cli_secrets.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_cli_snapshots.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_cli_volumes.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_devbox_terminals.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_http_transport_sharding.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_snapshot_ttl.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_ssh_connect_retry.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_template_local_runner.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_template_runner.py +0 -0
- {morphcloud-0.1.111 → morphcloud-0.1.113}/tests/unit/test_volumes_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphcloud
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.113
|
|
4
4
|
Summary: A Python SDK and CLI tool for creating, managing, and interacting with Morph Cloud VMs.
|
|
5
5
|
Author-email: Morph Labs <jobs@morph.so>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -216,6 +216,68 @@ morphcloud instance copy inst_123:/remote/file.log ./local_dir/
|
|
|
216
216
|
morphcloud instance copy -r ./local_dir inst_123:/remote/dir
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
+
### Plugins
|
|
220
|
+
|
|
221
|
+
Plugins are Python packages that register extra `morphcloud` commands through
|
|
222
|
+
the `morphcloud.cli_plugins` entry-point group. Catalog and package operations
|
|
223
|
+
use your normal `MORPH_API_KEY`. A catalog entry can install from the private
|
|
224
|
+
simple-index package name or from an explicit `artifact_url`.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Inspect plugin configuration and installed commands
|
|
228
|
+
morphcloud plugin doctor
|
|
229
|
+
|
|
230
|
+
# List or inspect plugins visible to your organization
|
|
231
|
+
morphcloud plugin list
|
|
232
|
+
morphcloud plugin show intro
|
|
233
|
+
|
|
234
|
+
# Validate install resolution without changing the environment
|
|
235
|
+
morphcloud plugin install intro --dry-run
|
|
236
|
+
|
|
237
|
+
# Install or update a plugin into the current Python environment
|
|
238
|
+
morphcloud plugin install intro
|
|
239
|
+
morphcloud plugin upgrade intro
|
|
240
|
+
|
|
241
|
+
# Run the command exposed by the installed plugin
|
|
242
|
+
morphcloud intro --help
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Common commands:
|
|
246
|
+
|
|
247
|
+
| Command | Purpose |
|
|
248
|
+
| --- | --- |
|
|
249
|
+
| `morphcloud plugin list [query]` | List visible enabled plugins. |
|
|
250
|
+
| `morphcloud plugin search <query>` | Search visible plugins by name, package, artifact, or command. |
|
|
251
|
+
| `morphcloud plugin show <name>` / `info` | Show one catalog entry. |
|
|
252
|
+
| `morphcloud plugin install <name>` | Install into the active Python environment. |
|
|
253
|
+
| `morphcloud plugin upgrade <name>` / `update` | Upgrade an installed plugin. |
|
|
254
|
+
| `morphcloud plugin uninstall <name>` | Remove the plugin package. |
|
|
255
|
+
| `morphcloud plugin register <name>` | Upsert a catalog entry from flags or `--manifest`. |
|
|
256
|
+
| `morphcloud plugin publish <name> --wheel dist/pkg-version-py3-none-any.whl` | Upload a wheel to simple-index, then register the catalog entry. |
|
|
257
|
+
| `morphcloud plugin enable/disable/delete <name>` | Manage catalog lifecycle. |
|
|
258
|
+
| `morphcloud plugin doctor` | Show auth, index, catalog, and installed entry-point status. |
|
|
259
|
+
|
|
260
|
+
Management commands accept `--visibility global|org` and `--organization-id`.
|
|
261
|
+
Org-scoped entries require `--organization-id`; global entries must omit it.
|
|
262
|
+
|
|
263
|
+
For local or preview simple-index services, set:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
export MORPH_SIMPLE_INDEX_BASE_URL="http://127.0.0.1:8912"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The installer uses the authenticated simple-index URL as the primary package
|
|
270
|
+
index for package-backed plugins and keeps PyPI as an extra index for public
|
|
271
|
+
dependencies. Credentials are redacted from CLI output, including direct
|
|
272
|
+
artifact URLs. Direct `artifact_url` installs are allowed only for URLs without
|
|
273
|
+
embedded credentials, query strings, or fragments; use package-backed simple-index
|
|
274
|
+
publishing for private or signed artifacts so secrets are not passed to pip as
|
|
275
|
+
process arguments.
|
|
276
|
+
|
|
277
|
+
For stricter private-package installs, set `MORPH_PLUGIN_DISABLE_EXTRA_INDEX=1`
|
|
278
|
+
to omit the public extra index. To use a controlled mirror instead of PyPI, set
|
|
279
|
+
`MORPH_PLUGIN_EXTRA_INDEX_URL`.
|
|
280
|
+
|
|
219
281
|
### Volumes
|
|
220
282
|
|
|
221
283
|
Morph Volumes exposes an S3-compatible object store. Bucket lifecycle and object operations use your normal `MORPH_API_KEY`.
|
|
@@ -658,16 +720,49 @@ morphcloud user api-key create
|
|
|
658
720
|
# Delete an API key
|
|
659
721
|
morphcloud user api-key delete <api_key_id>
|
|
660
722
|
|
|
661
|
-
#
|
|
662
|
-
morphcloud user ssh-key
|
|
723
|
+
# List named SSH public keys (fingerprints, expiry, and status)
|
|
724
|
+
morphcloud user ssh-key list
|
|
725
|
+
|
|
726
|
+
# Add a user-owned public key; never provide the private key
|
|
727
|
+
morphcloud user ssh-key add --name "Work laptop" --public-key-file ~/.ssh/id_ed25519.pub
|
|
728
|
+
|
|
729
|
+
# Inspect and edit one managed key by its stable ID
|
|
730
|
+
morphcloud user ssh-key get <ssh_key_id>
|
|
731
|
+
morphcloud user ssh-key edit <ssh_key_id> --expires 2030-01-01T00:00:00Z
|
|
663
732
|
|
|
664
|
-
#
|
|
733
|
+
# Revoke only that key across your current organization memberships
|
|
734
|
+
morphcloud user ssh-key revoke <ssh_key_id>
|
|
735
|
+
|
|
736
|
+
# Legacy singular compatibility commands remain available
|
|
737
|
+
morphcloud user ssh-key get
|
|
665
738
|
morphcloud user ssh-key set --public-key "ssh-rsa AAAA..."
|
|
666
739
|
|
|
667
740
|
# View usage (3-hour lookback by default; supports 30m, 3h, 7d, etc.)
|
|
668
741
|
morphcloud user usage --interval 3h
|
|
669
742
|
```
|
|
670
743
|
|
|
744
|
+
The same managed-key lifecycle is typed in the SDK, with async methods using the
|
|
745
|
+
usual `a` prefix (`alist_ssh_keys`, `aadd_ssh_key`, and so on):
|
|
746
|
+
|
|
747
|
+
```python
|
|
748
|
+
from pathlib import Path
|
|
749
|
+
|
|
750
|
+
from morphcloud.api import MorphCloudClient
|
|
751
|
+
|
|
752
|
+
client = MorphCloudClient()
|
|
753
|
+
key = client.user.add_ssh_key(
|
|
754
|
+
name="Work laptop",
|
|
755
|
+
public_key=Path("~/.ssh/id_ed25519.pub").expanduser().read_text().strip(),
|
|
756
|
+
)
|
|
757
|
+
key = client.user.get_managed_ssh_key(key.id)
|
|
758
|
+
key = client.user.edit_ssh_key(key.id, name="Work laptop (2026)")
|
|
759
|
+
client.user.revoke_ssh_key(key.id)
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Managed account public keys are separate from Morph-generated instance
|
|
763
|
+
credentials and browser terminal access. A managed key is owned by one user and
|
|
764
|
+
authorizes direct SSH through that user's current organization memberships.
|
|
765
|
+
|
|
671
766
|
## Advanced Features
|
|
672
767
|
|
|
673
768
|
### Profiles
|
|
@@ -185,6 +185,68 @@ morphcloud instance copy inst_123:/remote/file.log ./local_dir/
|
|
|
185
185
|
morphcloud instance copy -r ./local_dir inst_123:/remote/dir
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
### Plugins
|
|
189
|
+
|
|
190
|
+
Plugins are Python packages that register extra `morphcloud` commands through
|
|
191
|
+
the `morphcloud.cli_plugins` entry-point group. Catalog and package operations
|
|
192
|
+
use your normal `MORPH_API_KEY`. A catalog entry can install from the private
|
|
193
|
+
simple-index package name or from an explicit `artifact_url`.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Inspect plugin configuration and installed commands
|
|
197
|
+
morphcloud plugin doctor
|
|
198
|
+
|
|
199
|
+
# List or inspect plugins visible to your organization
|
|
200
|
+
morphcloud plugin list
|
|
201
|
+
morphcloud plugin show intro
|
|
202
|
+
|
|
203
|
+
# Validate install resolution without changing the environment
|
|
204
|
+
morphcloud plugin install intro --dry-run
|
|
205
|
+
|
|
206
|
+
# Install or update a plugin into the current Python environment
|
|
207
|
+
morphcloud plugin install intro
|
|
208
|
+
morphcloud plugin upgrade intro
|
|
209
|
+
|
|
210
|
+
# Run the command exposed by the installed plugin
|
|
211
|
+
morphcloud intro --help
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Common commands:
|
|
215
|
+
|
|
216
|
+
| Command | Purpose |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| `morphcloud plugin list [query]` | List visible enabled plugins. |
|
|
219
|
+
| `morphcloud plugin search <query>` | Search visible plugins by name, package, artifact, or command. |
|
|
220
|
+
| `morphcloud plugin show <name>` / `info` | Show one catalog entry. |
|
|
221
|
+
| `morphcloud plugin install <name>` | Install into the active Python environment. |
|
|
222
|
+
| `morphcloud plugin upgrade <name>` / `update` | Upgrade an installed plugin. |
|
|
223
|
+
| `morphcloud plugin uninstall <name>` | Remove the plugin package. |
|
|
224
|
+
| `morphcloud plugin register <name>` | Upsert a catalog entry from flags or `--manifest`. |
|
|
225
|
+
| `morphcloud plugin publish <name> --wheel dist/pkg-version-py3-none-any.whl` | Upload a wheel to simple-index, then register the catalog entry. |
|
|
226
|
+
| `morphcloud plugin enable/disable/delete <name>` | Manage catalog lifecycle. |
|
|
227
|
+
| `morphcloud plugin doctor` | Show auth, index, catalog, and installed entry-point status. |
|
|
228
|
+
|
|
229
|
+
Management commands accept `--visibility global|org` and `--organization-id`.
|
|
230
|
+
Org-scoped entries require `--organization-id`; global entries must omit it.
|
|
231
|
+
|
|
232
|
+
For local or preview simple-index services, set:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
export MORPH_SIMPLE_INDEX_BASE_URL="http://127.0.0.1:8912"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The installer uses the authenticated simple-index URL as the primary package
|
|
239
|
+
index for package-backed plugins and keeps PyPI as an extra index for public
|
|
240
|
+
dependencies. Credentials are redacted from CLI output, including direct
|
|
241
|
+
artifact URLs. Direct `artifact_url` installs are allowed only for URLs without
|
|
242
|
+
embedded credentials, query strings, or fragments; use package-backed simple-index
|
|
243
|
+
publishing for private or signed artifacts so secrets are not passed to pip as
|
|
244
|
+
process arguments.
|
|
245
|
+
|
|
246
|
+
For stricter private-package installs, set `MORPH_PLUGIN_DISABLE_EXTRA_INDEX=1`
|
|
247
|
+
to omit the public extra index. To use a controlled mirror instead of PyPI, set
|
|
248
|
+
`MORPH_PLUGIN_EXTRA_INDEX_URL`.
|
|
249
|
+
|
|
188
250
|
### Volumes
|
|
189
251
|
|
|
190
252
|
Morph Volumes exposes an S3-compatible object store. Bucket lifecycle and object operations use your normal `MORPH_API_KEY`.
|
|
@@ -627,16 +689,49 @@ morphcloud user api-key create
|
|
|
627
689
|
# Delete an API key
|
|
628
690
|
morphcloud user api-key delete <api_key_id>
|
|
629
691
|
|
|
630
|
-
#
|
|
631
|
-
morphcloud user ssh-key
|
|
692
|
+
# List named SSH public keys (fingerprints, expiry, and status)
|
|
693
|
+
morphcloud user ssh-key list
|
|
694
|
+
|
|
695
|
+
# Add a user-owned public key; never provide the private key
|
|
696
|
+
morphcloud user ssh-key add --name "Work laptop" --public-key-file ~/.ssh/id_ed25519.pub
|
|
697
|
+
|
|
698
|
+
# Inspect and edit one managed key by its stable ID
|
|
699
|
+
morphcloud user ssh-key get <ssh_key_id>
|
|
700
|
+
morphcloud user ssh-key edit <ssh_key_id> --expires 2030-01-01T00:00:00Z
|
|
632
701
|
|
|
633
|
-
#
|
|
702
|
+
# Revoke only that key across your current organization memberships
|
|
703
|
+
morphcloud user ssh-key revoke <ssh_key_id>
|
|
704
|
+
|
|
705
|
+
# Legacy singular compatibility commands remain available
|
|
706
|
+
morphcloud user ssh-key get
|
|
634
707
|
morphcloud user ssh-key set --public-key "ssh-rsa AAAA..."
|
|
635
708
|
|
|
636
709
|
# View usage (3-hour lookback by default; supports 30m, 3h, 7d, etc.)
|
|
637
710
|
morphcloud user usage --interval 3h
|
|
638
711
|
```
|
|
639
712
|
|
|
713
|
+
The same managed-key lifecycle is typed in the SDK, with async methods using the
|
|
714
|
+
usual `a` prefix (`alist_ssh_keys`, `aadd_ssh_key`, and so on):
|
|
715
|
+
|
|
716
|
+
```python
|
|
717
|
+
from pathlib import Path
|
|
718
|
+
|
|
719
|
+
from morphcloud.api import MorphCloudClient
|
|
720
|
+
|
|
721
|
+
client = MorphCloudClient()
|
|
722
|
+
key = client.user.add_ssh_key(
|
|
723
|
+
name="Work laptop",
|
|
724
|
+
public_key=Path("~/.ssh/id_ed25519.pub").expanduser().read_text().strip(),
|
|
725
|
+
)
|
|
726
|
+
key = client.user.get_managed_ssh_key(key.id)
|
|
727
|
+
key = client.user.edit_ssh_key(key.id, name="Work laptop (2026)")
|
|
728
|
+
client.user.revoke_ssh_key(key.id)
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Managed account public keys are separate from Morph-generated instance
|
|
732
|
+
credentials and browser terminal access. A managed key is owned by one user and
|
|
733
|
+
authorizes direct SSH through that user's current organization memberships.
|
|
734
|
+
|
|
640
735
|
## Advanced Features
|
|
641
736
|
|
|
642
737
|
### Profiles
|
|
@@ -13,7 +13,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
13
13
|
from functools import lru_cache
|
|
14
14
|
|
|
15
15
|
import httpx
|
|
16
|
-
from pydantic import BaseModel, Field, PrivateAttr
|
|
16
|
+
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
|
17
17
|
|
|
18
18
|
# Import Rich for fancy printing
|
|
19
19
|
from rich.console import Console
|
|
@@ -24,6 +24,7 @@ from morphcloud.config import resolve_settings
|
|
|
24
24
|
|
|
25
25
|
if typing.TYPE_CHECKING:
|
|
26
26
|
from morphcloud.devbox.client import AsyncDevboxClient, DevboxClient
|
|
27
|
+
from morphcloud.simple_index.client import SimpleIndexClient
|
|
27
28
|
from morphcloud.volumes.client import VolumesClient
|
|
28
29
|
|
|
29
30
|
# Global console instance
|
|
@@ -51,10 +52,29 @@ class ApiError(Exception):
|
|
|
51
52
|
def __init__(self, message: str, status_code: int, response_body: str):
|
|
52
53
|
self.status_code = status_code
|
|
53
54
|
self.response_body = response_body
|
|
55
|
+
self.error_code: typing.Optional[str] = None
|
|
56
|
+
self.detail: typing.Optional[str] = None
|
|
57
|
+
try:
|
|
58
|
+
payload = json.loads(response_body)
|
|
59
|
+
if isinstance(payload, dict):
|
|
60
|
+
error_code = payload.get("error_code")
|
|
61
|
+
detail = payload.get("detail")
|
|
62
|
+
if isinstance(error_code, str):
|
|
63
|
+
self.error_code = error_code
|
|
64
|
+
if isinstance(detail, str):
|
|
65
|
+
self.detail = detail
|
|
66
|
+
except (TypeError, ValueError):
|
|
67
|
+
pass
|
|
54
68
|
super().__init__(
|
|
55
69
|
f"{message}\nStatus Code: {status_code}\nResponse Body: {response_body}"
|
|
56
70
|
)
|
|
57
71
|
|
|
72
|
+
def as_managed_ssh_key_error(self) -> typing.Optional["ManagedSSHKeyError"]:
|
|
73
|
+
"""Return a typed managed-key error when the response uses that contract."""
|
|
74
|
+
if self.error_code is None or self.detail is None:
|
|
75
|
+
return None
|
|
76
|
+
return ManagedSSHKeyError(error_code=self.error_code, detail=self.detail)
|
|
77
|
+
|
|
58
78
|
|
|
59
79
|
class ApiClient(httpx.Client):
|
|
60
80
|
def raise_for_status(self, response: httpx.Response) -> None:
|
|
@@ -167,6 +187,7 @@ class MorphCloudClient:
|
|
|
167
187
|
volumes_service_api_key: typing.Optional[str] = None,
|
|
168
188
|
admin_base_url: typing.Optional[str] = None,
|
|
169
189
|
db_base_url: typing.Optional[str] = None,
|
|
190
|
+
simple_index_base_url: typing.Optional[str] = None,
|
|
170
191
|
ssh_hostname: typing.Optional[str] = None,
|
|
171
192
|
ssh_port: typing.Optional[int] = None,
|
|
172
193
|
devbox_base_url: typing.Optional[str] = None,
|
|
@@ -182,6 +203,7 @@ class MorphCloudClient:
|
|
|
182
203
|
"devbox_base_url": devbox_base_url,
|
|
183
204
|
"admin_base_url": admin_base_url,
|
|
184
205
|
"db_base_url": db_base_url,
|
|
206
|
+
"simple_index_base_url": simple_index_base_url,
|
|
185
207
|
"ssh_hostname": ssh_hostname,
|
|
186
208
|
"ssh_port": ssh_port,
|
|
187
209
|
},
|
|
@@ -200,10 +222,12 @@ class MorphCloudClient:
|
|
|
200
222
|
self.devbox_base_url = settings.devbox_base_url
|
|
201
223
|
self.admin_base_url = settings.admin_base_url
|
|
202
224
|
self.db_base_url = settings.db_base_url
|
|
225
|
+
self.simple_index_base_url = settings.simple_index_base_url
|
|
203
226
|
self.profile = settings.profile
|
|
204
227
|
self._devbox_client = None
|
|
205
228
|
self._devbox_async_client = None
|
|
206
229
|
self._volumes_client = None
|
|
230
|
+
self._simple_index_client = None
|
|
207
231
|
if not self.api_key:
|
|
208
232
|
raise ValueError(
|
|
209
233
|
"API key must be provided or set in MORPH_API_KEY environment variable"
|
|
@@ -254,9 +278,10 @@ class MorphCloudClient:
|
|
|
254
278
|
)
|
|
255
279
|
|
|
256
280
|
self._load_sdk_plugins()
|
|
257
|
-
# Ensure first-party
|
|
281
|
+
# Ensure first-party clients win even if an external plugin attempted to attach them.
|
|
258
282
|
self._devbox_client = None
|
|
259
283
|
self._devbox_async_client = None
|
|
284
|
+
self._simple_index_client = None
|
|
260
285
|
|
|
261
286
|
def _load_sdk_plugins(self):
|
|
262
287
|
"""Load SDK plugins from entry points."""
|
|
@@ -339,6 +364,18 @@ class MorphCloudClient:
|
|
|
339
364
|
def volumes(self, value: "VolumesClient") -> None:
|
|
340
365
|
self._volumes_client = value
|
|
341
366
|
|
|
367
|
+
@property
|
|
368
|
+
def simple_index(self) -> "SimpleIndexClient":
|
|
369
|
+
if self._simple_index_client is None:
|
|
370
|
+
from morphcloud.simple_index.client import SimpleIndexClient
|
|
371
|
+
|
|
372
|
+
self._simple_index_client = SimpleIndexClient(self)
|
|
373
|
+
return self._simple_index_client
|
|
374
|
+
|
|
375
|
+
@simple_index.setter
|
|
376
|
+
def simple_index(self, value: "SimpleIndexClient") -> None:
|
|
377
|
+
self._simple_index_client = value
|
|
378
|
+
|
|
342
379
|
|
|
343
380
|
class BaseAPI:
|
|
344
381
|
def __init__(self, client: MorphCloudClient):
|
|
@@ -423,28 +460,146 @@ class UserAPI(BaseAPI):
|
|
|
423
460
|
await self._client._async_http_client.delete(f"/user/secret/{secret_name}")
|
|
424
461
|
|
|
425
462
|
# ────────────────────────────────
|
|
426
|
-
# SSH
|
|
463
|
+
# Managed SSH public keys
|
|
464
|
+
# ────────────────────────────────
|
|
465
|
+
def list_ssh_keys(self) -> typing.List["ManagedSSHKey"]:
|
|
466
|
+
"""List all managed SSH public keys owned by the authenticated user."""
|
|
467
|
+
response = self._client._http_client.get("/user/ssh-keys")
|
|
468
|
+
return ManagedSSHKeyList.model_validate(response.json()).data
|
|
469
|
+
|
|
470
|
+
async def alist_ssh_keys(self) -> typing.List["ManagedSSHKey"]:
|
|
471
|
+
"""Asynchronously list all managed SSH public keys owned by the user."""
|
|
472
|
+
response = await self._client._async_http_client.get("/user/ssh-keys")
|
|
473
|
+
return ManagedSSHKeyList.model_validate(response.json()).data
|
|
474
|
+
|
|
475
|
+
def get_managed_ssh_key(self, ssh_key_id: str) -> "ManagedSSHKey":
|
|
476
|
+
"""Get one managed SSH public key by its stable ID."""
|
|
477
|
+
response = self._client._http_client.get(f"/user/ssh-keys/{ssh_key_id}")
|
|
478
|
+
return ManagedSSHKey.model_validate(response.json())
|
|
479
|
+
|
|
480
|
+
async def aget_managed_ssh_key(self, ssh_key_id: str) -> "ManagedSSHKey":
|
|
481
|
+
"""Asynchronously get one managed SSH public key by its stable ID."""
|
|
482
|
+
response = await self._client._async_http_client.get(
|
|
483
|
+
f"/user/ssh-keys/{ssh_key_id}"
|
|
484
|
+
)
|
|
485
|
+
return ManagedSSHKey.model_validate(response.json())
|
|
486
|
+
|
|
487
|
+
def add_ssh_key(
|
|
488
|
+
self,
|
|
489
|
+
name: str,
|
|
490
|
+
public_key: str,
|
|
491
|
+
expires: typing.Optional[int] = None,
|
|
492
|
+
) -> "ManagedSSHKey":
|
|
493
|
+
"""Add a named SSH public key, optionally expiring at Unix seconds."""
|
|
494
|
+
request_body = ManagedSSHKeyCreateRequest(
|
|
495
|
+
name=name, public_key=public_key, expires=expires
|
|
496
|
+
)
|
|
497
|
+
response = self._client._http_client.post(
|
|
498
|
+
"/user/ssh-keys", json=request_body.model_dump(exclude_none=True)
|
|
499
|
+
)
|
|
500
|
+
return ManagedSSHKey.model_validate(response.json())
|
|
501
|
+
|
|
502
|
+
async def aadd_ssh_key(
|
|
503
|
+
self,
|
|
504
|
+
name: str,
|
|
505
|
+
public_key: str,
|
|
506
|
+
expires: typing.Optional[int] = None,
|
|
507
|
+
) -> "ManagedSSHKey":
|
|
508
|
+
"""Asynchronously add a named SSH public key."""
|
|
509
|
+
request_body = ManagedSSHKeyCreateRequest(
|
|
510
|
+
name=name, public_key=public_key, expires=expires
|
|
511
|
+
)
|
|
512
|
+
response = await self._client._async_http_client.post(
|
|
513
|
+
"/user/ssh-keys", json=request_body.model_dump(exclude_none=True)
|
|
514
|
+
)
|
|
515
|
+
return ManagedSSHKey.model_validate(response.json())
|
|
516
|
+
|
|
517
|
+
def edit_ssh_key(
|
|
518
|
+
self,
|
|
519
|
+
ssh_key_id: str,
|
|
520
|
+
*,
|
|
521
|
+
name: typing.Optional[str] = None,
|
|
522
|
+
expires: typing.Optional[int] = None,
|
|
523
|
+
clear_expiry: bool = False,
|
|
524
|
+
) -> "ManagedSSHKey":
|
|
525
|
+
"""Edit a key's name or expiry without changing its public material.
|
|
526
|
+
|
|
527
|
+
Pass ``clear_expiry=True`` to send an explicit JSON null for ``expires``.
|
|
528
|
+
"""
|
|
529
|
+
request_body = _managed_ssh_key_patch_request(
|
|
530
|
+
name=name, expires=expires, clear_expiry=clear_expiry
|
|
531
|
+
)
|
|
532
|
+
response = self._client._http_client.patch(
|
|
533
|
+
f"/user/ssh-keys/{ssh_key_id}",
|
|
534
|
+
json=request_body.model_dump(exclude_unset=True),
|
|
535
|
+
)
|
|
536
|
+
return ManagedSSHKey.model_validate(response.json())
|
|
537
|
+
|
|
538
|
+
async def aedit_ssh_key(
|
|
539
|
+
self,
|
|
540
|
+
ssh_key_id: str,
|
|
541
|
+
*,
|
|
542
|
+
name: typing.Optional[str] = None,
|
|
543
|
+
expires: typing.Optional[int] = None,
|
|
544
|
+
clear_expiry: bool = False,
|
|
545
|
+
) -> "ManagedSSHKey":
|
|
546
|
+
"""Asynchronously edit a key's name or expiry."""
|
|
547
|
+
request_body = _managed_ssh_key_patch_request(
|
|
548
|
+
name=name, expires=expires, clear_expiry=clear_expiry
|
|
549
|
+
)
|
|
550
|
+
response = await self._client._async_http_client.patch(
|
|
551
|
+
f"/user/ssh-keys/{ssh_key_id}",
|
|
552
|
+
json=request_body.model_dump(exclude_unset=True),
|
|
553
|
+
)
|
|
554
|
+
return ManagedSSHKey.model_validate(response.json())
|
|
555
|
+
|
|
556
|
+
def revoke_ssh_key(self, ssh_key_id: str) -> None:
|
|
557
|
+
"""Permanently revoke one managed SSH public key by stable ID."""
|
|
558
|
+
self._client._http_client.delete(f"/user/ssh-keys/{ssh_key_id}")
|
|
559
|
+
|
|
560
|
+
async def arevoke_ssh_key(self, ssh_key_id: str) -> None:
|
|
561
|
+
"""Asynchronously revoke one managed SSH public key by stable ID."""
|
|
562
|
+
await self._client._async_http_client.delete(f"/user/ssh-keys/{ssh_key_id}")
|
|
563
|
+
|
|
564
|
+
# ────────────────────────────────
|
|
565
|
+
# Legacy singular SSH key compatibility
|
|
427
566
|
# ────────────────────────────────
|
|
428
567
|
def get_ssh_key(self) -> "UserSSHKey":
|
|
568
|
+
"""Get the legacy singular key through its compatibility endpoint."""
|
|
429
569
|
response = self._client._http_client.get("/user/ssh-key")
|
|
430
570
|
return UserSSHKey.model_validate(response.json())
|
|
431
571
|
|
|
432
572
|
async def aget_ssh_key(self) -> "UserSSHKey":
|
|
573
|
+
"""Asynchronously get the legacy singular compatibility key."""
|
|
433
574
|
response = await self._client._async_http_client.get("/user/ssh-key")
|
|
434
575
|
return UserSSHKey.model_validate(response.json())
|
|
435
576
|
|
|
436
577
|
def update_ssh_key(self, public_key: str) -> "UserSSHKey":
|
|
578
|
+
"""Set the legacy singular compatibility key.
|
|
579
|
+
|
|
580
|
+
New integrations should use :meth:`add_ssh_key`, then revoke the old key
|
|
581
|
+
only after verifying the replacement.
|
|
582
|
+
"""
|
|
437
583
|
response = self._client._http_client.put(
|
|
438
584
|
"/user/ssh-key", json={"public_key": public_key}
|
|
439
585
|
)
|
|
440
586
|
return UserSSHKey.model_validate(response.json())
|
|
441
587
|
|
|
442
588
|
async def aupdate_ssh_key(self, public_key: str) -> "UserSSHKey":
|
|
589
|
+
"""Asynchronously set the legacy singular compatibility key."""
|
|
443
590
|
response = await self._client._async_http_client.put(
|
|
444
591
|
"/user/ssh-key", json={"public_key": public_key}
|
|
445
592
|
)
|
|
446
593
|
return UserSSHKey.model_validate(response.json())
|
|
447
594
|
|
|
595
|
+
def set_ssh_key(self, public_key: str) -> "UserSSHKey":
|
|
596
|
+
"""Compatibility alias for :meth:`update_ssh_key`."""
|
|
597
|
+
return self.update_ssh_key(public_key)
|
|
598
|
+
|
|
599
|
+
async def aset_ssh_key(self, public_key: str) -> "UserSSHKey":
|
|
600
|
+
"""Async compatibility alias for :meth:`aupdate_ssh_key`."""
|
|
601
|
+
return await self.aupdate_ssh_key(public_key)
|
|
602
|
+
|
|
448
603
|
# ────────────────────────────────
|
|
449
604
|
# Usage
|
|
450
605
|
# ────────────────────────────────
|
|
@@ -517,10 +672,85 @@ class CreateSecretRequest(BaseModel):
|
|
|
517
672
|
|
|
518
673
|
|
|
519
674
|
class UserSSHKey(BaseModel):
|
|
675
|
+
"""Legacy singular user SSH-key response."""
|
|
676
|
+
|
|
520
677
|
public_key: str
|
|
521
678
|
created: int
|
|
522
679
|
|
|
523
680
|
|
|
681
|
+
class ManagedSSHKeyStatus(StrEnum):
|
|
682
|
+
ACTIVE = "active"
|
|
683
|
+
EXPIRED = "expired"
|
|
684
|
+
REVOKED = "revoked"
|
|
685
|
+
MIGRATION_WARNING = "migration_warning"
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class ManagedSSHKey(BaseModel):
|
|
689
|
+
"""A user-owned SSH public key accepted through current org memberships."""
|
|
690
|
+
|
|
691
|
+
object: typing.Literal["user_ssh_key"] = "user_ssh_key"
|
|
692
|
+
id: str
|
|
693
|
+
name: str
|
|
694
|
+
public_key: str
|
|
695
|
+
fingerprint: str
|
|
696
|
+
algorithm: str
|
|
697
|
+
created: int
|
|
698
|
+
updated: int
|
|
699
|
+
expires: typing.Optional[int] = None
|
|
700
|
+
last_used: typing.Optional[int] = None
|
|
701
|
+
revoked: typing.Optional[int] = None
|
|
702
|
+
status: ManagedSSHKeyStatus
|
|
703
|
+
migrated_from_legacy: bool = False
|
|
704
|
+
migration_warning: bool = False
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
class ManagedSSHKeyList(BaseModel):
|
|
708
|
+
object: typing.Literal["list"] = "list"
|
|
709
|
+
data: typing.List[ManagedSSHKey]
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class ManagedSSHKeyCreateRequest(BaseModel):
|
|
713
|
+
name: str
|
|
714
|
+
public_key: str
|
|
715
|
+
expires: typing.Optional[int] = None
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class ManagedSSHKeyPatchRequest(BaseModel):
|
|
719
|
+
name: typing.Optional[str] = None
|
|
720
|
+
expires: typing.Optional[int] = None
|
|
721
|
+
|
|
722
|
+
@model_validator(mode="after")
|
|
723
|
+
def require_change(self) -> "ManagedSSHKeyPatchRequest":
|
|
724
|
+
if not self.model_fields_set:
|
|
725
|
+
raise ValueError("at least one of name or expires must be provided")
|
|
726
|
+
return self
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class ManagedSSHKeyError(BaseModel):
|
|
730
|
+
"""Stable machine-readable error returned by managed-key endpoints."""
|
|
731
|
+
|
|
732
|
+
detail: str
|
|
733
|
+
error_code: str
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _managed_ssh_key_patch_request(
|
|
737
|
+
*,
|
|
738
|
+
name: typing.Optional[str],
|
|
739
|
+
expires: typing.Optional[int],
|
|
740
|
+
clear_expiry: bool,
|
|
741
|
+
) -> ManagedSSHKeyPatchRequest:
|
|
742
|
+
if clear_expiry and expires is not None:
|
|
743
|
+
raise ValueError("expires and clear_expiry cannot be used together")
|
|
744
|
+
values: typing.Dict[str, typing.Any] = {}
|
|
745
|
+
if name is not None:
|
|
746
|
+
values["name"] = name
|
|
747
|
+
if clear_expiry:
|
|
748
|
+
values["expires"] = None
|
|
749
|
+
elif expires is not None:
|
|
750
|
+
values["expires"] = expires
|
|
751
|
+
return ManagedSSHKeyPatchRequest(**values)
|
|
752
|
+
|
|
753
|
+
|
|
524
754
|
class UserInstanceUsage(BaseModel):
|
|
525
755
|
instance_cpu_time: int
|
|
526
756
|
instance_memory_time: int
|