Skip to content

Commit 4033bf1

Browse files
feat: add Python function template support
Add Python template alongside existing node-graphql template, enabling Python-based serverless functions in the FaaS framework. Changes: - Add templates/python/ with FastAPI-based HTTP server - Add functions/python-example/ as reference implementation - Add Dockerfile.python.dev for Python function development - Update generate.ts to support .py file symlinks - Update skaffold.yaml with Python image build config - Register python-example in job-service routing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 15a815c commit 4033bf1

13 files changed

Lines changed: 356 additions & 12 deletions

File tree

Dockerfile.python.dev

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /usr/src/app
4+
5+
# Install watchdog for hot reload
6+
RUN pip install --no-cache-dir watchdog[watchmedo]
7+
8+
# Copy all generated python functions and their requirements
9+
COPY generated/ generated/
10+
COPY functions/ functions/
11+
12+
# Install dependencies for all python functions
13+
RUN for req in generated/*/requirements.txt; do \
14+
if [ -f "$req" ]; then \
15+
pip install --no-cache-dir -r "$req"; \
16+
fi \
17+
done
18+
19+
ENV PYTHONUNBUFFERED=1
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "python-example",
3+
"version": "0.1.0",
4+
"description": "Example Python function",
5+
"type": "python"
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
async def handler(payload: dict) -> dict:
2+
"""
3+
Simple echo handler - returns the received payload.
4+
"""
5+
return {
6+
"received": payload,
7+
"status": "ok",
8+
"message": "Hello from Python!"
9+
}

scripts/generate.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ interface FunctionInfo {
181181
name: string;
182182
dir: string;
183183
port: number;
184+
type: string;
184185
}
185186

186187
const K8S_TEMPLATES_DIR: string = path.resolve(TEMPLATES_DIR, 'k8s');
@@ -220,18 +221,20 @@ function generateFunctionsConfigMap(fns: FunctionInfo[], perFunction?: FunctionI
220221
}
221222

222223
function generateSkaffoldYaml(fns: FunctionInfo[]): void {
223-
const profileTemplate = readTemplate('skaffold-profile.yaml');
224+
const nodeProfileTemplate = readTemplate('skaffold-profile.yaml');
225+
const pythonProfileTemplate = readTemplate('skaffold-profile-python.yaml');
224226
const skaffoldTemplate = readTemplate('skaffold.yaml');
225227

226-
// Render per-function profiles from template
227-
const perFnProfiles = fns.map((fn) =>
228-
renderTemplate(profileTemplate, {
228+
// Render per-function profiles from template (select based on type)
229+
const perFnProfiles = fns.map((fn) => {
230+
const template = fn.type === 'python' ? pythonProfileTemplate : nodeProfileTemplate;
231+
return renderTemplate(template, {
229232
name: fn.name,
230233
dir: fn.dir,
231234
port: String(fn.port),
232235
namespace: K8S_NAMESPACE,
233-
}).trimEnd()
234-
).join('\n');
236+
}).trimEnd();
237+
}).join('\n');
235238

236239
// Build dynamic lists for aggregate profiles
237240
const allRawYaml = fns
@@ -247,10 +250,24 @@ function generateSkaffoldYaml(fns: FunctionInfo[]): void {
247250
].join('\n'))
248251
.join('\n');
249252

253+
// Add Python artifacts if there are Python functions
254+
const hasPython = fns.some((fn) => fn.type === 'python');
255+
const pythonArtifacts = hasPython ? ` - image: constructive-functions-python
256+
context: .
257+
docker:
258+
dockerfile: Dockerfile.python.dev
259+
sync:
260+
manual:
261+
- src: 'functions/**/*.py'
262+
dest: /usr/src/app
263+
- src: 'generated/**/*.py'
264+
dest: /usr/src/app` : '';
265+
250266
const skaffold = renderTemplate(skaffoldTemplate, {
251267
per_function_profiles: perFnProfiles,
252268
all_raw_yaml: allRawYaml,
253269
all_port_forwards: allPortForwards,
270+
python_artifacts: pythonArtifacts,
254271
namespace: K8S_NAMESPACE,
255272
});
256273

@@ -327,17 +344,21 @@ function main(): void {
327344
}
328345
}
329346

330-
// Symlink handler.ts
331-
const handlerTarget = path.join(fnDir, 'handler.ts');
332-
if (fs.existsSync(handlerTarget)) {
333-
const linked = ensureSymlink(handlerTarget, path.join(genDir, 'handler.ts'));
347+
// Symlink handler.ts or handler.py
348+
const handlerTsTarget = path.join(fnDir, 'handler.ts');
349+
const handlerPyTarget = path.join(fnDir, 'handler.py');
350+
if (fs.existsSync(handlerTsTarget)) {
351+
const linked = ensureSymlink(handlerTsTarget, path.join(genDir, 'handler.ts'));
334352
if (linked) console.log(` - handler.ts -> functions/${fnName}/handler.ts`);
353+
} else if (fs.existsSync(handlerPyTarget)) {
354+
const linked = ensureSymlink(handlerPyTarget, path.join(genDir, 'handler.py'));
355+
if (linked) console.log(` - handler.py -> functions/${fnName}/handler.py`);
335356
}
336357

337-
// Symlink any .d.ts files
358+
// Symlink any .d.ts or .py files (excluding handler.py which is handled above)
338359
const files = fs.readdirSync(fnDir) as string[];
339360
for (const file of files) {
340-
if (file.endsWith('.d.ts')) {
361+
if (file.endsWith('.d.ts') || (file.endsWith('.py') && file !== 'handler.py')) {
341362
const target = path.join(fnDir, file);
342363
const linked = ensureSymlink(target, path.join(genDir, file));
343364
if (linked) console.log(` - ${file} -> functions/${fnName}/${file}`);
@@ -388,6 +409,7 @@ function main(): void {
388409
name: allManifests[i].name,
389410
dir,
390411
port: allManifests[i].port,
412+
type: allManifests[i].type || DEFAULT_TEMPLATE,
391413
})),
392414
};
393415

skaffold.yaml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,60 @@ profiles:
5151
namespace: constructive-functions
5252
port: 3000
5353
localPort: 3002
54+
- name: python-example
55+
build:
56+
artifacts:
57+
- image: constructive-functions
58+
context: .
59+
docker:
60+
dockerfile: Dockerfile.dev
61+
sync:
62+
manual:
63+
- src: 'functions/**/*.ts'
64+
dest: /usr/src/app
65+
- image: constructive-functions-python
66+
context: .
67+
docker:
68+
dockerfile: Dockerfile.python.dev
69+
sync:
70+
manual:
71+
- src: 'functions/**/*.py'
72+
dest: /usr/src/app
73+
- src: 'generated/**/*.py'
74+
dest: /usr/src/app
75+
local:
76+
push: false
77+
manifests:
78+
kustomize:
79+
paths:
80+
- k8s/overlays/local-simple
81+
rawYaml:
82+
- generated/python-example/k8s/local-deployment.yaml
83+
- generated/python-example/k8s/functions-configmap.yaml
84+
deploy:
85+
kubectl:
86+
defaultNamespace: constructive-functions
87+
portForward:
88+
- resourceType: service
89+
resourceName: python-example
90+
namespace: constructive-functions
91+
port: 80
92+
localPort: 8084
93+
- resourceType: service
94+
resourceName: knative-job-service
95+
namespace: constructive-functions
96+
port: 8080
97+
localPort: 8080
98+
- resourceType: service
99+
resourceName: postgres
100+
namespace: constructive-functions
101+
port: 5432
102+
localPort: 5432
103+
- resourceType: service
104+
resourceName: constructive-server
105+
namespace: constructive-functions
106+
port: 3000
107+
localPort: 3002
54108
- name: send-email-link
55109
build:
56110
artifacts:
@@ -152,6 +206,16 @@ profiles:
152206
manual:
153207
- src: 'functions/**/*.ts'
154208
dest: /usr/src/app
209+
- image: constructive-functions-python
210+
context: .
211+
docker:
212+
dockerfile: Dockerfile.python.dev
213+
sync:
214+
manual:
215+
- src: 'functions/**/*.py'
216+
dest: /usr/src/app
217+
- src: 'generated/**/*.py'
218+
dest: /usr/src/app
155219
local:
156220
push: false
157221
manifests:
@@ -160,6 +224,7 @@ profiles:
160224
- k8s/overlays/local-simple
161225
rawYaml:
162226
- generated/example/k8s/local-deployment.yaml
227+
- generated/python-example/k8s/local-deployment.yaml
163228
- generated/send-email-link/k8s/local-deployment.yaml
164229
- generated/simple-email/k8s/local-deployment.yaml
165230
- generated/functions-configmap.yaml
@@ -172,6 +237,11 @@ profiles:
172237
namespace: constructive-functions
173238
port: 80
174239
localPort: 8083
240+
- resourceType: service
241+
resourceName: python-example
242+
namespace: constructive-functions
243+
port: 80
244+
localPort: 8084
175245
- resourceType: service
176246
resourceName: send-email-link
177247
namespace: constructive-functions
@@ -222,6 +292,11 @@ profiles:
222292
namespace: constructive-functions
223293
port: 80
224294
localPort: 8083
295+
- resourceType: service
296+
resourceName: python-example
297+
namespace: constructive-functions
298+
port: 80
299+
localPort: 8084
225300
- resourceType: service
226301
resourceName: send-email-link
227302
namespace: constructive-functions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
- name: {{name}}
2+
build:
3+
artifacts:
4+
- image: constructive-functions
5+
context: .
6+
docker:
7+
dockerfile: Dockerfile.dev
8+
sync:
9+
manual:
10+
- src: 'functions/**/*.ts'
11+
dest: /usr/src/app
12+
- image: constructive-functions-python
13+
context: .
14+
docker:
15+
dockerfile: Dockerfile.python.dev
16+
sync:
17+
manual:
18+
- src: 'functions/**/*.py'
19+
dest: /usr/src/app
20+
- src: 'generated/**/*.py'
21+
dest: /usr/src/app
22+
local:
23+
push: false
24+
manifests:
25+
kustomize:
26+
paths:
27+
- k8s/overlays/local-simple
28+
rawYaml:
29+
- generated/{{dir}}/k8s/local-deployment.yaml
30+
- generated/{{dir}}/k8s/functions-configmap.yaml
31+
deploy:
32+
kubectl:
33+
defaultNamespace: {{namespace}}
34+
portForward:
35+
- resourceType: service
36+
resourceName: {{name}}
37+
namespace: {{namespace}}
38+
port: 80
39+
localPort: {{port}}
40+
- resourceType: service
41+
resourceName: knative-job-service
42+
namespace: {{namespace}}
43+
port: 8080
44+
localPort: 8080
45+
- resourceType: service
46+
resourceName: postgres
47+
namespace: {{namespace}}
48+
port: 5432
49+
localPort: 5432
50+
- resourceType: service
51+
resourceName: constructive-server
52+
namespace: {{namespace}}
53+
port: 3000
54+
localPort: 3002

templates/k8s/skaffold.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ profiles:
2121
manual:
2222
- src: 'functions/**/*.ts'
2323
dest: /usr/src/app
24+
{{python_artifacts}}
2425
local:
2526
push: false
2627
manifests:

templates/python/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
COPY generated/{{name}}/requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY generated/{{name}}/ .
9+
10+
ENV PORT=8080
11+
EXPOSE 8080
12+
13+
CMD ["python", "main.py"]

templates/python/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# {{name}}-fn
2+
3+
{{description}}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: {{name}}
5+
labels:
6+
app: {{name}}
7+
spec:
8+
replicas: 1
9+
selector:
10+
matchLabels:
11+
app: {{name}}
12+
template:
13+
metadata:
14+
labels:
15+
app: {{name}}
16+
spec:
17+
containers:
18+
- name: {{name}}
19+
image: constructive-functions-python:local
20+
command: ["watchmedo"]
21+
args: ["auto-restart", "--directory=generated/{{name}}", "--pattern=*.py", "--recursive", "--", "python", "generated/{{name}}/main.py"]
22+
ports:
23+
- containerPort: 8080
24+
env:
25+
- name: PORT
26+
value: "8080"
27+
- name: LOG_LEVEL
28+
value: "info"
29+
- name: PYTHONUNBUFFERED
30+
value: "1"
31+
resources:
32+
requests:
33+
memory: "256Mi"
34+
cpu: "100m"
35+
limits:
36+
memory: "1Gi"
37+
cpu: "500m"
38+
---
39+
apiVersion: v1
40+
kind: Service
41+
metadata:
42+
name: {{name}}
43+
spec:
44+
selector:
45+
app: {{name}}
46+
ports:
47+
- port: 80
48+
targetPort: 8080

0 commit comments

Comments
 (0)