Compare commits

..

16 commits
0.3.0 ... main

Author SHA1 Message Date
77d7092a24 patch: removed old gitlab ci file.
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 12:23:11 +02:00
014feb23c2 Adding info about the migration to readme.
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 12:16:59 +02:00
dc8b1ca9ed fix: skip twine file
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 12:09:32 +02:00
f4ef58672c fix: fixes publish to forgejo package
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 11:56:12 +02:00
364af1cb6e ci: now also upload to package on forgejo.
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-11 11:42:43 +02:00
03cf68b873 fix: use export for vars
All checks were successful
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline was successful
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 17:05:49 +02:00
2e2bba2aa5 fix: change source to dot
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 16:59:59 +02:00
bf5d680612 fix: changed order
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 16:48:17 +02:00
8f91f64d04 CI: woodpecker CI
Some checks failed
ci/woodpecker/pr/woodpecker_ci Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker_ci Pipeline was successful
ci/woodpecker/push/woodpecker_ci Pipeline failed
ci/woodpecker/tag/woodpecker_ci Pipeline was successful
2025-04-10 16:30:40 +02:00
23214d7d32 fix: removed debug leftover 2025-02-10 13:21:38 +01:00
712d80a0aa fix: Fixed that prereleases version were listed 2025-02-10 13:16:21 +01:00
9680e33730 feat: function to install single package. 2025-02-09 17:48:30 +01:00
09a37ae628 patch: Added classifiers for pypi 2025-02-09 13:26:21 +01:00
cb568730d8 feat: Added local update functions 2025-02-03 16:21:01 +01:00
3587ce1317 refactor: consistent return values 2025-02-01 17:43:54 +00:00
47fe83d1f7 feat: using json to store more information local 2025-01-31 16:31:18 +00:00
12 changed files with 329 additions and 250 deletions

View file

@ -1,69 +0,0 @@
---
include:
- local: .gitlab-ci/versioning/gitversion.yml
- local: .gitlab-ci/git/create_tag.yml
stages:
- build
- release
gitversion:
extends: .versioning:gitversion
stage: .pre
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
build:
stage: build
image: python:3.9.21
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
needs:
- job: gitversion
artifacts: true
script:
- sed -i "s/^__version__ = .*/__version__ = \"${GitVersion_MajorMinorPatch}\"/" src/PyPiUpdater/__init__.py
- cat src/PyPiUpdater/__init__.py
- python3 -m pip install build
- python3 -m build
artifacts:
paths:
- dist/*
expire_in: 1 day
publish:
stage: release
image: python:3.9.21
tags:
- gitlab-org-docker
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
variables:
TWINE_USERNAME: "__token__"
TWINE_PASSWORD: $TWINE_API
needs:
- job: build
artifacts: true
script:
- python3 -m pip install twine
- python3 -m twine upload dist/*
create_tag:
extends: .git:create_tag
stage: release
tags:
- gitlab-org-docker
variables:
VERSION: $GitVersion_SemVer
TOKEN: $GITLAB_TOKEN
needs:
- job: gitversion
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch

View file

@ -1,15 +0,0 @@
---
.git:create_tag:
image: alpine:3.21
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
GIT_LFS_SKIP_SMUDGE: 1
VERSION: ''
TOKEN: '' # Token with push privileges
script:
- apk add git
- git remote set-url origin https://oauth2:$TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH
- git tag $VERSION
- git push origin tag $VERSION

View file

@ -1,31 +0,0 @@
---
.versioning:gitversion:
image:
name: mcr.microsoft.com/dotnet/sdk:9.0
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0 # force a deep/non-shallow fetch need by gitversion
GIT_LFS_SKIP_SMUDGE: 1
cache: [] # caches and before / after scripts can mess things up
script:
- |
dotnet tool install --global GitVersion.Tool --version 5.*
export PATH="$PATH:/root/.dotnet/tools"
dotnet-gitversion -output buildserver
# We could just collect the output file gitversion.properties (with artifacts:report:dotenv: gitversion.properties as it is already in DOTENV format,
# however it contains ~33 variables which unnecessarily consumes many of the 50 max DOTENV variables of the free GitLab version.
# Limits are higher for licensed editions, see https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsdotenv
grep 'GitVersion_LegacySemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_SemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_FullSemVer=' gitversion.properties >> gitversion.env
grep 'GitVersion_Major=' gitversion.properties >> gitversion.env
grep 'GitVersion_Minor=' gitversion.properties >> gitversion.env
grep 'GitVersion_Patch=' gitversion.properties >> gitversion.env
grep 'GitVersion_MajorMinorPatch=' gitversion.properties >> gitversion.env
grep 'GitVersion_BuildMetaData=' gitversion.properties >> gitversion.env
artifacts:
reports:
# propagates variables into the pipeline level
dotenv: gitversion.env

View file

@ -0,0 +1,92 @@
steps:
- name: gitversion
depends_on: [] # nothing start emititly
when:
event: push
branch: main
image: mcr.microsoft.com/dotnet/sdk:9.0
environment:
CI_TOKEN:
from_secret: CI_TOKEN
commands:
- git remote set-url origin https://CodeByMrFinchum:$CI_TOKEN@code.boxyfoxy.net/$CI_REPO.git
- git fetch --unshallow --tags
- apt-get update && apt-get install -y jq
- dotnet tool install --global GitVersion.Tool --version 5.*
- export PATH="$PATH:/root/.dotnet/tools"
- dotnet-gitversion -output json > version.json
- ls
- cat version.json
- |
echo "GitVersion_SemVer=$(jq -r '.SemVer' version.json)" >> gitversion.env
echo "GitVersion_LegacySemVer=$(jq -r '.LegacySemVer' version.json)" >> gitversion.env
echo "GitVersion_FullSemVer=$(jq -r '.FullSemVer' version.json)" >> gitversion.env
echo "GitVersion_Major=$(jq -r '.Major' version.json)" >> gitversion.env
echo "GitVersion_Minor=$(jq -r '.Minor' version.json)" >> gitversion.env
echo "GitVersion_Patch=$(jq -r '.Patch' version.json)" >> gitversion.env
echo "GitVersion_MajorMinorPatch=$(jq -r '.MajorMinorPatch' version.json)" >> gitversion.env
echo "GitVersion_BuildMetaData=$(jq -r '.BuildMetaData' version.json)" >> gitversion.env
- name: tagging
depends_on: [gitversion]
when:
event: push
branch: main
image: alpine/git
environment:
CI_TOKEN:
from_secret: CI_TOKEN
commands:
- ls
- cat gitversion.env
- git config --global user.email "ci@noreply.boxyfoxy.net"
- git config --global user.name "CI Bot"
- git remote set-url origin https://CodeByMrFinchum:$${CI_TOKEN}@code.boxyfoxy.net/$${CI_REPO}.git
- . gitversion.env
- git tag $GitVersion_SemVer
- git push origin tag $GitVersion_SemVer
- name: build
depends_on: [gitversion, tagging]
when:
event: push
branch: main
image: python:3.9.21
commands:
- ls
- cat gitversion.env
- export $(cat gitversion.env | xargs)
- sed -i "s/^__version__ = .*/__version__ = \"$GitVersion_SemVer\"/" src/PyPiUpdater/__init__.py
- cat src/PyPiUpdater/__init__.py
- python3 -m pip install build
- python3 -m build
- name: publish_pypi
depends_on: [gitversion, tagging, build]
when:
event: push
branch: main
image: python:3.9.21
environment:
TWINE_PASSWORD:
from_secret: TWINE_API
TWINE_USERNAME: "__token__"
commands:
- ls
- python3 -m pip install twine
- python3 -m twine upload dist/*
- name: publish_forgejo
depends_on: [gitversion, tagging, build]
when:
event: push
branch: main
image: python:3.9.21
environment:
TWINE_PASSWORD:
from_secret: PKG_TOKEN
TWINE_USERNAME: "CodeByMrFinchum"
commands:
- ls
- python3 -m pip install twine
- python3 -m twine upload --verbose --repository-url https://code.boxyfoxy.net/api/packages/CodeByMrFinchum/pypi dist/*

View file

@ -1,11 +1,59 @@
# Changelog # Changelog
## 0.3.0: Rework BREAKING ## 0.8-0.9: CI woodpecker (25.04.10-11)
- Changes to the pipeline no
## 0.7.x
### 0.7.2: Removed Debugging Leftovers
- Cleaned up code used for debugging.
### 0.7.1: Fixed Prerelease Update Detection
- Prevented prerelease versions from being listed as updates, as they must be installed manually.
### 0.7.0: Added Function to Install Packages
- Introduced the `install_package` function, allowing packages to be installed directly through the app.
- Useful for optional dependencies that need to be installed separately. This enables installation via the UI.
---
## 0.6.x
### 0.6.1: Classifier
- Added Classifier for pypi
### 0.6.0: New Local Update Feature
- Added support for updating from a local folder containing package files.
- Scans a specified folder for available updates.
- Installs updates directly from local package files.
- **Note:** Local version handling depends on how dependencies are managed.
- Example: If a package requires **PyPiUpdater 0.6.0**, but the installed version is **0.0.1** (e.g., from a dev environment), **OptimaLab35 v0.9.1** may not install correctly.
- This is due to **pip's dependency checks**, ensuring all required versions are satisfied before installation.
---
## 0.5.0: Rework (BREAKING CHANGE)
- Improved code consistency: return values are now always **lists** when containing multiple objects.
- **Simplified the package**: Removed the default waiting period for update checks.
- It is now the **developer's responsibility** to decide when to check for updates. A separate independent function may be introduced later.
- The last update check is still saved in the config and returned as a `time.time()` object.
---
## 0.4.0: Rework (BREAKING CHANGE)
- The log file is now a JSON file, allowing it to store multiple package names, versions, and last update timestamps.
- Some return values are now lists.
---
## 0.3.0: Rework (BREAKING CHANGE)
- Changed how program behaves - Changed how program behaves
---
## 0.2.1: CI/CD pipeline ## 0.2.1: CI/CD pipeline
- Added auto tagging and publishing - Added auto tagging and publishing
---
## 0.0.1: Project Initiation ## 0.0.1: Project Initiation
- First working version - First working version
- ATM terminal promt to accept or deny update - ATM terminal promt to accept or deny update

View file

@ -1,4 +1,6 @@
# PyPiUpdater # PyPiUpdater
Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup.
**UNFINISHED** Still early code, functions might change drasticly **UNFINISHED** Still early code, functions might change drasticly
**PyPiUpdater** is a Python library for managing updates of packages installed via `pip`. **PyPiUpdater** is a Python library for managing updates of packages installed via `pip`.
@ -16,37 +18,4 @@ pip install PyPiUpdater
``` ```
## Usage example ## Usage example
**NOTE:** Example only work for client application, GUI apps such as OptimaLab35 with QT need also to close the window and should not use the restart function. Example has been removed, still very active development.
**Now the example by ChatGPT:**
Here's a short example code snippet for your `PyPiUpdater` package to include in your README:
```python
# Example usage of PyPiUpdater
from PyPiUpdater import PyPiUpdater
# Initialize the updater with the package name, current version, and log file path
updater = PyPiUpdater(package_name="OptimaLab35", local_version="0.7.0", log_path="update_log.txt")
# Check if an update is available (optionally forcing the check, otherwise only checked every 20 hours(default set with update_interval_seconds = int seconds))
is_newer, latest_version = updater.check_for_update(force=False)
if is_newer:
print(f"Update available! Latest version: {latest_version}")
# Update the package using pip
success, message = updater.update_package()
print(message)
if success:
# Restart the program after update
updater.restart_program()
else:
print("No update available or checked too recently.")
```
### Explanation:
- The example shows how to initialize the `PyPiUpdater` class with the package name, current version, and the path to a log file.
- It checks for an update by calling `check_for_update()`.
- If an update is available, it proceeds with updating the package using `update_package()` and restarts the program with `restart_program()`.
- If no update is available, or it was checked too recently, it prints an appropriate message.

View file

@ -1,3 +1,3 @@
Simple program to update package from PyPi with pip. Simple program to update package from PyPi with pip.
For more info see [PyPiUpdater gitlab](https://gitlab.com/CodeByMrFinchum/PyPiUpdater#). For more info see [PyPiUpdater forgejo](https://code.boxyfoxy.net/CodeByMrFinchum/PyPiUpdater) or backup repo [PyPiUpdater gitlab](https://gitlab.com/CodeByMrFinchum/PyPiUpdater#).

View file

@ -11,13 +11,17 @@ readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = ["requests", "packaging"] dependencies = ["requests", "packaging"]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
[project.urls] [project.urls]
Source = "https://gitlab.com/CodeByMrFinchum/PyPiUpdater" Source = "https://gitlab.com/CodeByMrFinchum/PyPiUpdater"
Documentation = "https://gitlab.com/CodeByMrFinchum/PyPiUpdater/-/blob/main/README.md"
Changelog = "https://gitlab.com/CodeByMrFinchum/PyPiUpdater/-/blob/main/CHANGELOG.md"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/PyPiUpdater"] packages = ["src/PyPiUpdater"]

View file

@ -1,4 +1,5 @@
from .pypi_updater import PyPiUpdater from .single_updater import PyPiUpdater
#from .multi_updater import MultiPackageUpdater
__all__ = ["PyPiUpdater"] __all__ = ["PyPiUpdater"]

View file

@ -0,0 +1,4 @@
class MultiPackageUpdater:
def __init__(self, log_path):
print("Not ready yet...")

View file

@ -1,98 +0,0 @@
import requests
import subprocess
import sys
import os
import time
from packaging import version
from xml.etree import ElementTree as ET
class PyPiUpdater:
def __init__(self, package_name, local_version, log_path, update_interval_seconds=20 * 3600):
"""
Initialize PyPiUpdater.
:param package_name: Name of the package on PyPI.
:param local_version: Currently installed version.
:param log_path: Path to the update log file (txt file).
:param update_interval_seconds: Seconds to wait before update is allowed again (default: 20 hours).
"""
self.package_name = package_name
self.local_version = version.parse(local_version)
self.log_path = log_path
self.update_interval = update_interval_seconds
def _get_latest_version(self):
"""Fetch the latest version from PyPI RSS feed."""
rss_url = f"https://pypi.org/rss/project/{self.package_name.lower()}/releases.xml"
try:
response = requests.get(rss_url, timeout=5)
response.raise_for_status()
root = ET.fromstring(response.content)
latest_version = root.find(".//item/title").text.strip()
return latest_version, None
except requests.exceptions.RequestException as e:
return None, f"Network error: {str(e)}"
except Exception as e:
return None, f"Error parsing feed: {str(e)}"
def check_for_update(self, force = False):
"""Check if an update is available."""
if not force and not self.should_check_for_update():
return None, "Checked too recently"
latest_version, error = self._get_latest_version()
if latest_version is None:
return None, error
is_newer = version.parse(latest_version) > self.local_version
if is_newer:
self.record_update_check() # Save the check timestamp only if successful
return is_newer, latest_version
def update_package(self):
"""Update the package using pip."""
print(f"Updating {self.package_name}...")
try:
subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", self.package_name], check=True)
return True, f"{self.package_name} updated successfully."
except subprocess.CalledProcessError as e:
return False, f"Update failed: {str(e)}"
def restart_program(self):
"""Restart the Python program after an update."""
print("Restarting the application...")
python = sys.executable
subprocess.run([python] + sys.argv)
sys.exit()
def last_update_check(self):
"""Retrieve the last update check timestamp from a text file."""
if not os.path.exists(self.log_path):
return 0 # If no log, assume a long time passed
try:
with open(self.log_path, "r") as f:
last_check = float(f.readline().strip()) # Read first line as timestamp
return last_check
except Exception:
return 0 # Handle read errors gracefully
def last_update_date_string(self):
time_float = self.last_update_check()
local_time = time.localtime(time_float)
time_string = f"{local_time.tm_mday:02d}/{local_time.tm_mon:02d} {local_time.tm_hour:02d}:{local_time.tm_min:02d}"
return time_string
def record_update_check(self):
"""Save the current time as the last update check timestamp."""
with open(self.log_path, "w") as f:
f.write(f"{time.time()}")
def should_check_for_update(self):
"""Returns True if enough time has passed since the last check."""
last_check = self.last_update_check()
elapsed_time = time.time() - last_check
return elapsed_time >= self.update_interval

View file

@ -0,0 +1,174 @@
import requests
import subprocess
import sys
import os
import time
import json
import re
from packaging import version
from packaging.version import parse, Version
from xml.etree import ElementTree as ET
class PyPiUpdater:
def __init__(self, package_name, local_version, log_path):
"""
Initialize PyPiUpdater.
:param package_name: Name of the package on PyPI.
:param local_version: Currently installed version.
:param log_path: Path to the update log file (JSON file).
:param update_interval_seconds: Seconds to wait before update is allowed again (default: 20 hours).
"""
self.package_name = package_name
self.local_version = version.parse(local_version)
self.log_path = log_path
self.latest_version = ""
self.last_update_check = 0.1
def _get_latest_version(self):
"""Fetch the latest stable version from PyPI RSS feed."""
rss_url = f"https://pypi.org/rss/project/{self.package_name.lower()}/releases.xml"
try:
response = requests.get(rss_url, timeout=5)
response.raise_for_status()
root = ET.fromstring(response.content)
# Extract all versions from the feed
versions = []
for item in root.findall(".//item/title"):
version_text = item.text.strip()
parsed_version = parse(version_text)
# Check if the version is stable (not a pre-release)
if isinstance(parsed_version, Version) and not parsed_version.is_prerelease:
versions.append(parsed_version)
# Return the latest stable version
if versions:
latest_version = str(max(versions))
self.latest_version = latest_version
return [latest_version, None]
return [None, "No stable versions found"]
except requests.exceptions.RequestException as e:
return [None, f"Network error: {str(e)}"]
except Exception as e:
return [None, f"Error parsing feed: {str(e)}"]
def check_for_update(self):
"""Check if an update is available."""
latest_version, error = self._get_latest_version()
if latest_version is None:
return [None, error]
is_newer = version.parse(latest_version) > self.local_version
self._record_update_check() # Save check timestamp & latest version
return [is_newer, latest_version]
def update_package(self):
"""Update the package using pip."""
print(f"Updating {self.package_name}...")
try:
subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", self.package_name], check = True)
return [True, f"{self.package_name} updated successfully."]
except subprocess.CalledProcessError as e:
return [False, f"Update failed: {str(e)}"]
def check_update_local(self, folder_path):
"""
Check if a newer version of the package is available in the local folder.
:param folder_path: Path to the folder containing package files.
:return: (bool, latest_version) - True if newer version found, otherwise False.
"""
if not os.path.exists(folder_path):
return [None, "Folder does not exist"]
pattern = re.compile(rf"{re.escape(self.package_name.lower())}-(\d+\.\d+\.\d+[a-zA-Z0-9]*)")
available_versions = []
for file in os.listdir(folder_path):
match = pattern.search(file)
if match:
found_version = match.group(1)
available_versions.append(version.parse(found_version))
if not available_versions:
return [None, "No valid package versions found in the folder"]
latest_version = max(available_versions)
is_newer = latest_version > self.local_version
return [is_newer, str(latest_version)]
def update_from_local(self, folder_path):
"""
Install the latest package version from a local folder.
:param folder_path: Path to the folder containing package files.
:return: (bool, message) - Success status and message.
"""
print(f"Installing {self.package_name} from {folder_path}...")
try:
subprocess.run(
[sys.executable, "-m", "pip", "install", "--no-index", "--find-links", folder_path, self.package_name, "-U"],
check=True
)
return [True, f"{self.package_name} updated successfully from local folder."]
except subprocess.CalledProcessError as e:
return [False, f"Update from local folder failed: {str(e)}"]
def restart_program(self):
"""Restart the Python program after an update."""
print("Restarting the application...")
python = sys.executable
subprocess.run([python] + sys.argv)
sys.exit()
def get_last_state(self):
"""Retrieve last update info for the package."""
data = self._read_json()
if self.package_name in data:
entry = data[self.package_name]
return [entry["last_checked"], entry["last_online_version"], self.package_name]
return [None, None, self.package_name]
def _record_update_check(self):
"""Save the last update check time and online version in JSON."""
data = self._read_json()
data[self.package_name] = {
"last_checked": time.time(),
"last_online_version": self.latest_version
}
self._write_json(data)
def clear_all_entries(self):
"""Clear all update history."""
self._write_json({})
def _read_json(self):
"""Read JSON log file."""
if not os.path.exists(self.log_path):
return {}
try:
with open(self.log_path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
return {}
def _write_json(self, data):
"""Write data to JSON log file."""
with open(self.log_path, "w") as f:
json.dump(data, f, indent=4)
@staticmethod
def install_package(package_name):
"""Attempts to install a package via pip."""
try:
subprocess.run([sys.executable, "-m", "pip", "install", package_name], check = True)
print("Successfull")
return [True, f"{package_name} installed successfully!"]
except subprocess.CalledProcessError as e:
print("Failed")
return [False, f"Failed to install {package_name}:\n{e.stderr}"]