Skip to content

Commit 8916343

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 8916343

13 files changed

Lines changed: 340 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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,52 @@ profiles:
5151
namespace: constructive-functions
5252
port: 3000
5353
localPort: 3002
54+
- name: python-example
55+
build:
56+
artifacts:
57+
- image: constructive-functions-python
58+
context: .
59+
docker:
60+
dockerfile: Dockerfile.python.dev
61+
sync:
62+
manual:
63+
- src: 'functions/**/*.py'
64+
dest: /usr/src/app
65+
- src: 'generated/**/*.py'
66+
dest: /usr/src/app
67+
local:
68+
push: false
69+
manifests:
70+
kustomize:
71+
paths:
72+
- k8s/overlays/local-simple
73+
rawYaml:
74+
- generated/python-example/k8s/local-deployment.yaml
75+
- generated/python-example/k8s/functions-configmap.yaml
76+
deploy:
77+
kubectl:
78+
defaultNamespace: constructive-functions
79+
portForward:
80+
- resourceType: service
81+
resourceName: python-example
82+
namespace: constructive-functions
83+
port: 80
84+
localPort: 8084
85+
- resourceType: service
86+
resourceName: knative-job-service
87+
namespace: constructive-functions
88+
port: 8080
89+
localPort: 8080
90+
- resourceType: service
91+
resourceName: postgres
92+
namespace: constructive-functions
93+
port: 5432
94+
localPort: 5432
95+
- resourceType: service
96+
resourceName: constructive-server
97+
namespace: constructive-functions
98+
port: 3000
99+
localPort: 3002
54100
- name: send-email-link
55101
build:
56102
artifacts:
@@ -152,6 +198,16 @@ profiles:
152198
manual:
153199
- src: 'functions/**/*.ts'
154200
dest: /usr/src/app
201+
- image: constructive-functions-python
202+
context: .
203+
docker:
204+
dockerfile: Dockerfile.python.dev
205+
sync:
206+
manual:
207+
- src: 'functions/**/*.py'
208+
dest: /usr/src/app
209+
- src: 'generated/**/*.py'
210+
dest: /usr/src/app
155211
local:
156212
push: false
157213
manifests:
@@ -160,6 +216,7 @@ profiles:
160216
- k8s/overlays/local-simple
161217
rawYaml:
162218
- generated/example/k8s/local-deployment.yaml
219+
- generated/python-example/k8s/local-deployment.yaml
163220
- generated/send-email-link/k8s/local-deployment.yaml
164221
- generated/simple-email/k8s/local-deployment.yaml
165222
- generated/functions-configmap.yaml
@@ -172,6 +229,11 @@ profiles:
172229
namespace: constructive-functions
173230
port: 80
174231
localPort: 8083
232+
- resourceType: service
233+
resourceName: python-example
234+
namespace: constructive-functions
235+
port: 80
236+
localPort: 8084
175237
- resourceType: service
176238
resourceName: send-email-link
177239
namespace: constructive-functions
@@ -222,6 +284,11 @@ profiles:
222284
namespace: constructive-functions
223285
port: 80
224286
localPort: 8083
287+
- resourceType: service
288+
resourceName: python-example
289+
namespace: constructive-functions
290+
port: 80
291+
localPort: 8084
225292
- resourceType: service
226293
resourceName: send-email-link
227294
namespace: constructive-functions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
- name: {{name}}
2+
build:
3+
artifacts:
4+
- image: constructive-functions-python
5+
context: .
6+
docker:
7+
dockerfile: Dockerfile.python.dev
8+
sync:
9+
manual:
10+
- src: 'functions/**/*.py'
11+
dest: /usr/src/app
12+
- src: 'generated/**/*.py'
13+
dest: /usr/src/app
14+
local:
15+
push: false
16+
manifests:
17+
kustomize:
18+
paths:
19+
- k8s/overlays/local-simple
20+
rawYaml:
21+
- generated/{{dir}}/k8s/local-deployment.yaml
22+
- generated/{{dir}}/k8s/functions-configmap.yaml
23+
deploy:
24+
kubectl:
25+
defaultNamespace: {{namespace}}
26+
portForward:
27+
- resourceType: service
28+
resourceName: {{name}}
29+
namespace: {{namespace}}
30+
port: 80
31+
localPort: {{port}}
32+
- resourceType: service
33+
resourceName: knative-job-service
34+
namespace: {{namespace}}
35+
port: 8080
36+
localPort: 8080
37+
- resourceType: service
38+
resourceName: postgres
39+
namespace: {{namespace}}
40+
port: 5432
41+
localPort: 5432
42+
- resourceType: service
43+
resourceName: constructive-server
44+
namespace: {{namespace}}
45+
port: 3000
46+
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)