123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- package python
- import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- )
- type Python struct {
- // Add fields as needed for Python analysis
- }
- // Name returns the name of the Python technology stack.
- func (p *Python) Name() string {
- return "Python"
- }
- // Analyze analyzes the codebase and returns a guessed technology stack for Python.
- func (p *Python) Analyze(codebasePath string) (bool, error) {
- // Verify if the codebasePath is valid
- if codebasePath == "" {
- return false, fmt.Errorf("codebase path is empty")
- }
- // Check if codebase exists
- if _, err := os.Stat(codebasePath); os.IsNotExist(err) {
- return false, fmt.Errorf("codebase path does not exist: %s", codebasePath)
- }
- // Placeholder: Check for a common Python file like requirements.txt or a .py file
- // This is a very basic check and should be expanded.
- foundPythonIndicator := false
- if _, err := os.Stat(filepath.Join(codebasePath, "requirements.txt")); err == nil {
- foundPythonIndicator = true
- }
- // You might want to walk the directory for .py files if requirements.txt isn't found
- if !foundPythonIndicator {
- walkErr := filepath.WalkDir(codebasePath, func(path string, d os.DirEntry, err error) error {
- if err != nil {
- return err
- }
- if !d.IsDir() && strings.HasSuffix(d.Name(), ".py") {
- foundPythonIndicator = true
- return filepath.SkipDir // Found a .py file, no need to search further in this dir for this check
- }
- return nil
- })
- if walkErr != nil {
- // Log or handle walk error, but it might not be fatal for analysis
- fmt.Printf("Error walking directory for python files: %v\n", walkErr)
- }
- }
- return foundPythonIndicator, nil
- }
- type PythonProjectAnalysis struct {
- PythonVersion string `json:"python_version"`
- Framework string `json:"framework"`
- UsePoetry bool `json:"use_poetry"`
- UsePipenv bool `json:"use_pipenv"`
- Entrypoint string `json:"entrypoint"`
- Port int `json:"port"`
- Packages []string `json:"packages"`
- SystemDeps []string `json:"system_deps"`
- StartCommand string `json:"start_command"`
- }
- func (p *Python) analyzePythonProject(codebasePath string) (*PythonProjectAnalysis, error) {
- analysis := &PythonProjectAnalysis{
- Port: 8000, // Default for Python
- }
- // Check for Poetry
- pyprojectPath := filepath.Join(codebasePath, "pyproject.toml")
- if p.fileExists(pyprojectPath) {
- analysis.UsePoetry = true
- // Could parse pyproject.toml for more details
- }
- // Check for Pipenv
- pipfilePath := filepath.Join(codebasePath, "Pipfile")
- if p.fileExists(pipfilePath) {
- analysis.UsePipenv = true
- }
- // Read requirements.txt if it exists
- reqPath := filepath.Join(codebasePath, "requirements.txt")
- if content, err := os.ReadFile(reqPath); err == nil {
- lines := strings.Split(string(content), "\n")
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if line != "" && !strings.HasPrefix(line, "#") {
- // Extract package name (before == or >=)
- parts := strings.FieldsFunc(line, func(r rune) bool {
- return r == '=' || r == '>' || r == '<' || r == '!' || r == '~'
- })
- if len(parts) > 0 {
- analysis.Packages = append(analysis.Packages, parts[0])
- }
- }
- }
- }
- // Detect framework from packages
- for _, pkg := range analysis.Packages {
- switch {
- case strings.Contains(pkg, "fastapi"):
- analysis.Framework = "fastapi"
- case strings.Contains(pkg, "flask"):
- analysis.Framework = "flask"
- case strings.Contains(pkg, "django"):
- analysis.Framework = "django"
- case strings.Contains(pkg, "tornado"):
- analysis.Framework = "tornado"
- case strings.Contains(pkg, "sanic"):
- analysis.Framework = "sanic"
- }
- }
- // Detect entrypoint
- entrypoints := []string{"main.py", "app.py", "server.py", "run.py", "wsgi.py"}
- for _, ep := range entrypoints {
- if p.fileExists(filepath.Join(codebasePath, ep)) {
- analysis.Entrypoint = ep
- break
- }
- }
- if analysis.Entrypoint == "" {
- if analysis.Framework == "django" && p.fileExists(filepath.Join(codebasePath, "manage.py")) {
- analysis.Entrypoint = "manage.py"
- } else {
- analysis.Entrypoint = "app.py" // Default
- }
- }
- // Check for system dependencies
- for _, pkg := range analysis.Packages {
- switch {
- case strings.Contains(pkg, "psycopg2") || strings.Contains(pkg, "pg"):
- analysis.SystemDeps = append(analysis.SystemDeps, "libpq-dev")
- case strings.Contains(pkg, "mysql"):
- analysis.SystemDeps = append(analysis.SystemDeps, "default-libmysqlclient-dev")
- case strings.Contains(pkg, "pillow") || strings.Contains(pkg, "PIL"):
- analysis.SystemDeps = append(analysis.SystemDeps, "libjpeg-dev", "zlib1g-dev")
- case strings.Contains(pkg, "lxml"):
- analysis.SystemDeps = append(analysis.SystemDeps, "libxml2-dev", "libxslt1-dev")
- }
- }
- // Determine start command
- switch analysis.Framework {
- case "fastapi":
- analysis.StartCommand = fmt.Sprintf("uvicorn %s:app --host 0.0.0.0 --port %d", strings.TrimSuffix(analysis.Entrypoint, ".py"), analysis.Port)
- case "flask":
- analysis.StartCommand = fmt.Sprintf("flask run --host=0.0.0.0 --port=%d", analysis.Port)
- case "django":
- analysis.StartCommand = fmt.Sprintf("python manage.py runserver 0.0.0.0:%d", analysis.Port)
- default:
- analysis.StartCommand = fmt.Sprintf("python %s", analysis.Entrypoint)
- }
- return analysis, nil
- }
- func (p *Python) fileExists(path string) bool {
- _, err := os.Stat(path)
- return err == nil
- }
- // GenerateDockerfile generates a Dockerfile for Python projects based on analysis
- func (p *Python) GenerateDockerfile(codebasePath string) (string, error) {
- // Analyze the Python project
- analysis, err := p.analyzePythonProject(codebasePath)
- if err != nil {
- return "", fmt.Errorf("failed to analyze Python project: %w", err)
- }
- pythonVersion := analysis.PythonVersion
- if pythonVersion == "" {
- pythonVersion = "3.11"
- }
- // Determine dependency management approach
- var copyDeps, installDeps string
- if analysis.UsePoetry {
- copyDeps = `COPY pyproject.toml poetry.lock ./`
- installDeps = `RUN pip install poetry && \
- poetry config virtualenvs.create false && \
- poetry install --only=main`
- } else if analysis.UsePipenv {
- copyDeps = `COPY Pipfile Pipfile.lock ./`
- installDeps = `RUN pip install pipenv && \
- pipenv install --system --deploy`
- } else {
- copyDeps = `COPY requirements.txt ./`
- installDeps = `RUN pip install --no-cache-dir -r requirements.txt`
- }
- // System dependencies
- systemDepsInstall := ""
- if len(analysis.SystemDeps) > 0 {
- systemDepsInstall = fmt.Sprintf(`RUN apt-get update && \
- apt-get install -y %s && \
- rm -rf /var/lib/apt/lists/*`, strings.Join(analysis.SystemDeps, " "))
- }
- dockerfile := fmt.Sprintf(`# Auto-generated Dockerfile for Python application
- # Generated by BYOP Engine - Python Stack Analyzer
- FROM python:%s-slim
- # Set working directory
- WORKDIR /app
- # Install system dependencies
- %s
- # Upgrade pip
- RUN pip install --upgrade pip
- # Copy dependency files
- %s
- # Install Python dependencies
- %s
- # Copy source code
- COPY . .
- # Create non-root user
- RUN useradd --create-home --shell /bin/bash app && \
- chown -R app:app /app
- USER app
- # Expose port
- EXPOSE %d
- # Start the application
- CMD ["%s"]
- `,
- pythonVersion,
- systemDepsInstall,
- copyDeps,
- installDeps,
- analysis.Port,
- analysis.StartCommand,
- )
- return dockerfile, nil
- }
|