1616 cast ,
1717)
1818from typing_extensions import Literal , NotRequired , TypeAlias
19+ import httpx
1920
2021from .stream import Streamable , complete_stream , start_stream
21-
22+ from . utils import convert_to_snake_case
2223from .env import AsyncSandboxEnv , SandboxEnv
2324from .fs import AsyncSandboxFs , SandboxFs
2425from .process import (
4344from .transport import (
4445 WebSocketTransport ,
4546)
47+ from .revisions import Revision
4648
4749
4850Mode : TypeAlias = Literal ["connect" , "create" ]
@@ -96,6 +98,48 @@ class ExposeHTTPResult(TypedDict):
9698 domain : str
9799
98100
101+ class DeployBuildOptions (TypedDict , total = False ):
102+ """Build configuration for deployment."""
103+
104+ mode : Literal ["none" ]
105+ """The build mode to use. Currently only 'none' is supported. Defaults to 'none'."""
106+
107+ entrypoint : str
108+ """The entrypoint file path relative to the path option. Defaults to 'main.ts'."""
109+
110+ args : builtins .list [str ]
111+ """Arguments to pass to the entrypoint script."""
112+
113+
114+ class DeployOptions (TypedDict , total = False ):
115+ """Options for deploying an app using deno.deploy()."""
116+
117+ path : str
118+ """The path to the directory to deploy. If relative, it is relative to /app. Defaults to '.'."""
119+
120+ production : bool
121+ """Whether to deploy in production mode. Defaults to True."""
122+
123+ preview : bool
124+ """Whether to deploy a preview deployment. Defaults to False."""
125+
126+ build : DeployBuildOptions
127+ """Build options to use."""
128+
129+
130+ class BuildLog (TypedDict ):
131+ """A build log entry from app deployment."""
132+
133+ timestamp : str
134+ """The timestamp of the build log."""
135+
136+ level : Literal ["info" , "error" ]
137+ """The level of the build log."""
138+
139+ message : str
140+ """The message of the build log."""
141+
142+
99143class AsyncSandboxApi :
100144 def __init__ (
101145 self ,
@@ -352,14 +396,125 @@ def list(
352396 return PaginatedList (self ._bridge , paginated )
353397
354398
399+ async def _parse_sse_stream (stream : AsyncIterator [bytes ]) -> AsyncIterator [str ]:
400+ """Parse Server-Sent Events from a byte stream."""
401+ buffer = b""
402+ async for chunk in stream :
403+ buffer += chunk
404+ while b"\n " in buffer :
405+ line , buffer = buffer .split (b"\n " , 1 )
406+ line = line .rstrip (b"\r " )
407+ if line .startswith (b"data: " ):
408+ data = line [6 :].decode ("utf-8" )
409+ yield data
410+
411+
412+ class AsyncBuild :
413+ """The result of a deno.deploy() operation."""
414+
415+ def __init__ (
416+ self ,
417+ revision_id : str ,
418+ app : str ,
419+ client : AsyncConsoleClient ,
420+ ):
421+ self .id = revision_id
422+ """The ID of the build."""
423+ self ._app = app
424+ self ._client = client
425+
426+ async def wait (self ) -> Revision :
427+ """A coroutine that resolves when the build is complete, returning the revision."""
428+
429+ url = self ._client ._options ["console_url" ].join (
430+ f"/api/v2/apps/{ self ._app } /revisions/{ self .id } /status"
431+ )
432+ headers = {
433+ "Authorization" : f"Bearer { self ._client ._options ['token' ]} " ,
434+ }
435+
436+ async with httpx .AsyncClient () as http_client :
437+ response = await http_client .get (str (url ), headers = headers )
438+ response .raise_for_status ()
439+ revision_data = response .json ()
440+
441+ # Fetch timelines
442+ timelines_url = self ._client ._options ["console_url" ].join (
443+ f"/api/v2/apps/{ self ._app } /revisions/{ self .id } /timelines"
444+ )
445+ timelines_response = await http_client .get (
446+ str (timelines_url ), headers = headers
447+ )
448+ timelines_response .raise_for_status ()
449+ timelines_data = timelines_response .json ()
450+
451+ result = convert_to_snake_case (revision_data )
452+ result ["timelines" ] = convert_to_snake_case (timelines_data )
453+ return cast (Revision , result )
454+
455+ async def logs (self ) -> AsyncIterator [BuildLog ]:
456+ """An async iterator of build logs."""
457+ url = self ._client ._options ["console_url" ].join (
458+ f"/api/v2/apps/{ self ._app } /revisions/{ self .id } /logs"
459+ )
460+ headers = {
461+ "Authorization" : f"Bearer { self ._client ._options ['token' ]} " ,
462+ }
463+
464+ async with httpx .AsyncClient () as http_client :
465+ async with http_client .stream ("GET" , str (url ), headers = headers ) as response :
466+ response .raise_for_status ()
467+ async for data in _parse_sse_stream (response .aiter_bytes ()):
468+ try :
469+ yield cast (BuildLog , json .loads (data ))
470+ except json .JSONDecodeError :
471+ # Skip malformed log entries
472+ continue
473+
474+
475+ class Build :
476+ """The result of a deno.deploy() operation (sync version)."""
477+
478+ def __init__ (
479+ self ,
480+ revision_id : str ,
481+ app : str ,
482+ client : AsyncConsoleClient ,
483+ bridge : AsyncBridge ,
484+ ):
485+ self .id = revision_id
486+ """The ID of the build."""
487+ self ._async_build = AsyncBuild (revision_id , app , client )
488+ self ._bridge = bridge
489+
490+ def wait (self ) -> Revision :
491+ """Returns the revision when the build is complete."""
492+ return self ._bridge .run (self ._async_build .wait ())
493+
494+ def logs (self ) -> builtins .list [BuildLog ]:
495+ """Returns a list of build logs."""
496+
497+ async def _collect_logs ():
498+ logs = []
499+ async for log in self ._async_build .logs ():
500+ logs .append (log )
501+ return logs
502+
503+ return self ._bridge .run (_collect_logs ())
504+
505+
355506class AsyncSandboxDeno :
356507 def __init__ (
357508 self ,
358509 rpc : AsyncRpcClient ,
359510 processes : builtins .list [AsyncChildProcess ],
511+ client : AsyncConsoleClient ,
512+ sandbox_id : str ,
360513 ):
361514 self ._rpc = rpc
362515 self ._processes = processes
516+ self ._client = client
517+ self ._sandbox_id = sandbox_id
363518
364519 async def run (
365520 self ,
@@ -520,18 +675,92 @@ async def repl(
520675 self ._processes .append (process )
521676 return process
522677
678+ async def deploy (
679+ self , app : str , * , options : Optional [DeployOptions ] = None
680+ ) -> AsyncBuild :
681+ """Deploy the contents of the sandbox to the specified app in Deno Deploy platform.
682+
683+ Args:
684+ app: The app ID or slug to deploy to.
685+ options: Deployment configuration options.
686+
687+ Returns:
688+ An AsyncBuild object with the revision ID and methods to check status and logs.
689+
690+ Example:
691+ ```python
692+ from deno_sandbox import AsyncDenoDeploy
693+
694+ async with AsyncDenoDeploy() as client:
695+ async with client.sandbox.create() as sandbox:
696+ await sandbox.fs.write_text_file(
697+ "main.ts",
698+ 'Deno.serve(() => new Response("Hi from sandbox.deploy()"))',
699+ )
700+ build = await sandbox.deno.deploy("my-deno-app", options={
701+ "build": {"entrypoint": "main.ts"}
702+ })
703+ print(f"Deployed revision ID: {build.id}")
704+ revision = await build.done
705+ print(f"Revision status: {revision['status']}")
706+ ```
707+ """
708+ url = self ._client ._options ["console_url" ].join (f"/api/v2/apps/{ app } /deploy" )
709+ headers = {
710+ "Content-Type" : "application/json" ,
711+ "Authorization" : f"Bearer { self ._client ._options ['token' ]} " ,
712+ }
713+
714+ # Build request body
715+ body : dict [str , Any ] = {
716+ "entrypoint" : (
717+ options .get ("build" , {}).get ("entrypoint" , "main.ts" )
718+ if options
719+ else "main.ts"
720+ ),
721+ "sandboxId" : self ._sandbox_id ,
722+ }
723+
724+ if options :
725+ if "build" in options and "args" in options ["build" ]:
726+ body ["args" ] = options ["build" ]["args" ]
727+ if "production" in options :
728+ body ["production" ] = options ["production" ]
729+ if "preview" in options :
730+ body ["preview" ] = options ["preview" ]
731+ if "path" in options :
732+ body ["path" ] = options ["path" ]
733+ else :
734+ body ["path" ] = "/app"
735+ else :
736+ body ["path" ] = "/app"
737+
738+ # Make the deploy request
739+ async with httpx .AsyncClient () as http_client :
740+ response = await http_client .post (
741+ str (url ), headers = headers , json = body , timeout = 30.0
742+ )
743+ response .raise_for_status ()
744+ result = response .json ()
745+ revision_id = result ["revisionId" ]
746+
747+ return AsyncBuild (revision_id , app , self ._client )
748+
523749
524750class SandboxDeno :
525751 def __init__ (
526752 self ,
527753 rpc : AsyncRpcClient ,
528754 bridge : AsyncBridge ,
529755 processes : builtins .list [AsyncChildProcess ],
756+ client : AsyncConsoleClient ,
757+ sandbox_id : str ,
530758 ):
531759 self ._rpc = rpc
532760 self ._bridge = bridge
761+ self ._client = client
533762
534- self ._async = AsyncSandboxDeno (rpc , processes )
763+ self ._async = AsyncSandboxDeno (rpc , processes , client , sandbox_id )
535764
536765 def run (
537766 self ,
@@ -632,6 +861,37 @@ def repl(
632861 )
633862 return DenoRepl (self ._rpc , self ._bridge , async_repl )
634863
864+ def deploy (self , app : str , * , options : Optional [DeployOptions ] = None ) -> Build :
865+ """Deploy the contents of the sandbox to the specified app in Deno Deploy platform.
866+
867+ Args:
868+ app: The app ID or slug to deploy to.
869+ options: Deployment configuration options.
870+
871+ Returns:
872+ A Build object with the revision ID and methods to check status and logs.
873+
874+ Example:
875+ ```python
876+ from deno_sandbox import DenoDeploy
877+
878+ client = DenoDeploy()
879+ with client.sandbox.create() as sandbox:
880+ sandbox.fs.write_text_file(
881+ "main.ts",
882+ 'Deno.serve(() => new Response("Hi from sandbox.deploy()"))',
883+ )
884+ build = sandbox.deno.deploy("my-deno-app", options={
885+ "build": {"entrypoint": "main.ts"}
886+ })
887+ print(f"Deployed revision ID: {build.id}")
888+ revision = build.done
889+ print(f"Revision status: {revision['status']}")
890+ ```
891+ """
892+ async_build = self ._bridge .run (self ._async .deploy (app , options = options ))
893+ return Build (async_build .id , app , self ._client , self ._bridge )
894+
635895
636896class AsyncSandbox :
637897 def __init__ (
@@ -645,7 +905,7 @@ def __init__(
645905 self .ssh : None = None
646906 self .id = sandbox_id
647907 self .fs = AsyncSandboxFs (rpc )
648- self .deno = AsyncSandboxDeno (rpc , self ._processes )
908+ self .deno = AsyncSandboxDeno (rpc , self ._processes , client , sandbox_id )
649909 self .env = AsyncSandboxEnv (rpc )
650910
651911 @property
@@ -839,7 +1099,9 @@ def __init__(
8391099 self .ssh : None = None
8401100 self .id = async_sandbox .id
8411101 self .fs = SandboxFs (rpc , bridge )
842- self .deno = SandboxDeno (rpc , bridge , self ._async ._processes )
1102+ self .deno = SandboxDeno (
1103+ rpc , bridge , self ._async ._processes , client , async_sandbox .id
1104+ )
8431105 self .env = SandboxEnv (rpc , bridge )
8441106
8451107 @property
0 commit comments