Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/deno_sandbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@
RuntimeLog,
RuntimeLogsResponse,
)
from .revisions import Revisions, AsyncRevisions, Revision, RevisionListItem
from .revisions import (
Revisions,
AsyncRevisions,
Revision,
RevisionListItem,
FileAsset,
SymlinkAsset,
Asset,
EnvVarInputForDeploy,
)
from .snapshots import Snapshots, AsyncSnapshots
from .timelines import Timelines, AsyncTimelines
from .volumes import Volumes, AsyncVolumes
Expand Down Expand Up @@ -42,6 +51,10 @@
"RuntimeLogsResponse",
"Revision",
"RevisionListItem",
"FileAsset",
"SymlinkAsset",
"Asset",
"EnvVarInputForDeploy",
]


Expand Down
79 changes: 78 additions & 1 deletion src/deno_sandbox/revisions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import warnings
from typing import Any, TypedDict, cast, overload
from typing import Any, Dict, List, TypedDict, Union, cast, overload
from typing_extensions import Literal, NotRequired, Optional

from deno_sandbox.apps import Config, EnvVar, LayerRef
Expand All @@ -15,6 +15,25 @@
from .utils import convert_to_snake_case


class FileAsset(TypedDict):
kind: Literal["file"]
encoding: Literal["utf-8", "base64"]
content: str


class SymlinkAsset(TypedDict):
kind: Literal["symlink"]
target: str


Asset = Union[FileAsset, SymlinkAsset]


class EnvVarInputForDeploy(TypedDict):
key: str
value: str


class RevisionListItem(TypedDict):
id: str
"""The unique identifier for the revision."""
Expand Down Expand Up @@ -154,6 +173,37 @@ async def cancel(self, revision: str) -> Revision:
result = await self._client.post(f"/api/v2/revisions/{revision}/cancel", {})
return cast(Revision, convert_to_snake_case(result))

async def deploy(
self,
app: str,
assets: Dict[str, Asset],
*,
config: Optional[Config] = None,
env_vars: Optional[List[EnvVarInputForDeploy]] = None,
labels: Optional[Dict[str, str]] = None,
) -> Revision:
"""Deploy a revision by uploading source files as assets.

Args:
app: The app ID or slug.
assets: Dict mapping file paths to Asset objects.
config: Optional build/runtime configuration.
env_vars: Optional environment variables for this revision.
labels: Optional labels (e.g., git metadata).

Returns:
The created Revision (build is async; poll for status).
"""
body: Dict[str, Any] = {"assets": assets}
if config is not None:
body["config"] = config
if env_vars is not None:
body["env_vars"] = env_vars
if labels is not None:
body["labels"] = labels
result = await self._client.post(f"/api/v2/apps/{app}/deploy", body)
return cast(Revision, convert_to_snake_case(result))


class Revisions:
def __init__(self, client: AsyncConsoleClient, bridge: AsyncBridge):
Expand Down Expand Up @@ -203,3 +253,30 @@ def cancel(self, revision: str) -> Revision:
revision: The revision ID.
"""
return self._bridge.run(self._async.cancel(revision))

def deploy(
self,
app: str,
assets: Dict[str, Asset],
*,
config: Optional[Config] = None,
env_vars: Optional[List[EnvVarInputForDeploy]] = None,
labels: Optional[Dict[str, str]] = None,
) -> Revision:
"""Deploy a revision by uploading source files as assets.

Args:
app: The app ID or slug.
assets: Dict mapping file paths to Asset objects.
config: Optional build/runtime configuration.
env_vars: Optional environment variables for this revision.
labels: Optional labels (e.g., git metadata).

Returns:
The created Revision (build is async; poll for status).
"""
return self._bridge.run(
self._async.deploy(
app, assets, config=config, env_vars=env_vars, labels=labels
)
)
54 changes: 54 additions & 0 deletions tests/test_revisions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import asyncio
import time

import pytest
import uuid
import warnings
Expand Down Expand Up @@ -139,3 +142,54 @@ async def test_revisions_get_deprecated_two_arg_async():
assert fetched["id"] == revision["id"]
finally:
await sdk.apps.delete(app["id"])


@pytest.mark.timeout(60)
@pytest.mark.asyncio(loop_scope="session")
async def test_revisions_deploy_async():
sdk = AsyncDenoDeploy()
app = await sdk.apps.create()
try:
revision = await sdk.revisions.deploy(
app["id"],
assets={
"main.ts": {
"kind": "file",
"encoding": "utf-8",
"content": 'Deno.serve(() => new Response("Hello"))',
}
},
)
assert revision["id"] is not None
while revision["status"] in ("queued", "building"):
await asyncio.sleep(1)
revision = await sdk.revisions.get(revision["id"])
assert revision is not None
assert revision["status"] == "succeeded", revision.get("failure_reason")
finally:
await sdk.apps.delete(app["id"])


@pytest.mark.timeout(60)
def test_revisions_deploy_sync():
sdk = DenoDeploy()
app = sdk.apps.create()
try:
revision = sdk.revisions.deploy(
app["id"],
assets={
"main.ts": {
"kind": "file",
"encoding": "utf-8",
"content": 'Deno.serve(() => new Response("Hello"))',
}
},
)
assert revision["id"] is not None
while revision["status"] in ("queued", "building"):
time.sleep(1)
revision = sdk.revisions.get(revision["id"])
assert revision is not None
assert revision["status"] == "succeeded", revision.get("failure_reason")
finally:
sdk.apps.delete(app["id"])