import streamlit as st import nbformat import ast import re import zipfile import io import hashlib from pathlib import Path from typing import List, Tuple, Dict, Any, Optional, Set from datetime import datetime # ============================== # SESSION STATE INITIALIZATION # ============================== def init_session_state(): """Initialize all session state variables.""" defaults = { 'conversion_results': {}, 'uploaded_files': [], 'uploaded_zip': None, 'theme': "light", 'conversion_mode': "Hybrid (Recommended)", 'large_file_threshold': 200, 'add_main_guard': False, 'preserve_comments': True, 'conversion_stats': { 'total_files': 0, 'successful': 0, 'failed': 0, 'total_conversions': 0 } } for key, value in defaults.items(): if key not in st.session_state: st.session_state[key] = value init_session_state() # ============================== # PAGE CONFIG & ENHANCED THEME # ============================== st.set_page_config( page_title="๐Ÿ“Š Python โ†’ Streamlit Converter Pro", page_icon="๐Ÿ“Š", layout="wide", initial_sidebar_state="expanded", menu_items={ 'Get Help': None, 'Report a bug': None, 'About': "Python to Streamlit Converter Pro - Transform your code into beautiful Streamlit apps!" } ) def apply_enhanced_theme(): """Apply enhanced theme with modern styling.""" is_dark = st.session_state.theme == "dark" # Color scheme if is_dark: bg_primary = "#0e1117" bg_secondary = "#1e2127" bg_tertiary = "#262730" text_primary = "#fafafa" text_secondary = "#d0d0d0" accent = "#ff4b4b" accent_hover = "#ff6b6b" border = "#3a3d47" success = "#00d4aa" warning = "#ffa726" card_bg = "#1a1d24" else: bg_primary = "#ffffff" bg_secondary = "#f8f9fa" bg_tertiary = "#e9ecef" text_primary = "#1a1a1a" text_secondary = "#4a4a4a" accent = "#ff4b4b" accent_hover = "#ff6b6b" border = "#dee2e6" success = "#00d4aa" warning = "#ffa726" card_bg = "#ffffff" st.markdown(f""" """, unsafe_allow_html=True) apply_enhanced_theme() # ============================== # UI COMPONENTS # ============================== def render_stat_card(title: str, value: str, icon: str = "๐Ÿ“Š"): """Render a statistics card.""" st.markdown(f"""
{icon}
{value}
{title}
""", unsafe_allow_html=True) def render_badge(text: str, color: str = "#ff4b4b"): """Render a custom badge.""" st.markdown(f'{text}', unsafe_allow_html=True) def render_hero_section(): """Render the hero section.""" st.markdown("""

๐Ÿ Python โ†’ Streamlit Converter Pro

Transform your Jupyter Notebooks and Python scripts into beautiful, interactive Streamlit apps in seconds!

โœจ Auto-Conversion ๐Ÿ“Š Handles Large Files ๐Ÿ’ฌ Preserves Comments ๐Ÿ“ฆ Batch Processing
""", unsafe_allow_html=True) # ============================== # ENHANCED SIDEBAR # ============================== with st.sidebar: # Logo and Title st.markdown("""

๐Ÿ

Converter Pro

""", unsafe_allow_html=True) st.divider() # Quick Actions st.markdown("### โšก Quick Actions") col1, col2 = st.columns(2) with col1: theme_icon = "๐ŸŒ™" if st.session_state.theme == "light" else "โ˜€๏ธ" if st.button(theme_icon, use_container_width=True, help="Toggle theme"): st.session_state.theme = "dark" if st.session_state.theme == "light" else "light" st.rerun() with col2: if (st.session_state.conversion_results or st.session_state.uploaded_files or st.session_state.uploaded_zip): if st.button("๐Ÿ”„", use_container_width=True, help="Clear all"): for key in list(st.session_state.keys()): del st.session_state[key] init_session_state() st.rerun() st.divider() # Upload Section st.markdown("### ๐Ÿ“ฅ Upload Files") upload_method = st.radio( "Upload Method", ["๐Ÿ“ Individual Files", "๐Ÿ“ฆ ZIP Archive"], index=0, help="Choose how to upload your files", label_visibility="collapsed" ) if upload_method == "๐Ÿ“ Individual Files": uploaded_files = st.file_uploader( "Select Python or Notebook files", type=["py", "ipynb"], accept_multiple_files=True, key="file_uploader", help="Upload one or more .py or .ipynb files" ) st.session_state.uploaded_files = uploaded_files if uploaded_files else [] st.session_state.uploaded_zip = None else: uploaded_zip = st.file_uploader( "Upload ZIP archive", type=["zip"], key="zip_uploader", help="Upload a ZIP file containing multiple Python/Notebook files" ) st.session_state.uploaded_zip = uploaded_zip st.session_state.uploaded_files = [] st.divider() # Advanced Settings with st.expander("๐Ÿ”ง Advanced Settings", expanded=False): st.session_state.conversion_mode = st.selectbox( "๐Ÿง  Conversion Strategy", ["Hybrid (Recommended)", "Auto", "AST (Precise)", "Regex (Fast)"], index=["Hybrid (Recommended)", "Auto", "AST (Precise)", "Regex (Fast)"].index( st.session_state.conversion_mode ) if st.session_state.conversion_mode in ["Hybrid (Recommended)", "Auto", "AST (Precise)", "Regex (Fast)"] else 0, help="Hybrid combines AST and regex for best results" ) st.session_state.large_file_threshold = st.slider( "๐Ÿ“ Large File Threshold (KB)", min_value=50, max_value=5000, value=st.session_state.large_file_threshold, help="Files larger than this use optimized processing", step=50 ) st.session_state.add_main_guard = st.checkbox( "๐Ÿ›ก๏ธ Add `if __name__ == '__main__':` guard", value=st.session_state.add_main_guard, help="Prevents execution issues when imported" ) st.session_state.preserve_comments = st.checkbox( "๐Ÿ’ฌ Preserve Comments & Docstrings", value=st.session_state.preserve_comments, help="Keep all comments and documentation" ) st.divider() # Sample Notebook Button if st.button("๐Ÿงช Load Sample Notebook", use_container_width=True, type="secondary"): sample_nb = get_sample_notebook() sample_bytes = nbformat.writes(sample_nb).encode('utf-8') st.session_state.uploaded_files = [io.BytesIO(sample_bytes)] st.session_state.uploaded_files[0].name = "sample_notebook.ipynb" st.rerun() st.divider() # Statistics if st.session_state.conversion_stats['total_files'] > 0: st.markdown("### ๐Ÿ“Š Statistics") stats = st.session_state.conversion_stats st.metric("Total Files", stats['total_files']) st.metric("Successful", stats['successful'], delta=f"{stats['total_conversions']} conversions") if stats['failed'] > 0: st.metric("Failed", stats['failed'], delta_color="inverse") # ============================== # SAMPLE FILE GENERATOR # ============================== @st.cache_data def get_sample_notebook(): """Generate a sample notebook for testing.""" nb = nbformat.v4.new_notebook() nb.cells = [ nbformat.v4.new_markdown_cell("# Data Analysis Sample\n\nThis notebook demonstrates data visualization."), nbformat.v4.new_code_cell("import pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport numpy as np\n\n# Load sample data\ndf = pd.DataFrame({'x': np.random.randn(100), 'y': np.random.randn(100)})"), nbformat.v4.new_code_cell("print('Dataset shape:', df.shape)\nprint(f'Total rows: {len(df)}')"), nbformat.v4.new_code_cell("display(df.head())\ndisplay(df.describe())"), nbformat.v4.new_code_cell("plt.figure(figsize=(8,5))\nsns.scatterplot(data=df, x='x', y='y')\nplt.title('Scatter Plot')\nplt.show()"), nbformat.v4.new_code_cell("import plotly.express as px\nfig = px.scatter(df, x='x', y='y', title='Plotly Scatter')\nfig.show()") ] return nb # ============================== # ENHANCED CONVERTER CORE # ============================== class CommentPreservingTransformer(ast.NodeTransformer): """Enhanced AST transformer that preserves comments and handles more patterns.""" def __init__(self, source_lines: List[str]): self.source_lines = source_lines self.conversion_log = [] self.imports_needed = set() self.line_comments = {} def visit_Expr(self, node): """Transform expression statements like print, display, plt.show, etc.""" if isinstance(node.value, ast.Call): call = node.value if self._is_print_call(call): self.conversion_log.append("Converted print() โ†’ st.write()") self.imports_needed.add("import streamlit as st") call.func = ast.Attribute( value=ast.Name(id='st', ctx=ast.Load()), attr='write', ctx=ast.Load() ) return node elif self._is_display_call(call): self.conversion_log.append("Converted display() โ†’ st.dataframe()") self.imports_needed.add("import streamlit as st") call.func = ast.Attribute( value=ast.Name(id='st', ctx=ast.Load()), attr='dataframe', ctx=ast.Load() ) return node elif self._is_plt_show_call(call): self.conversion_log.append("Converted plt.show() โ†’ st.pyplot()") self.imports_needed.add("import streamlit as st") self.imports_needed.add("import matplotlib.pyplot as plt") return ast.Expr( value=ast.Call( func=ast.Attribute( value=ast.Name(id='st', ctx=ast.Load()), attr='pyplot', ctx=ast.Load() ), args=[ast.Call( func=ast.Attribute( value=ast.Name(id='plt', ctx=ast.Load()), attr='gcf', ctx=ast.Load() ), args=[], keywords=[] )], keywords=[] ) ) elif self._is_plotly_show_call(call): var_name = self._get_call_attr_name(call) if var_name: self.conversion_log.append(f"Converted {var_name}.show() โ†’ st.plotly_chart()") self.imports_needed.add("import streamlit as st") return ast.Expr( value=ast.Call( func=ast.Attribute( value=ast.Name(id='st', ctx=ast.Load()), attr='plotly_chart', ctx=ast.Load() ), args=[ast.Name(id=var_name, ctx=ast.Load())], keywords=[] ) ) return self.generic_visit(node) def visit_Call(self, node): """Handle method calls like df.head(), df.tail(), etc.""" if isinstance(node.func, ast.Attribute): attr_name = node.func.attr if attr_name in ('head', 'tail') and isinstance(node.func.value, (ast.Name, ast.Attribute)): parent = getattr(node, '_parent', None) if parent is None or isinstance(parent, ast.Expr): self.conversion_log.append(f"Wrapped {attr_name}() โ†’ st.dataframe()") self.imports_needed.add("import streamlit as st") return ast.Call( func=ast.Attribute( value=ast.Name(id='st', ctx=ast.Load()), attr='dataframe', ctx=ast.Load() ), args=[node], keywords=[] ) return self.generic_visit(node) def _is_print_call(self, call): return (isinstance(call.func, ast.Name) and call.func.id == 'print') def _is_display_call(self, call): return (isinstance(call.func, ast.Name) and call.func.id == 'display') def _is_plt_show_call(self, call): return (isinstance(call.func, ast.Attribute) and isinstance(call.func.value, ast.Name) and call.func.value.id == 'plt' and call.func.attr == 'show') def _is_plotly_show_call(self, call): return (isinstance(call.func, ast.Attribute) and call.func.attr == 'show') def _get_call_attr_name(self, call): if isinstance(call.func, ast.Attribute): if isinstance(call.func.value, ast.Name): return call.func.value.id return None class HybridConverter: """Hybrid converter that combines AST parsing with regex for comprehensive conversion.""" def __init__(self, code: str, filename: str = "unknown", conversion_mode: str = "hybrid", large_file_threshold: int = 200, add_main_guard: bool = False, preserve_comments: bool = True): self.original_code = code self.filename = filename self.conversion_mode = conversion_mode.lower() self.large_file_threshold = large_file_threshold * 1024 self.add_main_guard = add_main_guard self.preserve_comments = preserve_comments self.conversion_report = [] self.imports_needed = set() self.source_lines = code.splitlines(keepends=True) def convert(self) -> str: """Main conversion entry point.""" file_size = len(self.original_code.encode('utf-8')) is_large = file_size > self.large_file_threshold if self.conversion_mode == "regex (fast)": self.conversion_report.append("โœ… Using fast regex-based conversion") return self._regex_convert() elif self.conversion_mode == "ast (precise)": self.conversion_report.append("โœ… Using precise AST-based conversion") return self._ast_convert() elif "hybrid" in self.conversion_mode: self.conversion_report.append("โœ… Using hybrid conversion (AST + Regex)") return self._hybrid_convert() else: # Auto mode if is_large: self.conversion_report.append(f"โœ… Large file detected ({file_size/1024:.1f}KB), using hybrid mode") return self._hybrid_convert() else: self.conversion_report.append("โœ… Using AST-based conversion") return self._ast_convert() def _hybrid_convert(self) -> str: """Hybrid approach: AST for structure, regex for patterns.""" try: code = self._ast_convert_core() code = self._apply_regex_patterns(code) self._detect_needed_imports(self.original_code) return self._add_streamlit_boilerplate(code) except Exception as e: self.conversion_report.append(f"โš ๏ธ Hybrid conversion issue: {e}, falling back to regex") return self._regex_convert() def _ast_convert(self) -> str: """Pure AST-based conversion.""" try: code = self._ast_convert_core() self._detect_needed_imports(self.original_code) return self._add_streamlit_boilerplate(code) except Exception as e: self.conversion_report.append(f"โš ๏ธ AST conversion failed: {e}, falling back to regex") return self._regex_convert() def _ast_convert_core(self) -> str: """Core AST conversion logic.""" try: tree = ast.parse(self.original_code, filename=self.filename) transformer = CommentPreservingTransformer(self.source_lines) transformed_tree = transformer.visit(tree) ast.fix_missing_locations(transformed_tree) self.imports_needed.update(transformer.imports_needed) self.conversion_report.extend(transformer.conversion_log) try: return ast.unparse(transformed_tree) except AttributeError: return self._ast_to_source(transformed_tree) except SyntaxError as e: self.conversion_report.append(f"โš ๏ธ Syntax error: {e}, using line-by-line fallback") return self._line_by_line_fallback() except Exception as e: self.conversion_report.append(f"โš ๏ธ AST parsing error: {e}") raise def _ast_to_source(self, node) -> str: """Custom AST to source converter for Python < 3.9.""" try: import astor return astor.to_source(node) except ImportError: self.conversion_report.append("โš ๏ธ Python < 3.9 detected, using regex fallback") return self._regex_convert() def _apply_regex_patterns(self, code: str) -> str: """Apply regex patterns for additional conversions.""" lines = code.splitlines(keepends=True) new_lines = [] for line in lines: stripped = line.strip() if stripped.startswith("%") or stripped.startswith("!"): continue if re.search(r'^\s*([a-zA-Z_]\w*\.(?:head|tail)\([^)]*\))\s*$', stripped): match = re.search(r'([a-zA-Z_]\w*\.(?:head|tail)\([^)]*\))', stripped) if match: indent = len(line) - len(line.lstrip()) new_lines.append(' ' * indent + f"st.dataframe({match.group(1)})\n") self.conversion_report.append("โœ… Wrapped DataFrame method โ†’ st.dataframe()") continue if re.search(r'\.show\(\)', stripped) and ('sns.' in stripped or 'seaborn' in stripped): var_match = re.search(r'([a-zA-Z_]\w*)\.show\(\)', stripped) if var_match: indent = len(line) - len(line.lstrip()) new_lines.append(' ' * indent + f"st.pyplot({var_match.group(1)})\n") self.conversion_report.append("โœ… Converted seaborn plot โ†’ st.pyplot()") continue new_lines.append(line) return "".join(new_lines) def _regex_convert(self) -> str: """Regex-based conversion for large files or fallback.""" lines = self.source_lines new_lines = [] in_multiline_string = False i = 0 while i < len(lines): line = lines[i] stripped = line.strip() if stripped.startswith("%") or stripped.startswith("!"): i += 1 continue if '"""' in line or "'''" in line: triple_quotes = '"""' if '"""' in line else "'''" count = line.count(triple_quotes) if count % 2 == 1: in_multiline_string = not in_multiline_string new_lines.append(line) i += 1 continue if in_multiline_string: new_lines.append(line) i += 1 continue if re.match(r'^\s*print\s*\(', stripped): new_line = re.sub(r'\bprint\s*\(', 'st.write(', line, count=1) new_lines.append(new_line) self.conversion_report.append("โœ… Replaced print() โ†’ st.write()") self.imports_needed.add("import streamlit as st") i += 1 continue if re.match(r'^\s*display\s*\(', stripped): new_line = re.sub(r'\bdisplay\s*\(', 'st.dataframe(', line, count=1) new_lines.append(new_line) self.conversion_report.append("โœ… Replaced display() โ†’ st.dataframe()") self.imports_needed.add("import streamlit as st") i += 1 continue if re.match(r'^\s*plt\.show\s*\(\s*\)', stripped): indent = len(line) - len(line.lstrip()) new_lines.append(' ' * indent + "st.pyplot(plt.gcf())\n") self.conversion_report.append("โœ… Replaced plt.show() โ†’ st.pyplot()") self.imports_needed.add("import streamlit as st") self.imports_needed.add("import matplotlib.pyplot as plt") i += 1 continue if re.match(r'^\s*[a-zA-Z_]\w*\.show\s*\(\s*\)', stripped): match = re.search(r'([a-zA-Z_]\w*)\.show\s*\(\s*\)', stripped) if match: var_name = match.group(1) indent = len(line) - len(line.lstrip()) new_lines.append(' ' * indent + f"st.plotly_chart({var_name})\n") self.conversion_report.append(f"โœ… Replaced {var_name}.show() โ†’ st.plotly_chart()") self.imports_needed.add("import streamlit as st") i += 1 continue if re.match(r'^\s*[a-zA-Z_]\w*\.(?:head|tail)\s*\([^)]*\)\s*$', stripped): match = re.search(r'([a-zA-Z_]\w*\.(?:head|tail)\s*\([^)]*\))', stripped) if match: indent = len(line) - len(line.lstrip()) new_lines.append(' ' * indent + f"st.dataframe({match.group(1)})\n") self.conversion_report.append("โœ… Wrapped DataFrame method โ†’ st.dataframe()") self.imports_needed.add("import streamlit as st") i += 1 continue new_lines.append(line) i += 1 self._detect_needed_imports(self.original_code) return self._add_streamlit_boilerplate("".join(new_lines)) def _line_by_line_fallback(self) -> str: """Fallback for when AST parsing fails.""" return self._regex_convert() def _detect_needed_imports(self, code: str): """Detect which imports are needed based on code content.""" code_lower = code.lower() self.imports_needed.add("import streamlit as st") if 'plt.' in code or 'matplotlib' in code_lower or 'pyplot' in code_lower: self.imports_needed.add("import matplotlib.pyplot as plt") if 'sns.' in code or 'seaborn' in code_lower: self.imports_needed.add("import seaborn as sns") if 'px.' in code or 'go.' in code or 'plotly' in code_lower: self.imports_needed.add("import plotly.express as px") self.imports_needed.add("import plotly.graph_objects as go") if 'pd.' in code or 'pandas' in code_lower or 'dataframe' in code_lower: self.imports_needed.add("import pandas as pd") if 'np.' in code or 'numpy' in code_lower: self.imports_needed.add("import numpy as np") def _add_streamlit_boilerplate(self, code: str) -> str: """Add Streamlit boilerplate and imports.""" imports = sorted(list(self.imports_needed)) existing_imports = [] code_lines = code.splitlines() for line in code_lines[:20]: if line.strip().startswith('import ') or line.strip().startswith('from '): existing_imports.append(line.strip()) filtered_imports = [] for imp in imports: imp_name = imp.split()[1].split('.')[0] if 'import' in imp else None if imp_name: if not any(imp_name in existing for existing in existing_imports): filtered_imports.append(imp) else: filtered_imports.append(imp) boilerplate = [ "# ==============================", "# AUTO-GENERATED STREAMLIT APP", f"# Source: {self.filename}", "# Converted with Python โ†’ Streamlit Converter Pro", "# ==============================\n", *filtered_imports, "", "st.set_page_config(", " page_title='Converted App',", " layout='wide'", ")\n", "st.title('๐Ÿ Converted Streamlit App')", f"st.caption(f'_Converted from: {self.filename}_')\n", "st.divider()\n" ] if self.add_main_guard: indented_code = "\n".join(" " + line if line.strip() else line for line in code.splitlines()) return "\n".join(boilerplate) + "\nif __name__ == '__main__':\n" + indented_code else: return "\n".join(boilerplate) + code def get_conversion_report(self) -> List[str]: """Get the conversion report.""" if not self.conversion_report: return ["โ„น๏ธ No transformations applied (code may already be Streamlit-compatible)"] seen = set() unique_report = [] for item in self.conversion_report: if item not in seen: seen.add(item) unique_report.append(item) return unique_report # ============================== # NOTEBOOK PROCESSING # ============================== def extract_code_from_notebook(notebook_content: str, preserve_markdown: bool = True) -> str: """Convert notebook to Python script with enhanced markdown handling.""" try: nb = nbformat.reads(notebook_content, as_version=4) except Exception as e: raise ValueError(f"Invalid notebook format: {e}") lines = [] cell_num = 0 for cell in nb.cells: cell_num += 1 if cell.cell_type == "markdown" and preserve_markdown: md_content = cell.source lines.append(f"\n# {'='*60}") lines.append(f"# MARKDOWN CELL {cell_num}") lines.append(f"# {'='*60}") for md_line in md_content.split('\n'): if not md_line.strip(): lines.append("#") else: clean_line = md_line.replace('"""', "'''").replace("'''", '"""') if md_line.strip().startswith('#'): lines.append(f"# {clean_line}") else: lines.append(f"# {clean_line}") lines.append("") elif cell.cell_type == "code": code_content = cell.source if lines and lines[-1].strip(): lines.append("") if hasattr(cell, 'metadata') and cell.metadata: lines.append(f"# Cell {cell_num} metadata: {cell.metadata}") lines.append(code_content) if not code_content.endswith('\n'): lines.append("") result = "\n".join(lines) if not result.endswith('\n'): result += "\n" return result # ============================== # FILE PROCESSING UTILITIES # ============================== @st.cache_data(show_spinner=False) def process_single_file_cached(file_bytes: bytes, filename: str, file_hash: str, conversion_mode: str, large_file_threshold: int, add_main_guard: bool, preserve_comments: bool): """Cached file processing function.""" file_extension = Path(filename).suffix.lower() try: if file_extension == ".ipynb": original_code = extract_code_from_notebook(file_bytes.decode('utf-8'), preserve_markdown=preserve_comments) else: original_code = file_bytes.decode("utf-8") converter = HybridConverter( original_code, filename, conversion_mode=conversion_mode, large_file_threshold=large_file_threshold, add_main_guard=add_main_guard, preserve_comments=preserve_comments ) streamlit_code = converter.convert() return streamlit_code, original_code, converter.get_conversion_report() except Exception as e: error_msg = f"Error processing {filename}: {str(e)}" return f"# {error_msg}\n# Original file could not be processed.", "", [f"โŒ {error_msg}"] def process_single_file(uploaded_file, **kwargs): """Process a single uploaded file.""" file_bytes = uploaded_file.getvalue() file_hash = hashlib.md5(file_bytes).hexdigest()[:8] return process_single_file_cached( file_bytes, uploaded_file.name, file_hash, **kwargs ) # ============================== # MAIN APP UI # ============================== # Hero Section render_hero_section() # Main Tabs tab1, tab2, tab3 = st.tabs(["๐Ÿš€ Convert Files", "๐Ÿ“Š Dashboard", "โ„น๏ธ How It Works"]) with tab1: if st.session_state.uploaded_files: st.markdown(f"### ๐Ÿ“„ Processing {len(st.session_state.uploaded_files)} File(s)") for idx, uploaded_file in enumerate(st.session_state.uploaded_files): file_key = f"file_{idx}_{uploaded_file.name}" with st.container(): st.markdown(f"
", unsafe_allow_html=True) # File Header col1, col2, col3 = st.columns([3, 1, 1]) with col1: st.markdown(f"#### ๐Ÿ“‘ {uploaded_file.name}") file_size = len(uploaded_file.getvalue()) st.caption(f"Size: {file_size:,} bytes ({file_size/1024:.2f} KB)") with col2: file_ext = Path(uploaded_file.name).suffix render_badge(file_ext.upper().replace('.', ''), "#00d4aa") with col3: render_badge("Ready", "#ff4b4b") try: with st.spinner(f"๐Ÿ”„ Converting {uploaded_file.name}..."): streamlit_code, original_code, report = process_single_file( uploaded_file, conversion_mode=st.session_state.conversion_mode.lower(), large_file_threshold=st.session_state.large_file_threshold, add_main_guard=st.session_state.add_main_guard, preserve_comments=st.session_state.preserve_comments ) # Update stats st.session_state.conversion_stats['total_files'] += 1 st.session_state.conversion_stats['successful'] += 1 st.session_state.conversion_stats['total_conversions'] += len([r for r in report if 'โœ…' in r]) st.success(f"โœ… Successfully converted {uploaded_file.name}!") # View Mode Selector view_mode = st.radio( "๐Ÿ‘๏ธ View Mode", ["๐Ÿ” Side-by-Side", "๐Ÿ“œ Original Only", "โœจ Converted Only"], index=0, horizontal=True, key=f"view_{file_key}" ) # Code Display if "Side-by-Side" in view_mode: col1, col2 = st.columns(2) with col1: st.markdown("##### ๐Ÿ“œ Original Code") display_original = original_code[:20000] + ("..." if len(original_code) > 20000 else "") st.code(display_original, language="python", line_numbers=True) with col2: st.markdown("##### โœจ Converted Streamlit App") display_converted = streamlit_code[:20000] + ("..." if len(streamlit_code) > 20000 else "") st.code(display_converted, language="python", line_numbers=True) elif "Original Only" in view_mode: st.markdown("##### ๐Ÿ“œ Original Code") display_original = original_code[:30000] + ("..." if len(original_code) > 30000 else "") st.code(display_original, language="python", line_numbers=True) else: st.markdown("##### โœจ Converted Streamlit App") display_converted = streamlit_code[:30000] + ("..." if len(streamlit_code) > 30000 else "") st.code(display_converted, language="python", line_numbers=True) # File Metrics col1, col2, col3, col4 = st.columns(4) with col1: st.metric("Original Size", f"{len(original_code):,}", "chars") with col2: st.metric("Converted Size", f"{len(streamlit_code):,}", "chars") with col3: size_diff = len(streamlit_code) - len(original_code) st.metric("Size Change", f"{size_diff:+,}", "chars") with col4: st.metric("Conversions", len([r for r in report if 'โœ…' in r]), "transformations") # Download Button st.download_button( f"โฌ‡๏ธ Download {Path(uploaded_file.name).stem}_streamlit.py", streamlit_code, file_name=f"{Path(uploaded_file.name).stem}_streamlit.py", mime="text/plain", key=f"dl_{file_key}", use_container_width=True, type="primary" ) # Conversion Report with st.expander("๐Ÿ“‹ Conversion Report", expanded=False): for item in report: if 'โœ…' in item: st.success(item) elif 'โš ๏ธ' in item: st.warning(item) elif 'โŒ' in item: st.error(item) else: st.info(item) except Exception as e: st.session_state.conversion_stats['total_files'] += 1 st.session_state.conversion_stats['failed'] += 1 st.error(f"โŒ Failed to convert `{uploaded_file.name}`: {str(e)}") st.exception(e) st.markdown("
", unsafe_allow_html=True) st.divider() elif st.session_state.uploaded_zip: st.markdown("### ๐Ÿ“ฆ Processing ZIP Archive") try: with st.spinner("Extracting and converting files..."): results = {} with zipfile.ZipFile(st.session_state.uploaded_zip) as zip_ref: file_list = [f for f in zip_ref.namelist() if f.endswith(('.py', '.ipynb'))] if not file_list: st.warning("โš ๏ธ No .py or .ipynb files found in ZIP!") else: progress_bar = st.progress(0) status_text = st.empty() for i, filename in enumerate(file_list): status_text.markdown(f"**Processing {i+1}/{len(file_list)}:** `{filename}`") with zip_ref.open(filename) as f: file_obj = io.BytesIO(f.read()) file_obj.name = filename try: code, orig, rep = process_single_file( file_obj, conversion_mode=st.session_state.conversion_mode.lower(), large_file_threshold=st.session_state.large_file_threshold, add_main_guard=st.session_state.add_main_guard, preserve_comments=st.session_state.preserve_comments ) results[filename] = (code, orig, rep) st.session_state.conversion_stats['successful'] += 1 except Exception as e: results[filename] = (f"# Conversion failed: {str(e)}", "", [f"โŒ Error: {str(e)}"]) st.session_state.conversion_stats['failed'] += 1 progress_bar.progress((i + 1) / len(file_list)) st.session_state.conversion_stats['total_files'] += 1 status_text.empty() progress_bar.empty() # Create ZIP of results zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zf: for name, (code, _, _) in results.items(): if not code.startswith("# Conversion failed"): zf.writestr(f"{Path(name).stem}_streamlit.py", code) successful = len([r for r in results.values() if not r[0].startswith('# Conversion failed')]) st.success(f"โœ… Successfully converted {successful}/{len(file_list)} file(s)") # Download Button st.download_button( "โฌ‡๏ธ Download All Converted Apps (ZIP)", zip_buffer.getvalue(), "streamlit_converted_apps.zip", "application/zip", use_container_width=True, type="primary" ) # Results Display for name, (code, orig, rep) in results.items(): with st.expander(f"๐Ÿ“„ {name}", expanded=False): if code.startswith("# Conversion failed"): st.error(code) else: view_mode = st.radio( "๐Ÿ‘๏ธ View Mode", ["๐Ÿ” Side-by-Side", "๐Ÿ“œ Original Only", "โœจ Converted Only"], index=0, horizontal=True, key=f"view_{name}" ) if "Side-by-Side" in view_mode: col1, col2 = st.columns(2) with col1: st.markdown("##### ๐Ÿ“œ Original") st.code(orig[:5000] + ("..." if len(orig) > 5000 else ""), language="python") with col2: st.markdown("##### โœจ Converted") st.code(code[:5000] + ("..." if len(code) > 5000 else ""), language="python") elif "Original Only" in view_mode: st.code(orig[:10000] + ("..." if len(orig) > 10000 else ""), language="python") else: st.code(code[:10000] + ("..." if len(code) > 10000 else ""), language="python") with st.expander("๐Ÿ“‹ Report"): for r in rep: if 'โœ…' in r: st.success(r) elif 'โŒ' in r: st.error(r) else: st.info(r) except Exception as e: st.error(f"โŒ ZIP processing failed: {str(e)}") st.exception(e) else: st.info(""" ๐Ÿ‘ˆ **Get Started:** 1. Upload files using the sidebar (or try the sample notebook) 2. Adjust settings if needed 3. View and download your converted Streamlit apps! """) with tab2: st.markdown("### ๐Ÿ“Š Conversion Dashboard") stats = st.session_state.conversion_stats if stats['total_files'] > 0: col1, col2, col3, col4 = st.columns(4) with col1: render_stat_card("Total Files", str(stats['total_files']), "๐Ÿ“") with col2: render_stat_card("Successful", str(stats['successful']), "โœ…") with col3: render_stat_card("Failed", str(stats['failed']), "โŒ") with col4: render_stat_card("Total Conversions", str(stats['total_conversions']), "๐Ÿ”„") # Success Rate if stats['total_files'] > 0: success_rate = (stats['successful'] / stats['total_files']) * 100 st.metric("Success Rate", f"{success_rate:.1f}%") else: st.info("๐Ÿ“Š No conversions yet. Upload files to see statistics here!") with tab3: st.markdown("### ๐Ÿ“– How It Works") st.markdown("""

โœจ Enhanced Conversion Engine

Hybrid Mode (Recommended): Combines AST parsing for structure with regex for patterns. - Preserves code structure and comments - Handles large files efficiently - Best balance of accuracy and performance

""", unsafe_allow_html=True) st.markdown(""" ### ๐Ÿ”„ Conversion Table | Original Code | โ†’ Streamlit Equivalent | |--------------|------------------------| | `print(x)` | `st.write(x)` | | `display(df)` | `st.dataframe(df)` | | `df.head()` / `df.tail()` | `st.dataframe(df.head())` | | `plt.show()` | `st.pyplot(plt.gcf())` | | `fig.show()` (Plotly) | `st.plotly_chart(fig)` | | Markdown cells | Commented markdown | | All comments | Preserved | """) st.markdown(""" ### ๐Ÿ“ฆ Key Features - โœ… **Large File Support**: Handles files up to 5MB+ efficiently - โœ… **Markdown Preservation**: Notebook markdown cells converted to comments - โœ… **Comment Preservation**: All comments and docstrings maintained - โœ… **ZIP Support**: Batch convert entire folders - โœ… **Error Recovery**: Graceful fallbacks for malformed code - โœ… **Import Detection**: Automatically adds required imports - โœ… **Real-time Statistics**: Track your conversion progress """) st.info("๐Ÿ’ก **Pro Tip**: Use Hybrid mode for best results. It combines the accuracy of AST with the speed of regex!")