Compare commits
3 Commits
68c7195d54
...
59daebbd38
| Author | SHA1 | Date | |
|---|---|---|---|
| 59daebbd38 | |||
| 42b71dbf77 | |||
| b88a741f85 |
@@ -193,6 +193,19 @@ import Settings from "../components/Settings.astro";
|
||||
if (fname === 'schemas.py') {
|
||||
if (/:\s*date\b/.test(code) && !/from datetime import/.test(code))
|
||||
issues.push('ISSUE: schemas.py: käyttää date-tyyppiä mutta "from datetime import date" puuttuu');
|
||||
if (/:\s*datetime\b/.test(code) && !/from datetime import/.test(code))
|
||||
issues.push('ISSUE: schemas.py: käyttää datetime-tyyppiä mutta "from datetime import datetime" puuttuu');
|
||||
}
|
||||
|
||||
// 4b. Python-syntaksi: JS-booleanit (false/true ilman isoa alkukirjainta)
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (/^\s*#/.test(line) || /^\s*$/.test(line)) continue;
|
||||
// Etsi false/true joita ei ole merkkijonon sisällä ja jotka eivät ole osa isompaa sanaa
|
||||
if (/(?<!["\w])false(?![\w"])/.test(line))
|
||||
issues.push(`ISSUE: ${fname}:${i+1}: "false" ei ole Python — pitäisi olla "False"`);
|
||||
if (/(?<!["\w])true(?![\w"])/.test(line))
|
||||
issues.push(`ISSUE: ${fname}:${i+1}: "true" ei ole Python — pitäisi olla "True"`);
|
||||
}
|
||||
|
||||
// 5. test_main.py: ei saa uudelleenmääritellä appia tai modeleita
|
||||
@@ -1121,6 +1134,29 @@ OUTPUT FORMAT:
|
||||
|
||||
// === Template Pipeline — rakennuspalaset ===
|
||||
|
||||
// JS-arvo → Python-literaali: false→False, true→True, "teksti"→'"teksti"'
|
||||
function pyLiteral(val) {
|
||||
if (val === true) return 'True';
|
||||
if (val === false) return 'False';
|
||||
if (val === null || val === undefined) return 'None';
|
||||
if (typeof val === 'string') return `"${val}"`;
|
||||
return String(val);
|
||||
}
|
||||
|
||||
// JSON-merkkijono jossa booleanit on Python-muodossa: {"a": True, "b": False}
|
||||
function pyJsonLiteral(obj) {
|
||||
const parts = Object.entries(obj).map(([k, v]) => {
|
||||
let pyVal;
|
||||
if (v === true) pyVal = 'True';
|
||||
else if (v === false) pyVal = 'False';
|
||||
else if (v === null) pyVal = 'None';
|
||||
else if (typeof v === 'string') pyVal = `"${v}"`;
|
||||
else pyVal = String(v);
|
||||
return `"${k}":${pyVal}`;
|
||||
});
|
||||
return '{' + parts.join(',') + '}';
|
||||
}
|
||||
|
||||
const SPEC_SYSTEM = `You are a software architect who designs database schemas for Python web applications.
|
||||
|
||||
THINK STEP BY STEP before outputting JSON:
|
||||
@@ -1166,20 +1202,29 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
|
||||
}
|
||||
|
||||
function tmplModels(spec) {
|
||||
// Kerää tarvittavat SA-tyypit + ForeignKey jos relaatioita
|
||||
const saTypes = new Set(['Integer']);
|
||||
for (const e of spec.entities) for (const f of e.fields) saTypes.add(f.sa_type.match(/^(\w+)/)[1]);
|
||||
const relMap = {};
|
||||
for (const r of (spec.relationships || [])) {
|
||||
const target = spec.entities.find(e => e.name === r.to);
|
||||
if (target) relMap[`${r.from}.${r.field}`] = target.table_name;
|
||||
}
|
||||
const hasFk = Object.keys(relMap).length > 0;
|
||||
if (hasFk) saTypes.add('ForeignKey');
|
||||
const imports = [...saTypes].sort().join(', ');
|
||||
let code = `from sqlalchemy import create_engine, Column, ${imports}\n`;
|
||||
code += `from sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\n`;
|
||||
code += `from sqlalchemy.orm import declarative_base, sessionmaker\n\n`;
|
||||
code += `DATABASE_URL = "sqlite:///./app.db"\n`;
|
||||
code += `engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})\n`;
|
||||
code += `SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\n`;
|
||||
for (const e of spec.entities) {
|
||||
code += `class ${e.name}(Base):\n __tablename__ = "${e.table_name}"\n id = Column(Integer, primary_key=True, index=True)\n`;
|
||||
for (const f of e.fields) {
|
||||
let parts = [`Column(${f.sa_type}`];
|
||||
const fkTarget = relMap[`${e.name}.${f.name}`];
|
||||
let parts = fkTarget ? [`Column(${f.sa_type}, ForeignKey("${fkTarget}.id")` ] : [`Column(${f.sa_type}`];
|
||||
if (!f.nullable) parts.push('nullable=False');
|
||||
if (f.default !== null && f.default !== undefined) parts.push(typeof f.default === 'string' ? `default="${f.default}"` : `default=${f.default}`);
|
||||
if (f.default !== null && f.default !== undefined) parts.push(`default=${pyLiteral(f.default)}`);
|
||||
code += ` ${f.name} = ${parts.join(', ')})\n`;
|
||||
}
|
||||
code += '\n';
|
||||
@@ -1189,17 +1234,31 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
|
||||
}
|
||||
|
||||
function tmplSchemas(spec) {
|
||||
let code = 'from pydantic import BaseModel\n';
|
||||
for (const imp of (spec.extra_imports || [])) code += imp + '\n';
|
||||
// Tunnista tarvittavat datetime-importit kenttätyypeistä
|
||||
const dtTypes = new Set();
|
||||
for (const e of spec.entities) for (const f of e.fields) {
|
||||
if (/\bdate\b/i.test(f.py_type) && !/datetime/.test(f.py_type)) dtTypes.add('date');
|
||||
if (/\bdatetime\b/i.test(f.py_type)) dtTypes.add('datetime');
|
||||
}
|
||||
|
||||
let code = 'from pydantic import BaseModel, ConfigDict\n';
|
||||
if (dtTypes.size > 0) code += `from datetime import ${[...dtTypes].sort().join(', ')}\n`;
|
||||
|
||||
// extra_imports: suodata pois pelkät nimet kuten "datetime" (jo käsitelty yllä)
|
||||
for (const imp of (spec.extra_imports || [])) {
|
||||
if (/^(date|datetime)$/.test(imp.trim())) continue; // käsitelty jo
|
||||
if (/^from\s/.test(imp) || /^import\s/.test(imp)) code += imp + '\n';
|
||||
}
|
||||
code += '\n';
|
||||
|
||||
for (const e of spec.entities) {
|
||||
code += `class ${e.name}Create(BaseModel):\n`;
|
||||
for (const f of e.fields) {
|
||||
if (f.default !== null && f.default !== undefined) code += ` ${f.name}: ${f.py_type} = ${typeof f.default === 'string' ? '"'+f.default+'"' : f.default}\n`;
|
||||
if (f.default !== null && f.default !== undefined) code += ` ${f.name}: ${f.py_type} = ${pyLiteral(f.default)}\n`;
|
||||
else if (f.nullable && f.py_type.includes('None')) code += ` ${f.name}: ${f.py_type} = None\n`;
|
||||
else code += ` ${f.name}: ${f.py_type}\n`;
|
||||
}
|
||||
code += `\nclass ${e.name}Response(${e.name}Create):\n id: int\n\n class Config:\n from_attributes = True\n\n`;
|
||||
code += `\nclass ${e.name}Response(${e.name}Create):\n id: int\n model_config = ConfigDict(from_attributes=True)\n\n`;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
@@ -1253,11 +1312,11 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
|
||||
else if (f.py_type.includes('bool')) testData[f.name] = true;
|
||||
else if (f.py_type.includes('date')) testData[f.name] = '2024-01-15';
|
||||
}
|
||||
const td = JSON.stringify(testData);
|
||||
const td = pyJsonLiteral(testData);
|
||||
const firstStr = e.fields.find(f => f.py_type.includes('str') && f.name !== 'status');
|
||||
const updateData = {...testData};
|
||||
if (firstStr) updateData[firstStr.name] = `Updated ${firstStr.name}`;
|
||||
const ud = JSON.stringify(updateData);
|
||||
const ud = pyJsonLiteral(updateData);
|
||||
|
||||
code += `def test_create_${lo}():\n response = client.post('/${tb}/', json=${td})\n assert response.status_code == 201\n assert 'id' in response.json()\n\n`;
|
||||
code += `def test_list_${lo}s():\n client.post('/${tb}/', json=${td})\n response = client.get('/${tb}/')\n assert response.status_code == 200\n assert len(response.json()) >= 1\n\n`;
|
||||
@@ -1278,6 +1337,15 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
|
||||
return `FROM python:3.12-slim\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\nENV UV_CACHE_DIR=/tmp/uv-cache\nWORKDIR /app\nCOPY pyproject.toml .\nRUN uv sync\nCOPY *.py .\nRUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache\nUSER appuser\nEXPOSE 8000\nCMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]\n`;
|
||||
}
|
||||
|
||||
function tmplDockerCompose(spec) {
|
||||
const name = (spec.project_name || 'app').toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||
return `services:\n app:\n build: .\n container_name: ${name}\n ports:\n - "8000:8000"\n volumes:\n - app-data:/app/data\n restart: unless-stopped\n\nvolumes:\n app-data:\n`;
|
||||
}
|
||||
|
||||
function tmplDockerignore() {
|
||||
return `.venv\n__pycache__\n*.pyc\n*.db\n.pytest_cache\n.git\n`;
|
||||
}
|
||||
|
||||
function tmplGenerate(spec) {
|
||||
return {
|
||||
'models.py': tmplModels(spec),
|
||||
@@ -1286,6 +1354,8 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
|
||||
'test_main.py': tmplTests(spec),
|
||||
'pyproject.toml': tmplPyproject(spec),
|
||||
'Dockerfile': tmplDockerfile(),
|
||||
'docker-compose.yml': tmplDockerCompose(spec),
|
||||
'.dockerignore': tmplDockerignore(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1326,7 +1396,7 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
|
||||
// === Vaihe 3: Koodigenerointi templateista ===
|
||||
const files = tmplGenerate(spec);
|
||||
const fileOrder = Object.keys(files);
|
||||
const agentMap = { 'models.py': 'data', 'schemas.py': 'coder', 'main.py': 'coder', 'test_main.py': 'qa', 'pyproject.toml': 'coder', 'Dockerfile': 'tester' };
|
||||
const agentMap = { 'models.py': 'data', 'schemas.py': 'coder', 'main.py': 'coder', 'test_main.py': 'qa', 'pyproject.toml': 'coder', 'Dockerfile': 'tester', 'docker-compose.yml': 'tester', '.dockerignore': 'tester' };
|
||||
const agentNames = { data: 'Data Engineer', coder: 'Coder', qa: 'QA', tester: 'DevOps' };
|
||||
|
||||
for (let i = 0; i < fileOrder.length; i++) {
|
||||
|
||||
BIN
projektit/luodut/rest-api-kyttjhallinnalle (1).zip
Normal file
BIN
projektit/luodut/rest-api-kyttjhallinnalle (1).zip
Normal file
Binary file not shown.
BIN
projektit/luodut/rest-api-kyttjhallinnalle.zip
Normal file
BIN
projektit/luodut/rest-api-kyttjhallinnalle.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user