stackai-cli 0.1.3__py3-none-any.whl → 0.2.0__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.
- devai/__init__.py +1 -1
- devai/cli.py +16 -14
- devai/detector.py +387 -14
- devai/generator.py +135 -0
- {stackai_cli-0.1.3.dist-info → stackai_cli-0.2.0.dist-info}/METADATA +1 -1
- stackai_cli-0.2.0.dist-info/RECORD +11 -0
- stackai_cli-0.1.3.dist-info/RECORD +0 -11
- {stackai_cli-0.1.3.dist-info → stackai_cli-0.2.0.dist-info}/WHEEL +0 -0
- {stackai_cli-0.1.3.dist-info → stackai_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {stackai_cli-0.1.3.dist-info → stackai_cli-0.2.0.dist-info}/top_level.txt +0 -0
devai/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.2.0"
|
devai/cli.py
CHANGED
|
@@ -2,11 +2,12 @@ import click
|
|
|
2
2
|
from devai.detector import detect_stack
|
|
3
3
|
from devai.generator import generate_files
|
|
4
4
|
from devai.debugger import debug_container
|
|
5
|
+
from devai import __version__
|
|
5
6
|
|
|
6
7
|
@click.group()
|
|
7
|
-
@click.version_option()
|
|
8
|
+
@click.version_option(version=__version__, prog_name="stackai")
|
|
8
9
|
def main():
|
|
9
|
-
"""
|
|
10
|
+
"""stackai — AI-powered Docker assistant. No configuration needed."""
|
|
10
11
|
pass
|
|
11
12
|
|
|
12
13
|
|
|
@@ -14,28 +15,29 @@ def main():
|
|
|
14
15
|
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
15
16
|
def init(path):
|
|
16
17
|
"""Analyse ton projet et génère Dockerfile + docker-compose."""
|
|
17
|
-
click.echo(click.style("🔍
|
|
18
|
+
click.echo(click.style("🔍 Analyzing project...", fg="cyan"))
|
|
18
19
|
|
|
19
20
|
stack = detect_stack(path)
|
|
20
21
|
if not stack:
|
|
21
|
-
click.echo(click.style("❌
|
|
22
|
+
click.echo(click.style("❌ Could not detect stack. Make sure you are in a project directory.", fg="red"))
|
|
22
23
|
raise SystemExit(1)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
click.echo(click.style("
|
|
25
|
+
services = ', '.join(stack['services']) if stack['services'] else 'none'
|
|
26
|
+
click.echo(click.style(f"✅ Stack detected: {stack['language']} / {stack.get('framework','generic')} + {services}", fg="green"))
|
|
27
|
+
click.echo(click.style("⚙️ Generating Docker files...", fg="cyan"))
|
|
26
28
|
|
|
27
29
|
generated = generate_files(path, stack)
|
|
28
30
|
for f in generated:
|
|
29
|
-
click.echo(click.style(f" ✅ {f}
|
|
31
|
+
click.echo(click.style(f" ✅ {f} created", fg="green"))
|
|
30
32
|
|
|
31
|
-
click.echo(click.style("\n🚀
|
|
33
|
+
click.echo(click.style("\n🚀 Ready! Run: docker compose up -d", fg="bright_green", bold=True))
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
@main.command()
|
|
35
37
|
@click.argument("container_name")
|
|
36
38
|
def debug(container_name):
|
|
37
39
|
"""Analyse les logs d'un container et explique l'erreur."""
|
|
38
|
-
click.echo(click.style(f"🔍
|
|
40
|
+
click.echo(click.style(f"🔍 Reading logs from '{container_name}'...", fg="cyan"))
|
|
39
41
|
debug_container(container_name)
|
|
40
42
|
|
|
41
43
|
|
|
@@ -48,9 +50,9 @@ def scan(path):
|
|
|
48
50
|
click.echo(click.style("❌ Aucune stack détectée.", fg="red"))
|
|
49
51
|
return
|
|
50
52
|
|
|
51
|
-
click.echo(click.style("📦 Stack
|
|
52
|
-
click.echo(f"
|
|
53
|
-
click.echo(f" Framework : {stack.get('framework', '
|
|
54
|
-
click.echo(f" Port : {stack.get('port', '
|
|
55
|
-
click.echo(f" Services : {', '.join(stack['services']) if stack['services'] else '
|
|
53
|
+
click.echo(click.style("📦 Stack detected:", fg="cyan", bold=True))
|
|
54
|
+
click.echo(f" Language : {stack['language']}")
|
|
55
|
+
click.echo(f" Framework : {stack.get('framework', 'unknown')}")
|
|
56
|
+
click.echo(f" Port : {stack.get('port', 'unknown')}")
|
|
57
|
+
click.echo(f" Services : {', '.join(stack['services']) if stack['services'] else 'none'}")
|
|
56
58
|
click.echo(f" Python ver. : {stack.get('python_version', 'N/A')}")
|
devai/detector.py
CHANGED
|
@@ -1,21 +1,394 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
+
import json
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# SERVICE MAP dep_name → [service_ids]
|
|
9
|
+
# A single dependency can imply multiple services.
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
SERVICE_MAP: dict[str, list[str]] = {
|
|
12
|
+
# ── Relational databases ──────────────────────────────────────────────
|
|
13
|
+
"psycopg2": ["postgres"], "psycopg2-binary": ["postgres"],
|
|
14
|
+
"asyncpg": ["postgres"], "sqlalchemy": ["postgres"],
|
|
15
|
+
"alembic": ["postgres"], "databases": ["postgres"],
|
|
16
|
+
"tortoise-orm": ["postgres"], "peewee": ["postgres"],
|
|
17
|
+
"dbt-core": ["postgres"], "dbt-postgres": ["postgres"],
|
|
18
|
+
"dbt-spark": ["spark"], "dbt-bigquery": [],
|
|
19
|
+
"pymysql": ["mysql"], "mysql-connector-python": ["mysql"],
|
|
20
|
+
"aiomysql": ["mysql"],
|
|
21
|
+
"cx-oracle": ["oracle"], "oracledb": ["oracle"],
|
|
22
|
+
"pyodbc": ["mssql"], "pymssql": ["mssql"],
|
|
23
|
+
"psycopg": ["postgres"],
|
|
24
|
+
|
|
25
|
+
# ── NoSQL ─────────────────────────────────────────────────────────────
|
|
26
|
+
"pymongo": ["mongodb"], "motor": ["mongodb"],
|
|
27
|
+
"mongoengine": ["mongodb"], "beanie": ["mongodb"],
|
|
28
|
+
"redis": ["redis"], "aioredis": ["redis"],
|
|
29
|
+
"redis-py": ["redis"], "coredis": ["redis"],
|
|
30
|
+
"celery": ["redis"], "dramatiq": ["redis"],
|
|
31
|
+
"rq": ["redis"],
|
|
32
|
+
"cassandra-driver": ["cassandra"],
|
|
33
|
+
"aiofiles": [],
|
|
34
|
+
|
|
35
|
+
# ── Search & Analytics ────────────────────────────────────────────────
|
|
36
|
+
"elasticsearch": ["elasticsearch"],
|
|
37
|
+
"elasticsearch-dsl": ["elasticsearch"],
|
|
38
|
+
"opensearch-py": ["opensearch"],
|
|
39
|
+
"opensearchpy": ["opensearch"],
|
|
40
|
+
"solr": ["solr"],
|
|
41
|
+
"clickhouse-driver": ["clickhouse"],
|
|
42
|
+
"clickhouse-connect": ["clickhouse"],
|
|
43
|
+
|
|
44
|
+
# ── Streaming / Messaging ─────────────────────────────────────────────
|
|
45
|
+
"kafka-python": ["kafka"], "confluent-kafka": ["kafka"],
|
|
46
|
+
"aiokafka": ["kafka"], "faust": ["kafka"],
|
|
47
|
+
"pika": ["rabbitmq"], "aio-pika": ["rabbitmq"],
|
|
48
|
+
"kombu": ["rabbitmq"],
|
|
49
|
+
"nats-py": ["nats"],
|
|
50
|
+
|
|
51
|
+
# ── Object Storage / Cloud ────────────────────────────────────────────
|
|
52
|
+
"boto3": ["minio"], "botocore": ["minio"],
|
|
53
|
+
"s3fs": ["minio"], "s3transfer": ["minio"],
|
|
54
|
+
"minio": ["minio"],
|
|
55
|
+
"google-cloud-storage": [], "azure-storage-blob": [],
|
|
56
|
+
|
|
57
|
+
# ── Big Data / Spark ──────────────────────────────────────────────────
|
|
58
|
+
"pyspark": ["spark"], "delta-spark": ["spark"],
|
|
59
|
+
"spark": ["spark"],
|
|
60
|
+
|
|
61
|
+
# ── Streaming / Real-time ─────────────────────────────────────────────
|
|
62
|
+
"pyflink": ["flink"], "apache-flink": ["flink"],
|
|
63
|
+
|
|
64
|
+
# ── Orchestration ─────────────────────────────────────────────────────
|
|
65
|
+
"apache-airflow": ["airflow", "postgres"],
|
|
66
|
+
"airflow": ["airflow", "postgres"],
|
|
67
|
+
"prefect": ["prefect"],
|
|
68
|
+
"dagster": ["dagster", "postgres"],
|
|
69
|
+
"luigi": [],
|
|
70
|
+
|
|
71
|
+
# ── ML / AI ───────────────────────────────────────────────────────────
|
|
72
|
+
"mlflow": ["mlflow"],
|
|
73
|
+
"torch": [], "tensorflow": [],
|
|
74
|
+
"onnxruntime": [], "triton": [],
|
|
75
|
+
|
|
76
|
+
# ── Tracing / Monitoring ──────────────────────────────────────────────
|
|
77
|
+
"opentelemetry-sdk": ["jaeger"],
|
|
78
|
+
"prometheus-client": ["prometheus"],
|
|
79
|
+
"grafana": ["grafana"],
|
|
80
|
+
|
|
81
|
+
# ── Web frameworks (no extra service needed) ──────────────────────────
|
|
82
|
+
"fastapi": [], "flask": [], "django": [], "tornado": [],
|
|
83
|
+
"aiohttp": [], "starlette": [], "litestar": [],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# IMPORT → dep alias (for scanning .py source files)
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
IMPORT_TO_DEP: dict[str, str] = {
|
|
90
|
+
"psycopg2": "psycopg2", "asyncpg": "asyncpg",
|
|
91
|
+
"sqlalchemy": "sqlalchemy", "alembic": "alembic",
|
|
92
|
+
"pymysql": "pymysql", "pymongo": "pymongo",
|
|
93
|
+
"motor": "motor", "redis": "redis",
|
|
94
|
+
"celery": "celery", "kafka": "kafka-python",
|
|
95
|
+
"confluent_kafka": "confluent-kafka", "aiokafka": "aiokafka",
|
|
96
|
+
"pika": "pika", "aio_pika": "aio-pika",
|
|
97
|
+
"boto3": "boto3", "botocore": "botocore",
|
|
98
|
+
"s3fs": "s3fs", "minio": "minio",
|
|
99
|
+
"pyspark": "pyspark", "delta": "delta-spark",
|
|
100
|
+
"airflow": "apache-airflow", "prefect": "prefect",
|
|
101
|
+
"dagster": "dagster", "mlflow": "mlflow",
|
|
15
102
|
"elasticsearch": "elasticsearch",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
103
|
+
"opensearchpy": "opensearch-py",
|
|
104
|
+
"clickhouse_driver": "clickhouse-driver",
|
|
105
|
+
"cassandra": "cassandra-driver",
|
|
106
|
+
"faust": "faust", "pyflink": "pyflink",
|
|
107
|
+
"torch": "torch", "tensorflow": "tensorflow",
|
|
108
|
+
"prometheus_client": "prometheus-client",
|
|
109
|
+
"opentelemetry": "opentelemetry-sdk",
|
|
110
|
+
"dbt": "dbt-core",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Framework detection: import name → (framework_label, default_port)
|
|
114
|
+
FRAMEWORK_MAP: dict[str, tuple[str, int]] = {
|
|
115
|
+
"fastapi": ("fastapi", 8000), "uvicorn": ("fastapi", 8000),
|
|
116
|
+
"flask": ("flask", 5000), "django": ("django", 8000),
|
|
117
|
+
"tornado": ("tornado", 8888), "aiohttp": ("aiohttp", 8080),
|
|
118
|
+
"starlette": ("starlette", 8000), "litestar": ("litestar", 8000),
|
|
119
|
+
"express": ("express", 3000), "next": ("nextjs", 3000),
|
|
120
|
+
"nuxt": ("nuxtjs", 3000),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Public entry point
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def detect_stack(project_path: str) -> dict | None:
|
|
129
|
+
path = Path(project_path)
|
|
130
|
+
|
|
131
|
+
if (path / "requirements.txt").exists() or (path / "pyproject.toml").exists():
|
|
132
|
+
return _detect_python(path)
|
|
133
|
+
if (path / "package.json").exists():
|
|
134
|
+
return _detect_node(path)
|
|
135
|
+
if (path / "pom.xml").exists() or (path / "build.gradle").exists():
|
|
136
|
+
return _detect_java(path)
|
|
137
|
+
if (path / "go.mod").exists():
|
|
138
|
+
return _detect_go(path)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Python
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def _detect_python(path: Path) -> dict:
|
|
147
|
+
deps = _read_python_deps(path)
|
|
148
|
+
deps |= _scan_python_imports(path) # enrich with source scan
|
|
149
|
+
deps |= _read_env_hints(path) # enrich with .env hints
|
|
150
|
+
|
|
151
|
+
framework, port = _detect_framework(deps)
|
|
152
|
+
services = _deps_to_services(deps)
|
|
153
|
+
python_version = _detect_python_version(path)
|
|
154
|
+
dep_file = "requirements.txt" if (path / "requirements.txt").exists() else "pyproject.toml"
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"language": "python",
|
|
158
|
+
"framework": framework,
|
|
159
|
+
"port": port,
|
|
160
|
+
"services": services,
|
|
161
|
+
"python_version": python_version,
|
|
162
|
+
"dep_file": dep_file,
|
|
163
|
+
"raw_deps": sorted(deps),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _read_python_deps(path: Path) -> set[str]:
|
|
168
|
+
"""Read requirements.txt and/or pyproject.toml."""
|
|
169
|
+
deps: set[str] = set()
|
|
170
|
+
|
|
171
|
+
req = path / "requirements.txt"
|
|
172
|
+
if req.exists():
|
|
173
|
+
for line in req.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
174
|
+
line = line.strip()
|
|
175
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
176
|
+
continue
|
|
177
|
+
# strip version specifiers
|
|
178
|
+
name = re.split(r"[>=<!;\[]", line)[0].strip().lower()
|
|
179
|
+
if name:
|
|
180
|
+
deps.add(name)
|
|
181
|
+
|
|
182
|
+
pyproject = path / "pyproject.toml"
|
|
183
|
+
if pyproject.exists():
|
|
184
|
+
content = pyproject.read_text(encoding="utf-8", errors="ignore")
|
|
185
|
+
for m in re.finditer(r'"([a-zA-Z0-9_\-]+)\s*[>=<!]', content):
|
|
186
|
+
deps.add(m.group(1).lower())
|
|
187
|
+
|
|
188
|
+
return deps
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _scan_python_imports(path: Path) -> set[str]:
|
|
192
|
+
"""Scan .py files for import statements and map to dep names."""
|
|
193
|
+
found: set[str] = set()
|
|
194
|
+
py_files = list(path.rglob("*.py"))[:200] # cap at 200 files
|
|
195
|
+
|
|
196
|
+
import_re = re.compile(
|
|
197
|
+
r"^(?:import|from)\s+([a-zA-Z0-9_]+)", re.MULTILINE
|
|
198
|
+
)
|
|
199
|
+
for f in py_files:
|
|
200
|
+
try:
|
|
201
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
202
|
+
for m in import_re.finditer(content):
|
|
203
|
+
root = m.group(1).lower()
|
|
204
|
+
if root in IMPORT_TO_DEP:
|
|
205
|
+
found.add(IMPORT_TO_DEP[root])
|
|
206
|
+
except OSError:
|
|
207
|
+
pass
|
|
208
|
+
return found
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _read_env_hints(path: Path) -> set[str]:
|
|
212
|
+
"""Infer services from .env or .env.example variable names."""
|
|
213
|
+
hints: set[str] = set()
|
|
214
|
+
for fname in [".env", ".env.example", ".env.sample"]:
|
|
215
|
+
env_file = path / fname
|
|
216
|
+
if not env_file.exists():
|
|
217
|
+
continue
|
|
218
|
+
content = env_file.read_text(encoding="utf-8", errors="ignore").upper()
|
|
219
|
+
if "POSTGRES" in content or "DATABASE_URL" in content:
|
|
220
|
+
hints.add("psycopg2")
|
|
221
|
+
if "REDIS" in content:
|
|
222
|
+
hints.add("redis")
|
|
223
|
+
if "MONGO" in content:
|
|
224
|
+
hints.add("pymongo")
|
|
225
|
+
if "KAFKA" in content:
|
|
226
|
+
hints.add("kafka-python")
|
|
227
|
+
if "MINIO" in content or "S3_ENDPOINT" in content or "AWS_" in content:
|
|
228
|
+
hints.add("boto3")
|
|
229
|
+
if "SPARK" in content:
|
|
230
|
+
hints.add("pyspark")
|
|
231
|
+
if "AIRFLOW" in content:
|
|
232
|
+
hints.add("apache-airflow")
|
|
233
|
+
if "MLFLOW" in content:
|
|
234
|
+
hints.add("mlflow")
|
|
235
|
+
if "ELASTICSEARCH" in content:
|
|
236
|
+
hints.add("elasticsearch")
|
|
237
|
+
return hints
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Node.js
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def _detect_node(path: Path) -> dict:
|
|
245
|
+
pkg = json.loads((path / "package.json").read_text(encoding="utf-8"))
|
|
246
|
+
all_deps = {
|
|
247
|
+
**pkg.get("dependencies", {}),
|
|
248
|
+
**pkg.get("devDependencies", {}),
|
|
249
|
+
}
|
|
250
|
+
deps = {k.lower() for k in all_deps}
|
|
251
|
+
|
|
252
|
+
framework, port = _detect_framework(deps)
|
|
253
|
+
services = _deps_to_services(deps)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"language": "nodejs",
|
|
257
|
+
"framework": framework,
|
|
258
|
+
"port": port,
|
|
259
|
+
"services": services,
|
|
260
|
+
"python_version": None,
|
|
261
|
+
"dep_file": "package.json",
|
|
262
|
+
"raw_deps": sorted(deps),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Java / Go
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def _detect_java(path: Path) -> dict:
|
|
271
|
+
content = ""
|
|
272
|
+
for f in ["pom.xml", "build.gradle"]:
|
|
273
|
+
p = path / f
|
|
274
|
+
if p.exists():
|
|
275
|
+
content = p.read_text(encoding="utf-8", errors="ignore").lower()
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
services = []
|
|
279
|
+
if "postgresql" in content or "postgres" in content:
|
|
280
|
+
services.append("postgres")
|
|
281
|
+
if "mysql" in content:
|
|
282
|
+
services.append("mysql")
|
|
283
|
+
if "mongodb" in content or "mongo" in content:
|
|
284
|
+
services.append("mongodb")
|
|
285
|
+
if "redis" in content:
|
|
286
|
+
services.append("redis")
|
|
287
|
+
if "kafka" in content:
|
|
288
|
+
services.append("kafka")
|
|
289
|
+
if "elasticsearch" in content:
|
|
290
|
+
services.append("elasticsearch")
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"language": "java",
|
|
294
|
+
"framework": "spring-boot",
|
|
295
|
+
"port": 8080,
|
|
296
|
+
"services": services,
|
|
297
|
+
"python_version": None,
|
|
298
|
+
"dep_file": "pom.xml" if (path / "pom.xml").exists() else "build.gradle",
|
|
299
|
+
"raw_deps": [],
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _detect_go(path: Path) -> dict:
|
|
304
|
+
content = (path / "go.mod").read_text(encoding="utf-8", errors="ignore").lower()
|
|
305
|
+
services = []
|
|
306
|
+
if "postgres" in content or "pgx" in content:
|
|
307
|
+
services.append("postgres")
|
|
308
|
+
if "mongo" in content:
|
|
309
|
+
services.append("mongodb")
|
|
310
|
+
if "redis" in content:
|
|
311
|
+
services.append("redis")
|
|
312
|
+
if "kafka" in content:
|
|
313
|
+
services.append("kafka")
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"language": "go",
|
|
317
|
+
"framework": "generic",
|
|
318
|
+
"port": 8080,
|
|
319
|
+
"services": services,
|
|
320
|
+
"python_version": None,
|
|
321
|
+
"dep_file": "go.mod",
|
|
322
|
+
"raw_deps": [],
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
# Helpers
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
def _deps_to_services(deps: set[str]) -> list[str]:
|
|
331
|
+
seen: set[str] = set()
|
|
332
|
+
result: list[str] = []
|
|
333
|
+
for dep in deps:
|
|
334
|
+
for svc in SERVICE_MAP.get(dep, []):
|
|
335
|
+
if svc and svc not in seen:
|
|
336
|
+
seen.add(svc)
|
|
337
|
+
result.append(svc)
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _detect_framework(deps: set[str]) -> tuple[str, int]:
|
|
342
|
+
for dep, (fw, port) in FRAMEWORK_MAP.items():
|
|
343
|
+
if dep in deps:
|
|
344
|
+
return fw, port
|
|
345
|
+
return "generic", 8000
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _detect_python_version(path: Path) -> str:
|
|
349
|
+
for f in [".python-version", "runtime.txt"]:
|
|
350
|
+
p = path / f
|
|
351
|
+
if p.exists():
|
|
352
|
+
m = re.search(r"3\.\d+", p.read_text())
|
|
353
|
+
if m:
|
|
354
|
+
return m.group()
|
|
355
|
+
pyproject = path / "pyproject.toml"
|
|
356
|
+
if pyproject.exists():
|
|
357
|
+
m = re.search(r'python_requires\s*=\s*">=\s*(3\.\d+)"', pyproject.read_text())
|
|
358
|
+
if m:
|
|
359
|
+
return m.group(1)
|
|
360
|
+
return "3.11"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# Mapping dépendance → service(s) Docker requis
|
|
365
|
+
# Un service peut en impliquer plusieurs (ex: airflow → airflow + postgres)
|
|
366
|
+
SERVICE_HINTS = {
|
|
367
|
+
# Databases
|
|
368
|
+
"psycopg2": ["postgres"],
|
|
369
|
+
"asyncpg": ["postgres"],
|
|
370
|
+
"sqlalchemy": ["postgres"],
|
|
371
|
+
"pymongo": ["mongodb"],
|
|
372
|
+
"motor": ["mongodb"],
|
|
373
|
+
"mysql-connector": ["mysql"],
|
|
374
|
+
"pymysql": ["mysql"],
|
|
375
|
+
# Cache / Queue
|
|
376
|
+
"redis": ["redis"],
|
|
377
|
+
"celery": ["redis"],
|
|
378
|
+
# Search
|
|
379
|
+
"elasticsearch": ["elasticsearch"],
|
|
380
|
+
# Streaming
|
|
381
|
+
"kafka": ["kafka"],
|
|
382
|
+
# Object storage
|
|
383
|
+
"boto3": ["minio"],
|
|
384
|
+
"s3fs": ["minio"],
|
|
385
|
+
# Big Data
|
|
386
|
+
"pyspark": ["spark"],
|
|
387
|
+
"delta-spark": ["spark"],
|
|
388
|
+
"pyflink": ["flink"],
|
|
389
|
+
# Orchestration (Airflow implique postgres)
|
|
390
|
+
"apache-airflow": ["airflow", "postgres"],
|
|
391
|
+
"airflow": ["airflow", "postgres"],
|
|
19
392
|
}
|
|
20
393
|
|
|
21
394
|
# Mapping dépendance → framework
|
|
@@ -65,7 +438,7 @@ def _detect_python(path: Path, dep_file: Path) -> dict:
|
|
|
65
438
|
port = p
|
|
66
439
|
break
|
|
67
440
|
|
|
68
|
-
services = list({
|
|
441
|
+
services = list({s for dep in deps if dep in SERVICE_HINTS for s in SERVICE_HINTS[dep]})
|
|
69
442
|
|
|
70
443
|
python_version = _detect_python_version(path)
|
|
71
444
|
|
|
@@ -97,7 +470,7 @@ def _detect_node(path: Path) -> dict:
|
|
|
97
470
|
framework = "react"
|
|
98
471
|
port = 3000
|
|
99
472
|
|
|
100
|
-
services = list({
|
|
473
|
+
services = list({s for dep in deps_lower if dep in SERVICE_HINTS for s in SERVICE_HINTS[dep]})
|
|
101
474
|
|
|
102
475
|
return {
|
|
103
476
|
"language": "nodejs",
|
devai/generator.py
CHANGED
|
@@ -143,6 +143,140 @@ SERVICE_TEMPLATES = {
|
|
|
143
143
|
- xpack.security.enabled=false
|
|
144
144
|
ports:
|
|
145
145
|
- "9200:9200"
|
|
146
|
+
""",
|
|
147
|
+
"minio": """\
|
|
148
|
+
minio:
|
|
149
|
+
image: minio/minio:latest
|
|
150
|
+
environment:
|
|
151
|
+
MINIO_ROOT_USER: minioadmin
|
|
152
|
+
MINIO_ROOT_PASSWORD: minioadmin
|
|
153
|
+
command: server /data --console-address ":9001"
|
|
154
|
+
ports:
|
|
155
|
+
- "9000:9000"
|
|
156
|
+
- "9001:9001"
|
|
157
|
+
volumes:
|
|
158
|
+
- minio_data:/data
|
|
159
|
+
""",
|
|
160
|
+
"airflow": """\
|
|
161
|
+
airflow-webserver:
|
|
162
|
+
image: apache/airflow:2.8.1-python3.11
|
|
163
|
+
environment:
|
|
164
|
+
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
|
|
165
|
+
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres:5432/airflow
|
|
166
|
+
- AIRFLOW__CORE__LOAD_EXAMPLES=False
|
|
167
|
+
volumes:
|
|
168
|
+
- ./dags:/opt/airflow/dags
|
|
169
|
+
ports:
|
|
170
|
+
- "8080:8080"
|
|
171
|
+
command: >
|
|
172
|
+
bash -c "airflow db migrate && airflow users create --username admin --password admin --firstname Admin --lastname User --role Admin --email admin@example.com; airflow webserver"
|
|
173
|
+
depends_on:
|
|
174
|
+
- postgres
|
|
175
|
+
|
|
176
|
+
airflow-scheduler:
|
|
177
|
+
image: apache/airflow:2.8.1-python3.11
|
|
178
|
+
environment:
|
|
179
|
+
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
|
|
180
|
+
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres:5432/airflow
|
|
181
|
+
- AIRFLOW__CORE__LOAD_EXAMPLES=False
|
|
182
|
+
volumes:
|
|
183
|
+
- ./dags:/opt/airflow/dags
|
|
184
|
+
command: airflow scheduler
|
|
185
|
+
depends_on:
|
|
186
|
+
- postgres
|
|
187
|
+
""",
|
|
188
|
+
|
|
189
|
+
"spark": """\
|
|
190
|
+
spark-master:
|
|
191
|
+
image: bitnami/spark:3.5
|
|
192
|
+
environment:
|
|
193
|
+
- SPARK_MODE=master
|
|
194
|
+
- SPARK_MASTER_HOST=spark-master
|
|
195
|
+
ports:
|
|
196
|
+
- "7077:7077"
|
|
197
|
+
- "8081:8080"
|
|
198
|
+
|
|
199
|
+
spark-worker:
|
|
200
|
+
image: bitnami/spark:3.5
|
|
201
|
+
environment:
|
|
202
|
+
- SPARK_MODE=worker
|
|
203
|
+
- SPARK_MASTER_URL=spark://spark-master:7077
|
|
204
|
+
- SPARK_WORKER_MEMORY=2g
|
|
205
|
+
- SPARK_WORKER_CORES=2
|
|
206
|
+
depends_on:
|
|
207
|
+
- spark-master
|
|
208
|
+
""",
|
|
209
|
+
"kafka": """\
|
|
210
|
+
zookeeper:
|
|
211
|
+
image: confluentinc/cp-zookeeper:7.6.0
|
|
212
|
+
environment:
|
|
213
|
+
ZOOKEEPER_CLIENT_PORT: 2181
|
|
214
|
+
|
|
215
|
+
kafka:
|
|
216
|
+
image: confluentinc/cp-kafka:7.6.0
|
|
217
|
+
ports:
|
|
218
|
+
- "9092:9092"
|
|
219
|
+
environment:
|
|
220
|
+
KAFKA_BROKER_ID: 1
|
|
221
|
+
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
|
222
|
+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
|
|
223
|
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
224
|
+
depends_on:
|
|
225
|
+
- zookeeper
|
|
226
|
+
""",
|
|
227
|
+
"rabbitmq": """\
|
|
228
|
+
rabbitmq:
|
|
229
|
+
image: rabbitmq:3-management-alpine
|
|
230
|
+
ports:
|
|
231
|
+
- "5672:5672"
|
|
232
|
+
- "15672:15672"
|
|
233
|
+
""",
|
|
234
|
+
"mlflow": """\
|
|
235
|
+
mlflow:
|
|
236
|
+
image: ghcr.io/mlflow/mlflow:v2.12.1
|
|
237
|
+
ports:
|
|
238
|
+
- "5000:5000"
|
|
239
|
+
command: mlflow server --host 0.0.0.0 --port 5000
|
|
240
|
+
""",
|
|
241
|
+
"prometheus": """\
|
|
242
|
+
prometheus:
|
|
243
|
+
image: prom/prometheus:latest
|
|
244
|
+
ports:
|
|
245
|
+
- "9090:9090"
|
|
246
|
+
volumes:
|
|
247
|
+
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
|
248
|
+
""",
|
|
249
|
+
"prefect": """\
|
|
250
|
+
prefect:
|
|
251
|
+
image: prefecthq/prefect:2-latest
|
|
252
|
+
command: prefect server start --host 0.0.0.0
|
|
253
|
+
ports:
|
|
254
|
+
- "4200:4200"
|
|
255
|
+
""",
|
|
256
|
+
"dagster": """\
|
|
257
|
+
dagster:
|
|
258
|
+
image: dagster/dagster-k8s:latest
|
|
259
|
+
ports:
|
|
260
|
+
- "3000:3000"
|
|
261
|
+
depends_on:
|
|
262
|
+
- postgres
|
|
263
|
+
""",
|
|
264
|
+
"flink": """\
|
|
265
|
+
flink-jobmanager:
|
|
266
|
+
image: flink:1.18-scala_2.12
|
|
267
|
+
command: jobmanager
|
|
268
|
+
ports:
|
|
269
|
+
- "8082:8081"
|
|
270
|
+
environment:
|
|
271
|
+
- JOB_MANAGER_RPC_ADDRESS=flink-jobmanager
|
|
272
|
+
|
|
273
|
+
flink-taskmanager:
|
|
274
|
+
image: flink:1.18-scala_2.12
|
|
275
|
+
command: taskmanager
|
|
276
|
+
depends_on:
|
|
277
|
+
- flink-jobmanager
|
|
278
|
+
environment:
|
|
279
|
+
- JOB_MANAGER_RPC_ADDRESS=flink-jobmanager
|
|
146
280
|
""",
|
|
147
281
|
}
|
|
148
282
|
|
|
@@ -150,6 +284,7 @@ VOLUME_NAMES = {
|
|
|
150
284
|
"postgres": "postgres_data",
|
|
151
285
|
"mongodb": "mongo_data",
|
|
152
286
|
"mysql": "mysql_data",
|
|
287
|
+
"minio": "minio_data",
|
|
153
288
|
}
|
|
154
289
|
|
|
155
290
|
FRAMEWORK_CMDS = {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
devai/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
|
2
|
+
devai/ai.py,sha256=K5T8_7BqkP5gLta0UsjoI-S-k5UGdALqy2DpPvWDfUg,852
|
|
3
|
+
devai/cli.py,sha256=tUVFY1RTTqO6X9JbNkfFGtPh_b5lJZn6UV8ye7pjalM,2322
|
|
4
|
+
devai/debugger.py,sha256=M1cRJD8c462EAxqbE7zMKf3ndM7PHFFpZDkwzmoFE7g,1909
|
|
5
|
+
devai/detector.py,sha256=dHoAdsLKKGg2MWFQaR34CPaqThcIcirxWEVlSg84_BI,18696
|
|
6
|
+
devai/generator.py,sha256=b5DQKaDBc--iKo4pUiecKlmjEqpd-bIWtsPihCn8xtw,8959
|
|
7
|
+
stackai_cli-0.2.0.dist-info/METADATA,sha256=QILe98aEsuBIfHSMLovrdbvCsHY76LicaPRg5JBGti0,3475
|
|
8
|
+
stackai_cli-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
stackai_cli-0.2.0.dist-info/entry_points.txt,sha256=NLmdAbALXQnZwvZM0Y8YwsqwH_pArHd6Da8f4pHcK8U,43
|
|
10
|
+
stackai_cli-0.2.0.dist-info/top_level.txt,sha256=Q-WXGK56TxXgAMLtaB1F77uzL4PBxPW22GkcKjJVdTc,6
|
|
11
|
+
stackai_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
devai/__init__.py,sha256=XEqb2aiIn8fzGE68Mph4ck1FtQqsR_am0wRWvrYPffQ,22
|
|
2
|
-
devai/ai.py,sha256=K5T8_7BqkP5gLta0UsjoI-S-k5UGdALqy2DpPvWDfUg,852
|
|
3
|
-
devai/cli.py,sha256=4jK_UZmlGHZmMH0tZOuTEn3qZnMkSurBzWqQVJ88zw8,2183
|
|
4
|
-
devai/debugger.py,sha256=M1cRJD8c462EAxqbE7zMKf3ndM7PHFFpZDkwzmoFE7g,1909
|
|
5
|
-
devai/detector.py,sha256=pPqwp3vsKDInoqrWNOLwTACymeq6ZdA1Tg4gy43reHI,4094
|
|
6
|
-
devai/generator.py,sha256=OCMGfp5qAl-rSvHsjsN65TNkHTMMLKXJX47K3a_CgK8,5627
|
|
7
|
-
stackai_cli-0.1.3.dist-info/METADATA,sha256=rEiTzqlmTEvPeQR8DyHt40wB_xo1Emkfg1bvuqb5boQ,3475
|
|
8
|
-
stackai_cli-0.1.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
-
stackai_cli-0.1.3.dist-info/entry_points.txt,sha256=NLmdAbALXQnZwvZM0Y8YwsqwH_pArHd6Da8f4pHcK8U,43
|
|
10
|
-
stackai_cli-0.1.3.dist-info/top_level.txt,sha256=Q-WXGK56TxXgAMLtaB1F77uzL4PBxPW22GkcKjJVdTc,6
|
|
11
|
-
stackai_cli-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|