Skip to content

Commit 7403822

Browse files
authored
Merge dev changes (support for Appwrite syncing) (#2)
* wip: agnostic playwright deployment * build; dockerfile for playwright awcli * build: for appwrite * wip: modular automations for appwrite via playwright * feat: wip appwrite project creation and refactor fix * feat: fix bug in config storing * feat: support for syncing projects * refactor: remove sync func
1 parent 2811f07 commit 7403822

27 files changed

+562
-98
lines changed

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
APPWRITE_CLI_TAG=latest
2+
APPWRITE_PLAYWRIGHT_TAG=latest
3+
4+
build_appwrite_cli:
5+
docker build -t appwrite-cli:$(APPWRITE_CLI_TAG) -f docker/Dockerfile.appwrite_cli ./docker
6+
7+
8+
build_appwrite_playwright:
9+
docker build -t appwrite-playwright:$(APPWRITE_PLAYWRIGHT_TAG) -f docker/Dockerfile.appwrite_playwright ./docker
10+
11+
# push_appwrite_cli:
12+
# docker tag appwrite-cli:latest appwrite-cli:$(APPWRITE_CLI_TAG)
13+
# docker push appwrite-cli:$(APPWRITE_CLI_TAG)

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
# appwrite-lab
2-
Zero-click Appwrite test environments
2+
Zero-click Appwrite test environments.
3+
4+
Allows you to spin up versioned Appwrite deployments for easy testing via CLI of through code, that can be ran in a sequence of E2E tests.
35

46
## Installation
57
```sh
68
pip install appwrite-lab
79
```
8-
## Appwrite Lab features (coming)
10+
## Appwrite Lab features (in progress)
911
- [x] Spin up ephemeral Appwrite instances with Docker/Podman
1012
- [x] Automatically grab API keys (for programmatic access)
1113
- [ ] Test suite
12-
- [ ] Environment syncing
14+
- [x] Environment syncing
1315
- [ ] Appwrite data population
1416
- [x] Clean teardowns
1517

@@ -31,3 +33,14 @@ To teardown,
3133
```sh
3234
appwrite-lab stop test-lab
3335
```
36+
37+
### Sync an Appwrite lab from your prod lab schema
38+
Run in the same folder where your `appwrite.json` is located to sync `all` resources:
39+
```sh
40+
appwrite-lab sync test-lab
41+
```
42+
or sync a specific resource:
43+
44+
```sh
45+
appwrite-lab sync test-lab --resource functions
46+
```

appwrite_lab/_orchestrator.py

Lines changed: 89 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@
44
import json
55
import tempfile
66
from pathlib import Path
7+
8+
from appwrite_lab.automations.models import BaseVarModel
79
from ._state import State
810
from dataclasses import dataclass
9-
from .models import LabService, Automation
11+
from .models import LabService, Automation, SyncType
1012
from dotenv import dotenv_values
1113
from appwrite_lab.utils import console
12-
from .utils import is_cli
13-
from .config import PLAYWRIGHT_IMAGE
14+
from .utils import is_cli, load_config
15+
from .config import APPWRITE_CLI_IMAGE, APPWRITE_PLAYWRIGHT_IMAGE
1416
from dataclasses import asdict
1517

1618

1719
@dataclass
1820
class Response:
1921
message: str
20-
data: any
22+
data: any = None
2123
error: bool = False
2224

2325
def __post_init__(self):
@@ -43,7 +45,23 @@ def __init__(self, state: State, backend: str = "auto"):
4345
Path(__file__).parent / "templates" / "environment" / "dotenv"
4446
)
4547

46-
def get_labs(self, collapsed: bool = False):
48+
def get_labs(self):
49+
"""
50+
Get all labs.
51+
"""
52+
labs: dict = self.state.get("labs", {})
53+
return [LabService(**lab) for lab in labs.values()]
54+
55+
def get_lab(self, name: str) -> LabService | None:
56+
"""
57+
Get a lab by name.
58+
"""
59+
labs: dict = self.state.get("labs", {})
60+
if not (lab := labs.get(name, None)):
61+
return None
62+
return LabService(**lab)
63+
64+
def get_formatted_labs(self, collapsed: bool = False):
4765
"""
4866
Get all labs.
4967
"""
@@ -192,7 +210,6 @@ def deploy_appwrite_lab(
192210
url = f"http://localhost:{port}"
193211
else:
194212
url = ""
195-
print("url", url)
196213
lab = LabService(
197214
name=name,
198215
version=version,
@@ -210,7 +227,7 @@ def deploy_appwrite_lab(
210227
f"Lab '{name}' deployed, but failed to create API key."
211228
)
212229
return api_key_res
213-
lab.api_key = api_key_res
230+
lab.api_key = api_key_res.data
214231

215232
stored_labs: dict = self.state.get("labs", {}).copy()
216233
stored_labs[name] = asdict(lab)
@@ -223,7 +240,11 @@ def deploy_appwrite_lab(
223240
)
224241

225242
def deploy_playwright_automation(
226-
self, lab: LabService, automation: str
243+
self,
244+
lab: LabService,
245+
automation: Automation,
246+
model: BaseVarModel = None,
247+
args: list[str] = [],
227248
) -> str | Response:
228249
"""
229250
Deploy playwright automations on a lab (very few automations supported).
@@ -236,55 +257,69 @@ def deploy_playwright_automation(
236257
Args:
237258
lab: The lab to deploy the automations for.
238259
automation: The automation to deploy.
260+
model: The model to use for the automation.
239261
"""
240-
if automation == Automation.CREATE_USER_AND_API_KEY:
241-
function = (
242-
Path(__file__).parent / "playwright" / "functions" / f"{automation}.py"
262+
automation = automation.value
263+
function = (
264+
Path(__file__).parent / "automations" / "functions" / f"{automation}.py"
265+
)
266+
if not function.exists():
267+
return Response(
268+
error=True,
269+
message=f"Function {automation} not found. This should not happen.",
270+
data=None,
243271
)
244-
if not function.exists():
245-
return Response(
246-
error=True,
247-
message=f"Function {automation} not found. This should not happen.",
248-
data=None,
272+
automation_dir = Path(__file__).parent / "automations"
273+
container_work_dir = "/work/automations"
274+
env_vars = {
275+
"APPWRITE_URL": lab.url,
276+
"APPWRITE_PROJECT_ID": lab.project_id,
277+
"APPWRITE_ADMIN_EMAIL": lab.admin_email,
278+
"APPWRITE_ADMIN_PASSWORD": lab.admin_password,
279+
"HOME": container_work_dir,
280+
**(model.as_dict_with_prefix("APPWRITE") if model else {}),
281+
}
282+
envs = " ".join([f"{key}={value}" for key, value in env_vars.items()])
283+
docker_env_args = []
284+
for key, value in env_vars.items():
285+
docker_env_args.extend(["-e", f"{key}={value}"])
286+
with tempfile.TemporaryDirectory() as temp_dir:
287+
shutil.copytree(automation_dir, temp_dir, dirs_exist_ok=True)
288+
function = Path(temp_dir) / "automations" / "functions" / f"{automation}.py"
289+
290+
cmd = [
291+
self.util,
292+
"run",
293+
"--network",
294+
"host",
295+
# "--rm",
296+
"-u",
297+
f"{os.getuid()}:{os.getgid()}",
298+
"-v",
299+
f"{temp_dir}:{container_work_dir}",
300+
*args,
301+
*docker_env_args,
302+
APPWRITE_PLAYWRIGHT_IMAGE,
303+
"python",
304+
"-m",
305+
f"automations.functions.{automation}",
306+
]
307+
cmd_res = self._run_cmd_safely(cmd)
308+
if type(cmd_res) is Response and cmd_res.error:
309+
cmd_res.message = (
310+
f"Failed to deploy playwright automation {automation}."
249311
)
250-
env_vars = {
251-
"APPWRITE_URL": lab.url,
252-
"APPWRITE_PROJECT_ID": lab.project_id,
253-
"APPWRITE_ADMIN_EMAIL": lab.admin_email,
254-
"APPWRITE_ADMIN_PASSWORD": lab.admin_password,
255-
}
256-
envs = " ".join([f"{key}={value}" for key, value in env_vars.items()])
257-
with tempfile.TemporaryDirectory() as temp_dir:
258-
temp_file = Path(temp_dir) / "function.py"
259-
shutil.copy(function, temp_file)
260-
cmd = [
261-
self.util,
262-
"run",
263-
"--network",
264-
"--rm",
265-
"host",
266-
"-v",
267-
f"{temp_dir}:/playwright",
268-
*[f"-e {key}={value}" for key, value in env_vars.items()],
269-
PLAYWRIGHT_IMAGE,
270-
"bash",
271-
"-c",
272-
f"pip install playwright asyncio && {envs} python /playwright/function.py",
273-
]
274-
cmd_res = self._run_cmd_safely(cmd)
275-
if type(cmd_res) is Response and cmd_res.error:
276-
cmd_res.message = (
277-
f"Failed to deploy playwright automation {automation}."
312+
return cmd_res
313+
# If successful, any data should be mounted as result.txt
314+
result_file = Path(temp_dir) / "result.txt"
315+
if result_file.exists():
316+
with open(result_file, "r") as f:
317+
data = f.read()
318+
return Response(
319+
error=False,
320+
message=f"Playwright automation{automation} deployed successfully.",
321+
data=data,
278322
)
279-
return cmd_res
280-
# If successful, any data should be mounted as result.txt
281-
result_file = Path(temp_dir) / "result.txt"
282-
if result_file.exists():
283-
with open(result_file, "r") as f:
284-
data = f.read()
285-
return data
286-
else:
287-
return None
288323

289324
def teardown_service(self, name: str):
290325
"""
@@ -377,7 +412,7 @@ def run_cmd(cmd: list[str], envs: dict[str, str] | None = None):
377412
)
378413
if result.returncode != 0:
379414
raise OrchestratorError(
380-
f"An error occured running a command: {result.stdout}"
415+
f"An error occured running a command: {result.stderr}"
381416
)
382417
return result
383418
except Exception as e:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .common import AppwriteCLI
2+
from .utils import PlaywrightAutomationError
3+
4+
__all__ = ("AppwriteCLI", "PlaywrightAutomationError")

appwrite_lab/automations/common.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from .utils import run_cmd, env_dict_to_str
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass
6+
class CommandExecutor:
7+
cmd: list[str]
8+
envs: dict[str, str] | None = None
9+
10+
def run(self):
11+
return run_cmd(self.cmd, self.envs)
12+
13+
def __call__(self):
14+
return self.run()
15+
16+
17+
class AppwriteCLI:
18+
def login(self, url: str, email: str, password: str) -> CommandExecutor:
19+
"""
20+
Login to Appwrite.
21+
22+
Args:
23+
url: The URL of the Appwrite instance.
24+
email: The email of the user.
25+
password: The password of the user.
26+
27+
Returns:
28+
bool: True if login was successful, False otherwise.
29+
"""
30+
cmd = [
31+
"appwrite",
32+
"login",
33+
"--endpoint",
34+
url,
35+
"--email",
36+
email,
37+
"--password",
38+
password,
39+
]
40+
return CommandExecutor(cmd, None)
41+
42+
def get_project(self, project_id: str) -> CommandExecutor:
43+
"""
44+
Get a project from Appwrite.
45+
46+
Args:
47+
project_id: The ID of the project.
48+
"""
49+
cmd = ["appwrite", "projects", "get", "--project-id", project_id]
50+
return CommandExecutor(cmd)
51+
52+
def sync_project(self, resource: str) -> CommandExecutor:
53+
"""
54+
Sync a project from Appwrite.
55+
"""
56+
cmd = ["bash", "-c", f"yes YES | appwrite push {resource}"]
57+
return CommandExecutor(cmd)
58+
59+
60+
def execute_same_shell(*execs: CommandExecutor) -> list[str]:
61+
"""
62+
Run a batch of commands in the same shell and return the outputs.
63+
64+
Args:
65+
execs: The commands to run.
66+
"""
67+
all_cmds = []
68+
for exc in execs:
69+
cmds = exc.cmd.copy()
70+
if exc.envs:
71+
# Prepend environment variables to the command
72+
env_str = env_dict_to_str(exc.envs)
73+
cmds.insert(0, env_str)
74+
all_cmds.append(" ".join(cmds))
75+
76+
# Join all commands with && to run in same shell
77+
combined_cmd = " && ".join(all_cmds)
78+
return run_cmd(["bash", "-c", combined_cmd])
File renamed without changes.

0 commit comments

Comments
 (0)