Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
77d7092a24 | |||
014feb23c2 | |||
dc8b1ca9ed | |||
f4ef58672c | |||
364af1cb6e | |||
03cf68b873 | |||
2e2bba2aa5 | |||
bf5d680612 | |||
8f91f64d04 | |||
23214d7d32 | |||
712d80a0aa | |||
9680e33730 | |||
09a37ae628 | |||
cb568730d8 | |||
3587ce1317 | |||
47fe83d1f7 | |||
282a2145ff | |||
5b462cd39f |
12 changed files with 351 additions and 210 deletions
|
@ -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/PyPiUpdateCheck/__init__.py
|
||||
- cat src/PyPiUpdateCheck/__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
|
|
@ -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
|
|
@ -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
|
92
.woodpecker/woodpecker_ci.yml
Normal file
92
.woodpecker/woodpecker_ci.yml
Normal 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/*
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -1,5 +1,59 @@
|
|||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
## 0.2.1: CI/CD pipeline
|
||||
- Added auto tagging and publishing
|
||||
|
||||
---
|
||||
|
||||
## 0.0.1: Project Initiation
|
||||
- First working version
|
||||
- ATM terminal promt to accept or deny update
|
||||
|
|
20
README.md
20
README.md
|
@ -1,3 +1,21 @@
|
|||
# PyPiUpdater
|
||||
Developed on my [forgejo instance](https://code.boxyfoxy.net/CodeByMrFinchum), [GitLab](https://gitlab.com/CodeByMrFinchum) is used as backup.
|
||||
|
||||
## More to come
|
||||
**UNFINISHED** Still early code, functions might change drasticly
|
||||
|
||||
**PyPiUpdater** is a Python library for managing updates of packages installed via `pip`.
|
||||
|
||||
## Features
|
||||
- Check the latest version of a package on PyPI
|
||||
- Determine if an update is available
|
||||
- Upgrade the package using `pip`
|
||||
- Restart the Python script after updating
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install PyPiUpdater
|
||||
```
|
||||
|
||||
## Usage example
|
||||
Example has been removed, still very active development.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
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#).
|
||||
|
|
|
@ -7,17 +7,21 @@ name = "PyPiUpdater"
|
|||
dynamic = ["version"]
|
||||
authors = [{ name = "Mr Finchum" }]
|
||||
description = "Simple program to update package from PyPi with pip."
|
||||
readme = "pip_README.md"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["requests", "packaging"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
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]
|
||||
packages = ["src/PyPiUpdater"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from .pypi_updater import PyPiUpdater
|
||||
from .single_updater import PyPiUpdater
|
||||
#from .multi_updater import MultiPackageUpdater
|
||||
|
||||
__all__ = ["PyPiUpdater"]
|
||||
|
||||
|
|
4
src/PyPiUpdater/multi_updater.py
Normal file
4
src/PyPiUpdater/multi_updater.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
class MultiPackageUpdater:
|
||||
def __init__(self, log_path):
|
||||
print("Not ready yet...")
|
|
@ -1,91 +0,0 @@
|
|||
import requests
|
||||
from packaging import version
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
class PyPiUpdater:
|
||||
|
||||
def _restart_program(self):
|
||||
"""Restarts the current Python script."""
|
||||
print("Restarting the application...")
|
||||
python = sys.executable
|
||||
subprocess.run([python] + sys.argv)
|
||||
sys.exit() # Ensure the old process exits
|
||||
|
||||
def update_package(self, package_name):
|
||||
"""Runs pip install -U <package_name>."""
|
||||
print(f"Updating {package_name}...")
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", package_name])
|
||||
|
||||
|
||||
def check_and_update(self, package_name, local_version):
|
||||
"""Checks for updates and installs if needed."""
|
||||
checker = PyPiUpdateCheck()
|
||||
is_newer, latest_version = checker.check_for_update(package_name, local_version)
|
||||
|
||||
if is_newer is None:
|
||||
print(f"Error checking for updates: {latest_version}")
|
||||
elif is_newer:
|
||||
print(f"New version available: {latest_version}")
|
||||
choice = input("Do you want to update (y/n)\n")
|
||||
if choice == "y":
|
||||
self.update_package(package_name)
|
||||
self._restart_program() # Restart after updating
|
||||
else:
|
||||
print("packge not updated")
|
||||
else:
|
||||
print("You are up to date!")
|
||||
|
||||
class PyPiUpdateCheck:
|
||||
|
||||
def _get_latest_version_from_rss(self, package_name):
|
||||
"""
|
||||
Fetch the RSS feed from PyPI and extract the latest version number.
|
||||
Returns the latest version or None if an error occurs.
|
||||
"""
|
||||
rss_url = f"https://pypi.org/rss/project/{package_name.lower()}/releases.xml"
|
||||
|
||||
try:
|
||||
response = requests.get(rss_url)
|
||||
response.raise_for_status() # Raise HTTPError for bad responses (4xx, 5xx)
|
||||
|
||||
# Extract the latest version from the RSS XML
|
||||
# The RSS feed is XML, so we just need to parse the version from the first entry
|
||||
from xml.etree import ElementTree as ET
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
# The version is in the <title> tag of the first <item> in the RSS feed
|
||||
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 _compare_versions(self, local_version, online_version):
|
||||
"""
|
||||
Compare the local and online version strings.
|
||||
Returns (True, online_version) if online is newer, (False, online_version) if same or older.
|
||||
"""
|
||||
try:
|
||||
local_ver = version.parse(local_version)
|
||||
online_ver = version.parse(online_version)
|
||||
|
||||
if online_ver > local_ver:
|
||||
return True, online_version # Online version is newer
|
||||
else:
|
||||
return False, online_version # Local version is the same or newer
|
||||
except Exception as e:
|
||||
return None, f"Error comparing versions: {str(e)}"
|
||||
|
||||
def check_for_update(self, package_name, local_version):
|
||||
"""
|
||||
Check if the given package has a newer version on PyPI compared to the local version.
|
||||
Returns (True, online_version), (False, online_version), or (None, error_message).
|
||||
"""
|
||||
online_version, error_message = self._get_latest_version_from_rss(package_name)
|
||||
|
||||
if online_version is None:
|
||||
return None, error_message # Error fetching or parsing feed
|
||||
|
||||
return self._compare_versions(local_version, online_version)
|
174
src/PyPiUpdater/single_updater.py
Normal file
174
src/PyPiUpdater/single_updater.py
Normal 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}"]
|
Loading…
Add table
Add a link
Reference in a new issue