python-catalin
Python tutorials with source code, examples, guides, and tips and tricks for Windows and Linux development.
Tuesday, June 2, 2026
News : Education Day 2026
Posted by
Cătălin George Feștilă
Labels:
2026,
2026 news,
artificial intelligence,
news,
python,
python 3,
qiskit,
tutorial,
tutorials,
video tutorial,
youtube
Saturday, May 30, 2026
Python 3.10.11 : MiniMax-M2.7 tested with python.
Today, I tested MiniMax-M2.7 with api key from nvidia and openai python package.
The script is one default example:
python -m pip install openai
Collecting openai
Downloading openai-2.38.0-py3-none-any.whl.metadata (31 kB)
...
Successfully installed distro-1.9.0 jiter-0.15.0 openai-2.38.0from openai import OpenAI
client = OpenAI(
base_url = "https://integrate.api.nvidia.com/v1",
api_key = "nvapi-nvidia"
)
completion = client.chat.completions.create(
model="minimaxai/minimax-m2.7",
messages=[{"role":"user","content":""}],
temperature=1,
top_p=0.95,
max_tokens=8192,
stream=False
)
print(completion.choices[0].message.content)This will show only:
python minimax_27_001.py
Hello! How can I help you today?I tested with this python script and works well:
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTextEdit, QLineEdit, QPushButton, QLabel)
from PyQt6.QtCore import QThread, pyqtSignal, Qt
from PyQt6.QtGui import QFont
from openai import OpenAI
# --- WORKER THREAD API ---
# Previne blocarea interfeței grafice în timpul apelului de rețea
class ApiWorker(QThread):
response_received = pyqtSignal(str)
error_occurred = pyqtSignal(str)
def __init__(self, user_message):
super().__init__()
self.user_message = user_message
def run(self):
try:
# Inițializare client OpenAI cu endpoint-ul Nvidia specificat de tine
client = OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key="nvapi-KEY" # nvidia api key
)
completion = client.chat.completions.create(
model="minimaxai/minimax-m2.7",
messages=[{"role": "user", "content": self.user_message}],
temperature=1,
top_p=0.95,
max_tokens=8192,
stream=False
)
# Trimite răspunsul înapoi către fereastra principală
answer = completion.choices[0].message.content
self.response_received.emit(answer)
except Exception as e:
self.error_occurred.emit(str(e))
class MiniMaxChatApp(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle("MiniMax M2.7 - Interactive Chat")
self.resize(600, 700)
# Widget-ul central și layout-ul principal
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 1. Zona de afișare a istoricului chat-ului
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setFont(QFont("Segoe UI", 11))
self.chat_display.setPlaceholderText("Conversația va apărea aici...")
main_layout.addWidget(self.chat_display)
# 2. Zona de introducere text și butonul (aranjate pe orizontală)
input_layout = QHBoxLayout()
self.input_field = QLineEdit()
self.input_field.setFont(QFont("Segoe UI", 11))
self.input_field.setPlaceholderText("Scrie un mesaj sau o comandă de Windows...")
# Trimite mesajul când apeși tasta Enter
self.input_field.returnPressed.connect(self.send_message)
input_layout.addWidget(self.input_field)
self.send_button = QPushButton("Trimite")
self.send_button.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
self.send_button.clicked.connect(self.send_message)
input_layout.addWidget(self.send_button)
main_layout.addLayout(input_layout)
# 3. Indicator de status (jos de tot)
self.status_label = QLabel("Pregătit")
self.status_label.setStyleSheet("color: gray;")
main_layout.addWidget(self.status_label)
def send_message(self):
user_text = self.input_field.text().strip()
if not user_text:
return # Nu trimite dacă e gol
# Adaugă mesajul utilizatorului în istoric și curăță câmpul de input
self.chat_display.append(f"Tu: {user_text}\n")
self.input_field.clear()
# Dezactivează butoanele în timp ce AI-ul se gândește
self.input_field.setEnabled(False)
self.send_button.setEnabled(False)
self.status_label.setText("MiniMax M2.7 gândește...")
# Pornirea Thread-ului separat pentru apelul API
self.worker = ApiWorker(user_text)
self.worker.response_received.connect(self.handle_response)
self.worker.error_occurred.connect(self.handle_error)
self.worker.finished.connect(self.cleanup_worker)
self.worker.start()
def handle_response(self, ai_response):
# Afișează răspunsul primit de la MiniMax
self.chat_display.append(f"MiniMax M2.7: {ai_response}\n")
self.chat_display.append("-" * 40 + "\n")
self.status_label.setText("Răspuns primit.")
def handle_error(self, error_msg):
# Afișează eroarea în caz că pică netul sau cheia API e greșită
self.chat_display.append(f"Eroare: {error_msg}\n")
self.status_label.setText("A apărut o eroare.")
def cleanup_worker(self):
# Reatonează butoanele după ce procesul s-a încheiat
self.input_field.setEnabled(True)
self.send_button.setEnabled(True)
self.input_field.setFocus()
if __name__ == "__main__":
app = QApplication(sys.argv)
chat_window = MiniMaxChatApp()
chat_window.show()
sys.exit(app.exec())This is the result:

Posted by
Cătălin George Feștilă
Labels:
2026,
artificial intelligence,
module,
modules,
OpenAI,
packages,
programming,
PyQt6,
python,
python modules,
python packages,
python3,
sys
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.
See the official website.
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:

Posted by
Cătălin George Feștilă
Labels:
2026,
artificial intelligence,
asyncio,
functools,
google_news_api,
module,
modules,
packages,
programming,
PyQt6,
python,
python modules,
python packages,
python3,
sys,
webbrowser
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:

Posted by
Cătălin George Feștilă
Labels:
2026,
artificial intelligence,
json,
module,
modules,
os,
packages,
pathlib,
plotly,
programming,
PyQt6,
python,
python modules,
python packages,
python3,
subppocess,
sys
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.4python -m pip install pytrends
Collecting pytrends
...
Installing collected packages: pytrends
Successfully installed pytrends-4.9.2python -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.0Now, 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 400Let'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.0527The PyQt6 will create this bad result, what 1970 ?:

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
Posted by
Cătălin George Feștilă
Labels:
2026,
artificial intelligence,
module,
modules,
packages,
pathlib,
plotly,
programming,
psutil,
PyQt6,
python,
python modules,
python packages,
python3,
pytrends,
sua,
tutorial,
tutorials
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
Posted by
Cătălin George Feștilă
Labels:
2026,
json,
module,
modules,
packages,
programming,
python,
python modules,
python packages,
python3,
re,
requests,
tutorial,
tutorials
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.1Let'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())
Posted by
Cătălin George Feștilă
Labels:
2026,
2D,
artificial intelligence,
module,
modules,
packages,
pathlib,
programming,
psutil,
PyQt6,
PyQt6-Charts,
python,
python modules,
python packages,
python3,
tutorial,
tutorials
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._pthcontrols 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 siteExplanation of each entry
python310.zipspecifies the location of the standard library packaged as a zip archive..allows Python to import modules from the root directory of the distribution.import siteactivates 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 siteTesting 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.pypython -m ensurepipVerifying pip
python -m pip --versionAdvantages 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 siteThis 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
Posted by
Cătălin George Feștilă
Labels:
2026,
module,
modules,
os,
packages,
pip,
programming,
python,
python modules,
python packages,
python3,
shutil,
tutorial,
tutorials,
urllib,
zipfile
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
See the official blogger.
Posted by
Cătălin George Feștilă
Labels:
2026,
2026 news,
news,
PSF,
python,
python 3,
Python Software Foundation,
web
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())
Posted by
Cătălin George Feștilă
Labels:
2026,
2D,
artificial intelligence,
module,
modules,
packages,
programming,
PyQt6,
python,
python modules,
python packages,
python3,
subprocess,
tutorial,
tutorials,
yt_dlp
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.
Posted by
Cătălin George Feștilă
Labels:
2026,
demo,
module,
modules,
packages,
programming,
python,
python modules,
python packages,
python3,
tutorial,
tutorials,
video,
youtube
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?
Posted by
Cătălin George Feștilă
Labels:
2026,
mimesis,
module,
modules,
packages,
programming,
python,
python modules,
python packages,
python3,
tutorial,
tutorials
Subscribe to:
Posts (Atom)