foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.4__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.
- ate/__init__.py +1 -1
- ate/bridge_server.py +622 -0
- ate/cli.py +2625 -242
- ate/compatibility.py +580 -0
- ate/generators/__init__.py +19 -0
- ate/generators/docker_generator.py +461 -0
- ate/generators/hardware_config.py +469 -0
- ate/generators/ros2_generator.py +617 -0
- ate/generators/skill_generator.py +783 -0
- ate/marketplace.py +524 -0
- ate/mcp_server.py +1341 -107
- ate/primitives.py +1016 -0
- ate/robot_setup.py +2222 -0
- ate/skill_schema.py +537 -0
- ate/telemetry/__init__.py +33 -0
- ate/telemetry/cli.py +455 -0
- ate/telemetry/collector.py +444 -0
- ate/telemetry/context.py +318 -0
- ate/telemetry/fleet_agent.py +419 -0
- ate/telemetry/formats/__init__.py +18 -0
- ate/telemetry/formats/hdf5_serializer.py +503 -0
- ate/telemetry/formats/mcap_serializer.py +457 -0
- ate/telemetry/types.py +334 -0
- foodforthought_cli-0.2.4.dist-info/METADATA +300 -0
- foodforthought_cli-0.2.4.dist-info/RECORD +44 -0
- foodforthought_cli-0.2.4.dist-info/top_level.txt +6 -0
- mechdog_labeled/__init__.py +3 -0
- mechdog_labeled/primitives.py +113 -0
- mechdog_labeled/servo_map.py +209 -0
- mechdog_output/__init__.py +3 -0
- mechdog_output/primitives.py +59 -0
- mechdog_output/servo_map.py +203 -0
- test_autodetect/__init__.py +3 -0
- test_autodetect/primitives.py +113 -0
- test_autodetect/servo_map.py +209 -0
- test_full_auto/__init__.py +3 -0
- test_full_auto/primitives.py +113 -0
- test_full_auto/servo_map.py +209 -0
- test_smart_detect/__init__.py +3 -0
- test_smart_detect/primitives.py +113 -0
- test_smart_detect/servo_map.py +209 -0
- foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
- foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
- foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker Generator - Generate containerized deployment for skills.
|
|
3
|
+
|
|
4
|
+
This generator creates:
|
|
5
|
+
- Dockerfile: Container definition
|
|
6
|
+
- docker-compose.yml: Local testing configuration
|
|
7
|
+
- entrypoint.sh: Container entry script
|
|
8
|
+
- .dockerignore: Build exclusions
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Set
|
|
14
|
+
|
|
15
|
+
from ..skill_schema import SkillSpecification
|
|
16
|
+
from .skill_generator import to_snake_case, to_pascal_case
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DockerGenerator:
|
|
20
|
+
"""
|
|
21
|
+
Generate Docker containerization for a skill.
|
|
22
|
+
|
|
23
|
+
Creates all necessary files for building and running the skill
|
|
24
|
+
in a Docker container, with optional ROS2 integration.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
spec: SkillSpecification,
|
|
30
|
+
base_image: str = "ros:humble",
|
|
31
|
+
include_ros2: bool = True,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the generator.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
spec: The skill specification
|
|
38
|
+
base_image: Base Docker image to use
|
|
39
|
+
include_ros2: Whether to include ROS2 support
|
|
40
|
+
"""
|
|
41
|
+
self.spec = spec
|
|
42
|
+
self.base_image = base_image
|
|
43
|
+
self.include_ros2 = include_ros2
|
|
44
|
+
self.package_name = to_snake_case(spec.name) + "_skill"
|
|
45
|
+
self.class_name = to_pascal_case(spec.name)
|
|
46
|
+
|
|
47
|
+
def get_apt_dependencies(self) -> List[str]:
|
|
48
|
+
"""Get apt packages needed based on skill requirements."""
|
|
49
|
+
deps = [
|
|
50
|
+
"python3-pip",
|
|
51
|
+
"python3-dev",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Add deps based on hardware requirements
|
|
55
|
+
for req in self.spec.hardware_requirements:
|
|
56
|
+
if req.component_type == "camera":
|
|
57
|
+
deps.extend(["libopencv-dev", "python3-opencv"])
|
|
58
|
+
elif req.component_type in ("arm", "gripper"):
|
|
59
|
+
deps.append("libserial-dev")
|
|
60
|
+
|
|
61
|
+
return sorted(set(deps))
|
|
62
|
+
|
|
63
|
+
def get_pip_dependencies(self) -> List[str]:
|
|
64
|
+
"""Get pip packages needed based on skill requirements."""
|
|
65
|
+
deps = [
|
|
66
|
+
"pyyaml",
|
|
67
|
+
"numpy",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Add deps based on hardware requirements
|
|
71
|
+
for req in self.spec.hardware_requirements:
|
|
72
|
+
if req.component_type == "camera":
|
|
73
|
+
deps.extend(["opencv-python", "pillow"])
|
|
74
|
+
elif req.component_type in ("arm", "gripper"):
|
|
75
|
+
deps.append("pyserial")
|
|
76
|
+
elif req.component_type == "force_sensor":
|
|
77
|
+
deps.append("scipy")
|
|
78
|
+
|
|
79
|
+
return sorted(set(deps))
|
|
80
|
+
|
|
81
|
+
def generate_dockerfile(self) -> str:
|
|
82
|
+
"""Generate Dockerfile for the skill."""
|
|
83
|
+
apt_deps = self.get_apt_dependencies()
|
|
84
|
+
pip_deps = self.get_pip_dependencies()
|
|
85
|
+
|
|
86
|
+
apt_deps_str = " \\\n ".join(apt_deps)
|
|
87
|
+
pip_deps_str = " ".join(pip_deps)
|
|
88
|
+
|
|
89
|
+
if self.include_ros2:
|
|
90
|
+
return self._generate_ros2_dockerfile(apt_deps_str, pip_deps_str)
|
|
91
|
+
else:
|
|
92
|
+
return self._generate_python_dockerfile(apt_deps_str, pip_deps_str)
|
|
93
|
+
|
|
94
|
+
def _generate_ros2_dockerfile(self, apt_deps: str, pip_deps: str) -> str:
|
|
95
|
+
"""Generate Dockerfile with ROS2 support."""
|
|
96
|
+
return f'''# {self.spec.name} Skill Container
|
|
97
|
+
# Generated by Skill Compiler v1.0.0
|
|
98
|
+
# Base image: {self.base_image}
|
|
99
|
+
|
|
100
|
+
FROM {self.base_image}
|
|
101
|
+
|
|
102
|
+
# Set environment variables
|
|
103
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
104
|
+
ENV ROS_DISTRO=humble
|
|
105
|
+
ENV SKILL_NAME={self.package_name}
|
|
106
|
+
|
|
107
|
+
# Install system dependencies
|
|
108
|
+
RUN apt-get update && apt-get install -y \\
|
|
109
|
+
{apt_deps} \\
|
|
110
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
111
|
+
|
|
112
|
+
# Install Python dependencies
|
|
113
|
+
RUN pip3 install --no-cache-dir {pip_deps}
|
|
114
|
+
|
|
115
|
+
# Create workspace
|
|
116
|
+
WORKDIR /ws
|
|
117
|
+
|
|
118
|
+
# Copy skill package
|
|
119
|
+
COPY . /ws/src/{self.package_name}
|
|
120
|
+
|
|
121
|
+
# Build workspace
|
|
122
|
+
RUN . /opt/ros/${{ROS_DISTRO}}/setup.sh && \\
|
|
123
|
+
colcon build --packages-select {self.package_name}
|
|
124
|
+
|
|
125
|
+
# Copy entrypoint
|
|
126
|
+
COPY entrypoint.sh /ws/entrypoint.sh
|
|
127
|
+
RUN chmod +x /ws/entrypoint.sh
|
|
128
|
+
|
|
129
|
+
# Set entrypoint
|
|
130
|
+
ENTRYPOINT ["/ws/entrypoint.sh"]
|
|
131
|
+
|
|
132
|
+
# Default command
|
|
133
|
+
CMD ["ros2", "launch", "{self.package_name}", "skill.launch.py"]
|
|
134
|
+
|
|
135
|
+
# Labels
|
|
136
|
+
LABEL org.opencontainers.image.title="{self.spec.name} Skill"
|
|
137
|
+
LABEL org.opencontainers.image.description="{self.spec.description}"
|
|
138
|
+
LABEL org.opencontainers.image.version="{self.spec.version}"
|
|
139
|
+
LABEL org.opencontainers.image.authors="{self.spec.author or 'Unknown'}"
|
|
140
|
+
LABEL skill.name="{self.spec.name}"
|
|
141
|
+
LABEL skill.version="{self.spec.version}"
|
|
142
|
+
'''
|
|
143
|
+
|
|
144
|
+
def _generate_python_dockerfile(self, apt_deps: str, pip_deps: str) -> str:
|
|
145
|
+
"""Generate Dockerfile without ROS2 (standalone Python)."""
|
|
146
|
+
return f'''# {self.spec.name} Skill Container (Standalone)
|
|
147
|
+
# Generated by Skill Compiler v1.0.0
|
|
148
|
+
|
|
149
|
+
FROM python:3.10-slim
|
|
150
|
+
|
|
151
|
+
# Set environment variables
|
|
152
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
153
|
+
ENV SKILL_NAME={self.package_name}
|
|
154
|
+
ENV PYTHONUNBUFFERED=1
|
|
155
|
+
|
|
156
|
+
# Install system dependencies
|
|
157
|
+
RUN apt-get update && apt-get install -y \\
|
|
158
|
+
{apt_deps} \\
|
|
159
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
160
|
+
|
|
161
|
+
# Install Python dependencies
|
|
162
|
+
RUN pip3 install --no-cache-dir {pip_deps}
|
|
163
|
+
|
|
164
|
+
# Create app directory
|
|
165
|
+
WORKDIR /app
|
|
166
|
+
|
|
167
|
+
# Copy skill package
|
|
168
|
+
COPY {self.package_name}/ /app/{self.package_name}/
|
|
169
|
+
COPY config/ /app/config/
|
|
170
|
+
COPY setup.py /app/
|
|
171
|
+
COPY requirements.txt /app/
|
|
172
|
+
|
|
173
|
+
# Install skill package
|
|
174
|
+
RUN pip3 install -e .
|
|
175
|
+
|
|
176
|
+
# Copy entrypoint
|
|
177
|
+
COPY entrypoint.sh /app/entrypoint.sh
|
|
178
|
+
RUN chmod +x /app/entrypoint.sh
|
|
179
|
+
|
|
180
|
+
# Set entrypoint
|
|
181
|
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
182
|
+
|
|
183
|
+
# Default command
|
|
184
|
+
CMD ["python3", "-m", "{self.package_name}"]
|
|
185
|
+
|
|
186
|
+
# Labels
|
|
187
|
+
LABEL org.opencontainers.image.title="{self.spec.name} Skill"
|
|
188
|
+
LABEL org.opencontainers.image.description="{self.spec.description}"
|
|
189
|
+
LABEL org.opencontainers.image.version="{self.spec.version}"
|
|
190
|
+
LABEL skill.name="{self.spec.name}"
|
|
191
|
+
LABEL skill.version="{self.spec.version}"
|
|
192
|
+
'''
|
|
193
|
+
|
|
194
|
+
def generate_compose(self) -> str:
|
|
195
|
+
"""Generate docker-compose.yml for local testing."""
|
|
196
|
+
hardware_volumes = []
|
|
197
|
+
hardware_devices = []
|
|
198
|
+
|
|
199
|
+
# Add device access based on hardware requirements
|
|
200
|
+
for req in self.spec.hardware_requirements:
|
|
201
|
+
if req.component_type in ("arm", "gripper"):
|
|
202
|
+
hardware_devices.append("/dev/ttyUSB0:/dev/ttyUSB0")
|
|
203
|
+
hardware_devices.append("/dev/ttyACM0:/dev/ttyACM0")
|
|
204
|
+
elif req.component_type == "camera":
|
|
205
|
+
hardware_devices.append("/dev/video0:/dev/video0")
|
|
206
|
+
|
|
207
|
+
devices_str = ""
|
|
208
|
+
if hardware_devices:
|
|
209
|
+
devices_str = "\n devices:\n" + "\n".join(
|
|
210
|
+
f" - {d}" for d in sorted(set(hardware_devices))
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return f'''# Docker Compose for {self.spec.name} Skill
|
|
214
|
+
# Generated by Skill Compiler v1.0.0
|
|
215
|
+
#
|
|
216
|
+
# Usage:
|
|
217
|
+
# docker-compose up # Run skill
|
|
218
|
+
# docker-compose up -d # Run in background
|
|
219
|
+
# docker-compose build # Rebuild image
|
|
220
|
+
# docker-compose logs -f # View logs
|
|
221
|
+
# docker-compose down # Stop and remove
|
|
222
|
+
|
|
223
|
+
version: "3.8"
|
|
224
|
+
|
|
225
|
+
services:
|
|
226
|
+
{self.package_name}:
|
|
227
|
+
build:
|
|
228
|
+
context: .
|
|
229
|
+
dockerfile: Dockerfile
|
|
230
|
+
container_name: {self.package_name}
|
|
231
|
+
environment:
|
|
232
|
+
- ROS_DOMAIN_ID=${{ROS_DOMAIN_ID:-0}}
|
|
233
|
+
- USE_SIM=${{USE_SIM:-false}}
|
|
234
|
+
volumes:
|
|
235
|
+
- ./config:/ws/config:ro
|
|
236
|
+
- /tmp/.X11-unix:/tmp/.X11-unix:rw # For GUI (if needed)
|
|
237
|
+
network_mode: host{devices_str}
|
|
238
|
+
restart: unless-stopped
|
|
239
|
+
|
|
240
|
+
# Optional: Simulation environment
|
|
241
|
+
sim:
|
|
242
|
+
image: osrf/ros:humble-simulation
|
|
243
|
+
container_name: {self.package_name}_sim
|
|
244
|
+
environment:
|
|
245
|
+
- DISPLAY=${{DISPLAY}}
|
|
246
|
+
- QT_X11_NO_MITSHM=1
|
|
247
|
+
volumes:
|
|
248
|
+
- /tmp/.X11-unix:/tmp/.X11-unix:rw
|
|
249
|
+
- ./config:/ws/config:ro
|
|
250
|
+
network_mode: host
|
|
251
|
+
profiles:
|
|
252
|
+
- simulation
|
|
253
|
+
|
|
254
|
+
networks:
|
|
255
|
+
default:
|
|
256
|
+
name: {self.package_name}_network
|
|
257
|
+
'''
|
|
258
|
+
|
|
259
|
+
def generate_entrypoint(self) -> str:
|
|
260
|
+
"""Generate entrypoint.sh script."""
|
|
261
|
+
if self.include_ros2:
|
|
262
|
+
return f'''#!/bin/bash
|
|
263
|
+
# Entrypoint for {self.spec.name} skill container
|
|
264
|
+
# Generated by Skill Compiler v1.0.0
|
|
265
|
+
|
|
266
|
+
set -e
|
|
267
|
+
|
|
268
|
+
# Source ROS2 environment
|
|
269
|
+
source /opt/ros/${{ROS_DISTRO}}/setup.bash
|
|
270
|
+
|
|
271
|
+
# Source workspace
|
|
272
|
+
if [ -f /ws/install/setup.bash ]; then
|
|
273
|
+
source /ws/install/setup.bash
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
# Print info
|
|
277
|
+
echo "========================================"
|
|
278
|
+
echo " {self.spec.name} Skill Container"
|
|
279
|
+
echo " Version: {self.spec.version}"
|
|
280
|
+
echo " ROS Distro: ${{ROS_DISTRO}}"
|
|
281
|
+
echo "========================================"
|
|
282
|
+
|
|
283
|
+
# Handle signals gracefully
|
|
284
|
+
trap 'echo "Shutting down..."; exit 0' SIGTERM SIGINT
|
|
285
|
+
|
|
286
|
+
# Execute command
|
|
287
|
+
exec "$@"
|
|
288
|
+
'''
|
|
289
|
+
else:
|
|
290
|
+
return f'''#!/bin/bash
|
|
291
|
+
# Entrypoint for {self.spec.name} skill container (standalone)
|
|
292
|
+
# Generated by Skill Compiler v1.0.0
|
|
293
|
+
|
|
294
|
+
set -e
|
|
295
|
+
|
|
296
|
+
# Print info
|
|
297
|
+
echo "========================================"
|
|
298
|
+
echo " {self.spec.name} Skill Container"
|
|
299
|
+
echo " Version: {self.spec.version}"
|
|
300
|
+
echo "========================================"
|
|
301
|
+
|
|
302
|
+
# Handle signals gracefully
|
|
303
|
+
trap 'echo "Shutting down..."; exit 0' SIGTERM SIGINT
|
|
304
|
+
|
|
305
|
+
# Execute command
|
|
306
|
+
exec "$@"
|
|
307
|
+
'''
|
|
308
|
+
|
|
309
|
+
def generate_dockerignore(self) -> str:
|
|
310
|
+
"""Generate .dockerignore file."""
|
|
311
|
+
return '''# Docker build exclusions
|
|
312
|
+
# Generated by Skill Compiler v1.0.0
|
|
313
|
+
|
|
314
|
+
# Git
|
|
315
|
+
.git
|
|
316
|
+
.gitignore
|
|
317
|
+
|
|
318
|
+
# Python
|
|
319
|
+
__pycache__/
|
|
320
|
+
*.py[cod]
|
|
321
|
+
*$py.class
|
|
322
|
+
*.so
|
|
323
|
+
.Python
|
|
324
|
+
build/
|
|
325
|
+
develop-eggs/
|
|
326
|
+
dist/
|
|
327
|
+
downloads/
|
|
328
|
+
eggs/
|
|
329
|
+
.eggs/
|
|
330
|
+
lib/
|
|
331
|
+
lib64/
|
|
332
|
+
parts/
|
|
333
|
+
sdist/
|
|
334
|
+
var/
|
|
335
|
+
wheels/
|
|
336
|
+
*.egg-info/
|
|
337
|
+
.installed.cfg
|
|
338
|
+
*.egg
|
|
339
|
+
.pytest_cache/
|
|
340
|
+
.coverage
|
|
341
|
+
htmlcov/
|
|
342
|
+
|
|
343
|
+
# Virtual environments
|
|
344
|
+
venv/
|
|
345
|
+
ENV/
|
|
346
|
+
env/
|
|
347
|
+
.venv/
|
|
348
|
+
|
|
349
|
+
# IDE
|
|
350
|
+
.idea/
|
|
351
|
+
.vscode/
|
|
352
|
+
*.swp
|
|
353
|
+
*.swo
|
|
354
|
+
*~
|
|
355
|
+
|
|
356
|
+
# Build artifacts
|
|
357
|
+
log/
|
|
358
|
+
install/
|
|
359
|
+
build/
|
|
360
|
+
|
|
361
|
+
# Documentation
|
|
362
|
+
docs/
|
|
363
|
+
*.md
|
|
364
|
+
!README.md
|
|
365
|
+
|
|
366
|
+
# Tests (optional, include if needed for debugging)
|
|
367
|
+
# test/
|
|
368
|
+
|
|
369
|
+
# Local config
|
|
370
|
+
.env
|
|
371
|
+
*.local.yaml
|
|
372
|
+
secrets/
|
|
373
|
+
'''
|
|
374
|
+
|
|
375
|
+
def generate_requirements_txt(self) -> str:
|
|
376
|
+
"""Generate requirements.txt for pip dependencies."""
|
|
377
|
+
deps = self.get_pip_dependencies()
|
|
378
|
+
return "\n".join(deps)
|
|
379
|
+
|
|
380
|
+
def generate_makefile(self) -> str:
|
|
381
|
+
"""Generate Makefile for common Docker operations."""
|
|
382
|
+
return f'''# Makefile for {self.spec.name} skill container
|
|
383
|
+
# Generated by Skill Compiler v1.0.0
|
|
384
|
+
|
|
385
|
+
IMAGE_NAME := {self.package_name}
|
|
386
|
+
VERSION := {self.spec.version}
|
|
387
|
+
|
|
388
|
+
.PHONY: build run stop logs shell clean test
|
|
389
|
+
|
|
390
|
+
# Build the Docker image
|
|
391
|
+
build:
|
|
392
|
+
docker build -t $(IMAGE_NAME):$(VERSION) -t $(IMAGE_NAME):latest .
|
|
393
|
+
|
|
394
|
+
# Run the container
|
|
395
|
+
run:
|
|
396
|
+
docker-compose up
|
|
397
|
+
|
|
398
|
+
# Run in background
|
|
399
|
+
run-detached:
|
|
400
|
+
docker-compose up -d
|
|
401
|
+
|
|
402
|
+
# Stop the container
|
|
403
|
+
stop:
|
|
404
|
+
docker-compose down
|
|
405
|
+
|
|
406
|
+
# View logs
|
|
407
|
+
logs:
|
|
408
|
+
docker-compose logs -f
|
|
409
|
+
|
|
410
|
+
# Open shell in container
|
|
411
|
+
shell:
|
|
412
|
+
docker exec -it {self.package_name} /bin/bash
|
|
413
|
+
|
|
414
|
+
# Clean up
|
|
415
|
+
clean:
|
|
416
|
+
docker-compose down -v --rmi local
|
|
417
|
+
|
|
418
|
+
# Run tests in container
|
|
419
|
+
test:
|
|
420
|
+
docker run --rm $(IMAGE_NAME):$(VERSION) python3 -m pytest
|
|
421
|
+
|
|
422
|
+
# Push to registry (customize registry URL)
|
|
423
|
+
push:
|
|
424
|
+
docker push $(IMAGE_NAME):$(VERSION)
|
|
425
|
+
docker push $(IMAGE_NAME):latest
|
|
426
|
+
|
|
427
|
+
# Run with simulation
|
|
428
|
+
sim:
|
|
429
|
+
docker-compose --profile simulation up
|
|
430
|
+
'''
|
|
431
|
+
|
|
432
|
+
def generate(self, output_dir: Path) -> Dict[str, str]:
|
|
433
|
+
"""
|
|
434
|
+
Generate complete Docker deployment structure.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
output_dir: Directory to write files to
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Dict mapping filenames to generated content
|
|
441
|
+
"""
|
|
442
|
+
output_dir = Path(output_dir)
|
|
443
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
|
|
445
|
+
files = {
|
|
446
|
+
"Dockerfile": self.generate_dockerfile(),
|
|
447
|
+
"docker-compose.yml": self.generate_compose(),
|
|
448
|
+
"entrypoint.sh": self.generate_entrypoint(),
|
|
449
|
+
".dockerignore": self.generate_dockerignore(),
|
|
450
|
+
"requirements.txt": self.generate_requirements_txt(),
|
|
451
|
+
"Makefile": self.generate_makefile(),
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# Write files
|
|
455
|
+
for filename, content in files.items():
|
|
456
|
+
file_path = output_dir / filename
|
|
457
|
+
file_path.write_text(content)
|
|
458
|
+
if filename.endswith(".sh"):
|
|
459
|
+
file_path.chmod(0o755)
|
|
460
|
+
|
|
461
|
+
return files
|