analitics

Pages

Saturday, May 30, 2026

News : ... Django 6.1 alpha 1 released by Jacob Walls on May 20, 2026!

Django 6.1 alpha 1 is now available. It represents the first stage in the 6.1 release cycle and is an opportunity to try out the changes coming in Django 6.1.
Django 6.1 offers a harmonious mélange of new features and usability improvements, which you can read about in the in-development 6.1 release notes.
This alpha milestone marks the feature freeze. The current release schedule calls for a beta release in about a month and a release candidate roughly a month after that. We'll only be able to keep this schedule with early and frequent testing from the community. Updates on the release schedule are available on the Django forum.

Python Qt : Testing new google_news_api released May 22, 2026.

Today, this script will help you too search and find results with google_news_api python package, released: May 22, 2026 by Paolo Mazza. Use your keywords, or make drag and drop ... See the official website - pypi.org.
import sys
import asyncio
import webbrowser
from functools import partial

from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QListWidget,
QListWidgetItem, QPushButton, QTextEdit, QLineEdit, QLabel,
QProgressBar, QFileDialog, QDialog
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal

from google_news_api import AsyncGoogleNewsClient


# ---------------------------------------------------------
# Worker Thread for Async Google News
# ---------------------------------------------------------
class NewsWorker(QThread):
progress = pyqtSignal(int)
finished = pyqtSignal(list)

def __init__(self, queries):
super().__init__()
self.queries = queries

async def fetch_news(self):
results = []
async with AsyncGoogleNewsClient(language="en", country="US") as client:
batch = await client.batch_search(
queries=self.queries,
when="7d",
max_results=10
)

for idx, (topic, articles) in enumerate(batch.items()):
for article in articles:
url = await client.decode_url(article["link"])
results.append({
"title": article["title"],
"source": article["source"],
"url": url
})

self.progress.emit(int((idx + 1) / len(batch) * 100))

return results

def run(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
results = loop.run_until_complete(self.fetch_news())
self.finished.emit(results)


# ---------------------------------------------------------
# HTML Results Dialog
# ---------------------------------------------------------
class ResultsDialog(QDialog):
def __init__(self, results):
super().__init__()
self.setWindowTitle("Search Results")

layout = QVBoxLayout(self)

self.text = QTextEdit()
self.text.setReadOnly(True)
layout.addWidget(self.text)

html = "<h2>Search Results</h2>"
for r in results:
html += f"""
<p>
<b>{r['title']}</b><br>
<i>{r['source']}</i><br>
<a href="{r['url']}">{r['url']}</a>
</p>
"""

self.text.setHtml(html)

save_btn = QPushButton("Save Results")
save_btn.clicked.connect(lambda: self.save_results(html))
layout.addWidget(save_btn)

def save_results(self, html):
file, _ = QFileDialog.getSaveFileName(self, "Save HTML", "", "HTML Files (*.html)")
if not file:
return
with open(file, "w", encoding="utf-8") as f:
f.write(html)


# ---------------------------------------------------------
# Main GUI
# ---------------------------------------------------------
class NewsGUI(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Google News Search Tool")
self.resize(900, 600)

main = QHBoxLayout(self)

# ---------------- LEFT: predefined keywords ----------------
left_layout = QVBoxLayout()
left_layout.addWidget(QLabel("Predefined Keywords"))

self.left_list = QListWidget()
self.left_list.setDragEnabled(True)
self.left_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection)

predefined = [
"inteligență artificială", "python", "godot engine", "shaders",
"2D", "3D", "C#", "programming", "catafest",
"Catalin George Festila", "Fălticeni", "Suceava",
"investiții", "2026"
]

for word in predefined:
self.left_list.addItem(word)

left_layout.addWidget(self.left_list)

# Add custom keyword
self.add_keyword_input = QLineEdit()
self.add_keyword_input.setPlaceholderText("Add new keyword...")
left_layout.addWidget(self.add_keyword_input)

add_btn = QPushButton("Add Keyword")
add_btn.clicked.connect(self.add_keyword)
left_layout.addWidget(add_btn)

main.addLayout(left_layout)

# ---------------- RIGHT: search keywords ----------------
right_layout = QVBoxLayout()
right_layout.addWidget(QLabel("Search Keywords (drag from left)"))

self.right_list = QListWidget()
self.right_list.setAcceptDrops(True)
self.right_list.setDragEnabled(True)
self.right_list.setDefaultDropAction(Qt.DropAction.MoveAction)
right_layout.addWidget(self.right_list)

# Custom search input
self.custom_query = QLineEdit()
self.custom_query.setPlaceholderText("Custom search query...")
right_layout.addWidget(self.custom_query)

# Progress bar
self.progress = QProgressBar()
right_layout.addWidget(self.progress)

# Search button
search_btn = QPushButton("Process Search")
search_btn.clicked.connect(self.start_search)
right_layout.addWidget(search_btn)

main.addLayout(right_layout)

# ---------------------------------------------------------
# Add keyword
# ---------------------------------------------------------
def add_keyword(self):
text = self.add_keyword_input.text().strip()
if text:
self.left_list.addItem(text)
self.add_keyword_input.clear()

# ---------------------------------------------------------
# Start search
# ---------------------------------------------------------
def start_search(self):
queries = []

# from right canvas
for i in range(self.right_list.count()):
queries.append(self.right_list.item(i).text())

# custom query
if self.custom_query.text().strip():
queries.append(self.custom_query.text().strip())

if not queries:
QMessageBox.warning(self, "Warning", "No search keywords selected.")
return

self.worker = NewsWorker(queries)
self.worker.progress.connect(self.progress.setValue)
self.worker.finished.connect(self.show_results)
self.worker.start()

# ---------------------------------------------------------
# Show results dialog
# ---------------------------------------------------------
def show_results(self, results):
dlg = ResultsDialog(results)
dlg.exec()


# ---------------------------------------------------------
# MAIN
# ---------------------------------------------------------
if __name__ == "__main__":
app = QApplication(sys.argv)
gui = NewsGUI()
gui.show()
sys.exit(app.exec())
This is the result:

Friday, May 29, 2026

Python Qt : simple tool for audio dialogue in game development

Today, this script tool is a small desktop tool that lets you visually synchronize spoken audio with written dialogue. You load an audio file and the program generates a waveform so you can click or drag to select exact time ranges. At the same time, you select the matching text, and the tool creates timestamped dialogue segments.
Each segment includes a dialog ID, start time, end time, and the associated text. Segments appear in a list, can be played individually, deleted, or tested in a separate window where each dialog ID becomes a playback button. This makes it easy to verify timing and structure.
The entire project—audio path, full text, and all segments—can be saved or loaded as a JSON file. Older JSON formats containing only segments are automatically converted. The final JSON is ready for use in game engines like Godot or Unity for precise voice‑over playback.
I used artificial intelligence to fix some issues, this is the result:

Sunday, May 24, 2026

Python Qt : testing the SUA library using the pytrends and plotly.

People’s interactions can serve as a foundation for certain studies; here is a simple way to process data at low cost using Google Trends and lightweight Python packages such as SUA.
The script retrieves Google Trends search‑interest data for several Romanian economic keywords (such as munca, recesiune, somaj, inflatie, dobanzi) and I used copilot and gemini artificial intelligence. The copilot comes with bad result for fast asking simple scripts. It analyzes the data using the SUA library to estimate simple metrics like interest growth and volatility, then displays the results in a PyQt6 interactive chart where the X‑axis shows real calendar dates and the Y‑axis shows the Google Trends 0–100 normalized interest scale.
However, the script is limited by Google Trends constraints:
Google Trends allows maximum 5 keywords per request, so the script cannot query more terms at once.
Google Trends treats diacritic and non‑diacritic words as different searches (e.g., șomaj ≠ somaj), so the script uses non‑diacritic versions to avoid errors and improve compatibility.
Google Trends returns normalized values (0–100), not real search counts, meaning the data shows relative interest, not absolute volume.
The time series resolution is fixed by Google (weekly data for 12 months), so the X‑axis length depends on how many points Google provides.
These limitations come from Google Trends itself, not from the script.
python -m pip install sua
Collecting sua
...
Successfully installed MarkupSafe-3.0.3 altair-6.1.0 asttokens-3.0.1 attrs-26.1.0 beautifulsoup4-4.14.3 blinker-1.9.0 
cachetools-7.1.4 cffi-2.0.0 clarabel-0.11.1 click-8.4.1 cloudpickle-3.1.2 cmdstanpy-1.3.0 colorama-0.4.6 contourpy-1.3.2 
curl_cffi-0.15.0 cvxpy-1.7.5 cycler-0.12.1 darts-0.44.1 datetime-6.0 decorator-5.3.1 empyrical-0.5.5 executing-2.2.1 
fonttools-4.63.0 fpdf-1.7.2 gitdb-4.0.12 gitpython-3.1.50 holidays-0.97 httptools-0.7.1 importlib_resources-7.1.0 
ipython-8.39.0 itsdangerous-2.2.0 jedi-0.20.0 jinja2-3.1.6 joblib-1.5.3 jsonschema-4.26.0 jsonschema-specifications-2025.9.1 
kiwisolver-1.5.0 llvmlite-0.47.0 lxml-6.1.1 markdown-it-py-4.2.0 matplotlib-3.10.9 matplotlib-inline-0.2.2 mdurl-0.1.2 
multitasking-0.0.13 narwhals-2.21.2 nfoursid-1.0.2 numba-0.65.1 numpy-2.2.6 osqp-1.1.1 pandas-2.3.3 pandas-datareader-0.10.0 
parso-0.8.7 patsy-1.0.2 peewee-4.0.6 pillow-12.2.0 platformdirs-4.9.6 prompt_toolkit-3.0.52 prophet-1.3.0 protobuf-7.35.0 
pure-eval-0.2.3 pyarrow-24.0.0 pycparser-3.0 pydeck-0.9.2 pygments-2.20.0 pyod-3.5.2 pyparsing-3.3.2 pyportfolioopt-1.6.0 
python-dateutil-2.9.0.post0 python-multipart-0.0.29 pytz-2026.2 quantstats-0.0.81 referencing-0.37.0 rich-15.0.0 rpds-py-0.30.0 
scikit-base-0.13.2 scikit-learn-1.7.2 scipy-1.15.3 scs-3.2.11 seaborn-0.13.2 shap-0.49.1 six-1.17.0 slicer-0.0.8 smmap-5.0.3 
soupsieve-2.8.3 stack_data-0.6.3 stanio-0.5.1 starlette-1.1.0 statsmodels-0.14.6 streamlit-1.57.0 sua-1.1.5.1 tabulate-0.10.0 
tenacity-9.1.4 threadpoolctl-3.6.0 toml-0.10.2 tqdm-4.67.3 traitlets-5.15.0 tzdata-2026.2 uvicorn-0.48.0 watchdog-6.0.0 
wcwidth-0.7.0 websockets-16.0 xarray-2025.6.1 yfinance-1.4.0 zope.interface-8.4
python -m pip install pytrends
Collecting pytrends
...
Installing collected packages: pytrends
Successfully installed pytrends-4.9.2
python -m pip install plotly
Collecting plotly
...
Installing collected packages: plotly
...
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed plotly-6.7.0
Now, the plotty use an little tool and this want to access my Scripts folder by environment variables.
If you got this error, then you need to limit the words, because Google returns a 400 Bad Request when your request is not valid for the internal Google Trends API.
    raise exceptions.ResponseError.from_response(response)
pytrends.exceptions.ResponseError: The request failed: Google returned a response with code 400
Let's run teh python script and output is this:
python main.py
Average interest growth (returns):
munca: 0.0072
recesiune: 0.3387
somaj: 0.0308
inflatie: 0.0660
dobanzi: 0.0157

Volatility (risk):
munca: 0.0256
recesiune: 2.9473
somaj: 0.1348
inflatie: 0.1607
dobanzi: 0.0527
The PyQt6 will create this bad result, what 1970 ?:
PyQt_Google_trends_May_2026_2025_bad
The issue in your script occurs because PyQt’s QDateTimeAxis expects the X‑axis timestamp to be expressed in milliseconds (since the Unix epoch), while Python’s .timestamp() function returns the value in seconds. When you send seconds instead of milliseconds, PyQt interprets a value that is 1000 times smaller, which places all chart points somewhere at the beginning of 1970 (right after January 1st, 1970).
Let's fix this issue:
from pytrends.request import TrendReq
# Din moment ce modulele de mai jos erau importate dar nefolosite în codul tău, 
# m-am asigurat că restul rulării rămâne intactă.
try:
    from sua import expected_returns, risk_models
except ImportError:
    pass

from PyQt6.QtWidgets import QApplication, QMainWindow
from PyQt6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis
from PyQt6.QtGui import QPainter
from PyQt6.QtCore import QPointF, QDateTime, Qt
import sys

# -----------------------------
# 1. Keywords (fără diacritice)
# -----------------------------
keywords = ["munca", "recesiune", "somaj", "inflatie", "dobanzi"]

# -----------------------------
# 2. Preluare date Pytrends
# -----------------------------
pytrends = TrendReq(hl='ro-RO', tz=180)
pytrends.build_payload(keywords, timeframe='today 12-m', geo='RO')

raw = pytrends.interest_over_time()

# Convertim în structuri simple
dates = list(raw.index)  # Python datetime objects
data = {k: list(raw[k]) for k in keywords}

# -----------------------------
# 3. Calcul SUA (randament + risc)
# -----------------------------
returns = {k: [] for k in keywords}

for k in keywords:
    series = data[k]
    for i in range(1, len(series)):
        prev = series[i-1] or 1
        curr = series[i] or 1
        returns[k].append((curr - prev) / prev)

mu = {k: sum(returns[k]) / len(returns[k]) for k in keywords}

def covariance(a, b):
    mean_a = sum(a) / len(a)
    mean_b = sum(b) / len(b)
    return sum((a[i]-mean_a)*(b[i]-mean_b) for i in range(len(a))) / len(a)

S = {k: covariance(returns[k], returns[k]) for k in keywords}

print("Average interest growth (returns):")
for k, v in mu.items():
    print(f"{k}: {v:.4f}")

print("\nVolatility (risk):")
for k, v in S.items():
    print(f"{k}: {v:.4f}")

# -----------------------------
# 4. PyQt6 Chart GUI cu date reale (CORECTAT)
# -----------------------------
class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Google Trends Economic Interest (RO)")

        chart = QChart()
        chart.setTitle("Interest Over Time (Google Trends Romania)")

        # Axa X = date reale
        axis_x = QDateTimeAxis()
        axis_x.setFormat("yyyy-MM-dd")
        axis_x.setTitleText("Date")
        axis_x.setTickCount(10)

        # !!! CORECȚIE 1: Să îi spunem axei care este limita minimă și maximă reală !!!
        # Conversie din primul și ultimul obiect datetime din Pytrends în QDateTime
        qt_start_date = QDateTime.fromMSecsSinceEpoch(int(dates[0].timestamp() * 1000))
        qt_end_date = QDateTime.fromMSecsSinceEpoch(int(dates[-1].timestamp() * 1000))
        axis_x.setRange(qt_start_date, qt_end_date)

        # Axa Y = valori 0–100
        axis_y = QValueAxis()
        axis_y.setRange(0, 100)
        axis_y.setTitleText("Interest (0–100)")

        chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom)
        chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft)

        # Adăugăm seriile
        for k in keywords:
            series = QLineSeries()
            series.setName(k)

            for i, val in enumerate(data[k]):
                # !!! CORECȚIE 2: Înmulțim cu 1000 pentru a transforma în milisecunde !!!
                timestamp_ms = int(dates[i].timestamp() * 1000)
                series.append(QPointF(float(timestamp_ms), float(val)))

            chart.addSeries(series)
            series.attachAxis(axis_x)
            series.attachAxis(axis_y)

        view = QChartView(chart)
        view.setRenderHint(QPainter.RenderHint.Antialiasing)

        self.setCentralWidget(view)

app = QApplication(sys.argv)
window = Window()
window.resize(1400, 700)
window.show()
sys.exit(app.exec())
This result is good and is fixed by Gemnini artificial intelligence, but if you want real development then you need to use more then simple issue to artificial intelligence:
python main.py
Average interest growth (returns):
munca: 0.0073
recesiune: 0.3387
somaj: 0.0308
inflatie: 0.0660
dobanzi: 0.0189

Volatility (risk):
munca: 0.0258
recesiune: 2.9473
somaj: 0.1348
inflatie: 0.1607
dobanzi: 0.0594
PyQt_Google_trends_May_2026_2025_good

Thursday, May 21, 2026

Python 3.10.11 : testing zernio social platform with python.

Today, I get two free account on the zernio webpage.
This python script does three things:
First calls Zernio by sends GET https://zernio.com/api/v1/accounts using your Bearer token and loads the JSON response.
Then filters YouTube accounts: from the returned accounts[] list, it keeps only the items that look like YouTube (based on fields like platform/provider/network == "youtube").
Last one, prints and searches identifiers:
It prints the full JSON object for each connected YouTube account (first ~4000 characters), so you can see what fields Zernio returns.
It recursively scans that object for any string that matches a YouTube Channel ID pattern (strings starting with UC...) and prints the path and value for each match (e.g., meta.channelId = UCxxxx).
The result returns by printing to the console:
how many YouTube accounts Zernio returned.
the JSON for each YouTube account object.
a list of candidate channelId strings (and where they appear in the JSON).
Let's see the script:
import requests
import json
import re

ZERNIO_TOKEN = "sk_API_KEY"
API_BASE = "https://zernio.com/api/v1"

headers = {
    "Authorization": f"Bearer {ZERNIO_TOKEN}",
    "Accept": "application/json",
}

def is_youtube_account(acc: dict) -> bool:
    for k in ("platform", "provider", "network"):
        v = acc.get(k)
        if isinstance(v, str) and v.lower() == "youtube":
            return True
    return False

def find_uc_strings(obj, path=""):
    hits = []
    if isinstance(obj, dict):
        for k, v in obj.items():
            hits += find_uc_strings(v, f"{path}.{k}" if path else k)
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            hits += find_uc_strings(v, f"{path}[{i}]")
    elif isinstance(obj, str):
        # Typical YouTube channel id: starts with UC and is 24 chars, but we’ll be flexible
        if re.match(r"^UC[a-zA-Z0-9_-]{10,}$", obj):
            hits.append((path, obj))
    return hits

resp = requests.get(f"{API_BASE}/accounts", headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()

accounts = data.get("accounts", [])
yt = [a for a in accounts if is_youtube_account(a)]

print(f"Found {len(yt)} YouTube accounts in Zernio.\n")

for idx, acc in enumerate(yt):
    print(f"--- YouTube account #{idx} full object ---")
    print(json.dumps(acc, ensure_ascii=False, indent=2)[:4000])  # first 4000 chars
    print()

    hits = find_uc_strings(acc)
    print("Possible channelId candidates (paths):")
    for p, v in hits:
        print(f"  - {p} = {v}")
    print()
This is a part of result:
...
  "displayName": "Cătălin George Feștilă",
  "enabled": true,
  "externalPostCount": 63,
  "followersCount": 110,
  "followersLastUpdated": "2026-05-21T18:31:51.857Z",
  "gcpProjectId": "default",
  "intentionalDisconnectAt": null,
  "isActive": true,
...
Possible channelId candidates (paths):
  - metadata.profileData.id = UC2Dv01HhPCb8Obb9IxO81Jw
  - platformUserId = UC2Dv01HhPCb8Obb9IxO81Jw

Sunday, May 17, 2026

Python Qt : network tool with PyQt6-Charts.

Today, this python script will create a tool for network with PyQt6-Charts.
You need to install the PyQt6-Charts:
python.exe -m pip install PyQt6-Charts
Collecting PyQt6-Charts
...
Installing collected packages: PyQt6-Charts-Qt6, PyQt6-Charts
Successfully installed PyQt6-Charts-6.11.0 PyQt6-Charts-Qt6-6.11.1
Let's see the python script.
import sys
import psutil
from pathlib import Path

from PyQt6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
    QTableWidget, QTableWidgetItem, QHeaderView
)
from PyQt6.QtCore import QTimer, Qt
from PyQt6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis


def bytes_to_human(n: int) -> str:
    symbols = ('B', 'KB', 'MB', 'GB', 'TB')
    prefix = {}
    for i, s in enumerate(symbols[1:], 1):
        prefix[s] = 1 << (i * 10)
    for s in reversed(symbols[1:]):
        if n >= prefix[s]:
            value = float(n) / prefix[s]
            return f"{value:.2f} {s}"
    return f"{n} B"


class NetworkMonitor(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Network Monitor – PyQt6 (Traffic + Ports)")
        self.resize(1000, 650)

        main_layout = QVBoxLayout(self)

        # ---------------- TOP LABELS ----------------
        self.label_up = QLabel("Upload: 0 B/s")
        self.label_down = QLabel("Download: 0 B/s")

        top_layout = QHBoxLayout()
        top_layout.addWidget(self.label_up)
        top_layout.addWidget(self.label_down)
        main_layout.addLayout(top_layout)

        # ---------------- CHART ----------------
        self.series_up = QLineSeries()
        self.series_down = QLineSeries()
        self.series_up.setName("Upload")
        self.series_down.setName("Download")

        self.chart = QChart()
        self.chart.addSeries(self.series_up)
        self.chart.addSeries(self.series_down)
        self.chart.setTitle("Network traffic (bytes/sec)")

        # Axes (Qt6 style)
        self.axis_x = QValueAxis()
        self.axis_y = QValueAxis()

        self.axis_x.setRange(0, 60)
        self.axis_y.setRange(0, 1024 * 1024)  # 1 MB/s default
        self.axis_x.setTitleText("Time (s)")
        self.axis_y.setTitleText("Bytes / second")

        self.chart.addAxis(self.axis_x, Qt.AlignmentFlag.AlignBottom)
        self.chart.addAxis(self.axis_y, Qt.AlignmentFlag.AlignLeft)

        self.series_up.attachAxis(self.axis_x)
        self.series_up.attachAxis(self.axis_y)
        self.series_down.attachAxis(self.axis_x)
        self.series_down.attachAxis(self.axis_y)

        self.chart_view = QChartView(self.chart)
        main_layout.addWidget(self.chart_view)

        # ---------------- PORTS TABLE ----------------
        self.table = QTableWidget(0, 5)
        self.table.setHorizontalHeaderLabels(
            ["Local IP:Port", "Remote IP:Port", "Status", "PID", "Process"]
        )
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)

        # 🔥 Sortare activată
        self.table.setSortingEnabled(True)

        main_layout.addWidget(self.table)

        # ---------------- STATE ----------------
        self.old_stats = psutil.net_io_counters()
        self.x_pos = 0

        # ---------------- TIMER ----------------
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_stats)
        self.timer.start(1000)  # 1s

    # ---------------------------------------------------------
    def update_stats(self):
        # --- traffic ---
        new_stats = psutil.net_io_counters()
        sent = new_stats.bytes_sent - self.old_stats.bytes_sent
        recv = new_stats.bytes_recv - self.old_stats.bytes_recv
        self.old_stats = new_stats

        self.label_up.setText(f"Upload: {bytes_to_human(sent)}/s")
        self.label_down.setText(f"Download: {bytes_to_human(recv)}/s")

        # --- chart data ---
        self.series_up.append(self.x_pos, sent)
        self.series_down.append(self.x_pos, recv)
        self.x_pos += 1

        # keep last 60 seconds visible
        if self.x_pos > 60:
            self.axis_x.setRange(self.x_pos - 60, self.x_pos)

        # auto-scale Y a bit
        max_val = max(sent, recv, 1)
        current_max = self.axis_y.max()
        if max_val > current_max * 0.9:
            self.axis_y.setRange(0, max_val * 1.5)

        # --- ports ---
        self.update_ports()

    # ---------------------------------------------------------
    def update_ports(self):
        conns = psutil.net_connections(kind="tcp")

        self.table.setSortingEnabled(False)  # prevenim flicker
        self.table.setRowCount(0)

        for c in conns:
            if not c.laddr:
                continue

            row = self.table.rowCount()
            self.table.insertRow(row)

            # Local
            local = f"{c.laddr.ip}:{c.laddr.port}"
            item_local = QTableWidgetItem(local)
            item_local.setData(Qt.ItemDataRole.UserRole, c.laddr.port)

            # Remote
            if c.raddr:
                remote = f"{c.raddr.ip}:{c.raddr.port}"
                remote_port = c.raddr.port
            else:
                remote = "-"
                remote_port = -1

            item_remote = QTableWidgetItem(remote)
            item_remote.setData(Qt.ItemDataRole.UserRole, remote_port)

            # Status
            item_status = QTableWidgetItem(c.status)

            # PID
            pid = c.pid if c.pid else -1
            item_pid = QTableWidgetItem(str(pid))
            item_pid.setData(Qt.ItemDataRole.UserRole, pid)

            # Process name
            proc_name = "-"
            if c.pid:
                try:
                    proc_name = psutil.Process(c.pid).name()
                except:
                    proc_name = "?"

            item_proc = QTableWidgetItem(proc_name)

            self.table.setItem(row, 0, item_local)
            self.table.setItem(row, 1, item_remote)
            self.table.setItem(row, 2, item_status)
            self.table.setItem(row, 3, item_pid)
            self.table.setItem(row, 4, item_proc)

        self.table.setSortingEnabled(True)  # reactivăm sortarea


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = NetworkMonitor()
    w.show()
    sys.exit(app.exec())

Saturday, May 16, 2026

Python 3.10.11 : about the windows embeddable portable Python distributions fix pip.

Let's learn about the windows embeddable portable Python distributions.
  • The Windows embeddable Python distribution is a minimal, self‑contained build of Python designed to run entirely from its own directory without installation.
  • This distribution does not modify system settings, environment variables, or the Windows registry.
  • Its structure makes it suitable for embedding Python inside applications or distributing Python as a portable runtime.
Typical use cases
  • Bundling Python with standalone software that requires a predictable runtime environment.
  • Running Python scripts in isolated environments where system‑wide installations must not be affected.
  • Deploying portable utilities that must operate from removable storage or restricted systems.
When the embeddable distribution is not ideal
  • General development workflows that rely on pip, external packages, or virtual environments.
  • Educational or experimental setups where tutorials assume a standard Python installation.
  • Projects that depend on automatic module discovery and dynamic package management.
The Role of python310._pth
  • The file named python310._pth controls how the embeddable distribution locates and loads Python modules.
  • When this file is present, Python enters an isolated mode in which only the paths explicitly listed inside the file are used.
  • If the file does not include the line import site, the standard site initialization process is disabled, preventing access to site‑packages.
Typical structure of python310._pth
python310.zip
.
import site
Explanation of each entry
  • python310.zip specifies the location of the standard library packaged as a zip archive.
  • . allows Python to import modules from the root directory of the distribution.
  • import site activates the site module, enabling automatic loading of Lib and site‑packages.
Enabling pip and external modules
  • The embeddable distribution does not load external modules unless the appropriate paths are added to python310._pth.
  • To enable pip and other installed packages, the file must include the Lib and Lib\site-packages directories.
Example of a Fully Enabled python310._pth
python310.zip
.
Lib
Lib\site-packages
import site
Testing the updated configuration
python -c "import sys; print(sys.path)"
Installing pip after enabling site‑packages
  • Once the module paths are active, pip can be installed using standard methods.
python get-pip.py
python -m ensurepip
Verifying pip
python -m pip --version
Advantages of the embeddable distribution:
  • Provides a predictable and isolated runtime environment.
  • Does not interfere with system‑wide Python installations.
  • Ideal for packaging Python with standalone applications.
Disadvantages of the embeddable distribution:
  • pip and external modules are disabled by default.
  • Requires manual configuration to behave like a standard installation.
  • Not suitable for typical development workflows.
Clean, Ready‑to‑Use python310._pth File
python310.zip
.
Lib
Lib\site-packages
import site
This will fix the embeddable distribution, let's use this source code to fix the pip tool:
import os
import urllib.request
import zipfile
import shutil

PYTHON_DIR = r"C:\python-3_10_11"
SITE = fr"{PYTHON_DIR}\Lib\site-packages"

print("[INFO] Descarc pip.zip...")
urllib.request.urlretrieve(
    "https://github.com/pypa/pip/archive/refs/heads/main.zip",
    "pip.zip"
)

print("[INFO] Dezarhivez pip.zip...")
with zipfile.ZipFile("pip.zip", "r") as z:
    z.extractall("pip_src")

pip_src = "pip_src/pip-main/src/pip"

print("[INFO] Copiez pip în site-packages...")
target = os.path.join(SITE, "pip")
if os.path.exists(target):
    shutil.rmtree(target)

shutil.copytree(pip_src, target)

print("[INFO] Creez pip.dist-info minimal...")
dist = os.path.join(SITE, "pip.dist-info")
os.makedirs(dist, exist_ok=True)

with open(os.path.join(dist, "METADATA"), "w") as f:
    f.write("Name: pip\nVersion: 0\n")

print("[OK] pip instalat direct în Python.")
print("Rulează acum:")
print("   python -m pip --version")
Let's tun and test with PyQt6:
python fix_pip.py
[INFO] Descarc pip.zip...
[INFO] Dezarhivez pip.zip...
[INFO] Copiez pip în site-packages...
[INFO] Creez pip.dist-info minimal...
[OK] pip instalat direct în Python.
Rulează acum:
   python -m pip --version

python -m pip --version
pip 26.2.dev0 from C:\python-3_10_11\Lib\site-packages\pip (python 3.10)

python -m pip install PyQt6
Collecting PyQt6
  Downloading pyqt6-6.11.0-cp310-abi3-win_amd64.whl.metadata (2.2 kB)
...
Installing collected packages: PyQt6-Qt6, PyQt6-sip, PyQt6
Successfully installed PyQt6-6.11.0 PyQt6-Qt6-6.11.1 PyQt6-sip-13.11.1

Thursday, May 14, 2026

News : Good news from the Python Software Foundation 14.05.2026

Good news from the Python Software Foundation official blogger.
  • May 13, 2026 – The Python Software Foundation (PSF) is excited to announce that Hudson River Trading (HRT), a global leader in quantitative trading, has made a commitment to support Python and the PSF as a Visionary Sponsor.
  • May 12, 2026 - Announcing PSF Community Service Award Recipients!
  • May 11, 2026 - Strategic Planning at the PSF
  • May 10, 2026 - Python 3.14.5 is out!
  • May 7, 2026 - Python 3.15.0 beta 1 is here!
  • May 4, 2026 - Python 3.14.5 release candidate

Thursday, May 7, 2026

Python Qt : simple tiktok downloader.

Today, simple example with PyQt6 and yt_dlp.
Get the link from tiktok browser and use it to download the video for your storage.
I used the Copilot tool. It seems to know the Romanian language. For a developer, comments and source code are not an impediment, because it is very simplistic.
Let's see the source code:
import sys
import os
import yt_dlp
from PyQt6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
    QLineEdit, QLabel, QFileDialog, QListWidget, QListWidgetItem,
    QProgressBar, QMessageBox
)
from PyQt6.QtCore import Qt

class TikTokDownloader(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("TikTok Downloader (yt-dlp)")
        self.setMinimumWidth(600)

        self.url_input = QLineEdit()
        self.url_input.setPlaceholderText("Introdu URL TikTok...")

        self.folder_input = QLineEdit()
        self.folder_input.setPlaceholderText("Folder download...")

        browse_btn = QPushButton("Selectează folder")
        browse_btn.clicked.connect(self.select_folder)

        fetch_btn = QPushButton("Caută stream-uri")
        fetch_btn.clicked.connect(self.fetch_streams)

        self.stream_list = QListWidget()

        download_btn = QPushButton("Descarcă")
        download_btn.clicked.connect(self.download_selected)

        self.progress = QProgressBar()
        self.progress.setValue(0)

        layout = QVBoxLayout()
        layout.addWidget(QLabel("URL TikTok:"))
        layout.addWidget(self.url_input)

        folder_layout = QHBoxLayout()
        folder_layout.addWidget(self.folder_input)
        folder_layout.addWidget(browse_btn)
        layout.addLayout(folder_layout)

        layout.addWidget(fetch_btn)
        layout.addWidget(QLabel("Stream-uri găsite:"))
        layout.addWidget(self.stream_list)
        layout.addWidget(download_btn)
        layout.addWidget(self.progress)

        self.setLayout(layout)

        self.streams = []
        self.selected_format = None

    def select_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "Selectează folder")
        if folder:
            self.folder_input.setText(folder)

    def fetch_streams(self):
        url = self.url_input.text().strip()
        if not url:
            QMessageBox.warning(self, "Eroare", "Introdu URL TikTok")
            return

        self.stream_list.clear()
        self.streams = []

        ydl_opts = {
            "quiet": True,
            "skip_download": True,
            "forcejson": True,
        }

        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                info = ydl.extract_info(url, download=False)

            formats = info.get("formats", [])

            for f in formats:
                desc = f"{f.get('format_id')} | {f.get('ext')} | {f.get('resolution', '')} | {f.get('filesize', 'N/A')}"
                item = QListWidgetItem(desc)
                self.stream_list.addItem(item)
                self.streams.append(f)

        except Exception as e:
            QMessageBox.critical(self, "Eroare", str(e))

    def progress_hook(self, d):
        if d["status"] == "downloading":
            if d.get("total_bytes"):
                pct = int(d["downloaded_bytes"] * 100 / d["total_bytes"])
                self.progress.setValue(pct)

        if d["status"] == "finished":
            self.progress.setValue(100)

    def download_selected(self):
        folder = self.folder_input.text().strip()
        if not folder:
            QMessageBox.warning(self, "Eroare", "Selectează folderul de download")
            return

        selected = self.stream_list.currentRow()
        if selected < 0:
            QMessageBox.warning(self, "Eroare", "Selectează un stream din listă")
            return

        fmt = self.streams[selected]
        url = self.url_input.text().strip()

        ydl_opts = {
            "outtmpl": os.path.join(folder, "%(id)s.%(ext)s"),
            "format": fmt["format_id"],
            "progress_hooks": [self.progress_hook],
        }

        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                ydl.download([url])

            QMessageBox.information(self, "Succes", "Download complet!")

        except Exception as e:
            QMessageBox.critical(self, "Eroare", str(e))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = TikTokDownloader()
    win.show()
    sys.exit(app.exec())

Wednesday, May 6, 2026

Python 3.13.0 : another tool with PyQt6 to build an NPC database.

... another tool for game development, see more on my youtube channel.

Python 3.13.0 : mimesis python example.

Mimesis is a powerful data generator for Python that can produce a wide range of fake data in multiple languages. This tool is useful for populating testing databases, creating fake API endpoints, generating custom structures in JSON and XML files, and anonymizing production data, among other things. With Mimesis, developers can obtain realistic, randomized data easily to facilitate development and testing.
This python module can be install with pip tool easy.
See this simple example:
from mimesis import Person, Address, Text, Datetime
from mimesis.enums import Gender
def test_mimesis(locale: str, sex: str):
    person = Person(locale)
    address = Address(locale)
    text = Text(locale)
    dt = Datetime()
    gender = Gender.MALE if sex == "male" else Gender.FEMALE
    data = {
        "first_name": person.first_name(gender=gender),
        "last_name": person.last_name(),
        "full_name": person.full_name(gender=gender),
        "email": person.email(),
        "telephone": person.telephone(),
        "occupation": person.occupation(),
        "address": address.address().replace("\n", ", "),
        "city": address.city(),
        "postal_code": address.postal_code(),
        "country": address.country(),
        "birth_date": dt.date(start=1970, end=2005),
        "bio": text.text(quantity=2),
    }
    return data
if __name__ == "__main__":
    npc = test_mimesis("en", "female")
    for k, v in npc.items():
        print(f"{k}: {v}")
The result is:
python_313 test_mimesis.py
first_name: Nisha
last_name: Rocha
full_name: Hertha Wynn
email: stakeholders2063@protonmail.com
telephone: +14172413972
occupation: Medical Physicist
address: 865 Cooper Highway
city: Elkhart
postal_code: 69168
country: France
birth_date: 2004-06-11
bio: It is also a garbage-collected runtime system. Do you come here often?

Tuesday, May 5, 2026

News : Pythono and nuitka best optimization.

Nuitka is the optimizing Python compiler written in Python that creates executables that run without a separate installer. Data files can both be included or put alongside.
You can read more on the official website.
The install is easy with pip tool, then you can use this command.
python -m nuitka --help
Usage: python.exe -m nuitka [--mode=compilation_mode] [--run] [options] main_module.py

    Note: For general plugin help (they often have their own
    command line options too), consider the output of
    '--help-plugins'.

Options:
  --help                show this help message and exit
  --version             Show version information and important details for bug
                        reports, then exit. Defaults to off.
This python package need to have the Visual Studio Build Tools 2022 and need to use:
  • Desktop development with C++
  • MSVC v143 build tools
  • Windows 10/11 SDK
  • C++ CMake tools
  • C++ ATL/MFC (optional but useful)
Let's install it.
I used this simple example:
import sys
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout

def main():
    app = QApplication(sys.argv)

    window = QWidget()
    window.setWindowTitle("PyQt6 + Nuitka Demo")

    layout = QVBoxLayout()
    label = QLabel("Hello! This is a PyQt6 application compiled with Nuitka.")
    layout.addWidget(label)

    window.setLayout(layout)
    window.show()

    sys.exit(app.exec())

if __name__ == "__main__":
    main()
Use this command to see all plugins:
nuitka --plugin-list
                 The following plugins are available in Nuitka
--------------------------------------------------------------------------------
 delvewheel        Required by 'delvewheel' using packages. (core)
 implicit-imports  Provide implicit imports of package as per package configuration files. (core) [auto-enabled]
 data-files        Include data files specified by package configuration files. (core, feature)
 dll-files         Include DLLs as per package configuration files. (core, feature)
 anti-bloat        Patch stupid imports out of widely used library modules source codes. (core, feature, package-support) [auto-enabled]
 options-nanny     Inform user about potential problems as per package configuration files. (core, package-support) [auto-enabled]
 pylint-warnings   Support PyLint / PyDev linting source markers. (feature)
 upx               Compress created binaries with UPX automatically. (integration)
 dill-compat       Required by 'dill' and 'cloudpickle' packages. (package-support) [has detector]
 eventlet          Required by 'eventlet' package. (package-support) [auto-enabled]
 gevent            Required by 'gevent' package. (package-support)
 gi                Required by 'gi' package. (package-support) [auto-enabled]
 glfw              Required by 'glfw' and 'PyOpenGL' packages. (package-support)
 kivy              Required by 'kivy' package. (package-support)
 matplotlib        Required by 'matplotlib' package. (package-support)
 multiprocessing   Required by 'multiprocessing' package. (package-support) [auto-enabled]
 no-qt             Disable inclusion of all Qt bindings. (package-support)
 pbr-compat        Required by 'pbr' package. (package-support)
 pkg-resources     Required by 'pkg_resources' package. (package-support) [auto-enabled]
 playwright        Required by 'playwright' package. (package-support)
 pmw-freezer       Required by 'Pmw' package. (package-support) [has detector]
 pywebview         Required by 'webview' package. (package-support)
 spacy             Required by 'spacy' package. (package-support)
 tk-inter          Required by 'tkinter' package. (package-support) [has detector]
 transformers      Required by 'transformers' package. (package-support) [auto-enabled]
 enum-compat       Required by 'enum' package on Python2. (package-support, python2)
 pyqt5             Required by 'PyQt5' package. (package-support, qt-binding) [has detector]
 pyqt6             Required by 'PyQt6' package. (package-support, qt-binding) [has detector]
 pyside2           Required by 'PySide2' package. (package-support, qt-binding) [has detector]
 pyside6           Required by 'PySide6' package. (package-support, qt-binding) [has detector]
... and standalone build process:
nuitka --standalone --enable-plugin=pyqt6 test_app.py
Nuitka-Options: Used command line options:
Nuitka-Options:   --standalone --enable-plugin=pyqt6 test_app.py
Nuitka-Plugins:pyqt6: Support for PyQt6 is not perfect, e.g. Qt threading does not work, so prefer
Nuitka-Plugins:pyqt6: PySide6 if you can.
Nuitka: Starting Python compilation with:
Nuitka:   Version '4.0.8' on Python 3.10 (flavor 'CPython Official')
Nuitka:   commercial grade 'not installed'.
Nuitka-Plugins:pyqt6: Including Qt plugins 'iconengines,imageformats,platforms,styles,tls' below
Nuitka-Plugins:pyqt6: 'PyQt6\Qt6\plugins'.
Nuitka: Completed Python level compilation and optimization.
Nuitka: Generating source code for C backend compiler.
Nuitka: Running data composer tool for optimal constant value handling.
Nuitka: Running C compilation via Scons.
Nuitka will use gcc from MinGW64 of winlibs to compile on Windows.

Is it OK to download and put it in local user cache.

Fully automatic, cached. Proceed and download? [Yes]/No :
Nuitka: Downloading
Nuitka: 'https://github.com/brechtsanders/winlibs_mingw/releases/download/14.2.0posix-19.1.1-12.0.0-msvcrt-r2/winlibs-x86_64-posix-seh-gcc-14.2.0-llvm-19.1.1-mingw-w64msvcrt-12.0.0-r2.zip'.
Nuitka: Extracting to
Nuitka: 'C:\Users\CATALI~1\AppData\Local\Nuitka\Nuitka\Cache\DOWNLO~1\gcc\x86_64\14.2.0posix-19.1.1-12.0.0-msvcrt-r2\mingw64\bin\gcc.exe'
Nuitka-Scons: Backend C compiler: gcc (gcc 14.2.0).
Nuitka-Scons: Backend C linking with 9 files (no progress information available for this stage).
Nuitka-Scons: Compiled 9 C files using ccache.
Nuitka-Scons: Cached C files (using ccache) with result 'cache miss': 9
Nuitka: Keeping build directory 'test_app.build'.
Nuitka: Successfully created 'C:\lucru\PyQt6\test_nuitka\test_app.dist\test_app.exe'.

Python Qt : python alias manager tool.

This script allow you to add alias commands on each python install.
NOTE : If this script find some python.exe and not works, it means that this python.exe is not a full Python installation, but only a launcher or an embedded runtime that is designed to run inside a virtual environment or a complete Python directory structure.
I used artificial intelligence, tested and works well, lets see the source code:
import sys
import os
import json
import subprocess
from PyQt6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QPushButton,
    QTableWidget, QTableWidgetItem, QFileDialog, QMessageBox
)
from PyQt6.QtCore import Qt

CONFIG_FILE = "python_aliases.json"
ALIAS_DIR = os.path.join(os.environ["LOCALAPPDATA"], "Microsoft", "WindowsApps")

# ------------------------------
# Persistență JSON
# ------------------------------

def load_aliases():
    if os.path.isfile(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, "r") as f:
                return json.load(f)
        except:
            return []
    return []

def save_aliases(data):
    with open(CONFIG_FILE, "w") as f:
        json.dump(data, f, indent=4)


# ------------------------------
# Detectare Python
# ------------------------------

def detect_where_python():
    """Detectează instalările Python folosind 'where python'."""
    found = []
    try:
        out = subprocess.check_output(["where", "python"], stderr=subprocess.STDOUT)
        lines = out.decode().splitlines()
        for line in lines:
            if line.lower().endswith("python.exe"):
                found.append(line.strip())
    except:
        pass
    return found


def search_python_in_folder_recursive(folder):
    """Caută python.exe în folder și în toate subfolderele recursiv."""
    found = []
    for root, dirs, files in os.walk(folder):
        if "python.exe" in files:
            found.append(os.path.join(root, "python.exe"))
    return found

# ------------------------------
# Detectare aliasuri existente
# ------------------------------

def detect_existing_aliases():
    """Caută aliasuri existente (*.cmd) în WindowsApps."""
    aliases = {}
    if not os.path.isdir(ALIAS_DIR):
        return aliases

    for file in os.listdir(ALIAS_DIR):
        if file.endswith(".cmd"):
            alias_name = file[:-4]
            cmd_path = os.path.join(ALIAS_DIR, file)

            try:
                with open(cmd_path, "r") as f:
                    line = f.readline().strip()
                    if line.startswith("@\"") and line.endswith("%*"):
                        python_path = line[2:-3].strip('"')
                        aliases[alias_name] = python_path
            except:
                pass

    return aliases

# ------------------------------
# Creare alias
# ------------------------------

def create_alias(alias_name, python_path):
    """Creează un fișier .cmd în WindowsApps pentru alias."""
    cmd_path = os.path.join(ALIAS_DIR, alias_name + ".cmd")

    try:
        with open(cmd_path, "w") as f:
            f.write(f'@"{python_path}" %*\n')
        return True
    except Exception as e:
        print("Eroare alias:", e)
        return False

# ------------------------------
# Interfață PyQt6
# ------------------------------

class PythonAliasManager(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Python Alias Manager – PyQt6")
        self.resize(800, 550)

        layout = QVBoxLayout(self)

        self.table = QTableWidget(0, 2)
        self.table.setHorizontalHeaderLabels(["Path Python", "Alias (editabil)"])
        self.table.horizontalHeader().setStretchLastSection(True)
        layout.addWidget(self.table)

        btn_detect = QPushButton("Detectează instalări (where python)")
        btn_detect.clicked.connect(self.detect_where)
        layout.addWidget(btn_detect)

        btn_add_folder = QPushButton("Adaugă folder custom (recursiv)")
        btn_add_folder.clicked.connect(self.add_folder)
        layout.addWidget(btn_add_folder)

        btn_search_folder = QPushButton("Caută python.exe recursiv în folder selectat")
        btn_search_folder.clicked.connect(self.search_folder)
        layout.addWidget(btn_search_folder)

        btn_aliases = QPushButton("Afișează aliasuri existente")
        btn_aliases.clicked.connect(self.show_existing_aliases)
        layout.addWidget(btn_aliases)

        btn_apply = QPushButton("Aplică aliasuri în Windows")
        btn_apply.clicked.connect(self.apply_aliases)
        layout.addWidget(btn_apply)

        self.data = load_aliases()
        self.refresh_table()

    # --------------------------
    # Tabel
    # --------------------------

    def refresh_table(self):
        self.table.setRowCount(0)
        for entry in self.data:
            row = self.table.rowCount()
            self.table.insertRow(row)

            self.table.setItem(row, 0, QTableWidgetItem(entry["path"]))

            alias_item = QTableWidgetItem(entry["alias"])
            alias_item.setFlags(alias_item.flags() | Qt.ItemFlag.ItemIsEditable)
            self.table.setItem(row, 1, alias_item)

    # --------------------------
    # Detectare instalări
    # --------------------------

    def detect_where(self):
        paths = detect_where_python()
        for p in paths:
            self.add_entry(p)
        self.refresh_table()
        save_aliases(self.data)

    def add_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "Selectează folder Python")
        if folder:
            paths = search_python_in_folder_recursive(folder)
            for p in paths:
                self.add_entry(p)
            self.refresh_table()
            save_aliases(self.data)

    def search_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "Selectează folder pentru scanare")
        if folder:
            paths = search_python_in_folder_recursive(folder)
            for p in paths:
                self.add_entry(p)
            self.refresh_table()
            save_aliases(self.data)

    # --------------------------
    # Aliasuri existente
    # --------------------------

    def show_existing_aliases(self):
        aliases = detect_existing_aliases()
        if not aliases:
            QMessageBox.information(self, "Aliasuri", "Nu există aliasuri în WindowsApps.")
            return

        msg = "\n".join([f"{a} → {p}" for a, p in aliases.items()])
        QMessageBox.information(self, "Aliasuri existente", msg)

    # --------------------------
    # Adăugare în listă
    # --------------------------

    def add_entry(self, path):
        if not any(e["path"] == path for e in self.data):
            alias = "python_" + os.path.basename(os.path.dirname(path)).replace(".", "_")
            self.data.append({"path": path, "alias": alias})

    # --------------------------
    # Aplicare aliasuri
    # --------------------------

    def apply_aliases(self):
        # actualizează aliasurile din tabel
        for row in range(self.table.rowCount()):
            self.data[row]["path"] = self.table.item(row, 0).text()
            self.data[row]["alias"] = self.table.item(row, 1).text()

        save_aliases(self.data)

        ok = 0
        for entry in self.data:
            if create_alias(entry["alias"], entry["path"]):
                ok += 1

        QMessageBox.information(
            self,
            "Aliasuri aplicate",
            f"Aliasuri create: {ok}\nAcum poți folosi comenzile în CMD/PowerShell."
        )

# ------------------------------
# Main
# ------------------------------

if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = PythonAliasManager()
    win.show()
    sys.exit(app.exec())