Skip to main content

pecto_python/
lib.rs

1pub mod context;
2pub mod dependencies;
3pub mod extractors;
4pub mod flow;
5pub mod parser;
6
7use context::{AnalysisContext, ParsedFile};
8use pecto_core::model::ProjectSpec;
9use rayon::prelude::*;
10use std::path::Path;
11
12/// Analyze a Python project directory and extract behavior specs.
13pub fn analyze_project(path: &Path) -> Result<ProjectSpec, PythonAnalysisError> {
14    let project_name = path
15        .file_name()
16        .map(|n| n.to_string_lossy().to_string())
17        .unwrap_or_else(|| "unknown".to_string());
18
19    let py_files: Vec<walkdir::DirEntry> = walkdir::WalkDir::new(path)
20        .into_iter()
21        .filter_map(|e| e.ok())
22        .filter(|e| {
23            let p = e.path().to_string_lossy();
24            e.path().extension().is_some_and(|ext| ext == "py")
25                && !p.contains("__pycache__")
26                && !p.contains("/venv/")
27                && !p.contains("/.venv/")
28                && !p.contains("/env/")
29                && !p.contains("/node_modules/")
30                && !p.contains("/migrations/")
31                && !p.contains("/test/")
32                && !p.contains("/tests/")
33        })
34        .collect();
35
36    let parsed_results: Vec<Result<ParsedFile, PythonAnalysisError>> = py_files
37        .par_iter()
38        .map(|entry| {
39            let source = std::fs::read_to_string(entry.path()).map_err(|e| {
40                PythonAnalysisError::IoError(entry.path().to_string_lossy().to_string(), e)
41            })?;
42
43            let relative_path = entry
44                .path()
45                .strip_prefix(path)
46                .unwrap_or(entry.path())
47                .to_string_lossy()
48                .to_string();
49
50            ParsedFile::parse(source, relative_path)
51        })
52        .collect();
53
54    let mut parsed_files = Vec::with_capacity(parsed_results.len());
55    for result in parsed_results {
56        parsed_files.push(result?);
57    }
58
59    let files_analyzed = parsed_files.len();
60    let ctx = AnalysisContext::new(parsed_files);
61
62    let mut spec = ProjectSpec::new(project_name);
63    spec.files_analyzed = files_analyzed;
64
65    for file in &ctx.files {
66        if let Some(capability) = extractors::controller::extract(file)
67            && !capability.is_empty()
68        {
69            spec.capabilities.push(capability);
70        }
71
72        if let Some(capability) = extractors::entity::extract(file)
73            && !capability.is_empty()
74        {
75            spec.capabilities.push(capability);
76        }
77
78        if let Some(capability) = extractors::service::extract(file)
79            && !capability.is_empty()
80        {
81            spec.capabilities.push(capability);
82        }
83
84        if let Some(capability) = extractors::scheduled::extract(file)
85            && !capability.is_empty()
86        {
87            spec.capabilities.push(capability);
88        }
89    }
90
91    dependencies::resolve_dependencies(&mut spec, &ctx);
92    flow::extract_flows(&mut spec, &ctx);
93
94    Ok(spec)
95}
96
97#[derive(Debug, thiserror::Error)]
98pub enum PythonAnalysisError {
99    #[error("Failed to read {0}: {1}")]
100    IoError(String, std::io::Error),
101    #[error("Parse error in {0}: {1}")]
102    ParseError(String, String),
103}