Skip to content

Commit 27cba25

Browse files
authored
add PythonExtension
1 parent 1f1a6b0 commit 27cba25

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import fs from "node:fs";
2+
import { $ } from "execa";
3+
import { assert } from "@std/assert";
4+
import { BuildManifest } from "@trigger.dev/core/v3";
5+
import { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build";
6+
import { logger } from "@trigger.dev/sdk/v3";
7+
8+
export type PythonOptions = {
9+
requirements?: string[];
10+
requirementsFile?: string;
11+
/**
12+
* [Dev-only] The path to the python binary.
13+
*
14+
* @remarks
15+
* This option is typically used during local development or in specific testing environments
16+
* where a particular Python installation needs to be targeted. It should point to the full path of the python executable.
17+
*
18+
* Example: `/usr/bin/python3` or `C:\\Python39\\python.exe`
19+
*/
20+
pythonBinaryPath?: string;
21+
};
22+
23+
export function pythonExtension(options: PythonOptions = {}): BuildExtension {
24+
return new PythonExtension(options);
25+
}
26+
27+
class PythonExtension implements BuildExtension {
28+
public readonly name = "PythonExtension";
29+
30+
constructor(private options: PythonOptions = {}) {
31+
assert(
32+
!(this.options.requirements && this.options.requirementsFile),
33+
"Cannot specify both requirements and requirementsFile"
34+
);
35+
36+
if (this.options.requirementsFile) {
37+
this.options.requirements = fs
38+
.readFileSync(this.options.requirementsFile, "utf-8")
39+
.split("\n");
40+
}
41+
}
42+
43+
async onBuildComplete(context: BuildContext, manifest: BuildManifest) {
44+
if (context.target === "dev") {
45+
if (this.options.pythonBinaryPath) {
46+
process.env.PYTHON_BIN_PATH = this.options.pythonBinaryPath;
47+
}
48+
49+
return;
50+
}
51+
52+
context.logger.debug(`Adding ${this.name} to the build`);
53+
54+
context.addLayer({
55+
id: "python-extension",
56+
build: {
57+
env: {
58+
REQUIREMENTS_CONTENT: this.options.requirements?.join("\n") || "",
59+
},
60+
},
61+
image: {
62+
instructions: `
63+
# Install Python
64+
RUN apt-get update && apt-get install -y --no-install-recommends \
65+
python3 python3-pip python3-venv && \
66+
apt-get clean && rm -rf /var/lib/apt/lists/*
67+
68+
# Set up Python environment
69+
RUN python3 -m venv /opt/venv
70+
ENV PATH="/opt/venv/bin:$PATH"
71+
72+
ARG REQUIREMENTS_CONTENT
73+
RUN echo "$REQUIREMENTS_CONTENT" > requirements.txt
74+
75+
# Install dependenciess
76+
RUN pip install --no-cache-dir -r requirements.txt
77+
`.split("\n"),
78+
},
79+
deploy: {
80+
env: {
81+
PYTHON_BIN_PATH: `/opt/venv/bin/python`,
82+
},
83+
override: true,
84+
},
85+
});
86+
}
87+
}
88+
89+
export const run = async (
90+
args?: string,
91+
options: Parameters<typeof $>[1] = {}
92+
) => {
93+
const cmd = `${process.env.PYTHON_BIN_PATH || "python"} ${args}`;
94+
95+
logger.debug(
96+
`Running python:\t${cmd} ${options.input ? `(with stdin)` : ""}`,
97+
options
98+
);
99+
100+
const result = await $({
101+
shell: true,
102+
...options,
103+
})`${cmd}`;
104+
105+
try {
106+
assert(!result.failed, `Command failed: ${result.stderr}`);
107+
assert(result.exitCode === 0, `Non-zero exit code: ${result.exitCode}`);
108+
} catch (e) {
109+
logger.error(e.message, result);
110+
throw e;
111+
}
112+
113+
return result;
114+
};
115+
116+
export default run;

0 commit comments

Comments
 (0)