Pages

▼

Tuesday, October 6, 2026

thinter : manage python scripts !!!

Today, this simple manager for python scripts with autodetect python modules and add to description for each script with Claude AI.
See the source code:
# -*- coding: utf-8 -*-
import os
import re
import ast
import sys
import json
import hashlib
import subprocess
import tkinter as tk
from tkinter import ttk, messagebox
import tkinter.font as tkfont
DEFAULT_ROOT = "/storage/emulated/0/Download"
CATALOG_NAME = ".scripts_catalog.json"
FONT_SIZE = 10
try:
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
except NameError:
    BASE_DIR = os.getcwd()
STD = getattr(sys, "stdlib_module_names", None)
if STD is None:
    STD = set((
        "__future__ abc argparse array ast asyncio base64 bisect bz2 calendar cmd code codecs collections "
        "configparser contextlib copy csv ctypes curses dataclasses datetime decimal difflib email enum "
        "fnmatch fractions ftplib functools gc getopt getpass gettext glob gzip hashlib heapq hmac html http "
        "imaplib importlib inspect io itertools json locale logging lzma math mimetypes msvcrt multiprocessing "
        "operator optparse os pathlib pdb pickle platform pprint profile queue random re readline sched secrets "
        "select shelve shutil signal smtplib socket sqlite3 ssl stat statistics string struct subprocess sys "
        "tarfile telnetlib tempfile textwrap threading time timeit tkinter traceback turtle types typing "
        "unittest urllib uuid warnings weakref webbrowser winreg xml zipfile zlib"
    ).split())
ALIASES = {
    "PIL": "Pillow", "cv2": "opencv-python", "yt_dlp": "yt-dlp", "bs4": "beautifulsoup4",
    "sklearn": "scikit-learn", "yaml": "PyYAML", "dotenv": "python-dotenv", "serial": "pyserial",
    "win32api": "pywin32", "win32com": "pywin32", "Crypto": "pycryptodome", "skimage": "scikit-image",
    "dateutil": "python-dateutil", "OpenGL": "PyOpenGL", "bpy": "bpy (Blender)",
}
def safe(text):
    return str(text).encode("utf-8", "replace").decode("utf-8")
def read_json(path):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return None
def is_local(mod, full, root_folder):
    root_n = os.path.normpath(root_folder)
    cur = os.path.normpath(os.path.dirname(full))
    while True:
        is_root = (cur == root_n)
        if os.path.isfile(os.path.join(cur, mod + ".py")):
            return True
        if not is_root and os.path.isdir(os.path.join(cur, mod)):
            return True
        if is_root or len(cur) <= len(root_n):
            return False
        cur = os.path.dirname(cur)
def analyze(full, root_folder):
    try:
        with open(full, "r", encoding="utf-8", errors="replace") as f:
            text = f.read(300000)
    except OSError:
        return "Libs: nu pot citi fișierul"
    mods = []
    try:
        for node in ast.walk(ast.parse(text)):
            if isinstance(node, ast.Import):
                for a in node.names:
                    mods.append(a.name.split(".")[0])
            elif isinstance(node, ast.ImportFrom):
                if node.module and node.level == 0:
                    mods.append(node.module.split(".")[0])
    except (SyntaxError, ValueError):
        pattern = r"^\s*(?:from\s+([A-Za-z_]\w*)[\w.]*\s+import\s|import\s+([A-Za-z_]\w*))"
        for m in re.finditer(pattern, text, re.M):
            mods.append(m.group(1) or m.group(2))
    libs = []
    for m in mods:
        if m in STD or is_local(m, full, root_folder):
            continue
        name = ALIASES.get(m, m)
        if name not in libs:
            libs.append(name)
    libs.sort(key=str.lower)
    return "Libs: " + (", ".join(libs) if libs else "doar biblioteca standard")
class App:
    def __init__(self, root):
        self.root = root
        root.title("Python Scripts Manager")
        root.geometry("900x600")
        self.scripts = []
        self.files = {}
        self.root_folder = ""
        self.font = tkfont.Font(family="Arial", size=FONT_SIZE)
        style = ttk.Style()
        style.configure("Treeview", font=self.font, rowheight=self.font.metrics("linespace") + 14)
        style.configure("Treeview.Heading", font=(self.font.actual("family"), FONT_SIZE, "bold"))
        top = tk.Frame(root)
        top.pack(fill="x", padx=6, pady=4)
        tk.Label(top, text="Folder:").pack(side="left")
        self.path_var = tk.StringVar(value=DEFAULT_ROOT)
        tk.Entry(top, textvariable=self.path_var, font=self.font).pack(side="left", fill="x", expand=True, padx=4)
        tk.Button(top, text="Scan", command=self.load_scripts).pack(side="left")
        search = tk.Frame(root)
        search.pack(fill="x", padx=6)
        tk.Label(search, text="Caută:").pack(side="left")
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *a: self.refresh_table())
        tk.Entry(search, textvariable=self.search_var, font=self.font).pack(side="left", fill="x", expand=True, padx=4)
        frame = tk.Frame(root)
        frame.pack(fill="both", expand=True, padx=6, pady=4)
        self.tree = ttk.Treeview(frame, columns=("name", "rel", "desc"), show="headings", selectmode="browse")
        self.tree.heading("name", text="Script")
        self.tree.heading("rel", text="Path relativ")
        self.tree.heading("desc", text="Descriere")
        self.tree.column("name", width=180)
        self.tree.column("rel", width=300)
        self.tree.column("desc", width=300)
        self.tree.tag_configure("auto", foreground="gray")
        vsb = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
        hsb = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
        self.tree.grid(row=0, column=0, sticky="nsew")
        vsb.grid(row=0, column=1, sticky="ns")
        hsb.grid(row=1, column=0, sticky="ew")
        frame.rowconfigure(0, weight=1)
        frame.columnconfigure(0, weight=1)
        self.tree.bind("", lambda e: self.open_detail())
        btns = tk.Frame(root)
        btns.pack(fill="x", padx=6, pady=4)
        tk.Button(btns, text="Run", command=self.run_script, bg="#2196F3", fg="white", width=8).pack(side="left", padx=2)
        tk.Button(btns, text="Copy path", command=self.copy_path, width=8).pack(side="left", padx=2)
        tk.Button(btns, text="Detalii", command=self.open_detail, width=8).pack(side="left", padx=2)
        tk.Button(btns, text="Auto toate", command=self.auto_all, width=8).pack(side="left", padx=2)
        self.status = tk.Label(root, text="Ready", anchor="w")
        self.status.pack(fill="x", padx=6, pady=2)
        self.load_scripts()
    def primary_file(self):
        return os.path.join(self.root_folder, CATALOG_NAME)
    def fallback_file(self):
        h = hashlib.md5(self.root_folder.encode("utf-8", "replace")).hexdigest()[:8]
        return os.path.join(BASE_DIR, "catalog_" + h + ".json")
    def save_catalog(self):
        payload = {"version": 1, "root": self.root_folder, "files": self.files}
        for path in (self.primary_file(), self.fallback_file()):
            try:
                with open(path, "w", encoding="utf-8") as f:
                    json.dump(payload, f, indent=2, ensure_ascii=False)
                return path
            except OSError:
                continue
        messagebox.showerror("Eroare", "Nu pot salva catalogul.")
        return None
    def load_scripts(self):
        folder = self.path_var.get().strip()
        if not os.path.isdir(folder):
            messagebox.showerror("Eroare", "Folder inexistent:\n" + safe(folder))
            return
        self.root_folder = folder
        data = read_json(self.primary_file()) or read_json(self.fallback_file()) or {}
        old = data.get("files", {})
        found = []
        for dirpath, dirs, files in os.walk(folder):
            dirs[:] = [d for d in dirs if d != "__pycache__"]
            for file in files:
                if file.endswith(".py"):
                    full = os.path.join(dirpath, file)
                    rel = os.path.relpath(full, folder).replace("\\", "/")
                    found.append((file, rel, full))
        found.sort(key=lambda s: s[1].lower())
        found_rels = set(s[1] for s in found)
        orphans = {r: e for r, e in old.items() if r not in found_rels and e.get("desc")}
        new_files = {}
        for name, rel, full in found:
            try:
                st = os.stat(full)
                size, mtime = st.st_size, int(st.st_mtime)
            except OSError:
                size, mtime = 0, 0
            entry = old.get(rel)
            if entry is None:
                entry = {"desc": ""}
                for orel, oe in list(orphans.items()):
                    if os.path.basename(orel) == name and oe.get("size") == size:
                        entry = {"desc": oe.get("desc", ""), "auto": oe.get("auto", "")}
                        del orphans[orel]
                        break
            entry = dict(entry)
            entry["size"] = size
            entry["mtime"] = mtime
            new_files[rel] = entry
        new_files.update(orphans)
        self.files = new_files
        self.scripts = found
        saved = self.save_catalog()
        self.refresh_table()
        if saved:
            self.status.config(text=str(len(found)) + " scripturi | catalog: " + safe(saved))
    def refresh_table(self):
        q = self.search_var.get().strip().lower()
        self.tree.delete(*self.tree.get_children())
        shown = 0
        for i, (name, rel, full) in enumerate(self.scripts):
            entry = self.files.get(rel, {})
            desc = entry.get("desc", "")
            auto = entry.get("auto", "")
            text = desc if desc else ("~ " + auto if auto else "")
            if q and q not in name.lower() and q not in rel.lower() and q not in text.lower():
                continue
            tags = ("auto",) if (not desc and auto) else ()
            self.tree.insert("", "end", iid=str(i), values=(safe(name), safe(rel), safe(text)), tags=tags)
            shown += 1
        if q:
            self.status.config(text=str(shown) + " din " + str(len(self.scripts)) + " scripturi")
    def selected(self):
        sel = self.tree.selection()
        if not sel:
            messagebox.showinfo("Info", "Selectează un script din listă.")
            return None
        return int(sel[0])
    def open_detail(self):
        idx = self.selected()
        if idx is None:
            return
        name, rel, full = self.scripts[idx]
        entry = self.files.setdefault(rel, {})
        win = tk.Toplevel(self.root)
        win.title("Detalii")
        win.transient(self.root)
        w = max(self.root.winfo_width(), 320)
        win.geometry(str(w) + "x" + str(max(self.root.winfo_height() // 2, 280)))
        tk.Label(win, text=safe(rel), wraplength=w - 20, justify="left", anchor="w").pack(fill="x", padx=8, pady=4)
        bar = tk.Frame(win)
        bar.pack(fill="x", padx=8)
        box = tk.Text(win, wrap="word", height=8, font=self.font)
        box.pack(fill="both", expand=True, padx=8, pady=6)
        box.insert("1.0", entry.get("desc") or entry.get("auto", ""))
        def do_auto():
            if box.get("1.0", "end").strip():
                if not messagebox.askyesno("Auto detect", "Înlocuiesc textul din câmp?", parent=win):
                    return
            info = analyze(full, self.root_folder)
            entry["auto"] = info
            box.delete("1.0", "end")
            box.insert("1.0", info)
        def do_update():
            entry["desc"] = box.get("1.0", "end").strip()
            self.save_catalog()
            self.refresh_table()
            win.destroy()
        tk.Button(bar, text="Auto detect", command=do_auto, width=10).pack(side="left", padx=2)
        tk.Button(bar, text="Update", command=do_update, bg="#4CAF50", fg="white", width=8).pack(side="left", padx=2)
        tk.Button(bar, text="Închide", command=win.destroy, width=8).pack(side="left", padx=2)
        win.grab_set()
    def auto_all(self):
        total = len(self.scripts)
        if not total:
            return
        if not messagebox.askyesno("Auto toate", "Regenerez Libs pentru " + str(total) + " scripturi?\nDescrierile tale nu se modifică."):
            return
        for n, (name, rel, full) in enumerate(self.scripts, 1):
            self.files.setdefault(rel, {})["auto"] = analyze(full, self.root_folder)
            if n % 5 == 0:
                self.status.config(text="Analiză " + str(n) + "/" + str(total))
                self.root.update_idletasks()
        self.save_catalog()
        self.refresh_table()
        self.status.config(text="Libs generat pentru " + str(total) + " scripturi")
    def run_script(self):
        idx = self.selected()
        if idx is None:
            return
        full = self.scripts[idx][2]
        try:
            subprocess.Popen([sys.executable, full], cwd=os.path.dirname(full))
            self.status.config(text="Rulat: " + safe(os.path.basename(full)))
        except Exception as e:
            messagebox.showerror("Eroare", "Eroare la rulare:\n" + safe(e))
    def copy_path(self):
        idx = self.selected()
        if idx is None:
            return
        full = self.scripts[idx][2]
        self.root.clipboard_clear()
        self.root.clipboard_append(full)
        self.status.config(text="Copiat: " + safe(full))
if __name__ == "__main__":
    root = tk.Tk()
    App(root)
    root.mainloop()

Python 3.13.13 : Exemple with random and string.

See this source code:
import random
import string

# Generate a random password of length 10
def generate_password(length):
    characters = string.ascii_letters + string.digits + string.punctuation
    return ''.join(random.choices(characters, k=length))


password = generate_password(10)
print(password)