Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
77d7092a24 | |||
014feb23c2 | |||
dc8b1ca9ed | |||
f4ef58672c | |||
364af1cb6e | |||
03cf68b873 | |||
2e2bba2aa5 | |||
bf5d680612 | |||
8f91f64d04 | |||
23214d7d32 | |||
712d80a0aa | |||
9680e33730 | |||
09a37ae628 | |||
cb568730d8 | |||
3587ce1317 | |||
47fe83d1f7 |
12 changed files with 329 additions and 250 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/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
|
|
|
@ -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/*
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -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
|
||||||
|
|
37
README.md
37
README.md
|
@ -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.
|
|
||||||
|
|
|
@ -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#).
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .pypi_updater import PyPiUpdater
|
from .single_updater import PyPiUpdater
|
||||||
|
#from .multi_updater import MultiPackageUpdater
|
||||||
|
|
||||||
__all__ = ["PyPiUpdater"]
|
__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,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
|
|
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