import os import yaml from collections import OrderedDict from flask import Flask, jsonify, send_from_directory, request, redirect, url_for, session, render_template from werkzeug.security import check_password_hash import database # Imports the database.py you created earlier # --- Path Configurations --- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) FRONTEND_DIR = os.path.join(BASE_DIR, '..', 'frontend') # Use os.path.join exclusively for cross-platform safety YML_PATH = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra', 'directories.yaml') COMPS_ROOT = os.path.join(BASE_DIR, '..', 'mxpic', 'PDKs', 'Silterra') # Initialize Flask, pointing to the frontend folder for HTML/CSS/JS app = Flask(__name__, template_folder=FRONTEND_DIR, static_folder=FRONTEND_DIR) app.secret_key = 'super_secret_mxpic_key' # Required for session management app.json.sort_keys = False # Keep dictionary order # Ensure database tables exist when the server boots database.init_db() # --- YAML & PDK Parsing Helper Functions (Unchanged) --- def countSpaces(line): """Count leading spaces (tab=4).""" expanded = line.expandtabs(4) return len(expanded) - len(expanded.lstrip(' ')) def buildTree(filepath): """Build nested tree from indented yaml.""" if not os.path.exists(filepath): return OrderedDict() with open(filepath, 'r', encoding='utf-8') as f: lines = f.readlines() rootIdx = None for i, line in enumerate(lines): if line.strip().startswith('root') and ':' in line.strip(): rootIdx = i break if rootIdx is None: return OrderedDict() entries = [] for line in lines[rootIdx + 1:]: stripped = line.strip() if not stripped or stripped.startswith('#'): continue if stripped.startswith('- '): spaceNum = countSpaces(line) # FIX 1: Strip trailing colons off the string so 'composites:' becomes 'composites' name = stripped[2:].strip().rstrip(':') if name: entries.append((spaceNum, name)) if not entries: return OrderedDict() minIndent = min(indent for indent, _ in entries) nest = OrderedDict() levelStack = [(minIndent - 1, nest)] for spaceNum, name in entries: while levelStack and levelStack[-1][0] >= spaceNum: levelStack.pop() parent = levelStack[-1][1] child = OrderedDict() parent[name] = child levelStack.append((spaceNum, child)) return nest # def addCompsToTree(tree, compMap): # """Insert component nodes into the tree.""" # for pathSeg, compItem in compMap.items(): # compName = compItem['folder'] # curNode = tree # # FIX 2: Automatically build missing folder paths # for seg in pathSeg: # if seg not in curNode: # # If a folder like MZM_1600G isn't in the YAML, gracefully auto-create it # curNode[seg] = OrderedDict() # curNode = curNode[seg] # curNode[compName] = OrderedDict({ # "__type__": "component", # "__name__": compName, # "__yml__": compItem['yml'] # }) # return tree def addCompsToTree(compMap): """ Build a completely fresh tree from scratch and insert component nodes. No previous tree object or inspection required. """ # Initialize a clean, empty root tree fresh_tree = OrderedDict() for pathSeg, compItem in compMap.items(): compName = compItem['folder'] curNode = fresh_tree # Sequentially build the nested path segments dynamically for seg in pathSeg: if seg not in curNode: curNode[seg] = OrderedDict() curNode = curNode[seg] # Place the component metadata dictionary into its leaf node curNode[compName] = OrderedDict({ "__type__": "component", "__name__": compName, "__yml__": compItem['yml'] }) return fresh_tree def findComps(baseDir): """Scan component folders, return map of paths -> component info.""" compMap = {} # refDir = os.path.dirname(baseDir) refDir = baseDir for root, dirs, files in os.walk(baseDir): ymlFiles = [f for f in files if f.endswith('.yml')] if ymlFiles: parentDir = os.path.dirname(root) relPath = os.path.relpath(parentDir, refDir) parts = () if relPath == '.' else tuple(relPath.split(os.sep)) compName = os.path.basename(root) compMap[parts] = { 'folder': compName, 'yml': ymlFiles[0] } dirs.clear() return compMap # def addCompsToTree(tree, compMap): # """Insert component nodes into the tree.""" # for pathSeg, compItem in compMap.items(): # compName = compItem['folder'] # curNode = tree # try: # for seg in pathSeg: # curNode = curNode[seg] # except KeyError: # continue # curNode[compName] = OrderedDict({ # "__type__": "component", # "__name__": compName, # "__yml__": compItem['yml'] # }) # return tree def readCompYaml(compName): """Load YAML from component folder.""" for root, dirs, files in os.walk(COMPS_ROOT): if os.path.basename(root) == compName: dirs.clear() ymlFiles = [f for f in files if f.endswith('.yml')] if ymlFiles: ymlPath = os.path.join(root, ymlFiles[0]) with open(ymlPath, 'r', encoding='utf-8') as f: return yaml.safe_load(f) return None # --- AUTHENTICATION & PAGE ROUTES --- @app.route('/') def home(): """Route to login page, or bypass to dashboard if already authenticated.""" if 'user_id' in session: return redirect(url_for('dashboard')) return render_template('login.html') @app.route('/login', methods=['POST']) def login(): """Verify credentials against the database.""" username = request.form.get('username') password = request.form.get('password') user = database.get_user(username) # Verify hash from database matches entered password if user and check_password_hash(user[2], password): session['user_id'] = user[0] session['username'] = user[1] return redirect(url_for('dashboard')) else: return render_template('login.html', error="Invalid username or password") @app.route('/dashboard') def dashboard(): """User project list.""" if 'user_id' not in session: return redirect(url_for('home')) return render_template('dashboard.html', username=session['username']) @app.route('/canvas') def canvas(): """The main EDA editor.""" if 'user_id' not in session: return redirect(url_for('home')) # Note: Ensure your old index.html is renamed to canvas.html in the frontend folder return render_template('canvas.html') @app.route('/logout') def logout(): """Clear session and return to login.""" session.clear() return redirect(url_for('home')) # --- API ROUTES (Library & Components) --- @app.route('/api/library') def getLib(): """Get library structure.""" tree = buildTree(YML_PATH) if os.path.isdir(COMPS_ROOT): compMap = findComps(COMPS_ROOT) fresh_tree = addCompsToTree(compMap) return jsonify(fresh_tree) @app.route('/api/component/') def getComp(component_name): """Return component YAML data.""" data = readCompYaml(component_name) if data is None: return jsonify({"error": "Component not found"}), 404 return jsonify(data) @app.route('/api/component//image') def getCompImg(component_name): """Return first image in component folder.""" for root, dirs, files in os.walk(COMPS_ROOT): if os.path.basename(root) == component_name: dirs.clear() for ext in ('.png', '.jpg', '.jpeg', '.svg'): for f in files: if f.lower().endswith(ext): return send_from_directory(root, f) break return jsonify({"error": "No image found"}), 404 if __name__ == '__main__': print("Starting mxpic EDA Server on http://127.0.0.1:3000") app.run(host='127.0.0.1', port=3000, debug=True)