first pass
This commit is contained in:
186
cli/detectors/python.js
Normal file
186
cli/detectors/python.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Detect Python project type and configuration
|
||||
*/
|
||||
export function detectPython(projectPath) {
|
||||
const requirementsPath = join(projectPath, 'requirements.txt');
|
||||
const pyprojectPath = join(projectPath, 'pyproject.toml');
|
||||
const setupPath = join(projectPath, 'setup.py');
|
||||
|
||||
const hasRequirements = existsSync(requirementsPath);
|
||||
const hasPyproject = existsSync(pyprojectPath);
|
||||
const hasSetup = existsSync(setupPath);
|
||||
|
||||
if (!hasRequirements && !hasPyproject && !hasSetup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read requirements to detect project type
|
||||
let requirements = '';
|
||||
if (hasRequirements) {
|
||||
requirements = readFileSync(requirementsPath, 'utf-8').toLowerCase();
|
||||
}
|
||||
|
||||
// Read pyproject.toml for additional info
|
||||
let pyproject = '';
|
||||
if (hasPyproject) {
|
||||
pyproject = readFileSync(pyprojectPath, 'utf-8').toLowerCase();
|
||||
}
|
||||
|
||||
// Detect ML/PyTorch project
|
||||
const mlIndicators = [
|
||||
'torch',
|
||||
'pytorch',
|
||||
'tensorflow',
|
||||
'keras',
|
||||
'transformers',
|
||||
'opencv',
|
||||
'faster-whisper',
|
||||
'whisper',
|
||||
'scikit-learn',
|
||||
'numpy'
|
||||
];
|
||||
|
||||
const isML = mlIndicators.some(ind => requirements.includes(ind) || pyproject.includes(ind));
|
||||
|
||||
if (isML) {
|
||||
return {
|
||||
type: 'python-ml-pytorch',
|
||||
dockerizable: true,
|
||||
template: 'python/ml-pytorch',
|
||||
port: detectPythonPort(projectPath) || 8000,
|
||||
entryPoint: detectPythonEntryPoint(projectPath),
|
||||
buildCommand: null,
|
||||
description: 'Python ML/AI application',
|
||||
systemDeps: detectSystemDeps(requirements)
|
||||
};
|
||||
}
|
||||
|
||||
// Check for web frameworks
|
||||
const hasFlask = requirements.includes('flask') || pyproject.includes('flask');
|
||||
const hasFastAPI = requirements.includes('fastapi') || pyproject.includes('fastapi');
|
||||
const hasDjango = requirements.includes('django') || pyproject.includes('django');
|
||||
|
||||
let description = 'Python application';
|
||||
let port = 8000;
|
||||
|
||||
if (hasFlask) {
|
||||
description = 'Flask web application';
|
||||
port = 5000;
|
||||
} else if (hasFastAPI) {
|
||||
description = 'FastAPI web application';
|
||||
port = 8000;
|
||||
} else if (hasDjango) {
|
||||
description = 'Django web application';
|
||||
port = 8000;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'python-standard',
|
||||
dockerizable: true,
|
||||
template: 'python/standard',
|
||||
port: detectPythonPort(projectPath) || port,
|
||||
entryPoint: detectPythonEntryPoint(projectPath),
|
||||
buildCommand: null,
|
||||
description,
|
||||
framework: hasFlask ? 'flask' : hasFastAPI ? 'fastapi' : hasDjango ? 'django' : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Python entry point
|
||||
*/
|
||||
function detectPythonEntryPoint(projectPath) {
|
||||
const commonEntries = [
|
||||
'main.py',
|
||||
'app.py',
|
||||
'server.py',
|
||||
'run.py',
|
||||
'src/main.py',
|
||||
'src/app.py'
|
||||
];
|
||||
|
||||
for (const entry of commonEntries) {
|
||||
if (existsSync(join(projectPath, entry))) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return 'main.py';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect port from Python files
|
||||
*/
|
||||
function detectPythonPort(projectPath) {
|
||||
const mainFiles = ['main.py', 'app.py', 'server.py', 'run.py'];
|
||||
|
||||
for (const file of mainFiles) {
|
||||
const filePath = join(projectPath, file);
|
||||
if (existsSync(filePath)) {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Look for port definitions
|
||||
const portMatch = content.match(/port[=\s:]+(\d{4})/i) ||
|
||||
content.match(/PORT[=\s:]+(\d{4})/i);
|
||||
if (portMatch) {
|
||||
return parseInt(portMatch[1], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect system dependencies needed for ML projects
|
||||
*/
|
||||
function detectSystemDeps(requirements) {
|
||||
const deps = [];
|
||||
|
||||
if (requirements.includes('opencv') || requirements.includes('cv2')) {
|
||||
deps.push('libgl1-mesa-glx', 'libglib2.0-0');
|
||||
}
|
||||
|
||||
if (requirements.includes('whisper') || requirements.includes('faster-whisper')) {
|
||||
deps.push('ffmpeg');
|
||||
}
|
||||
|
||||
if (requirements.includes('soundfile') || requirements.includes('librosa')) {
|
||||
deps.push('libsndfile1');
|
||||
}
|
||||
|
||||
if (requirements.includes('pillow') || requirements.includes('pil')) {
|
||||
deps.push('libjpeg-dev', 'zlib1g-dev');
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional info about Python project
|
||||
*/
|
||||
export function getPythonInfo(projectPath) {
|
||||
const requirementsPath = join(projectPath, 'requirements.txt');
|
||||
const pyprojectPath = join(projectPath, 'pyproject.toml');
|
||||
|
||||
const info = {
|
||||
hasRequirements: existsSync(requirementsPath),
|
||||
hasPyproject: existsSync(pyprojectPath),
|
||||
hasSetupPy: existsSync(join(projectPath, 'setup.py')),
|
||||
hasVenv: existsSync(join(projectPath, 'venv')) || existsSync(join(projectPath, '.venv')),
|
||||
dependencies: []
|
||||
};
|
||||
|
||||
if (info.hasRequirements) {
|
||||
const content = readFileSync(requirementsPath, 'utf-8');
|
||||
info.dependencies = content
|
||||
.split('\n')
|
||||
.filter(line => line.trim() && !line.startsWith('#'))
|
||||
.map(line => line.split('==')[0].split('>=')[0].split('<=')[0].trim());
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
Reference in New Issue
Block a user