pdt-cli 0.1.0__py3-none-any.whl

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.
pdt/deploy_common.py ADDED
@@ -0,0 +1,94 @@
1
+ """Code shared by every provider deploy script.
2
+
3
+ Every env var the app declares goes into one json secret, mounted on the
4
+ job as PDT_ENV_JSON and expanded back into env vars by
5
+ pdt.config.load_env_json. A var that ends in _PATH is replaced by
6
+ <NAME>_B64 holding the base64 of the file it points to, because the
7
+ cloud job gets no files, only string secrets.
8
+
9
+ A build context holds the app directory and pdt.yml, nothing else. The
10
+ app's run.py declares pdt in its script header, so every deployment
11
+ installs the package from the index the same way a local run does.
12
+ PDT_PROJECT names the project directory, so no job depends on its cwd.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import tempfile
22
+ from pathlib import Path
23
+
24
+ from pdt import config
25
+
26
+ DOCKERFILE = """\
27
+ FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
28
+ COPY . /workspace
29
+ WORKDIR /workspace/{app}
30
+ ENV PDT_PROJECT=/workspace
31
+ RUN uv sync --script run.py
32
+ ENTRYPOINT ["uv", "run", "--script", "run.py"]
33
+ """
34
+ BUILD_EXCLUDES = (
35
+ ".env", ".env.*", ".secrets", ".git", ".venv", "__pycache__",
36
+ ".DS_Store", ".gcloud", "*.json.key", "*-key.json",
37
+ "service-account*.json",
38
+ )
39
+
40
+
41
+ def fail(message: str) -> None:
42
+ print(f"error: {message}")
43
+ raise SystemExit(1)
44
+
45
+
46
+ def run_build(command: list[str]) -> None:
47
+ proc = subprocess.run(
48
+ command, capture_output=True, text=True, check=False)
49
+ if proc.returncode:
50
+ if proc.stdout.strip():
51
+ print(proc.stdout.strip())
52
+ if proc.stderr.strip():
53
+ print(proc.stderr.strip())
54
+ fail(f"{' '.join(command[:3])} failed")
55
+
56
+
57
+ def gather_secrets(app: dict) -> dict[str, str]:
58
+ spec = app["env"]
59
+ names = list(spec.get("required") or [])
60
+ for group in spec.get("one_of") or []:
61
+ names.extend(group)
62
+ names.extend(spec.get("optional") or [])
63
+ values = {}
64
+ for name in names:
65
+ value = os.environ.get(name, "").strip()
66
+ if value and name not in values:
67
+ values[name] = value
68
+ for name in [key for key in values if key.endswith("_PATH")]:
69
+ target = name[:-5] + "_B64"
70
+ path = Path(values[name]).expanduser()
71
+ if not path.is_absolute():
72
+ bases = [env_file.parent for env_file in config.find_env_files(app["dir"])]
73
+ bases += [app["dir"], config.find_project()]
74
+ path = next((base / path for base in bases if (base / path).is_file()), path)
75
+ if not path.is_file():
76
+ fail(f"{name} points to {path}, which does not exist")
77
+ values.setdefault(target, base64.b64encode(path.read_bytes()).decode("ascii"))
78
+ del values[name]
79
+ return values
80
+
81
+
82
+ def stage_build_context(app: dict) -> Path:
83
+ # Stage a clean build context so .env and .secrets never reach the image.
84
+ stage = Path(tempfile.mkdtemp(prefix="pdt-build-"))
85
+ skip = shutil.ignore_patterns(*BUILD_EXCLUDES)
86
+ try:
87
+ shutil.copytree(app["dir"], stage / app["name"], ignore=skip)
88
+ project_file = config.find_project() / config.PROJECT_FILE
89
+ if project_file.is_file():
90
+ shutil.copy(project_file, stage / config.PROJECT_FILE)
91
+ return stage
92
+ except BaseException:
93
+ shutil.rmtree(stage, ignore_errors=True)
94
+ raise