python.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package python
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. )
  8. type Python struct {
  9. // Add fields as needed for Python analysis
  10. }
  11. // Name returns the name of the Python technology stack.
  12. func (p *Python) Name() string {
  13. return "Python"
  14. }
  15. // Analyze analyzes the codebase and returns a guessed technology stack for Python.
  16. func (p *Python) Analyze(codebasePath string) (bool, error) {
  17. // Verify if the codebasePath is valid
  18. if codebasePath == "" {
  19. return false, fmt.Errorf("codebase path is empty")
  20. }
  21. // Check if codebase exists
  22. if _, err := os.Stat(codebasePath); os.IsNotExist(err) {
  23. return false, fmt.Errorf("codebase path does not exist: %s", codebasePath)
  24. }
  25. // Placeholder: Check for a common Python file like requirements.txt or a .py file
  26. // This is a very basic check and should be expanded.
  27. foundPythonIndicator := false
  28. if _, err := os.Stat(filepath.Join(codebasePath, "requirements.txt")); err == nil {
  29. foundPythonIndicator = true
  30. }
  31. // You might want to walk the directory for .py files if requirements.txt isn't found
  32. if !foundPythonIndicator {
  33. walkErr := filepath.WalkDir(codebasePath, func(path string, d os.DirEntry, err error) error {
  34. if err != nil {
  35. return err
  36. }
  37. if !d.IsDir() && strings.HasSuffix(d.Name(), ".py") {
  38. foundPythonIndicator = true
  39. return filepath.SkipDir // Found a .py file, no need to search further in this dir for this check
  40. }
  41. return nil
  42. })
  43. if walkErr != nil {
  44. // Log or handle walk error, but it might not be fatal for analysis
  45. fmt.Printf("Error walking directory for python files: %v\n", walkErr)
  46. }
  47. }
  48. return foundPythonIndicator, nil
  49. }
  50. type PythonProjectAnalysis struct {
  51. PythonVersion string `json:"python_version"`
  52. Framework string `json:"framework"`
  53. UsePoetry bool `json:"use_poetry"`
  54. UsePipenv bool `json:"use_pipenv"`
  55. Entrypoint string `json:"entrypoint"`
  56. Port int `json:"port"`
  57. Packages []string `json:"packages"`
  58. SystemDeps []string `json:"system_deps"`
  59. StartCommand string `json:"start_command"`
  60. }
  61. func (p *Python) analyzePythonProject(codebasePath string) (*PythonProjectAnalysis, error) {
  62. analysis := &PythonProjectAnalysis{
  63. Port: 8000, // Default for Python
  64. }
  65. // Check for Poetry
  66. pyprojectPath := filepath.Join(codebasePath, "pyproject.toml")
  67. if p.fileExists(pyprojectPath) {
  68. analysis.UsePoetry = true
  69. // Could parse pyproject.toml for more details
  70. }
  71. // Check for Pipenv
  72. pipfilePath := filepath.Join(codebasePath, "Pipfile")
  73. if p.fileExists(pipfilePath) {
  74. analysis.UsePipenv = true
  75. }
  76. // Read requirements.txt if it exists
  77. reqPath := filepath.Join(codebasePath, "requirements.txt")
  78. if content, err := os.ReadFile(reqPath); err == nil {
  79. lines := strings.Split(string(content), "\n")
  80. for _, line := range lines {
  81. line = strings.TrimSpace(line)
  82. if line != "" && !strings.HasPrefix(line, "#") {
  83. // Extract package name (before == or >=)
  84. parts := strings.FieldsFunc(line, func(r rune) bool {
  85. return r == '=' || r == '>' || r == '<' || r == '!' || r == '~'
  86. })
  87. if len(parts) > 0 {
  88. analysis.Packages = append(analysis.Packages, parts[0])
  89. }
  90. }
  91. }
  92. }
  93. // Detect framework from packages
  94. for _, pkg := range analysis.Packages {
  95. switch {
  96. case strings.Contains(pkg, "fastapi"):
  97. analysis.Framework = "fastapi"
  98. case strings.Contains(pkg, "flask"):
  99. analysis.Framework = "flask"
  100. case strings.Contains(pkg, "django"):
  101. analysis.Framework = "django"
  102. case strings.Contains(pkg, "tornado"):
  103. analysis.Framework = "tornado"
  104. case strings.Contains(pkg, "sanic"):
  105. analysis.Framework = "sanic"
  106. }
  107. }
  108. // Detect entrypoint
  109. entrypoints := []string{"main.py", "app.py", "server.py", "run.py", "wsgi.py"}
  110. for _, ep := range entrypoints {
  111. if p.fileExists(filepath.Join(codebasePath, ep)) {
  112. analysis.Entrypoint = ep
  113. break
  114. }
  115. }
  116. if analysis.Entrypoint == "" {
  117. if analysis.Framework == "django" && p.fileExists(filepath.Join(codebasePath, "manage.py")) {
  118. analysis.Entrypoint = "manage.py"
  119. } else {
  120. analysis.Entrypoint = "app.py" // Default
  121. }
  122. }
  123. // Check for system dependencies
  124. for _, pkg := range analysis.Packages {
  125. switch {
  126. case strings.Contains(pkg, "psycopg2") || strings.Contains(pkg, "pg"):
  127. analysis.SystemDeps = append(analysis.SystemDeps, "libpq-dev")
  128. case strings.Contains(pkg, "mysql"):
  129. analysis.SystemDeps = append(analysis.SystemDeps, "default-libmysqlclient-dev")
  130. case strings.Contains(pkg, "pillow") || strings.Contains(pkg, "PIL"):
  131. analysis.SystemDeps = append(analysis.SystemDeps, "libjpeg-dev", "zlib1g-dev")
  132. case strings.Contains(pkg, "lxml"):
  133. analysis.SystemDeps = append(analysis.SystemDeps, "libxml2-dev", "libxslt1-dev")
  134. }
  135. }
  136. // Determine start command
  137. switch analysis.Framework {
  138. case "fastapi":
  139. analysis.StartCommand = fmt.Sprintf("uvicorn %s:app --host 0.0.0.0 --port %d", strings.TrimSuffix(analysis.Entrypoint, ".py"), analysis.Port)
  140. case "flask":
  141. analysis.StartCommand = fmt.Sprintf("flask run --host=0.0.0.0 --port=%d", analysis.Port)
  142. case "django":
  143. analysis.StartCommand = fmt.Sprintf("python manage.py runserver 0.0.0.0:%d", analysis.Port)
  144. default:
  145. analysis.StartCommand = fmt.Sprintf("python %s", analysis.Entrypoint)
  146. }
  147. return analysis, nil
  148. }
  149. func (p *Python) fileExists(path string) bool {
  150. _, err := os.Stat(path)
  151. return err == nil
  152. }
  153. // GenerateDockerfile generates a Dockerfile for Python projects based on analysis
  154. func (p *Python) GenerateDockerfile(codebasePath string) (string, error) {
  155. // Analyze the Python project
  156. analysis, err := p.analyzePythonProject(codebasePath)
  157. if err != nil {
  158. return "", fmt.Errorf("failed to analyze Python project: %w", err)
  159. }
  160. pythonVersion := analysis.PythonVersion
  161. if pythonVersion == "" {
  162. pythonVersion = "3.11"
  163. }
  164. // Determine dependency management approach
  165. var copyDeps, installDeps string
  166. if analysis.UsePoetry {
  167. copyDeps = `COPY pyproject.toml poetry.lock ./`
  168. installDeps = `RUN pip install poetry && \
  169. poetry config virtualenvs.create false && \
  170. poetry install --only=main`
  171. } else if analysis.UsePipenv {
  172. copyDeps = `COPY Pipfile Pipfile.lock ./`
  173. installDeps = `RUN pip install pipenv && \
  174. pipenv install --system --deploy`
  175. } else {
  176. copyDeps = `COPY requirements.txt ./`
  177. installDeps = `RUN pip install --no-cache-dir -r requirements.txt`
  178. }
  179. // System dependencies
  180. systemDepsInstall := ""
  181. if len(analysis.SystemDeps) > 0 {
  182. systemDepsInstall = fmt.Sprintf(`RUN apt-get update && \
  183. apt-get install -y %s && \
  184. rm -rf /var/lib/apt/lists/*`, strings.Join(analysis.SystemDeps, " "))
  185. }
  186. dockerfile := fmt.Sprintf(`# Auto-generated Dockerfile for Python application
  187. # Generated by BYOP Engine - Python Stack Analyzer
  188. FROM python:%s-slim
  189. # Set working directory
  190. WORKDIR /app
  191. # Install system dependencies
  192. %s
  193. # Upgrade pip
  194. RUN pip install --upgrade pip
  195. # Copy dependency files
  196. %s
  197. # Install Python dependencies
  198. %s
  199. # Copy source code
  200. COPY . .
  201. # Create non-root user
  202. RUN useradd --create-home --shell /bin/bash app && \
  203. chown -R app:app /app
  204. USER app
  205. # Expose port
  206. EXPOSE %d
  207. # Start the application
  208. CMD ["%s"]
  209. `,
  210. pythonVersion,
  211. systemDepsInstall,
  212. copyDeps,
  213. installDeps,
  214. analysis.Port,
  215. analysis.StartCommand,
  216. )
  217. return dockerfile, nil
  218. }