Skip to content

Commit 06f60f2

Browse files
zvictorcoderabbitai[bot]matt-aitkenericallam
authored
feat(build): Add support for Python scripts via pythonExtension (#1686)
* add PythonExtension * remove potential shell injection risk * Filter out blank lines or comment lines * fix spelling * add pythonExtension's `runInline` * changes to requirements don’t invalidate the entire install layer * copy script files on-demand * improve PythonExtension types and logging * add changeset * fix broken imports * Improve security of inline script execution Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add file existence check for requirementsFile Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * update lock file * Enhance error handling with detailed error information * Add portable type annotation * fix error TS18046: 'e' is of type 'unknown' * export the python extension * add `pythonExtension` to the catalog * fix `Cannot find module '@trigger.dev/build/extensions/core' (TS2307) * replace execa by tinyexec * Update pnpm-lock.yaml * add custom traces instead of logging * The cleanup in the finally block does not fail silently anymore * move python runtime/extension to independent package * fix build package readme * update lock file * add documentation to python's package * add missing dependency * Update little-trains-begin.md --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Matt Aitken <[email protected]> Co-authored-by: Eric Allam <[email protected]>
1 parent ca05a54 commit 06f60f2

File tree

13 files changed

+472
-3
lines changed

13 files changed

+472
-3
lines changed

.changeset/little-trains-begin.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/python": patch
3+
---
4+
5+
Introduced a new Python extension to enhance the build process. It now allows users to execute Python scripts with improved support and error handling.

packages/build/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
# Official TypeScript SDK for Trigger.dev
1+
# Official Build Package of Trigger.dev
22

3-
View the full documentation for the [here](https://trigger.dev/docs)
3+
View the full documentation [here](https://trigger.dev/docs)

packages/python/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# @trigger.dev/python

packages/python/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Trigger.dev
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/python/README.md

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Python Extension for Trigger.dev
2+
3+
The Python extension enhances Trigger.dev's build process by enabling limited support for executing Python scripts within your tasks.
4+
5+
## Overview
6+
7+
This extension introduces the <code>pythonExtension</code> build extension, which offers several key capabilities:
8+
9+
- **Install Python Dependencies (Except in Dev):** Automatically installs Python and specified dependencies using <code>pip</code>.
10+
- **Requirements File Support:** You can specify dependencies in a <code>requirements.txt</code> file.
11+
- **Inline Requirements:** Define dependencies directly within your <code>trigger.config.ts</code> file using the <code>requirements</code> option.
12+
- **Virtual Environment:** Creates a virtual environment (<code>/opt/venv</code>) inside containers to isolate Python dependencies.
13+
- **Helper Functions:** Provides a variety of functions for executing Python code:
14+
- <code>run</code>: Executes Python commands with proper environment setup.
15+
- <code>runInline</code>: Executes inline Python code directly from Node.
16+
- <code>runScript</code>: Executes standalone <code>.py</code> script files.
17+
- **Custom Python Path:** In development, you can configure <code>pythonBinaryPath</code> to point to a custom Python installation.
18+
19+
## Usage
20+
21+
1. Add the extension to your <code>trigger.config.ts</code> file:
22+
23+
```typescript
24+
import { defineConfig } from "@trigger.dev/sdk/v3";
25+
import pythonExtension from "@trigger.dev/python/extension";
26+
27+
export default defineConfig({
28+
project: "<project ref>",
29+
build: {
30+
extensions: [
31+
pythonExtension({
32+
requirementsFile: "./requirements.txt", // Optional: Path to your requirements file
33+
pythonBinaryPath: path.join(rootDir, `.venv/bin/python`), // Optional: Custom Python binary path
34+
scripts: ["my_script.py"], // List of Python scripts to include
35+
}),
36+
],
37+
},
38+
});
39+
```
40+
41+
2. (Optional) Create a <code>requirements.txt</code> file in your project root with the necessary Python dependencies.
42+
43+
3. Execute Python scripts within your tasks using one of the provided functions:
44+
45+
### Running a Python Script
46+
47+
```typescript
48+
import { task } from "@trigger.dev/sdk/v3";
49+
import python from "@trigger.dev/python";
50+
51+
export const myScript = task({
52+
id: "my-python-script",
53+
run: async () => {
54+
const result = await python.runScript("my_script.py", ["hello", "world"]);
55+
return result.stdout;
56+
},
57+
});
58+
```
59+
60+
### Running Inline Python Code
61+
62+
```typescript
63+
import { task } from "@trigger.dev/sdk/v3";
64+
import python from "@trigger.dev/python";
65+
66+
export const myTask = task({
67+
id: "to_datetime-task",
68+
run: async () => {
69+
const result = await python.runInline(`
70+
import pandas as pd
71+
72+
pandas.to_datetime("${+new Date() / 1000}")
73+
`);
74+
return result.stdout;
75+
},
76+
});
77+
```
78+
79+
### Running Lower-Level Commands
80+
81+
```typescript
82+
import { task } from "@trigger.dev/sdk/v3";
83+
import python from "@trigger.dev/python";
84+
85+
export const pythonVersionTask = task({
86+
id: "python-version-task",
87+
run: async () => {
88+
const result = await python.run(["--version"]);
89+
return result.stdout; // Expected output: Python 3.12.8
90+
},
91+
});
92+
```
93+
94+
## Limitations
95+
96+
- This is a **partial implementation** and does not provide full Python support as an execution runtime for tasks.
97+
- Only basic Python script execution is supported; scripts are not automatically copied to staging/production containers.
98+
- Manual intervention may be required for installing and configuring binary dependencies in development environments.
99+
100+
## Additional Information
101+
102+
For more detailed documentation, visit the official docs at [Trigger.dev Documentation](https://trigger.dev/docs).

packages/python/package.json

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"name": "@trigger.dev/python",
3+
"version": "3.3.16",
4+
"description": "Python runtime and build extension for Trigger.dev",
5+
"license": "MIT",
6+
"publishConfig": {
7+
"access": "public"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/triggerdotdev/trigger.dev",
12+
"directory": "packages/python"
13+
},
14+
"type": "module",
15+
"files": [
16+
"dist"
17+
],
18+
"tshy": {
19+
"selfLink": false,
20+
"main": true,
21+
"module": true,
22+
"project": "./tsconfig.src.json",
23+
"exports": {
24+
"./package.json": "./package.json",
25+
".": "./src/index.ts",
26+
"./extension": "./src/extension.ts"
27+
},
28+
"sourceDialects": [
29+
"@triggerdotdev/source"
30+
]
31+
},
32+
"typesVersions": {
33+
"*": {
34+
"extension": [
35+
"dist/commonjs/extension.d.ts"
36+
]
37+
}
38+
},
39+
"scripts": {
40+
"clean": "rimraf dist",
41+
"build": "tshy && pnpm run update-version",
42+
"dev": "tshy --watch",
43+
"typecheck": "tsc --noEmit -p tsconfig.src.json",
44+
"update-version": "tsx ../../scripts/updateVersion.ts",
45+
"check-exports": "attw --pack ."
46+
},
47+
"dependencies": {
48+
"@trigger.dev/build": "workspace:3.3.16",
49+
"@trigger.dev/core": "workspace:3.3.16",
50+
"@trigger.dev/sdk": "workspace:3.3.16",
51+
"tinyexec": "^0.3.2"
52+
},
53+
"devDependencies": {
54+
"@types/node": "20.14.14",
55+
"rimraf": "6.0.1",
56+
"tshy": "^3.0.2",
57+
"typescript": "^5.5.4",
58+
"tsx": "4.17.0",
59+
"esbuild": "^0.23.0",
60+
"@arethetypeswrong/cli": "^0.15.4"
61+
},
62+
"engines": {
63+
"node": ">=18.20.0"
64+
},
65+
"exports": {
66+
"./package.json": "./package.json",
67+
".": {
68+
"import": {
69+
"@triggerdotdev/source": "./src/index.ts",
70+
"types": "./dist/esm/index.d.ts",
71+
"default": "./dist/esm/index.js"
72+
},
73+
"require": {
74+
"types": "./dist/commonjs/index.d.ts",
75+
"default": "./dist/commonjs/index.js"
76+
}
77+
},
78+
"./extension": {
79+
"import": {
80+
"@triggerdotdev/source": "./src/extension.ts",
81+
"types": "./dist/esm/extension.d.ts",
82+
"default": "./dist/esm/extension.js"
83+
},
84+
"require": {
85+
"types": "./dist/commonjs/extension.d.ts",
86+
"default": "./dist/commonjs/extension.js"
87+
}
88+
}
89+
},
90+
"main": "./dist/commonjs/index.js",
91+
"types": "./dist/commonjs/index.d.ts",
92+
"module": "./dist/esm/index.js"
93+
}

packages/python/src/extension.ts

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

0 commit comments

Comments
 (0)