navred61 commited on
Commit
99a41ea
·
1 Parent(s): 29cc16e

copied from private space

Browse files
.gitignore ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ pip-wheel-metadata/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual Environment
27
+ venv/
28
+ env/
29
+ ENV/
30
+ env.bak/
31
+ venv.bak/
32
+
33
+ # IDE
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ *.swo
38
+ *~
39
+
40
+ # OS
41
+ .DS_Store
42
+ .DS_Store?
43
+ ._*
44
+ .Spotlight-V100
45
+ .Trashes
46
+ ehthumbs.db
47
+ Thumbs.db
48
+
49
+ # Gradio
50
+ .gradio/
51
+ gradio_cached_examples/
52
+
53
+ # Temporary files
54
+ testing_space/
55
+ temp_diagrams/
56
+ *.tmp
57
+ *.temp
58
+ temp_*/
59
+ output/
60
+
61
+ # Logs
62
+ *.log
63
+ logs/
64
+
65
+ # PlantUML output
66
+ *.puml
67
+ *.png
68
+ *.svg
69
+ diagrams/
70
+
71
+ # Environment variables
72
+ .env
73
+ .env.local
74
+ .env.*.local
75
+
76
+ # Node modules (for MCP tools)
77
+ node_modules/
78
+ package-lock.json
79
+
80
+ # Test files
81
+ test_*.py
82
+ *_test.py
83
+ tests/
84
+
85
+ # Documentation build
86
+ docs/_build/
87
+
88
+ # Coverage reports
89
+ htmlcov/
90
+ .coverage
91
+ .coverage.*
92
+ coverage.xml
93
+ *.cover
94
+ .hypothesis/
95
+ .pytest_cache/
96
+
97
+ # Jupyter Notebook
98
+ .ipynb_checkpoints
99
+
100
+ # pyenv
101
+ .python-version
102
+
103
+ # Celery
104
+ celerybeat-schedule
105
+ celerybeat.pid
106
+
107
+ # SageMath parsed files
108
+ *.sage.py
109
+
110
+ # Spyder project settings
111
+ .spyderproject
112
+ .spyproject
113
+
114
+ # Rope project settings
115
+ .ropeproject
116
+
117
+ # mkdocs documentation
118
+ /site
119
+
120
+ # mypy
121
+ .mypy_cache/
122
+ .dmypy.json
123
+ dmypy.json
124
+
125
+ repomix-output.xml
QUICKSTART.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Quick Start Guide
2
+
3
+ ## Getting Started in 3 Steps
4
+
5
+ ### Step 1: Test Your Setup
6
+ ```bash
7
+ python test_setup.py
8
+ ```
9
+ This will verify all dependencies are working correctly.
10
+
11
+ ### Step 2: Start the Application
12
+
13
+ **Option A: Use the batch file (Windows)**
14
+ ```bash
15
+ run.bat
16
+ ```
17
+
18
+ **Option B: Manual start**
19
+ ```bash
20
+ # Activate virtual environment
21
+ venv\Scripts\activate # Windows
22
+ # or
23
+ source venv/bin/activate # Linux/Mac
24
+
25
+ # Run the app
26
+ python app.py
27
+ ```
28
+
29
+ ### Step 3: Access the Application
30
+ - **Web Interface**: http://127.0.0.1:7860
31
+ - **MCP Endpoint**: http://127.0.0.1:7860/gradio_api/mcp/sse
32
+
33
+ ## Claude Desktop Integration
34
+
35
+ ### Prerequisites
36
+ 1. Install Node.js from https://nodejs.org
37
+ 2. Install mcp-remote:
38
+ ```bash
39
+ npm install -g mcp-remote
40
+ ```
41
+
42
+ ### Configuration
43
+ 1. **Find your Claude Desktop config file:**
44
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
45
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
46
+
47
+ 2. **Copy the contents of `claude_config.json` to your Claude config file**
48
+
49
+ 3. **Restart Claude Desktop**
50
+
51
+ ### Testing Claude Integration
52
+ In Claude Desktop, try these prompts:
53
+
54
+ ```
55
+ Generate a UML diagram for this Python code:
56
+
57
+ class Vehicle:
58
+ def __init__(self, brand):
59
+ self.brand = brand
60
+
61
+ def start(self):
62
+ pass
63
+
64
+ class Car(Vehicle):
65
+ def start(self):
66
+ return "Engine started"
67
+ ```
68
+
69
+ or
70
+
71
+ ```
72
+ Analyze the structure of this Python code:
73
+
74
+ class Calculator:
75
+ def add(self, a, b):
76
+ return a + b
77
+
78
+ def multiply(self, a, b):
79
+ return a * b
80
+ ```
81
+
82
+ ## Troubleshooting
83
+
84
+ ### Common Issues
85
+
86
+ 1. **Import errors when running test_setup.py:**
87
+ ```bash
88
+ pip install -r requirements.txt
89
+ ```
90
+
91
+ 2. **Claude Desktop doesn't recognize the MCP server:**
92
+ - Check Node.js installation: `node --version`
93
+ - Check mcp-remote installation: `npx mcp-remote --version`
94
+ - Verify the app is running: visit http://127.0.0.1:7860
95
+ - Restart Claude Desktop after config changes
96
+
97
+ 3. **No diagram generated:**
98
+ - Ensure your Python code contains class definitions
99
+ - Check for syntax errors in your code
100
+ - Verify internet connection (PlantUML service)
101
+
102
+ ### Debug Commands
103
+
104
+ ```bash
105
+ # Check if the app is running
106
+ curl http://127.0.0.1:7860/gradio_api/mcp/schema
107
+
108
+ # Test MCP endpoint
109
+ npx mcp-remote http://127.0.0.1:7860/gradio_api/mcp/sse --transport sse-only
110
+ ```
111
+
112
+ ## Next Steps
113
+
114
+ 1. Try the sample code in the web interface
115
+ 2. Experiment with your own Python classes
116
+ 3. Test the MCP integration with Claude Desktop
117
+ 4. Explore the code analysis features
118
+
119
+ Happy diagramming! 🎨
README.md CHANGED
@@ -1,14 +1,198 @@
1
  ---
2
- title: Python Code To Diagram Generator MCP
3
- emoji: 🚀
4
  colorFrom: blue
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 5.33.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: Transform your Python code into comprehensive visual diagram
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Python UML Diagram Generator & MCP Server
3
+ emoji: 🐍
4
  colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 5.33.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ models:
12
+ - PlantUML
13
+ tags:
14
+ - uml
15
+ - python
16
+ - diagrams
17
+ - mcp
18
+ - code-analysis
19
+ - visualization
20
+ - mcp-server-track
21
+ short_description: Python UML diagram generator with MCP server support
22
  ---
23
 
24
+ # 🐍 Python UML Diagram Generator & MCP Server
25
+
26
+ A powerful tool that generates UML class diagrams from Python code with integrated Model Context Protocol (MCP) server functionality for AI assistants.
27
+
28
+ ## 🚀 Features
29
+
30
+ - **🎨 Interactive Web Interface**: Generate UML class diagrams interactively from Python code
31
+ - **🤖 MCP Server Integration**: Works with Claude Desktop, Cursor, and other MCP clients
32
+ - **📊 Advanced Code Analysis**: Enhanced analysis with call graphs, complexity metrics, and function dependencies
33
+ - **🔗 Function Call Graphs**: Visualize function relationships and dependencies using pyan3 + graphviz
34
+ - **📈 Complexity Metrics**: Cyclomatic complexity, lines of code, parameter analysis
35
+ - **⚙️ Standalone Function Analysis**: Perfect for utility scripts with functions (not just classes)
36
+ - **🔄 Real-time Processing**: Fast diagram generation using PlantUML
37
+ - **💾 Multiple Formats**: PNG images, PlantUML source code, and call graph visualizations
38
+ - **🔍 Inheritance Visualization**: Clear parent-child class relationships
39
+
40
+ ## 🎯 Quick Start
41
+
42
+ 1. **Paste Python Code**: Enter your Python code in the text area
43
+ 2. **Generate Diagram**: Click "Generate Diagram" to create UML visualization
44
+ 3. **Analyze Structure**: Use "Analyze Code" for detailed code analysis
45
+ 4. **Download Results**: Save generated diagrams and analysis
46
+
47
+ ## 🤖 MCP Server Integration
48
+
49
+ This Space automatically serves as an MCP server! To integrate with AI assistants:
50
+
51
+ ### For Claude Desktop (with mcp-remote):
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "python-diagram-generator": {
56
+ "command": "npx",
57
+ "args": [
58
+ "mcp-remote",
59
+ "https://your-username-space-name.hf.space/gradio_api/mcp/sse",
60
+ "--transport",
61
+ "sse-only"
62
+ ]
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### For Cursor, Cline, and other SSE-compatible clients:
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "python-diagram-generator": {
73
+ "url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse"
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### For Private Spaces:
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "python-diagram-generator": {
84
+ "url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse",
85
+ "headers": {
86
+ "Authorization": "Bearer hf_your_token_here"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## 🛠️ Available MCP Tools
94
+
95
+ 1. **generate_diagram**: Creates UML class diagrams from Python code
96
+ 2. **analyze_code_structure**: Provides detailed code structure analysis with enhanced metrics
97
+
98
+ ## 📊 Advanced Analysis Features
99
+
100
+ ### **🔬 Advanced Analysis Tab**
101
+ - **Function Call Graphs**: Visual network diagrams showing function dependencies
102
+ - **Complexity Analysis**: Cyclomatic complexity scoring for each function
103
+ - **Function Metrics**: Lines of code, parameter counts, docstring detection
104
+ - **Call Relationship Mapping**: Which functions call which, and call frequency analysis
105
+ - **Isolated Function Detection**: Find functions that aren't connected to others
106
+ - **Code Quality Recommendations**: Suggestions for refactoring and improvements
107
+
108
+ ### **Enhanced for Standalone Functions**
109
+ Perfect for analyzing utility scripts, mathematical functions, data processing pipelines, and other function-heavy code that traditional class-based UML tools miss.
110
+
111
+ ## 💡 Usage Examples
112
+
113
+ ### Example 1: Basic Classes
114
+ ```python
115
+ class Animal:
116
+ def __init__(self, name: str):
117
+ self.name = name
118
+
119
+ def speak(self) -> str:
120
+ pass
121
+
122
+ class Dog(Animal):
123
+ def speak(self) -> str:
124
+ return f"{self.name} says Woof!"
125
+ ```
126
+
127
+ ### Example 2: Complex Inheritance
128
+ ```python
129
+ class Vehicle:
130
+ def __init__(self, brand: str, model: str):
131
+ self.brand = brand
132
+ self.model = model
133
+
134
+ class Car(Vehicle):
135
+ def __init__(self, brand: str, model: str, doors: int):
136
+ super().__init__(brand, model)
137
+ self.doors = doors
138
+
139
+ def start_engine(self):
140
+ return "Engine started"
141
+
142
+ class ElectricCar(Car):
143
+ def __init__(self, brand: str, model: str, doors: int, battery_capacity: float):
144
+ super().__init__(brand, model, doors)
145
+ self.battery_capacity = battery_capacity
146
+
147
+ def charge(self):
148
+ return "Charging battery"
149
+ ```
150
+
151
+ ## 🔧 Technical Details
152
+
153
+ - **Built with**: Gradio 5.33+ with MCP support
154
+ - **Analysis Engine**: Python AST + py2puml + pyan3
155
+ - **Call Graph Generation**: pyan3 + graphviz for function dependency visualization
156
+ - **Diagram Rendering**: PlantUML web service + graphviz DOT rendering
157
+ - **Output Formats**: PNG images, PlantUML source code, DOT call graphs
158
+ - **MCP Protocol**: Server-Sent Events (SSE) transport
159
+ - **Function Analysis**: Cyclomatic complexity, parameter analysis, docstring detection
160
+
161
+ ## 🎨 Features Showcase
162
+
163
+ - **Real-time Analysis**: Code structure updates as you type
164
+ - **Inheritance Detection**: Visualizes parent-child relationships
165
+ - **Method & Attribute Mapping**: Complete class member analysis
166
+ - **Function Call Graphs**: Interactive network diagrams of function dependencies
167
+ - **Complexity Metrics**: Cyclomatic complexity analysis with recommendations
168
+ - **Standalone Function Support**: Perfect for utility scripts and mathematical functions
169
+ - **Error Handling**: Syntax error detection and reporting
170
+ - **Sample Code**: Ready-to-use examples for both classes and functions
171
+
172
+ ## 📱 Compatible MCP Clients
173
+
174
+ - **Claude Desktop** (via mcp-remote)
175
+ - **Cursor IDE**
176
+ - **Cline VS Code Extension**
177
+ - **Continue VS Code Extension**
178
+ - **Custom MCP implementations**
179
+
180
+ ## 🌟 Perfect For
181
+
182
+ - **Developers**: Understanding code structure and function dependencies quickly
183
+ - **Code Reviews**: Visualizing class relationships and call patterns
184
+ - **Documentation**: Generating architectural diagrams and function flow charts
185
+ - **Education**: Teaching OOP concepts and functional programming patterns
186
+ - **Utility Script Analysis**: Understanding standalone function files and mathematical algorithms
187
+ - **Refactoring**: Identifying complex functions and isolated code segments
188
+ - **AI Assistants**: Enhanced code analysis capabilities with complexity metrics
189
+
190
+ ## 📞 Support & Feedback
191
+
192
+ - Test with the provided sample code first
193
+ - Check the MCP configuration tab for setup instructions
194
+ - View API documentation via the "View API" link in the footer
195
+
196
+ ---
197
+
198
+ **Transform your Python code into beautiful UML diagrams! 🎨✨**
app.py ADDED
@@ -0,0 +1,1012 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import sys
3
+ import os
4
+ import tempfile
5
+ import shutil
6
+ import ast
7
+ import time
8
+ import subprocess
9
+ import re
10
+ from typing import List, Dict, Optional, Tuple, Any
11
+ from py2puml.py2puml import py2puml
12
+ from plantuml import PlantUML
13
+ import pyan
14
+ from pathlib import Path
15
+
16
+ if os.name == "nt": # nt == Windows
17
+ graphviz_bin = r"C:\\Program Files\\Graphviz\\bin"
18
+ if graphviz_bin not in os.environ["PATH"]:
19
+ os.environ["PATH"] += os.pathsep + graphviz_bin
20
+
21
+
22
+ def setup_testing_space():
23
+ """Create persistent testing_space directory and __init__.py at startup."""
24
+ testing_dir = os.path.join(os.getcwd(), "testing_space")
25
+ os.makedirs(testing_dir, exist_ok=True)
26
+
27
+ init_file = os.path.join(testing_dir, "__init__.py")
28
+ if not os.path.exists(init_file):
29
+ with open(init_file, "w", encoding="utf-8") as f:
30
+ f.write("# Testing space for py2puml analysis\n")
31
+ print("📁 Created testing_space directory and __init__.py")
32
+ else:
33
+ print("🔄 testing_space directory already exists")
34
+
35
+
36
+ def cleanup_testing_space():
37
+ """Remove all .py files except __init__.py from testing_space."""
38
+ testing_dir = os.path.join(os.getcwd(), "testing_space")
39
+ if not os.path.exists(testing_dir):
40
+ print("⚠️ testing_space directory not found, creating it...")
41
+ setup_testing_space()
42
+ return
43
+
44
+ # Clean up any leftover .py files (keep only __init__.py)
45
+ files_removed = 0
46
+ for file in os.listdir(testing_dir):
47
+ if file.endswith(".py") and file != "__init__.py":
48
+ file_path = os.path.join(testing_dir, file)
49
+ try:
50
+ os.remove(file_path)
51
+ files_removed += 1
52
+ except Exception as e:
53
+ print(f"⚠️ Could not remove {file}: {e}")
54
+
55
+ if files_removed > 0:
56
+ print(f"🧹 Cleaned up {files_removed} leftover .py files from testing_space")
57
+
58
+
59
+ def verify_testing_space():
60
+ """Verify testing_space contains only __init__.py."""
61
+ testing_dir = os.path.join(os.getcwd(), "testing_space")
62
+ if not os.path.exists(testing_dir):
63
+ return False
64
+
65
+ files = os.listdir(testing_dir)
66
+ expected_files = ["__init__.py"]
67
+
68
+ return files == expected_files
69
+
70
+
71
+ def generate_call_graph_with_pyan3(
72
+ python_code: str, filename: str = "analysis"
73
+ ) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]:
74
+ """Generate call graph using pyan3 and return DOT content, PNG path, and structured data.
75
+
76
+ Args:
77
+ python_code: The Python code to analyze
78
+ filename: Base filename for temporary files
79
+
80
+ Returns:
81
+ Tuple of (dot_content, png_path, structured_data)
82
+ """
83
+ if not python_code.strip():
84
+ return None, None, {}
85
+
86
+ # Create unique filename using timestamp
87
+ timestamp = str(int(time.time() * 1000))
88
+ unique_filename = f"{filename}_{timestamp}"
89
+
90
+ # Paths
91
+ testing_dir = os.path.join(os.getcwd(), "testing_space")
92
+ code_file = os.path.join(testing_dir, f"{unique_filename}.py")
93
+
94
+ try:
95
+ # Write Python code to file
96
+ with open(code_file, "w", encoding="utf-8") as f:
97
+ f.write(python_code)
98
+
99
+ print(f"📊 Generating call graph for: {unique_filename}.py")
100
+
101
+ try:
102
+
103
+ dot_content = pyan.create_callgraph(
104
+ filenames=[str(code_file)],
105
+ format="dot",
106
+ colored=True,
107
+ grouped=True,
108
+ annotated=True,
109
+ )
110
+
111
+ png_path = None
112
+ with tempfile.TemporaryDirectory() as temp_dir:
113
+ dot_file = os.path.join(temp_dir, f"{unique_filename}.dot")
114
+ temp_png = os.path.join(temp_dir, f"{unique_filename}.png")
115
+
116
+ # Write DOT content to file
117
+ with open(dot_file, "w", encoding="utf-8") as f:
118
+ f.write(dot_content)
119
+
120
+ # Generate PNG using dot command
121
+ dot_cmd = ["dot", "-Tpng", dot_file, "-o", temp_png]
122
+
123
+ try:
124
+ subprocess.run(dot_cmd, check=True, timeout=30)
125
+
126
+ if os.path.exists(temp_png):
127
+ # Copy to permanent location
128
+ permanent_dir = os.path.join(os.getcwd(), "temp_diagrams")
129
+ os.makedirs(permanent_dir, exist_ok=True)
130
+ png_path = os.path.join(
131
+ permanent_dir, f"callgraph_{unique_filename}.png"
132
+ )
133
+ shutil.copy2(temp_png, png_path)
134
+ print(f"🎨 Call graph PNG saved: {os.path.basename(png_path)}")
135
+
136
+ except subprocess.SubprocessError as e:
137
+ print(f"⚠️ Graphviz PNG generation failed: {e}")
138
+ # Continue without PNG, DOT content is still useful
139
+
140
+ # Parse DOT content for structured data
141
+ structured_data = parse_call_graph_data(dot_content)
142
+
143
+ return dot_content, png_path, structured_data
144
+
145
+ except subprocess.TimeoutExpired:
146
+ print("⚠️ pyan3 analysis timed out, trying simplified approach...")
147
+ return try_fallback_analysis(python_code, unique_filename)
148
+ except subprocess.SubprocessError as e:
149
+ print(f"⚠️ pyan3 execution failed: {e}, trying fallback...")
150
+ return try_fallback_analysis(python_code, unique_filename)
151
+
152
+ except Exception as e:
153
+ print(f"❌ Call graph generation error: {e}")
154
+ return None, None, {"error": str(e)}
155
+
156
+ finally:
157
+ # Clean up temporary file
158
+ if os.path.exists(code_file):
159
+ try:
160
+ os.remove(code_file)
161
+ print(f"🧹 Cleaned up analysis file: {unique_filename}.py")
162
+ except Exception as e:
163
+ print(f"⚠️ Could not remove analysis file: {e}")
164
+
165
+
166
+ def parse_call_graph_data(dot_content: str) -> Dict[str, Any]:
167
+ """Parse pyan3 DOT output into structured function call data.
168
+
169
+ Args:
170
+ dot_content: DOT format string from pyan3
171
+
172
+ Returns:
173
+ Dictionary with parsed call graph information
174
+ """
175
+ if not dot_content:
176
+ return {}
177
+
178
+ try:
179
+ # Extract nodes (functions/classes)
180
+ node_pattern = r'"([^"]+)"\s*\['
181
+ nodes = re.findall(node_pattern, dot_content)
182
+
183
+ # Extract edges (function calls)
184
+ edge_pattern = r'"([^"]+)"\s*->\s*"([^"]+)"'
185
+ edges = re.findall(edge_pattern, dot_content)
186
+
187
+ # Build function call mapping
188
+ call_graph = {}
189
+ called_by = {}
190
+
191
+ for caller, callee in edges:
192
+ if caller not in call_graph:
193
+ call_graph[caller] = []
194
+ call_graph[caller].append(callee)
195
+
196
+ if callee not in called_by:
197
+ called_by[callee] = []
198
+ called_by[callee].append(caller)
199
+
200
+ # Calculate metrics
201
+ function_metrics = {}
202
+ for node in nodes:
203
+ out_degree = len(call_graph.get(node, []))
204
+ in_degree = len(called_by.get(node, []))
205
+
206
+ function_metrics[node] = {
207
+ "calls_made": out_degree,
208
+ "called_by_count": in_degree,
209
+ "calls_to": call_graph.get(node, []),
210
+ "called_by": called_by.get(node, []),
211
+ }
212
+
213
+ return {
214
+ "nodes": nodes,
215
+ "edges": edges,
216
+ "total_functions": len(nodes),
217
+ "total_calls": len(edges),
218
+ "call_graph": call_graph,
219
+ "function_metrics": function_metrics,
220
+ }
221
+
222
+ except Exception as e:
223
+ return {"parse_error": str(e)}
224
+
225
+
226
+ def try_fallback_analysis(
227
+ python_code: str, unique_filename: str
228
+ ) -> Tuple[Optional[str], Optional[str], Dict[str, Any]]:
229
+ """Fallback analysis when pyan3 fails - basic function call detection.
230
+
231
+ Args:
232
+ python_code: The Python code to analyze
233
+ unique_filename: Unique filename for this analysis
234
+
235
+ Returns:
236
+ Tuple of (None, None, fallback_analysis_data)
237
+ """
238
+ print("🔄 Using fallback analysis approach...")
239
+
240
+ try:
241
+ import ast
242
+ import re
243
+
244
+ tree = ast.parse(python_code)
245
+ functions = []
246
+ calls = []
247
+
248
+ # Extract function definitions
249
+ for node in ast.walk(tree):
250
+ if isinstance(node, ast.FunctionDef):
251
+ functions.append(node.name)
252
+
253
+ # Simple regex-based call detection (fallback approach)
254
+ for func in functions:
255
+ # Look for calls to this function
256
+ pattern = rf"\b{re.escape(func)}\s*\("
257
+ if re.search(pattern, python_code):
258
+ calls.append(("unknown", func))
259
+
260
+ return (
261
+ None,
262
+ None,
263
+ {
264
+ "fallback": True,
265
+ "functions_detected": functions,
266
+ "total_functions": len(functions),
267
+ "total_calls": len(calls),
268
+ "info": f"Fallback analysis: detected {len(functions)} functions",
269
+ "function_metrics": {
270
+ func: {
271
+ "calls_made": 0,
272
+ "called_by_count": 0,
273
+ "calls_to": [],
274
+ "called_by": [],
275
+ }
276
+ for func in functions
277
+ },
278
+ },
279
+ )
280
+
281
+ except Exception as e:
282
+ return None, None, {"error": f"Fallback analysis also failed: {str(e)}"}
283
+
284
+
285
+ def analyze_function_complexity(python_code: str) -> Dict[str, Any]:
286
+ """Analyze function complexity using AST.
287
+
288
+ Args:
289
+ python_code: The Python code to analyze
290
+
291
+ Returns:
292
+ Dictionary with function complexity metrics
293
+ """
294
+ if not python_code.strip():
295
+ return {}
296
+
297
+ try:
298
+ tree = ast.parse(python_code)
299
+ function_analysis = {}
300
+
301
+ for node in ast.walk(tree):
302
+ if isinstance(node, ast.FunctionDef):
303
+ # Calculate cyclomatic complexity (simplified)
304
+ complexity = 1 # Base complexity
305
+
306
+ for child in ast.walk(node):
307
+ if isinstance(
308
+ child,
309
+ (
310
+ ast.If,
311
+ ast.While,
312
+ ast.For,
313
+ ast.Try,
314
+ ast.ExceptHandler,
315
+ ast.With,
316
+ ast.Assert,
317
+ ),
318
+ ):
319
+ complexity += 1
320
+ elif isinstance(child, ast.BoolOp):
321
+ complexity += len(child.values) - 1
322
+
323
+ # Count lines of code
324
+ lines = (
325
+ node.end_lineno - node.lineno + 1
326
+ if hasattr(node, "end_lineno")
327
+ else 0
328
+ )
329
+
330
+ # Extract parameters
331
+ params = [arg.arg for arg in node.args.args]
332
+
333
+ # Check for docstring
334
+ has_docstring = (
335
+ len(node.body) > 0
336
+ and isinstance(node.body[0], ast.Expr)
337
+ and isinstance(node.body[0].value, ast.Constant)
338
+ and isinstance(node.body[0].value.value, str)
339
+ )
340
+
341
+ function_analysis[node.name] = {
342
+ "complexity": complexity,
343
+ "lines_of_code": lines,
344
+ "parameter_count": len(params),
345
+ "parameters": params,
346
+ "has_docstring": has_docstring,
347
+ "line_start": node.lineno,
348
+ "line_end": getattr(node, "end_lineno", node.lineno),
349
+ }
350
+
351
+ return function_analysis
352
+
353
+ except Exception as e:
354
+ return {"error": str(e)}
355
+
356
+
357
+ def generate_diagram(python_code: str, filename: str = "diagram") -> Optional[str]:
358
+ """Generate a UML class diagram from Python code.
359
+
360
+ Args:
361
+ python_code: The Python code to analyze and convert to UML
362
+ filename: Optional name for the generated diagram file
363
+
364
+ Returns:
365
+ Path to the generated PNG diagram image or None if failed
366
+ """
367
+ if not python_code.strip():
368
+ return None
369
+
370
+ print(f"🔄 Processing code for diagram generation...")
371
+
372
+ # Clean testing space (ensure only __init__.py exists)
373
+ cleanup_testing_space()
374
+
375
+ # Verify clean state
376
+ if not verify_testing_space():
377
+ print("⚠️ testing_space verification failed, recreating...")
378
+ setup_testing_space()
379
+ cleanup_testing_space()
380
+
381
+ # Create unique filename using timestamp
382
+ timestamp = str(int(time.time() * 1000)) # millisecond timestamp
383
+ unique_filename = f"{filename}_{timestamp}"
384
+
385
+ # Paths
386
+ testing_dir = os.path.join(os.getcwd(), "testing_space")
387
+ code_file = os.path.join(testing_dir, f"{unique_filename}.py")
388
+
389
+ # Use PlantUML web service for rendering
390
+ server = PlantUML(url="http://www.plantuml.com/plantuml/img/")
391
+
392
+ try:
393
+ # Write Python code to file in testing_space
394
+ with open(code_file, "w", encoding="utf-8") as f:
395
+ f.write(python_code)
396
+
397
+ print(f"📝 Created temporary file: testing_space/{unique_filename}.py")
398
+
399
+ # Generate PlantUML content using py2puml (no sys.path manipulation needed)
400
+ print(f"📝 Generating PlantUML content...")
401
+ puml_content_lines = py2puml(
402
+ os.path.join(
403
+ testing_dir, unique_filename
404
+ ), # path to the .py file (without extension)
405
+ f"testing_space.{unique_filename}", # module name
406
+ )
407
+ puml_content = "".join(puml_content_lines)
408
+
409
+ if not puml_content.strip():
410
+ print("⚠️ No UML content generated - check if your code contains classes")
411
+ return None
412
+
413
+ # Create temporary directory for PlantUML processing
414
+ with tempfile.TemporaryDirectory() as temp_dir:
415
+ # Save PUML file
416
+ puml_file = os.path.join(temp_dir, f"{unique_filename}.puml")
417
+ with open(puml_file, "w", encoding="utf-8") as f:
418
+ f.write(puml_content)
419
+
420
+ print(f"🎨 Rendering diagram...")
421
+ # Generate PNG
422
+ output_png = os.path.join(temp_dir, f"{unique_filename}.png")
423
+ server.processes_file(puml_file, outfile=output_png)
424
+
425
+ if os.path.exists(output_png):
426
+ print("✅ Diagram generated successfully!")
427
+ # Copy to a permanent location for Gradio to serve
428
+ permanent_dir = os.path.join(os.getcwd(), "temp_diagrams")
429
+ os.makedirs(permanent_dir, exist_ok=True)
430
+ permanent_path = os.path.join(
431
+ permanent_dir, f"{filename}_{hash(python_code) % 10000}.png"
432
+ )
433
+ shutil.copy2(output_png, permanent_path)
434
+ return permanent_path
435
+ else:
436
+ print("❌ Failed to generate PNG")
437
+ return None
438
+
439
+ except Exception as e:
440
+ print(f"❌ Error: {e}")
441
+ return None
442
+
443
+ finally:
444
+ # Always clean up the temporary .py file
445
+ if os.path.exists(code_file):
446
+ try:
447
+ os.remove(code_file)
448
+ print(f"🧹 Cleaned up temporary file: {unique_filename}.py")
449
+ except Exception as e:
450
+ print(f"⚠️ Could not remove temporary file: {e}")
451
+
452
+
453
+ def analyze_code_structure(python_code: str) -> str:
454
+ """Enhanced code analysis combining AST + pyan3 call graphs.
455
+
456
+ Args:
457
+ python_code: The Python code to analyze
458
+
459
+ Returns:
460
+ Comprehensive analysis report in markdown format
461
+ """
462
+ if not python_code.strip():
463
+ return "No code provided for analysis."
464
+
465
+ try:
466
+ # Basic AST analysis
467
+ tree = ast.parse(python_code)
468
+ classes = []
469
+ functions = []
470
+ imports = []
471
+
472
+ for node in ast.walk(tree):
473
+ if isinstance(node, ast.ClassDef):
474
+ methods = []
475
+ attributes = []
476
+
477
+ for item in node.body:
478
+ if isinstance(item, ast.FunctionDef):
479
+ methods.append(item.name)
480
+ elif isinstance(item, ast.Assign):
481
+ for target in item.targets:
482
+ if isinstance(target, ast.Name):
483
+ attributes.append(target.id)
484
+
485
+ # Check for inheritance
486
+ parents = [base.id for base in node.bases if isinstance(base, ast.Name)]
487
+
488
+ classes.append(
489
+ {
490
+ "name": node.name,
491
+ "methods": methods,
492
+ "attributes": attributes,
493
+ "parents": parents,
494
+ }
495
+ )
496
+
497
+ elif isinstance(node, ast.FunctionDef):
498
+ # Check if it's a top-level function (not inside a class)
499
+ is_method = any(
500
+ isinstance(parent, ast.ClassDef)
501
+ for parent in ast.walk(tree)
502
+ if hasattr(parent, "body") and node in getattr(parent, "body", [])
503
+ )
504
+ if not is_method:
505
+ functions.append(node.name)
506
+
507
+ elif isinstance(node, (ast.Import, ast.ImportFrom)):
508
+ if isinstance(node, ast.Import):
509
+ for alias in node.names:
510
+ imports.append(alias.name)
511
+ else:
512
+ module = node.module or ""
513
+ for alias in node.names:
514
+ imports.append(
515
+ f"{module}.{alias.name}" if module else alias.name
516
+ )
517
+
518
+ # Enhanced function complexity analysis
519
+ function_complexity = analyze_function_complexity(python_code)
520
+
521
+ # Call graph analysis (for files with functions)
522
+ call_graph_data = {}
523
+ if functions or any(classes): # Only run if there are functions to analyze
524
+ try:
525
+ cleanup_testing_space() # Ensure clean state
526
+ dot_content, png_path, call_graph_data = generate_call_graph_with_pyan3(
527
+ python_code
528
+ )
529
+ except Exception as e:
530
+ print(f"⚠️ Call graph analysis failed: {e}")
531
+ call_graph_data = {"error": str(e)}
532
+
533
+ # Build comprehensive summary
534
+ summary = "📊 **Enhanced Code Analysis Results**\n\n"
535
+
536
+ # === OVERVIEW SECTION ===
537
+ summary += "## 📋 **Overview**\n"
538
+ summary += f"• **{len(classes)}** classes found\n"
539
+ summary += f"• **{len(functions)}** standalone functions found\n"
540
+ summary += f"• **{len(set(imports))}** unique imports\n"
541
+
542
+ if call_graph_data and "total_functions" in call_graph_data:
543
+ summary += f"• **{call_graph_data['total_functions']}** total functions/methods in call graph\n"
544
+ summary += (
545
+ f"• **{call_graph_data['total_calls']}** function calls detected\n"
546
+ )
547
+
548
+ summary += "\n"
549
+
550
+ # === CLASSES SECTION ===
551
+ if classes:
552
+ summary += "## 🏗️ **Classes**\n"
553
+ for cls in classes:
554
+ summary += f"### **{cls['name']}**\n"
555
+ if cls["parents"]:
556
+ summary += f" - **Inherits from**: {', '.join(cls['parents'])}\n"
557
+ summary += f" - **Methods**: {len(cls['methods'])}"
558
+ if cls["methods"]:
559
+ summary += f" ({', '.join(cls['methods'])})"
560
+ summary += "\n"
561
+ if cls["attributes"]:
562
+ summary += f" - **Attributes**: {', '.join(cls['attributes'])}\n"
563
+ summary += "\n"
564
+
565
+ # === STANDALONE FUNCTIONS SECTION ===
566
+ if functions:
567
+ summary += "## ⚙️ **Standalone Functions**\n"
568
+ for func in functions:
569
+ summary += f"### **{func}()**\n"
570
+
571
+ # Add complexity metrics if available
572
+ if func in function_complexity:
573
+ metrics = function_complexity[func]
574
+ summary += (
575
+ f" - **Complexity**: {metrics['complexity']} (cyclomatic)\n"
576
+ )
577
+ summary += f" - **Lines of Code**: {metrics['lines_of_code']}\n"
578
+ summary += f" - **Parameters**: {metrics['parameter_count']}"
579
+ if metrics["parameters"]:
580
+ summary += f" ({', '.join(metrics['parameters'])})"
581
+ summary += "\n"
582
+ summary += f" - **Has Docstring**: {'✅' if metrics['has_docstring'] else '❌'}\n"
583
+ summary += f" - **Lines**: {metrics['line_start']}-{metrics['line_end']}\n"
584
+
585
+ # Add call graph info if available
586
+ if call_graph_data and "function_metrics" in call_graph_data:
587
+ if func in call_graph_data["function_metrics"]:
588
+ call_metrics = call_graph_data["function_metrics"][func]
589
+ summary += f" - **Calls Made**: {call_metrics['calls_made']}\n"
590
+ if call_metrics["calls_to"]:
591
+ summary += (
592
+ f" - Calls: {', '.join(call_metrics['calls_to'])}\n"
593
+ )
594
+ summary += f" - **Called By**: {call_metrics['called_by_count']} functions\n"
595
+ if call_metrics["called_by"]:
596
+ summary += f" - Called by: {', '.join(call_metrics['called_by'])}\n"
597
+
598
+ summary += "\n"
599
+
600
+ # === CALL GRAPH ANALYSIS ===
601
+ if (
602
+ call_graph_data
603
+ and "function_metrics" in call_graph_data
604
+ and call_graph_data["total_calls"] > 0
605
+ ):
606
+ summary += "## 🔗 **Function Call Analysis**\n"
607
+
608
+ # Most called functions
609
+ sorted_by_calls = sorted(
610
+ call_graph_data["function_metrics"].items(),
611
+ key=lambda x: x[1]["called_by_count"],
612
+ reverse=True,
613
+ )[:5]
614
+
615
+ if sorted_by_calls and sorted_by_calls[0][1]["called_by_count"] > 0:
616
+ summary += "**Most Called Functions:**\n"
617
+ for func_name, metrics in sorted_by_calls:
618
+ if metrics["called_by_count"] > 0:
619
+ summary += f"• **{func_name}**: called {metrics['called_by_count']} times\n"
620
+ summary += "\n"
621
+
622
+ # Most complex functions (by calls made)
623
+ sorted_by_complexity = sorted(
624
+ call_graph_data["function_metrics"].items(),
625
+ key=lambda x: x[1]["calls_made"],
626
+ reverse=True,
627
+ )[:5]
628
+
629
+ if sorted_by_complexity and sorted_by_complexity[0][1]["calls_made"] > 0:
630
+ summary += "**Functions Making Most Calls:**\n"
631
+ for func_name, metrics in sorted_by_complexity:
632
+ if metrics["calls_made"] > 0:
633
+ summary += (
634
+ f"• **{func_name}**: makes {metrics['calls_made']} calls\n"
635
+ )
636
+ summary += "\n"
637
+
638
+ # === COMPLEXITY ANALYSIS ===
639
+ if function_complexity:
640
+ summary += "## 📈 **Complexity Analysis**\n"
641
+
642
+ # Sort by complexity
643
+ sorted_complexity = sorted(
644
+ function_complexity.items(),
645
+ key=lambda x: x[1]["complexity"],
646
+ reverse=True,
647
+ )[:5]
648
+
649
+ summary += "**Most Complex Functions:**\n"
650
+ for func_name, metrics in sorted_complexity:
651
+ summary += f"• **{func_name}**: complexity {metrics['complexity']}, {metrics['lines_of_code']} lines\n"
652
+
653
+ # Overall stats
654
+ total_functions = len(function_complexity)
655
+ avg_complexity = (
656
+ sum(m["complexity"] for m in function_complexity.values())
657
+ / total_functions
658
+ )
659
+ avg_lines = (
660
+ sum(m["lines_of_code"] for m in function_complexity.values())
661
+ / total_functions
662
+ )
663
+ functions_with_docs = sum(
664
+ 1 for m in function_complexity.values() if m["has_docstring"]
665
+ )
666
+
667
+ summary += "\n**Overall Function Metrics:**\n"
668
+ summary += f"• **Average Complexity**: {avg_complexity:.1f}\n"
669
+ summary += f"• **Average Lines per Function**: {avg_lines:.1f}\n"
670
+ summary += f"• **Functions with Docstrings**: {functions_with_docs}/{total_functions} ({100*functions_with_docs/total_functions:.1f}%)\n"
671
+ summary += "\n"
672
+
673
+ # === IMPORTS SECTION ===
674
+ if imports:
675
+ summary += "## 📦 **Imports**\n"
676
+ unique_imports = list(set(imports))
677
+ for imp in unique_imports[:10]: # Show first 10 imports
678
+ summary += f"• {imp}\n"
679
+ if len(unique_imports) > 10:
680
+ summary += f"• ... and {len(unique_imports) - 10} more\n"
681
+ summary += "\n"
682
+
683
+ # === CALL GRAPH ERROR/INFO ===
684
+ if call_graph_data and "error" in call_graph_data:
685
+ summary += "## ⚠️ **Call Graph Analysis**\n"
686
+ summary += f"Call graph generation failed: {call_graph_data['error']}\n\n"
687
+ elif call_graph_data and "info" in call_graph_data:
688
+ summary += "## 📊 **Call Graph Analysis**\n"
689
+ summary += f"{call_graph_data['info']}\n\n"
690
+
691
+ # === RECOMMENDATIONS ===
692
+ summary += "## 💡 **Recommendations**\n"
693
+ if function_complexity:
694
+ high_complexity = [
695
+ f for f, m in function_complexity.items() if m["complexity"] > 10
696
+ ]
697
+ if high_complexity:
698
+ summary += f"• Consider refactoring high-complexity functions: {', '.join(high_complexity)}\n"
699
+
700
+ no_docs = [
701
+ f for f, m in function_complexity.items() if not m["has_docstring"]
702
+ ]
703
+ if no_docs:
704
+ summary += f"• Add docstrings to: {', '.join(no_docs[:5])}{'...' if len(no_docs) > 5 else ''}\n"
705
+
706
+ if call_graph_data and "function_metrics" in call_graph_data:
707
+ isolated_functions = [
708
+ f
709
+ for f, m in call_graph_data["function_metrics"].items()
710
+ if m["calls_made"] == 0 and m["called_by_count"] == 0
711
+ ]
712
+ if isolated_functions:
713
+ summary += f"• Review isolated functions: {', '.join(isolated_functions[:3])}{'...' if len(isolated_functions) > 3 else ''}\n"
714
+
715
+ return summary
716
+
717
+ except SyntaxError as e:
718
+ return f"❌ **Syntax Error in Python code:**\n```\n{str(e)}\n```"
719
+ except Exception as e:
720
+ return f"❌ **Error analyzing code:**\n```\n{str(e)}\n```"
721
+
722
+
723
+ def list_example_files() -> list:
724
+ """List all example .py files in the examples/ directory."""
725
+ examples_dir = os.path.join(os.getcwd(), "examples")
726
+ if not os.path.exists(examples_dir):
727
+ return []
728
+ return [f for f in os.listdir(examples_dir) if f.endswith(".py")]
729
+
730
+
731
+ def get_sample_code(filename: str) -> str:
732
+ """Return sample Python code from examples/ directory."""
733
+ examples_dir = os.path.join(os.getcwd(), "examples")
734
+ file_path = os.path.join(examples_dir, filename)
735
+ with open(file_path, "r", encoding="utf-8") as f:
736
+ return f.read()
737
+
738
+
739
+ def generate_all_diagrams(python_code: str, filename: str = "diagram") -> Tuple[Optional[str], Optional[str], str]:
740
+ """Generate all diagrams and analysis at once.
741
+
742
+ Args:
743
+ python_code: The Python code to analyze
744
+ filename: Base filename for diagrams
745
+
746
+ Returns:
747
+ Tuple of (uml_diagram_path, call_graph_path, analysis_text)
748
+ """
749
+ if not python_code.strip():
750
+ return None, None, "No code provided for analysis."
751
+
752
+ print("🚀 Starting comprehensive diagram generation...")
753
+
754
+ # Step 1: Generate UML Class Diagram
755
+ print("📊 Step 1/3: Generating UML class diagram...")
756
+ uml_diagram_path = generate_diagram(python_code, filename)
757
+
758
+ # Step 2: Generate Call Graph
759
+ print("🔗 Step 2/3: Generating call graph...")
760
+ try:
761
+ cleanup_testing_space()
762
+ dot_content, call_graph_path, structured_data = generate_call_graph_with_pyan3(python_code)
763
+ except Exception as e:
764
+ print(f"⚠️ Call graph generation failed: {e}")
765
+ call_graph_path = None
766
+
767
+ # Step 3: Generate Analysis
768
+ print("📈 Step 3/3: Performing code analysis...")
769
+ analysis_text = analyze_code_structure(python_code)
770
+
771
+ print("✅ All diagrams and analysis completed!")
772
+
773
+ return uml_diagram_path, call_graph_path, analysis_text
774
+
775
+
776
+ # Create Gradio interface
777
+ with gr.Blocks(
778
+ title="Python UML Diagram Generator & MCP Server",
779
+ theme=gr.themes.Soft(),
780
+ css="""
781
+ .gradio-container {
782
+ max-width: 1400px !important;
783
+ }
784
+ .code-input {
785
+ font-family: 'Courier New', monospace !important;
786
+ }
787
+ """,
788
+ ) as demo:
789
+ # Header
790
+ gr.Markdown(
791
+ """
792
+ # 🐍 Python UML Diagram Generator & MCP Server
793
+
794
+ **Dual Functionality:**
795
+ - 🖥️ **Web Interface**: Generate UML class diagrams and call graphs from Python code
796
+ - 🤖 **MCP Server**: Provides tools for AI assistants (Claude Desktop, Cursor, etc.)
797
+
798
+ Transform your Python code into comprehensive visual diagrams and analysis!
799
+ """
800
+ )
801
+
802
+ with gr.Tab("🎨 Diagram Generator"):
803
+ with gr.Row():
804
+ with gr.Column(scale=1):
805
+ gr.Markdown("### Input")
806
+
807
+ example_files = list_example_files()
808
+ example_dropdown = gr.Dropdown(
809
+ label="Choose Example",
810
+ choices=example_files,
811
+ value=example_files[0] if example_files else None,
812
+ )
813
+
814
+ code_input = gr.Textbox(
815
+ label="Python Code",
816
+ placeholder="Paste your Python code here...",
817
+ lines=20,
818
+ max_lines=35,
819
+ value=get_sample_code(example_files[0]) if example_files else "",
820
+ elem_classes=["code-input"],
821
+ )
822
+
823
+ with gr.Row():
824
+ filename_input = gr.Textbox(
825
+ label="Diagram Name",
826
+ value="my_diagram",
827
+ placeholder="Enter a name for your diagram",
828
+ scale=2,
829
+ )
830
+
831
+ with gr.Row():
832
+ generate_diagrams_btn = gr.Button(
833
+ "🔄 Generate Diagrams", variant="primary", size="lg"
834
+ )
835
+
836
+ with gr.Column(scale=1):
837
+ gr.Markdown("### Generated UML Class Diagram")
838
+
839
+ uml_diagram_output = gr.Image(
840
+ label="UML Class Diagram",
841
+ show_download_button=True,
842
+ height=300,
843
+ )
844
+
845
+ gr.Markdown("### Generated Call Graph Diagram")
846
+
847
+ call_graph_output = gr.Image(
848
+ label="Function Call Graph",
849
+ show_download_button=True,
850
+ height=300,
851
+ )
852
+
853
+ with gr.Row():
854
+ gr.Markdown("### Code Analysis")
855
+
856
+ with gr.Row():
857
+ analysis_output = gr.Textbox(
858
+ label="Comprehensive Code Analysis",
859
+ lines=15,
860
+ max_lines=25,
861
+ interactive=False,
862
+ show_copy_button=True,
863
+ )
864
+
865
+ with gr.Tab("ℹ️ About & Help"):
866
+ gr.Markdown(
867
+ """
868
+ ## About This Tool
869
+
870
+ This Python UML Diagram Generator helps you visualize the structure of your Python code by creating comprehensive diagrams and analysis.
871
+ ### Inspiration:
872
+ The idea for this mcp server was inspired by a tweet made by karpathy [tweet](https://x.com/karpathy/status/1930305209747812559).
873
+ He makes the point that generated images are easy to discriminate by humans while going through a 300 line LLM generated code is time consuming.
874
+ This tool aims to provide a visual quick smell test for generated code so that user can quickly identify issues instead of going through the code line by line.
875
+ This is only a very rough and basic implementation of the idea.
876
+ Making compound AI systems instead of text-to-text chatbots is the necessary direction.
877
+
878
+ ### ✨ Features:
879
+ - **UML Class Diagrams**: Automatically identifies classes, methods, attributes, and inheritance
880
+ - **Call Graph Diagrams**: Visualizes function dependencies and call relationships
881
+ - **Code Analysis**: Provides detailed structure analysis with complexity metrics
882
+ - **MCP Integration**: Works with AI assistants via Model Context Protocol
883
+
884
+ ### 📚 How to Use:
885
+ 1. **Paste Code**: Enter your Python code in the text area
886
+ 2. **Set Name**: Choose a name for your diagrams (optional)
887
+ 3. **Generate**: Click "Generate Diagrams" to create all visualizations and analysis
888
+ 4. **Download**: Save the generated diagram images
889
+ 5. **Review**: Read the comprehensive code analysis
890
+
891
+ ### 🔧 Technical Details:
892
+ - Built with **Gradio** for the web interface
893
+ - Uses **py2puml** for Python-to-PlantUML conversion
894
+ - **PlantUML** for UML diagram rendering
895
+ - **pyan3** and **Graphviz** for call graph generation
896
+ - **AST** (Abstract Syntax Tree) for code analysis
897
+
898
+ ### 💡 Tips:
899
+ - Include type hints for better diagram quality
900
+ - Use meaningful class and method names
901
+ - Keep inheritance hierarchies clear
902
+ - Add docstrings for better understanding
903
+ - Works great with both class-based and function-based code
904
+
905
+ ### 🐛 Troubleshooting:
906
+ - **No UML diagram generated**: Check if your code contains class definitions
907
+ - **No call graph generated**: Ensure your code has function definitions and calls
908
+ - **Syntax errors**: Ensure your Python code is valid
909
+ - **Import errors**: Stick to standard library imports for best results
910
+
911
+ ## Model Context Protocol (MCP) Server
912
+
913
+ This application automatically serves as an MCP server for AI assistants!
914
+
915
+ ### 🌐 For Hugging Face Spaces (Public):
916
+ ```json
917
+ {
918
+ "mcpServers": {
919
+ "python-diagram-generator": {
920
+ "url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse"
921
+ }
922
+ }
923
+ }
924
+ ```
925
+
926
+ ### 🏠 For Local Development:
927
+ ```json
928
+ {
929
+ "mcpServers": {
930
+ "python-diagram-generator": {
931
+ "command": "npx",
932
+ "args": [
933
+ "mcp-remote",
934
+ "http://127.0.0.1:7860/gradio_api/mcp/sse",
935
+ "--transport",
936
+ "sse-only"
937
+ ]
938
+ }
939
+ }
940
+ }
941
+ ```
942
+
943
+ ### 🔒 For Private Spaces:
944
+ ```json
945
+ {
946
+ "mcpServers": {
947
+ "python-diagram-generator": {
948
+ "url": "https://your-username-space-name.hf.space/gradio_api/mcp/sse",
949
+ "headers": {
950
+ "Authorization": "Bearer hf_your_token_here"
951
+ }
952
+ }
953
+ }
954
+ }
955
+ ```
956
+
957
+ ### 📋 Setup Instructions:
958
+ 1. Install Node.js and mcp-remote: `npm install -g mcp-remote`
959
+ 2. Add the configuration above to your MCP client
960
+ 3. Restart your MCP client (e.g., Claude Desktop)
961
+ 4. Test with prompts like: "Generate a UML diagram for this Python code: [your code]"
962
+
963
+ ---
964
+
965
+ **Local MCP Endpoint**: `http://127.0.0.1:7860/gradio_api/mcp/sse`
966
+ **MCP Schema**: View at `/gradio_api/mcp/schema`
967
+
968
+ ### 🚀 Future Features:
969
+ - Logic flowcharts
970
+ - Data flow diagrams
971
+ - State machine diagrams
972
+ - Multi-file analysis
973
+ - Enhanced UML features
974
+ """
975
+ )
976
+
977
+ # Event handlers
978
+ def load_example(example_filename):
979
+ return get_sample_code(example_filename)
980
+
981
+ example_dropdown.change(
982
+ fn=load_example,
983
+ inputs=example_dropdown,
984
+ outputs=code_input,
985
+ )
986
+
987
+ generate_diagrams_btn.click(
988
+ fn=generate_all_diagrams,
989
+ inputs=[code_input, filename_input],
990
+ outputs=[uml_diagram_output, call_graph_output, analysis_output],
991
+ show_progress=True,
992
+ )
993
+
994
+ code_input.change(
995
+ fn=analyze_code_structure,
996
+ inputs=code_input,
997
+ outputs=analysis_output,
998
+ show_progress=False,
999
+ )
1000
+
1001
+ # Launch configuration
1002
+ if __name__ == "__main__":
1003
+ # Setup persistent testing space at startup
1004
+ setup_testing_space()
1005
+
1006
+ demo.launch(
1007
+ mcp_server=True, # Enable MCP functionality
1008
+ show_api=True, # Show API documentation
1009
+ show_error=True, # Show errors in interface
1010
+ # share = False # Share the app publicly
1011
+ debug=True, # Enable debug mode for development
1012
+ )
examples/complex_class.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Complex class example with properties, decorators, and advanced features."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional, List
5
+
6
+
7
+ class Product:
8
+ """Product class with advanced Python features."""
9
+
10
+ # Class variable
11
+ total_products = 0
12
+
13
+ def __init__(self, name: str, price: float, category: str):
14
+ self._name = name
15
+ self._price = price
16
+ self._category = category
17
+ self._created_at = datetime.now()
18
+ self._discount = 0.0
19
+ Product.total_products += 1
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ """Product name property."""
24
+ return self._name
25
+
26
+ @name.setter
27
+ def name(self, value: str):
28
+ if not value.strip():
29
+ raise ValueError("Product name cannot be empty")
30
+ self._name = value
31
+
32
+ @property
33
+ def price(self) -> float:
34
+ """Product price with discount applied."""
35
+ return self._price * (1 - self._discount)
36
+
37
+ @property
38
+ def original_price(self) -> float:
39
+ """Original price before discount."""
40
+ return self._price
41
+
42
+ @original_price.setter
43
+ def original_price(self, value: float):
44
+ if value < 0:
45
+ raise ValueError("Price cannot be negative")
46
+ self._price = value
47
+
48
+ def apply_discount(self, percentage: float):
49
+ """Apply discount percentage."""
50
+ if 0 <= percentage <= 100:
51
+ self._discount = percentage / 100
52
+
53
+ @staticmethod
54
+ def validate_category(category: str) -> bool:
55
+ """Validate if category is allowed."""
56
+ allowed_categories = ["electronics", "clothing", "books", "food"]
57
+ return category.lower() in allowed_categories
58
+
59
+ @classmethod
60
+ def create_book(cls, title: str, price: float):
61
+ """Factory method to create a book product."""
62
+ return cls(title, price, "books")
63
+
64
+ @classmethod
65
+ def get_total_products(cls) -> int:
66
+ """Get total number of products created."""
67
+ return cls.total_products
68
+
69
+ def __str__(self) -> str:
70
+ return f"{self._name} - ${self.price:.2f}"
71
+
72
+ def __repr__(self) -> str:
73
+ return f"Product(name='{self._name}', price={self._price}, category='{self._category}')"
examples/fibonacci_functions.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fibonacci computation functions for testing function analysis.
3
+ """
4
+
5
+ def fibonacci_recursive(n):
6
+ """Calculate fibonacci number using recursion."""
7
+ if n <= 1:
8
+ return n
9
+ return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
10
+
11
+
12
+ def fibonacci_iterative(n):
13
+ """Calculate fibonacci number using iteration."""
14
+ if n <= 1:
15
+ return n
16
+
17
+ a, b = 0, 1
18
+ for _ in range(2, n + 1):
19
+ a, b = b, a + b
20
+ return b
21
+
22
+
23
+ def fibonacci_memoized(n, memo=None):
24
+ """Calculate fibonacci number using memoization."""
25
+ if memo is None:
26
+ memo = {}
27
+
28
+ if n in memo:
29
+ return memo[n]
30
+
31
+ if n <= 1:
32
+ memo[n] = n
33
+ return n
34
+
35
+ memo[n] = fibonacci_memoized(n - 1, memo) + fibonacci_memoized(n - 2, memo)
36
+ return memo[n]
37
+
38
+
39
+ def fibonacci_sequence(count):
40
+ """Generate a sequence of fibonacci numbers."""
41
+ sequence = []
42
+ for i in range(count):
43
+ sequence.append(fibonacci_iterative(i))
44
+ return sequence
45
+
46
+
47
+ def compare_fibonacci_methods(n):
48
+ """Compare different fibonacci calculation methods."""
49
+ import time
50
+
51
+ methods = [
52
+ ("Recursive", fibonacci_recursive),
53
+ ("Iterative", fibonacci_iterative),
54
+ ("Memoized", fibonacci_memoized)
55
+ ]
56
+
57
+ results = {}
58
+ for name, func in methods:
59
+ start_time = time.time()
60
+ result = func(n)
61
+ end_time = time.time()
62
+ results[name] = {
63
+ 'result': result,
64
+ 'time': end_time - start_time
65
+ }
66
+
67
+ return results
68
+
69
+
70
+ def validate_fibonacci_result(n, result):
71
+ """Validate if a fibonacci result is correct."""
72
+ if n <= 1:
73
+ return result == n
74
+
75
+ # Use iterative method as baseline for validation
76
+ expected = fibonacci_iterative(n)
77
+ return result == expected
78
+
79
+
80
+ if __name__ == "__main__":
81
+ n = 10
82
+ print(f"Fibonacci({n}) using different methods:")
83
+
84
+ results = compare_fibonacci_methods(n)
85
+ for method, data in results.items():
86
+ print(f"{method}: {data['result']} (took {data['time']:.6f} seconds)")
87
+
88
+ print(f"\nFirst 15 fibonacci numbers: {fibonacci_sequence(15)}")
examples/inheritance_example.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Class inheritance example for testing AST parsing."""
2
+
3
+ class Animal:
4
+ """Base animal class."""
5
+
6
+ def __init__(self, name, species):
7
+ self.name = name
8
+ self.species = species
9
+ self.is_alive = True
10
+
11
+ def speak(self):
12
+ return "Some generic animal sound"
13
+
14
+ def eat(self):
15
+ return f"{self.name} is eating"
16
+
17
+
18
+ class Dog(Animal):
19
+ """Dog class inheriting from Animal."""
20
+
21
+ def __init__(self, name, breed):
22
+ super().__init__(name, "Canine")
23
+ self.breed = breed
24
+ self.is_trained = False
25
+
26
+ def speak(self):
27
+ return "Woof!"
28
+
29
+ def fetch(self):
30
+ return f"{self.name} is fetching the ball"
31
+
32
+ def train(self):
33
+ self.is_trained = True
34
+
35
+
36
+ class Cat(Animal):
37
+ """Cat class inheriting from Animal."""
38
+
39
+ def __init__(self, name, color):
40
+ super().__init__(name, "Feline")
41
+ self.color = color
42
+ self.lives_left = 9
43
+
44
+ def speak(self):
45
+ return "Meow!"
46
+
47
+ def climb(self):
48
+ return f"{self.name} is climbing"
examples/main_with_helpers.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main function with multiple helper functions for testing function analysis.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ from datetime import datetime
8
+ from typing import Dict, List, Any
9
+
10
+
11
+ def read_config_file(config_path):
12
+ """Read configuration from JSON file."""
13
+ try:
14
+ with open(config_path, 'r') as file:
15
+ return json.load(file)
16
+ except FileNotFoundError:
17
+ print(f"Config file not found: {config_path}")
18
+ return {}
19
+ except json.JSONDecodeError:
20
+ print(f"Invalid JSON in config file: {config_path}")
21
+ return {}
22
+
23
+
24
+ def validate_config(config):
25
+ """Validate configuration parameters."""
26
+ required_keys = ['input_directory', 'output_directory', 'file_extensions']
27
+
28
+ for key in required_keys:
29
+ if key not in config:
30
+ print(f"Missing required config key: {key}")
31
+ return False
32
+
33
+ # Validate directories exist
34
+ if not os.path.exists(config['input_directory']):
35
+ print(f"Input directory does not exist: {config['input_directory']}")
36
+ return False
37
+
38
+ return True
39
+
40
+
41
+ def create_output_directory(output_path):
42
+ """Create output directory if it doesn't exist."""
43
+ try:
44
+ os.makedirs(output_path, exist_ok=True)
45
+ print(f"Output directory ready: {output_path}")
46
+ return True
47
+ except PermissionError:
48
+ print(f"Permission denied creating directory: {output_path}")
49
+ return False
50
+
51
+
52
+ def scan_files(directory, extensions):
53
+ """Scan directory for files with specified extensions."""
54
+ found_files = []
55
+
56
+ for root, dirs, files in os.walk(directory):
57
+ for file in files:
58
+ file_extension = os.path.splitext(file)[1].lower()
59
+ if file_extension in extensions:
60
+ full_path = os.path.join(root, file)
61
+ found_files.append(full_path)
62
+
63
+ return found_files
64
+
65
+
66
+ def analyze_file(file_path):
67
+ """Analyze a single file and return statistics."""
68
+ try:
69
+ with open(file_path, 'r', encoding='utf-8') as file:
70
+ content = file.read()
71
+
72
+ stats = {
73
+ 'file_path': file_path,
74
+ 'file_size': os.path.getsize(file_path),
75
+ 'line_count': len(content.splitlines()),
76
+ 'character_count': len(content),
77
+ 'word_count': len(content.split()),
78
+ 'last_modified': datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat()
79
+ }
80
+
81
+ return stats
82
+
83
+ except Exception as e:
84
+ print(f"Error analyzing file {file_path}: {e}")
85
+ return None
86
+
87
+
88
+ def process_files(file_list):
89
+ """Process multiple files and return aggregated results."""
90
+ results = []
91
+ total_size = 0
92
+ total_lines = 0
93
+
94
+ print(f"Processing {len(file_list)} files...")
95
+
96
+ for file_path in file_list:
97
+ print(f" Analyzing: {os.path.basename(file_path)}")
98
+
99
+ file_stats = analyze_file(file_path)
100
+ if file_stats:
101
+ results.append(file_stats)
102
+ total_size += file_stats['file_size']
103
+ total_lines += file_stats['line_count']
104
+
105
+ summary = {
106
+ 'total_files': len(results),
107
+ 'total_size_bytes': total_size,
108
+ 'total_lines': total_lines,
109
+ 'average_file_size': total_size / len(results) if results else 0,
110
+ 'average_lines_per_file': total_lines / len(results) if results else 0
111
+ }
112
+
113
+ return results, summary
114
+
115
+
116
+ def generate_report(results, summary, output_file):
117
+ """Generate a detailed report of the analysis."""
118
+ report = {
119
+ 'generated_at': datetime.now().isoformat(),
120
+ 'summary': summary,
121
+ 'file_details': results
122
+ }
123
+
124
+ try:
125
+ with open(output_file, 'w') as file:
126
+ json.dump(report, file, indent=2)
127
+ print(f"Report generated: {output_file}")
128
+ return True
129
+ except Exception as e:
130
+ print(f"Error generating report: {e}")
131
+ return False
132
+
133
+
134
+ def print_summary(summary):
135
+ """Print a human-readable summary."""
136
+ print("\n" + "="*50)
137
+ print("FILE ANALYSIS SUMMARY")
138
+ print("="*50)
139
+ print(f"Total files processed: {summary['total_files']}")
140
+ print(f"Total size: {summary['total_size_bytes']:,} bytes")
141
+ print(f"Total lines: {summary['total_lines']:,}")
142
+ print(f"Average file size: {summary['average_file_size']:.1f} bytes")
143
+ print(f"Average lines per file: {summary['average_lines_per_file']:.1f}")
144
+ print("="*50)
145
+
146
+
147
+ def cleanup_temp_files(temp_directory):
148
+ """Clean up temporary files."""
149
+ if not os.path.exists(temp_directory):
150
+ return
151
+
152
+ try:
153
+ temp_files = os.listdir(temp_directory)
154
+ for temp_file in temp_files:
155
+ if temp_file.startswith('temp_'):
156
+ file_path = os.path.join(temp_directory, temp_file)
157
+ os.remove(file_path)
158
+ print(f"Removed temp file: {temp_file}")
159
+ except Exception as e:
160
+ print(f"Error during cleanup: {e}")
161
+
162
+
163
+ def main():
164
+ """Main function that orchestrates the file analysis process."""
165
+ print("Starting File Analysis Tool")
166
+ print("-" * 30)
167
+
168
+ # Step 1: Read configuration
169
+ config_file = "config.json"
170
+ config = read_config_file(config_file)
171
+
172
+ if not config:
173
+ print("Using default configuration...")
174
+ config = {
175
+ 'input_directory': '.',
176
+ 'output_directory': './output',
177
+ 'file_extensions': ['.py', '.txt', '.md'],
178
+ 'generate_report': True
179
+ }
180
+
181
+ # Step 2: Validate configuration
182
+ if not validate_config(config):
183
+ print("Configuration validation failed. Exiting.")
184
+ return False
185
+
186
+ # Step 3: Create output directory
187
+ if not create_output_directory(config['output_directory']):
188
+ print("Failed to create output directory. Exiting.")
189
+ return False
190
+
191
+ # Step 4: Scan for files
192
+ print(f"Scanning for files in: {config['input_directory']}")
193
+ file_list = scan_files(config['input_directory'], config['file_extensions'])
194
+
195
+ if not file_list:
196
+ print("No files found matching criteria.")
197
+ return False
198
+
199
+ print(f"Found {len(file_list)} files to process")
200
+
201
+ # Step 5: Process files
202
+ results, summary = process_files(file_list)
203
+
204
+ # Step 6: Generate report
205
+ if config.get('generate_report', True):
206
+ report_file = os.path.join(config['output_directory'], 'analysis_report.json')
207
+ generate_report(results, summary, report_file)
208
+
209
+ # Step 7: Display summary
210
+ print_summary(summary)
211
+
212
+ # Step 8: Cleanup
213
+ temp_dir = config.get('temp_directory')
214
+ if temp_dir:
215
+ cleanup_temp_files(temp_dir)
216
+
217
+ print("\nFile analysis completed successfully!")
218
+ return True
219
+
220
+
221
+ if __name__ == "__main__":
222
+ success = main()
223
+ exit(0 if success else 1)
examples/multiple_classes.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Multiple classes example for testing AST parsing."""
2
+
3
+ class BankAccount:
4
+ """Simple bank account class."""
5
+
6
+ def __init__(self, account_number, initial_balance=0):
7
+ self.account_number = account_number
8
+ self.balance = initial_balance
9
+ self.transaction_history = []
10
+
11
+ def deposit(self, amount):
12
+ self.balance += amount
13
+ self.transaction_history.append(f"Deposit: +{amount}")
14
+
15
+ def withdraw(self, amount):
16
+ if amount <= self.balance:
17
+ self.balance -= amount
18
+ self.transaction_history.append(f"Withdrawal: -{amount}")
19
+ return True
20
+ return False
21
+
22
+ def get_balance(self):
23
+ return self.balance
24
+
25
+
26
+ class Customer:
27
+ """Customer class to hold account information."""
28
+
29
+ def __init__(self, customer_id, name, email):
30
+ self.customer_id = customer_id
31
+ self.name = name
32
+ self.email = email
33
+ self.accounts = []
34
+
35
+ def add_account(self, account):
36
+ self.accounts.append(account)
37
+
38
+ def get_total_balance(self):
39
+ return sum(account.get_balance() for account in self.accounts)
40
+
41
+
42
+ class Bank:
43
+ """Bank class to manage customers and accounts."""
44
+
45
+ def __init__(self, name):
46
+ self.name = name
47
+ self.customers = {}
48
+ self.next_account_number = 1000
49
+
50
+ def create_customer(self, name, email):
51
+ customer_id = len(self.customers) + 1
52
+ customer = Customer(customer_id, name, email)
53
+ self.customers[customer_id] = customer
54
+ return customer
55
+
56
+ def create_account(self, customer_id, initial_balance=0):
57
+ if customer_id in self.customers:
58
+ account = BankAccount(self.next_account_number, initial_balance)
59
+ self.customers[customer_id].add_account(account)
60
+ self.next_account_number += 1
61
+ return account
62
+ return None
examples/sample_classes.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Animal:
2
+ def __init__(self, name: str, age: int):
3
+ self.name = name
4
+ self.age = age
5
+
6
+ def speak(self) -> str:
7
+ pass
8
+
9
+ def get_info(self) -> str:
10
+ return f"{self.name} is {self.age} years old"
11
+
12
+
13
+ class Dog(Animal):
14
+ def __init__(self, name: str, age: int, breed: str):
15
+ super().__init__(name, age)
16
+ self.breed = breed
17
+
18
+ def speak(self) -> str:
19
+ return f"{self.name} says Woof!"
20
+
21
+ def fetch(self) -> str:
22
+ return f"{self.name} is fetching the ball"
23
+
24
+
25
+ class Cat(Animal):
26
+ def __init__(self, name: str, age: int, indoor: bool = True):
27
+ super().__init__(name, age)
28
+ self.indoor = indoor
29
+
30
+ def speak(self) -> str:
31
+ return f"{self.name} says Meow!"
32
+
33
+ def climb(self) -> str:
34
+ return f"{self.name} is climbing"
35
+
36
+
37
+ class PetOwner:
38
+ def __init__(self, name: str):
39
+ self.name = name
40
+ self.pets = []
41
+
42
+ def add_pet(self, pet: Animal):
43
+ self.pets.append(pet)
44
+
45
+ def call_all_pets(self) -> list:
46
+ # return [pet.speak() for pet in self.pets]
47
+ result = []
48
+
49
+ for pet in self.pets:
50
+ result.append(pet.speak())
51
+ return result
52
+
53
+
54
+ def create_pet_family():
55
+ owner = PetOwner("Alice")
56
+ dog = Dog("Buddy", 3, "Golden Retriever")
57
+ cat = Cat("Whiskers", 2, True)
58
+
59
+ owner.add_pet(dog)
60
+ owner.add_pet(cat)
61
+
62
+ return owner
examples/sample_functions.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import random
3
+ from typing import List, Tuple
4
+
5
+ def calculate_factorial(n: int) -> int:
6
+ """Calculate factorial of a number recursively."""
7
+ if n <= 1:
8
+ return 1
9
+ return n * calculate_factorial(n - 1)
10
+
11
+ def fibonacci_sequence(n: int) -> List[int]:
12
+ """Generate fibonacci sequence up to n terms."""
13
+ if n <= 0:
14
+ return []
15
+ elif n == 1:
16
+ return [0]
17
+ elif n == 2:
18
+ return [0, 1]
19
+
20
+ sequence = [0, 1]
21
+ for i in range(2, n):
22
+ sequence.append(sequence[i-1] + sequence[i-2])
23
+ return sequence
24
+
25
+ def is_prime(num: int) -> bool:
26
+ """Check if a number is prime."""
27
+ if num < 2:
28
+ return False
29
+ if num == 2:
30
+ return True
31
+ if num % 2 == 0:
32
+ return False
33
+
34
+ for i in range(3, int(math.sqrt(num)) + 1, 2):
35
+ if num % i == 0:
36
+ return False
37
+ return True
38
+
39
+ def find_primes_in_range(start: int, end: int) -> List[int]:
40
+ """Find all prime numbers in a given range."""
41
+ primes = []
42
+ for num in range(start, end + 1):
43
+ if is_prime(num):
44
+ primes.append(num)
45
+ return primes
46
+
47
+ def calculate_statistics(numbers: List[float]) -> dict:
48
+ """Calculate basic statistics for a list of numbers."""
49
+ if not numbers:
50
+ return {"error": "Empty list provided"}
51
+
52
+ n = len(numbers)
53
+ mean = sum(numbers) / n
54
+ sorted_nums = sorted(numbers)
55
+
56
+ # Calculate median
57
+ if n % 2 == 0:
58
+ median = (sorted_nums[n//2 - 1] + sorted_nums[n//2]) / 2
59
+ else:
60
+ median = sorted_nums[n//2]
61
+
62
+ # Calculate variance and standard deviation
63
+ variance = sum((x - mean) ** 2 for x in numbers) / n
64
+ std_dev = math.sqrt(variance)
65
+
66
+ return {
67
+ "count": n,
68
+ "mean": mean,
69
+ "median": median,
70
+ "min": min(numbers),
71
+ "max": max(numbers),
72
+ "variance": variance,
73
+ "std_deviation": std_dev
74
+ }
75
+
76
+ def monte_carlo_pi(iterations: int) -> float:
77
+ """Estimate Pi using Monte Carlo method."""
78
+ inside_circle = 0
79
+
80
+ for _ in range(iterations):
81
+ x, y = random.random(), random.random()
82
+ if x*x + y*y <= 1:
83
+ inside_circle += 1
84
+
85
+ return 4 * inside_circle / iterations
86
+
87
+ def process_data_pipeline(data: List[int]) -> dict:
88
+ """Complex data processing pipeline demonstrating function calls."""
89
+ # Step 1: Filter for positive numbers
90
+ positive_data = []
91
+ for x in data:
92
+ if x > 0:
93
+ positive_data.append(x)
94
+
95
+ # Step 2: Find primes in the data
96
+ primes_in_data = []
97
+ for x in positive_data:
98
+ if is_prime(x):
99
+ primes_in_data.append(x)
100
+
101
+ # Step 3: Calculate statistics
102
+ positive_data_floats = []
103
+ for x in positive_data:
104
+ positive_data_floats.append(float(x))
105
+ stats = calculate_statistics(positive_data_floats)
106
+
107
+ # Step 4: Generate fibonacci sequence up to max value
108
+ max_val = max(positive_data) if positive_data else 0
109
+ fib_count = min(max_val, 20) # Limit to reasonable size
110
+ fib_sequence = fibonacci_sequence(fib_count)
111
+
112
+ # Step 5: Calculate factorials for small numbers
113
+ small_numbers = []
114
+ for x in positive_data:
115
+ if x <= 10:
116
+ small_numbers.append(x)
117
+ factorials = {}
118
+ for x in small_numbers:
119
+ factorials[x] = calculate_factorial(x)
120
+
121
+ return {
122
+ "original_count": len(data),
123
+ "positive_count": len(positive_data),
124
+ "primes_found": primes_in_data,
125
+ "statistics": stats,
126
+ "fibonacci_sequence": fib_sequence,
127
+ "factorials": factorials,
128
+ "pi_estimate": monte_carlo_pi(1000)
129
+ }
130
+
131
+ def main():
132
+ """Main function demonstrating the pipeline."""
133
+ sample_data = [1, 2, 3, 5, 8, 13, 21, -1, 0, 17, 19, 23]
134
+ result = process_data_pipeline(sample_data)
135
+ print(f"Processing complete: {len(result)} metrics calculated")
136
+ return result
137
+
138
+ if __name__ == "__main__":
139
+ main()
examples/simple_class.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Simple class example for testing AST parsing."""
2
+
3
+ class Person:
4
+ """A simple Person class."""
5
+
6
+ def __init__(self, name, age):
7
+ self.name = name
8
+ self.age = age
9
+ self.email = None
10
+
11
+ def get_name(self):
12
+ return self.name
13
+
14
+ def set_email(self, email):
15
+ self.email = email
16
+
17
+ def greet(self):
18
+ return f"Hello, I'm {self.name}"
19
+
20
+ def is_adult(self):
21
+ return self.age >= 18
examples/string_processing.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ String processing pipeline functions for testing function analysis.
3
+ """
4
+
5
+ import re
6
+ from typing import List
7
+
8
+
9
+ def normalize_whitespace(text):
10
+ """Normalize whitespace by removing extra spaces and newlines."""
11
+ # Replace multiple whitespace with single space
12
+ text = re.sub(r'\s+', ' ', text)
13
+ # Strip leading and trailing whitespace
14
+ return text.strip()
15
+
16
+
17
+ def remove_special_characters(text, keep_chars=""):
18
+ """Remove special characters, optionally keeping specified characters."""
19
+ # Keep alphanumeric, spaces, and specified characters
20
+ pattern = fr"[^a-zA-Z0-9\s{re.escape(keep_chars)}]"
21
+ return re.sub(pattern, '', text)
22
+
23
+
24
+ def convert_to_lowercase(text):
25
+ """Convert text to lowercase."""
26
+ return text.lower()
27
+
28
+
29
+ def remove_stopwords(text, stopwords=None):
30
+ """Remove common stopwords from text."""
31
+ if stopwords is None:
32
+ stopwords = {
33
+ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to',
34
+ 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be',
35
+ 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did',
36
+ 'will', 'would', 'could', 'should', 'may', 'might', 'must'
37
+ }
38
+
39
+ words = text.split()
40
+ filtered_words = [word for word in words if word.lower() not in stopwords]
41
+ return ' '.join(filtered_words)
42
+
43
+
44
+ def extract_keywords(text, min_length=3):
45
+ """Extract keywords (words longer than min_length)."""
46
+ words = text.split()
47
+ keywords = [word for word in words if len(word) >= min_length]
48
+ return keywords
49
+
50
+
51
+ def count_word_frequency(text):
52
+ """Count frequency of each word in text."""
53
+ words = text.split()
54
+ frequency = {}
55
+ for word in words:
56
+ frequency[word] = frequency.get(word, 0) + 1
57
+ return frequency
58
+
59
+
60
+ def capitalize_words(text, exceptions=None):
61
+ """Capitalize first letter of each word, with exceptions."""
62
+ if exceptions is None:
63
+ exceptions = {'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'}
64
+
65
+ words = text.split()
66
+ capitalized = []
67
+
68
+ for i, word in enumerate(words):
69
+ if i == 0 or word.lower() not in exceptions:
70
+ capitalized.append(word.capitalize())
71
+ else:
72
+ capitalized.append(word.lower())
73
+
74
+ return ' '.join(capitalized)
75
+
76
+
77
+ def truncate_text(text, max_length=100, suffix="..."):
78
+ """Truncate text to specified length with suffix."""
79
+ if len(text) <= max_length:
80
+ return text
81
+
82
+ truncated = text[:max_length - len(suffix)]
83
+ # Try to break at last complete word
84
+ last_space = truncated.rfind(' ')
85
+ if last_space > max_length * 0.8: # If we can break at a word boundary
86
+ truncated = truncated[:last_space]
87
+
88
+ return truncated + suffix
89
+
90
+
91
+ def text_processing_pipeline(text, operations=None):
92
+ """Process text through a pipeline of operations."""
93
+ if operations is None:
94
+ operations = [
95
+ 'normalize_whitespace',
96
+ 'remove_special_characters',
97
+ 'convert_to_lowercase',
98
+ 'remove_stopwords'
99
+ ]
100
+
101
+ # Map operation names to functions
102
+ operation_map = {
103
+ 'normalize_whitespace': normalize_whitespace,
104
+ 'remove_special_characters': remove_special_characters,
105
+ 'convert_to_lowercase': convert_to_lowercase,
106
+ 'remove_stopwords': remove_stopwords,
107
+ 'capitalize_words': capitalize_words,
108
+ 'truncate_text': truncate_text
109
+ }
110
+
111
+ result = text
112
+ processing_steps = []
113
+
114
+ for operation in operations:
115
+ if operation in operation_map:
116
+ before = result
117
+ result = operation_map[operation](result)
118
+ processing_steps.append({
119
+ 'operation': operation,
120
+ 'before': before[:50] + "..." if len(before) > 50 else before,
121
+ 'after': result[:50] + "..." if len(result) > 50 else result
122
+ })
123
+
124
+ return result, processing_steps
125
+
126
+
127
+ def analyze_text_statistics(text):
128
+ """Analyze various statistics about the text."""
129
+ words = text.split()
130
+
131
+ stats = {
132
+ 'character_count': len(text),
133
+ 'word_count': len(words),
134
+ 'sentence_count': len(re.findall(r'[.!?]+', text)),
135
+ 'average_word_length': sum(len(word) for word in words) / len(words) if words else 0,
136
+ 'longest_word': max(words, key=len) if words else "",
137
+ 'shortest_word': min(words, key=len) if words else ""
138
+ }
139
+
140
+ return stats
141
+
142
+
143
+ if __name__ == "__main__":
144
+ sample_text = """
145
+ This is a SAMPLE text with various formatting issues!!!
146
+ It has multiple spaces, special @#$% characters, and
147
+ needs some serious cleaning & processing...
148
+ """
149
+
150
+ print("Original text:")
151
+ print(repr(sample_text))
152
+
153
+ processed_text, steps = text_processing_pipeline(sample_text)
154
+
155
+ print("\nProcessing steps:")
156
+ for step in steps:
157
+ print(f"After {step['operation']}:")
158
+ print(f" {step['after']}")
159
+
160
+ print(f"\nFinal result: {processed_text}")
161
+
162
+ stats = analyze_text_statistics(processed_text)
163
+ print(f"\nText statistics: {stats}")
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ graphviz
pyproject.toml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "gradio-plant-uml"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "datasets>=3.6.0",
9
+ "fastapi>=0.115.12",
10
+ "flask>=3.1.1",
11
+ "gradio[mcp]>=5.33.0",
12
+ "graphviz>=0.20.3",
13
+ "matplotlib>=3.10.3",
14
+ "numpy>=2.2.6",
15
+ "pandas>=2.3.0",
16
+ "plantuml>=0.3.0",
17
+ "polars>=1.30.0",
18
+ "py2puml>=0.10.0",
19
+ "pyan3>=1.2.0",
20
+ "requests>=2.32.3",
21
+ "scikit-learn>=1.7.0",
22
+ "transformers>=4.52.4",
23
+ ]
requirements.txt ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile pyproject.toml -o requirements.txt
3
+ aiofiles==24.1.0
4
+ # via gradio
5
+ aiohappyeyeballs==2.6.1
6
+ # via aiohttp
7
+ aiohttp==3.12.9
8
+ # via fsspec
9
+ aiosignal==1.3.2
10
+ # via aiohttp
11
+ annotated-types==0.7.0
12
+ # via pydantic
13
+ anyio==4.9.0
14
+ # via
15
+ # gradio
16
+ # httpx
17
+ # mcp
18
+ # sse-starlette
19
+ # starlette
20
+ attrs==25.3.0
21
+ # via aiohttp
22
+ blinker==1.9.0
23
+ # via flask
24
+ certifi==2025.4.26
25
+ # via
26
+ # httpcore
27
+ # httpx
28
+ # requests
29
+ charset-normalizer==3.4.2
30
+ # via requests
31
+ click==8.2.1
32
+ # via
33
+ # flask
34
+ # typer
35
+ # uvicorn
36
+ colorama==0.4.6
37
+ # via
38
+ # click
39
+ # tqdm
40
+ contourpy==1.3.2
41
+ # via matplotlib
42
+ cycler==0.12.1
43
+ # via matplotlib
44
+ datasets==3.6.0
45
+ # via gradio-plant-uml (pyproject.toml)
46
+ dill==0.3.8
47
+ # via
48
+ # datasets
49
+ # multiprocess
50
+ fastapi==0.115.12
51
+ # via
52
+ # gradio-plant-uml (pyproject.toml)
53
+ # gradio
54
+ ffmpy==0.6.0
55
+ # via gradio
56
+ filelock==3.18.0
57
+ # via
58
+ # datasets
59
+ # huggingface-hub
60
+ # transformers
61
+ flask==3.1.1
62
+ # via gradio-plant-uml (pyproject.toml)
63
+ fonttools==4.58.1
64
+ # via matplotlib
65
+ frozenlist==1.6.2
66
+ # via
67
+ # aiohttp
68
+ # aiosignal
69
+ fsspec==2025.3.0
70
+ # via
71
+ # datasets
72
+ # gradio-client
73
+ # huggingface-hub
74
+ gradio==5.33.0
75
+ # via gradio-plant-uml (pyproject.toml)
76
+ gradio-client==1.10.2
77
+ # via gradio
78
+ graphviz==0.20.3
79
+ # via gradio-plant-uml (pyproject.toml)
80
+ groovy==0.1.2
81
+ # via gradio
82
+ h11==0.16.0
83
+ # via
84
+ # httpcore
85
+ # uvicorn
86
+ httpcore==1.0.9
87
+ # via httpx
88
+ httplib2==0.22.0
89
+ # via plantuml
90
+ httpx==0.28.1
91
+ # via
92
+ # gradio
93
+ # gradio-client
94
+ # mcp
95
+ # safehttpx
96
+ httpx-sse==0.4.0
97
+ # via mcp
98
+ huggingface-hub==0.32.4
99
+ # via
100
+ # datasets
101
+ # gradio
102
+ # gradio-client
103
+ # tokenizers
104
+ # transformers
105
+ idna==3.10
106
+ # via
107
+ # anyio
108
+ # httpx
109
+ # requests
110
+ # yarl
111
+ itsdangerous==2.2.0
112
+ # via flask
113
+ jinja2==3.1.6
114
+ # via
115
+ # flask
116
+ # gradio
117
+ # pyan3
118
+ joblib==1.5.1
119
+ # via scikit-learn
120
+ kiwisolver==1.4.8
121
+ # via matplotlib
122
+ markdown-it-py==3.0.0
123
+ # via rich
124
+ markupsafe==3.0.2
125
+ # via
126
+ # flask
127
+ # gradio
128
+ # jinja2
129
+ # werkzeug
130
+ matplotlib==3.10.3
131
+ # via gradio-plant-uml (pyproject.toml)
132
+ mcp==1.9.0
133
+ # via gradio
134
+ mdurl==0.1.2
135
+ # via markdown-it-py
136
+ multidict==6.4.4
137
+ # via
138
+ # aiohttp
139
+ # yarl
140
+ multiprocess==0.70.16
141
+ # via datasets
142
+ numpy==2.2.6
143
+ # via
144
+ # gradio-plant-uml (pyproject.toml)
145
+ # contourpy
146
+ # datasets
147
+ # gradio
148
+ # matplotlib
149
+ # pandas
150
+ # scikit-learn
151
+ # scipy
152
+ # transformers
153
+ orjson==3.10.18
154
+ # via gradio
155
+ packaging==25.0
156
+ # via
157
+ # datasets
158
+ # gradio
159
+ # gradio-client
160
+ # huggingface-hub
161
+ # matplotlib
162
+ # transformers
163
+ pandas==2.3.0
164
+ # via
165
+ # gradio-plant-uml (pyproject.toml)
166
+ # datasets
167
+ # gradio
168
+ pillow==11.2.1
169
+ # via
170
+ # gradio
171
+ # matplotlib
172
+ plantuml==0.3.0
173
+ # via gradio-plant-uml (pyproject.toml)
174
+ polars==1.30.0
175
+ # via gradio-plant-uml (pyproject.toml)
176
+ propcache==0.3.1
177
+ # via
178
+ # aiohttp
179
+ # yarl
180
+ py2puml==0.10.0
181
+ # via gradio-plant-uml (pyproject.toml)
182
+ pyan3==1.2.0
183
+ # via gradio-plant-uml (pyproject.toml)
184
+ pyarrow==20.0.0
185
+ # via datasets
186
+ pydantic==2.11.5
187
+ # via
188
+ # fastapi
189
+ # gradio
190
+ # mcp
191
+ # pydantic-settings
192
+ pydantic-core==2.33.2
193
+ # via pydantic
194
+ pydantic-settings==2.9.1
195
+ # via mcp
196
+ pydub==0.25.1
197
+ # via gradio
198
+ pygments==2.19.1
199
+ # via rich
200
+ pyparsing==3.2.3
201
+ # via
202
+ # httplib2
203
+ # matplotlib
204
+ python-dateutil==2.9.0.post0
205
+ # via
206
+ # matplotlib
207
+ # pandas
208
+ python-dotenv==1.1.0
209
+ # via pydantic-settings
210
+ python-multipart==0.0.20
211
+ # via
212
+ # gradio
213
+ # mcp
214
+ pytz==2025.2
215
+ # via pandas
216
+ pyyaml==6.0.2
217
+ # via
218
+ # datasets
219
+ # gradio
220
+ # huggingface-hub
221
+ # transformers
222
+ regex==2024.11.6
223
+ # via transformers
224
+ requests==2.32.3
225
+ # via
226
+ # gradio-plant-uml (pyproject.toml)
227
+ # datasets
228
+ # huggingface-hub
229
+ # transformers
230
+ rich==14.0.0
231
+ # via typer
232
+ ruff==0.11.13
233
+ # via gradio
234
+ safehttpx==0.1.6
235
+ # via gradio
236
+ safetensors==0.5.3
237
+ # via transformers
238
+ scikit-learn==1.7.0
239
+ # via gradio-plant-uml (pyproject.toml)
240
+ scipy==1.15.3
241
+ # via scikit-learn
242
+ semantic-version==2.10.0
243
+ # via gradio
244
+ shellingham==1.5.4
245
+ # via typer
246
+ six==1.17.0
247
+ # via python-dateutil
248
+ sniffio==1.3.1
249
+ # via anyio
250
+ sse-starlette==2.3.6
251
+ # via mcp
252
+ starlette==0.46.2
253
+ # via
254
+ # fastapi
255
+ # gradio
256
+ # mcp
257
+ threadpoolctl==3.6.0
258
+ # via scikit-learn
259
+ tokenizers==0.21.1
260
+ # via transformers
261
+ tomlkit==0.13.3
262
+ # via gradio
263
+ tqdm==4.67.1
264
+ # via
265
+ # datasets
266
+ # huggingface-hub
267
+ # transformers
268
+ transformers==4.52.4
269
+ # via gradio-plant-uml (pyproject.toml)
270
+ typer==0.16.0
271
+ # via gradio
272
+ typing-extensions==4.14.0
273
+ # via
274
+ # anyio
275
+ # fastapi
276
+ # gradio
277
+ # gradio-client
278
+ # huggingface-hub
279
+ # pydantic
280
+ # pydantic-core
281
+ # typer
282
+ # typing-inspection
283
+ typing-inspection==0.4.1
284
+ # via
285
+ # pydantic
286
+ # pydantic-settings
287
+ tzdata==2025.2
288
+ # via pandas
289
+ urllib3==2.4.0
290
+ # via requests
291
+ uvicorn==0.34.3
292
+ # via
293
+ # gradio
294
+ # mcp
295
+ websockets==15.0.1
296
+ # via gradio-client
297
+ werkzeug==3.1.3
298
+ # via flask
299
+ xxhash==3.5.0
300
+ # via datasets
301
+ yarl==1.20.0
302
+ # via aiohttp
uv.lock ADDED
The diff for this file is too large to render. See raw diff