66import tempfile
77from pathlib import Path
88
9- from appwrite_lab .automations .models import BaseVarModel
9+ from appwrite_lab .automations .models import BaseVarModel , AppwriteAPIKeyCreation
1010from ._state import State
1111from dataclasses import dataclass
12- from .models import LabService , Automation , SyncType
12+ from .models import Lab , Automation , SyncType , Project
1313from dotenv import dotenv_values
1414from appwrite_lab .utils import console
1515from .utils import is_cli
@@ -22,13 +22,16 @@ class Response:
2222 message : str
2323 data : any = None
2424 error : bool = False
25+ _print_data : bool = False
2526
2627 def __post_init__ (self ):
2728 if is_cli :
2829 if self .error :
2930 console .print (self .message , style = "red" )
3031 else :
31- console .print (self .message , style = "green" )
32+ console .print (
33+ self .message if not self ._print_data else self .data , style = "green"
34+ )
3235
3336
3437@dataclass
@@ -51,16 +54,18 @@ def get_labs(self):
5154 Get all labs.
5255 """
5356 labs : dict = self .state .get ("labs" , {})
54- return [LabService (** lab ) for lab in labs .values ()]
57+ return [Lab (** lab ) for lab in labs .values ()]
5558
56- def get_lab (self , name : str ) -> LabService | None :
59+ def get_lab (self , name : str ) -> Lab | None :
5760 """
5861 Get a lab by name.
5962 """
6063 labs : dict = self .state .get ("labs" , {})
6164 if not (lab := labs .get (name , None )):
6265 return None
63- return LabService (** lab )
66+ projects = lab .get ("projects" , {})
67+ _projects = {key : Project (** project ) for key , project in projects .items ()}
68+ return Lab (** {** lab , "projects" : _projects })
6469
6570 def get_formatted_labs (self , collapsed : bool = False ):
6671 """
@@ -69,17 +74,20 @@ def get_formatted_labs(self, collapsed: bool = False):
6974 labs : dict = self .state .get ("labs" , {})
7075 if collapsed :
7176 headers = ["Name" , "Version" , "URL" , "Admin Email" , "Project ID" , "API Key" ]
72- return headers , [
73- [
74- val ["name" ],
75- val ["version" ],
76- val ["url" ],
77- val ["admin_email" ],
78- val ["project_id" ],
79- val ["api_key" ],
80- ]
81- for val in labs .values ()
82- ]
77+ data = []
78+ for val in labs .values ():
79+ project = Project (** val .get ("projects" , {}).get ("default" ))
80+ data .append (
81+ [
82+ val ["name" ],
83+ val ["version" ],
84+ val ["url" ],
85+ val ["admin_email" ],
86+ project .project_id ,
87+ project .api_key ,
88+ ]
89+ )
90+ return headers , data
8391 return labs
8492
8593 def get_running_pods (self ):
@@ -146,19 +154,6 @@ def _deploy_service(
146154 ]
147155 return self ._run_cmd_safely (cmd , envs = new_env )
148156
149- def _run_cmd_safely (self , cmd : list [str ], envs : dict [str , str ] = {}):
150- """
151- Private function to run a command and return the output.
152-
153- Args:
154- cmd: The command to run.
155- envs: The environment variables to set.
156- """
157- try :
158- return run_cmd (cmd , envs )
159- except OrchestratorError as e :
160- return Response (error = True , message = f"Failed to run command: { e } " , data = e )
161-
162157 def deploy_appwrite_lab (
163158 self , name : str , version : str , port : int , meta : dict [str , str ]
164159 ):
@@ -195,7 +190,7 @@ def deploy_appwrite_lab(
195190 if port != 80 :
196191 env_vars ["_APP_PORT" ] = str (port )
197192
198- # What actually deploys the service
193+ # What actually deploys the initial appwrite service
199194 cmd_res = self ._deploy_service (
200195 project = name , template_path = template_path , env_vars = env_vars
201196 )
@@ -213,26 +208,39 @@ def deploy_appwrite_lab(
213208 )
214209 port = extract_port_from_pod_info (traefik_pod )
215210 url = f"http://localhost:{ port } "
216- print ("url" , url )
217211 else :
218212 url = ""
219- lab = LabService (
213+ proj_id = appwrite_config .pop ("project_id" , None )
214+ proj_name = appwrite_config .pop ("project_name" , None )
215+ kwargs = {
216+ ** appwrite_config ,
217+ "projects" : {"default" : Project (proj_id , proj_name , None )},
218+ }
219+ lab = Lab (
220220 name = name ,
221221 version = version ,
222222 url = url ,
223- ** appwrite_config ,
223+ ** kwargs ,
224224 )
225225
226226 lab .generate_missing_config ()
227+ # ensure project_id and project_name are set
228+ proj_id = proj_id or lab .projects .get ("default" ).project_id
229+ proj_name = proj_name or lab .projects .get ("default" ).project_name
230+
227231 # Deploy playwright automations for creating user and API key
228232 api_key_res = self .deploy_playwright_automation (
229- lab , Automation .CREATE_USER_AND_API_KEY
233+ lab = lab ,
234+ automation = Automation .CREATE_USER_AND_API_KEY ,
235+ model = AppwriteAPIKeyCreation (
236+ key_name = "default_key" , project_name = proj_name , key_expiry = "Never"
237+ ),
230238 )
231239 if type (api_key_res ) is Response and api_key_res .error :
232240 api_key_res .message = f"Lab '{ name } ' deployed, but failed to create API key. Spinning down lab."
233241 self .teardown_service (name )
234242 return api_key_res
235- lab .api_key = api_key_res .data
243+ lab .projects [ "default" ]. api_key = api_key_res .data
236244
237245 stored_labs : dict = self .state .get ("labs" , {}).copy ()
238246 stored_labs [name ] = asdict (lab )
@@ -246,10 +254,13 @@ def deploy_appwrite_lab(
246254
247255 def deploy_playwright_automation (
248256 self ,
249- lab : LabService ,
257+ lab : Lab ,
250258 automation : Automation ,
259+ project : Project | None = None ,
251260 model : BaseVarModel = None ,
252261 args : list [str ] = [],
262+ * ,
263+ print_data : bool = False ,
253264 ) -> str | Response :
254265 """
255266 Deploy playwright automations on a lab (very few automations supported).
@@ -262,11 +273,16 @@ def deploy_playwright_automation(
262273 Args:
263274 lab: The lab to deploy the automations for.
264275 automation: The automation to deploy.
265- model: The model to use for the automation.
276+ model: The model args to use for the automation.
277+ args: Extra arguments to the container.
278+ project: The project to use for the automation, if not provided, the default project is used.
279+
280+ Keyword Args:
281+ print_data: Whether to print the data of the response instead of the message.
266282 """
267283 automation = automation .value
268284 function = (
269- Path (__file__ ).parent / "automations" / "functions " / f"{ automation } .py"
285+ Path (__file__ ).parent / "automations" / "scripts " / f"{ automation } .py"
270286 )
271287 if not function .exists ():
272288 return Response (
@@ -276,28 +292,34 @@ def deploy_playwright_automation(
276292 )
277293 automation_dir = Path (__file__ ).parent / "automations"
278294 container_work_dir = "/work/automations"
295+ project = project or lab .projects ["default" ]
296+ project = Project (** project ) if isinstance (project , dict ) else project
297+ proj_id = project .project_id
298+ api_key = project .api_key
299+
279300 env_vars = {
280301 "APPWRITE_URL" : lab .url ,
281- "APPWRITE_PROJECT_ID" : lab . project_id ,
302+ "APPWRITE_PROJECT_ID" : proj_id ,
282303 "APPWRITE_ADMIN_EMAIL" : lab .admin_email ,
283304 "APPWRITE_ADMIN_PASSWORD" : lab .admin_password ,
305+ "APPWRITE_API_KEY" : api_key ,
306+ "APPWRITE_PROJECT_NAME" : project .project_name ,
284307 "HOME" : container_work_dir ,
285308 ** (model .as_dict_with_prefix ("APPWRITE" ) if model else {}),
286309 }
287- envs = " " .join ([f"{ key } ={ value } " for key , value in env_vars .items ()])
288310 docker_env_args = []
289311 for key , value in env_vars .items ():
290312 docker_env_args .extend (["-e" , f"{ key } ={ value } " ])
291313 with tempfile .TemporaryDirectory () as temp_dir :
292314 shutil .copytree (automation_dir , temp_dir , dirs_exist_ok = True )
293- function = Path (temp_dir ) / "automations" / "functions " / f"{ automation } .py"
315+ function = Path (temp_dir ) / "automations" / "scripts " / f"{ automation } .py"
294316
295317 cmd = [
296318 self .util ,
297319 "run" ,
298320 "--network" ,
299321 "host" ,
300- "--rm" ,
322+ # "--rm",
301323 "-u" ,
302324 f"{ os .getuid ()} :{ os .getgid ()} " ,
303325 "-v" ,
@@ -307,7 +329,7 @@ def deploy_playwright_automation(
307329 APPWRITE_PLAYWRIGHT_IMAGE ,
308330 "python" ,
309331 "-m" ,
310- f"automations.functions .{ automation } " ,
332+ f"automations.scripts .{ automation } " ,
311333 ]
312334 cmd_res = self ._run_cmd_safely (cmd )
313335 if type (cmd_res ) is Response and cmd_res .error :
@@ -317,14 +339,16 @@ def deploy_playwright_automation(
317339 return cmd_res
318340 # If successful, any data should be mounted as result.txt
319341 result_file = Path (temp_dir ) / "result.txt"
342+ _data = None
320343 if result_file .exists ():
321344 with open (result_file , "r" ) as f :
322- data = f .read ()
323- return Response (
324- error = False ,
325- message = f"Playwright automation{ automation } deployed successfully." ,
326- data = data ,
327- )
345+ _data = f .read ()
346+ return Response (
347+ error = False ,
348+ message = f"Playwright automation { automation } deployed successfully." ,
349+ data = _data ,
350+ _print_data = print_data ,
351+ )
328352
329353 def teardown_service (self , name : str ):
330354 """
@@ -340,7 +364,16 @@ def teardown_service(self, name: str):
340364 message = f"Nothing to stop by name of '{ name } '." ,
341365 data = None ,
342366 )
343- cmd = [self .compose , "-p" , name , "down" , "-v" ]
367+ cmd = [
368+ self .compose ,
369+ "-p" ,
370+ name ,
371+ "down" ,
372+ "-v" ,
373+ "--timeout" ,
374+ "0" ,
375+ "--remove-orphans" ,
376+ ]
344377 cmd_res = self ._run_cmd_safely (cmd )
345378 if type (cmd_res ) is Response and cmd_res .error :
346379 cmd_res .message = f"Failed to teardown lab { name } . \
@@ -383,6 +416,19 @@ def get_pods_by_project(self, project_name: str):
383416 )
384417 return _stdout_to_json (result .stdout )
385418
419+ def _run_cmd_safely (self , cmd : list [str ], envs : dict [str , str ] = {}):
420+ """
421+ Private function to run a command and return the output.
422+
423+ Args:
424+ cmd: The command to run.
425+ envs: The environment variables to set.
426+ """
427+ try :
428+ return run_cmd (cmd , envs )
429+ except OrchestratorError as e :
430+ return Response (error = True , message = f"{ str (e )} " , data = str (e ))
431+
386432 @property
387433 def util (self ):
388434 return shutil .which (self .backend )
@@ -408,20 +454,25 @@ def run_cmd(cmd: list[str], envs: dict[str, str] | None = None):
408454 cmd: The command to run.
409455 envs: The environment variables to set.
410456 """
411- try :
412- result = subprocess .run (
413- cmd ,
414- capture_output = True ,
415- text = True ,
416- env = {** os .environ , ** envs } if envs else None ,
417- )
418- if result .returncode != 0 :
419- raise OrchestratorError (
420- f"An error occured running a command: { result .stderr } "
421- )
422- return result
423- except Exception as e :
424- raise OrchestratorError (f"An error occured running a command: { e } " )
457+ result = subprocess .run (
458+ cmd ,
459+ capture_output = True ,
460+ text = True ,
461+ env = {** os .environ , ** envs } if envs else None ,
462+ )
463+ if result .returncode != 0 :
464+ error_msg = result .stderr .strip ()
465+ if error_msg :
466+ # Look for the actual error message in the traceback
467+ lines = error_msg .split ("\n " )
468+ for line in reversed (lines ):
469+ if "PlaywrightAutomationError:" in line or "OrchestratorError:" in line :
470+ # Extract just the error message part
471+ if ":" in line :
472+ error_msg = line .split (":" , 1 )[1 ].strip ()
473+ break
474+ raise OrchestratorError (f"An error occured running a command: { error_msg } " )
475+ return result
425476
426477
427478def get_template_versions ():
0 commit comments