Skip to content

Commit

Permalink
Merge branch 'master' into issue50
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisvang committed Nov 8, 2023
2 parents da8ff95 + 0a944ab commit e7e5531
Show file tree
Hide file tree
Showing 21 changed files with 273 additions and 331 deletions.
17 changes: 7 additions & 10 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python package

Expand All @@ -20,22 +20,19 @@ jobs:

runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8
python -m pip install -r requirements.txt
- name: Lint with flake8
pip install -r requirements.txt
- name: Lint with ruff
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
pip install ruff
ruff check --output-format=github .
- name: Test with unittest
run: |
python -m unittest
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The example repository shows how to integrate the `tufup` client into your app,
## Questions and Issues

If you have questions about `tufup`, or need help getting started, please post a question on [Stack Overflow][13], add a `tufup` tag, and we will help you there.
If you have questions about `tufup`, or need help getting started, please start a [new Q&A discussion][22], or post a question on [Stack Overflow][13].

If you encounter bugs or other problems that are likely to affect other users, please create a [new issue][14] here.

Expand Down Expand Up @@ -93,7 +93,9 @@ Archive filenames and patch filenames follow the pattern

`<name>-<version><suffix>`

where `name` is a short string that may contain alphanumeric characters, underscores, and hyphens, `version` is a version string according to the [PEP440][6] specification, and `suffix` is either `'.tar.gz'` or `'.patch'`.
where `name` is a short string that may *only* contain *alphanumeric characters*, *underscores*, and *hyphens*, `version` is a version string according to the [PEP440][6] specification, and `suffix` is either `'.tar.gz'` or `'.patch'`.

***BEWARE***: *whitespace* is NOT allowed in the filename.

Patches are typically smaller than archives, so the tufup *client* will always attempt to update using one or more patches.
However, if the total amount of patch data is greater than the desired full archive file, a full update will be performed.
Expand Down Expand Up @@ -207,3 +209,4 @@ A custom, platform *de*pendent, installation procedure can be specified via the
[19]: https://peps.python.org/pep-0440/#public-version-identifiers
[20]: https://packaging.pypa.io/en/stable/version.html#packaging.version.Version
[21]: https://github.com/dennisvang/tufup/blob/master/src/tufup/client.py
[22]: https://github.com/dennisvang/tufup/discussions/new?category=q-a
10 changes: 3 additions & 7 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.todo",
"sphinx_sitemap"
]
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx_sitemap']


# Add any paths that contain templates here, relative to this directory.
Expand Down Expand Up @@ -77,8 +73,8 @@
'collapse_navigation': True,
'sticky_navigation': True,
'navigation_depth': 2,
'includehidden': True,
'titles_only': False
'includehidden': True,
'titles_only': False,
}

# config for autodoc. See https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_default_options
Expand Down
2 changes: 1 addition & 1 deletion examples/client/example_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# python -m http.server -d examples/repo/content

# App info
APP_NAME = 'example_app'
APP_NAME = 'example_app' # BEWARE: app name cannot contain whitespace
CURRENT_VERSION = '1.0'

# For this example, all files are stored in the tufup/examples/client
Expand Down
8 changes: 2 additions & 6 deletions examples/repo/repo_workflow_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,7 @@
# Create archive and patch and register the new update (here we sign
# everything at once, for convenience)
repo.add_bundle(new_version=new_version, new_bundle_dir=dummy_bundle_dir)
repo.publish_changes(
private_key_dirs=[OFFLINE_DIR_1, OFFLINE_DIR_2, ONLINE_DIR]
)
repo.publish_changes(private_key_dirs=[OFFLINE_DIR_1, OFFLINE_DIR_2, ONLINE_DIR])

# Time goes by
...
Expand All @@ -153,6 +151,4 @@
new_public_key_path=new_public_key_path,
new_private_key_encrypted=False,
)
repo.publish_changes(
private_key_dirs=[OFFLINE_DIR_1, OFFLINE_DIR_2, ONLINE_DIR]
)
repo.publish_changes(private_key_dirs=[OFFLINE_DIR_1, OFFLINE_DIR_2, ONLINE_DIR])
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ version = {attr = "tufup.__version__"}
# this is still in beta (as of setuptools v65.6.3)
# https://setuptools.pypa.io/en/stable/userguide/pyproject_config.html#setuptools-specific-configuration
where = ["src"]

[tool.ruff.format]
quote-style = "single"
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# install dev dependencies
build
ruff
twine
1 change: 0 additions & 1 deletion src/tufup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import sys

from tufup.repo import cli
from tufup.utils import input_bool

# https://packaging.python.org/en/latest/guides/single-sourcing-package-version/
# https://semver.org/
Expand Down
78 changes: 39 additions & 39 deletions src/tufup/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@

class Client(tuf.ngclient.Updater):
def __init__(
self,
app_name: str,
app_install_dir: pathlib.Path,
current_version: str,
metadata_dir: pathlib.Path,
metadata_base_url: str,
target_dir: pathlib.Path,
target_base_url: str,
extract_dir: Optional[pathlib.Path] = None,
refresh_required: bool = False,
session_auth: Optional[Dict[str, Union[Tuple[str, str], AuthBase]]] = None,
**kwargs,
self,
app_name: str,
app_install_dir: pathlib.Path,
current_version: str,
metadata_dir: pathlib.Path,
metadata_base_url: str,
target_dir: pathlib.Path,
target_base_url: str,
extract_dir: Optional[pathlib.Path] = None,
refresh_required: bool = False,
session_auth: Optional[Dict[str, Union[Tuple[str, str], AuthBase]]] = None,
**kwargs,
):
# tuf.ngclient.Updater.__init__ loads local root metadata automatically
super().__init__(
Expand Down Expand Up @@ -74,7 +74,9 @@ def trusted_target_metas(self) -> list:
logger.warning('targets metadata not found')
return _trusted_target_metas

def get_targetinfo(self, target_path: Union[str, TargetMeta]) -> Optional[tuf.ngclient.TargetFile]:
def get_targetinfo(
self, target_path: Union[str, TargetMeta]
) -> Optional[tuf.ngclient.TargetFile]:
"""Extend Updater.get_targetinfo to handle TargetMeta input args."""
if isinstance(target_path, TargetMeta):
target_path = target_path.target_path_str
Expand All @@ -89,11 +91,11 @@ def updates_available(self):
return len(self.new_targets) > 0

def download_and_apply_update(
self,
skip_confirmation: bool = False,
install: Optional[Callable] = None,
progress_hook: Optional[Callable] = None,
**kwargs,
self,
skip_confirmation: bool = False,
install: Optional[Callable] = None,
progress_hook: Optional[Callable] = None,
**kwargs,
):
"""
Download and apply available updates.
Expand Down Expand Up @@ -126,14 +128,14 @@ def download_and_apply_update(
if install is None:
install = install_update
if self.updates_available and self._download_updates(
progress_hook=progress_hook
progress_hook=progress_hook
):
self._apply_updates(
install=install, skip_confirmation=skip_confirmation, **kwargs
)

def check_for_updates(
self, pre: Optional[str] = None, patch: bool = True
self, pre: Optional[str] = None, patch: bool = True
) -> Optional[TargetMeta]:
"""
Check if any updates are available, based on current app version.
Expand Down Expand Up @@ -169,25 +171,24 @@ def check_for_updates(
logger.debug(f'{len(all_new_targets)} new *targets* found')
# determine latest archive, filtered by the specified pre-release level
new_archives = dict(
item for item in all_new_targets.items()
item
for item in all_new_targets.items()
if item[0].is_archive
and (not item[0].version.pre or item[0].version.pre[0] in included[pre])
)
new_archive_meta = None
if new_archives:
logger.debug(f'{len(new_archives)} new *archives* found')
new_archive_meta, self.new_archive_info = sorted(
new_archives.items()
)[-1]
new_archive_meta, self.new_archive_info = sorted(new_archives.items())[-1]
self.new_archive_local_path = pathlib.Path(
self.target_dir, new_archive_meta.path.name
)
# patches must include all pre-releases and final releases up to,
# and including, the latest archive as determined above
new_patches = dict(
item for item in all_new_targets.items()
if item[0].is_patch
and item[0].version <= new_archive_meta.version
item
for item in all_new_targets.items()
if item[0].is_patch and item[0].version <= new_archive_meta.version
)
# determine size of patch update and archive update
total_patch_size = sum(
Expand Down Expand Up @@ -227,10 +228,10 @@ def _download_updates(self, progress_hook: Optional[Callable]) -> bool:
return len(self.downloaded_target_files) == len(self.new_targets)

def _apply_updates(
self,
install: Callable,
skip_confirmation: bool,
**kwargs,
self,
install: Callable,
skip_confirmation: bool,
**kwargs,
):
"""
kwargs are passed on to the 'install' callable
Expand Down Expand Up @@ -284,8 +285,8 @@ def _apply_updates(
class AuthRequestsFetcher(tuf.ngclient.RequestsFetcher):
# RequestsFetcher is public as of python-tuf v2.1.0 (see python-tuf #2277)
def __init__(
self,
session_auth: Optional[Dict[str, Union[Tuple[str, str], AuthBase]]] = None,
self,
session_auth: Optional[Dict[str, Union[Tuple[str, str], AuthBase]]] = None,
) -> None:
"""
This extends the default tuf RequestsFetcher, so we can specify
Expand Down Expand Up @@ -326,15 +327,14 @@ def attach_progress_hook(self, hook: Callable, bytes_expected: int):
The hook must accept two kwargs: bytes_downloaded and bytes_expected
"""

def progress(
bytes_new: int,
_cache: List[int] = [], # noqa: mutable default intentional
bytes_new: int,
_cache: List[int] = [], # noqa: mutable default intentional
):
# mutable default is used to keep track of downloaded chunk sizes
_cache.append(bytes_new)
return hook(
bytes_downloaded=sum(_cache), bytes_expected=bytes_expected
)
return hook(bytes_downloaded=sum(_cache), bytes_expected=bytes_expected)

self._progress = progress

Expand All @@ -351,7 +351,7 @@ def _get_session(self, url: str) -> requests.Session:
session.auth = self.session_auth.get(key)
return session

def _chunks(self, response: "requests.Response") -> Iterator[bytes]:
def _chunks(self, response: 'requests.Response') -> Iterator[bytes]:
"""Call progress hook for every chunk."""
for data in super()._chunks(response=response):
self._progress(bytes_new=len(data))
Expand Down
24 changes: 16 additions & 8 deletions src/tufup/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ class TargetMeta(object):
)

def __init__(
self,
target_path: Union[None, str, pathlib.Path] = None,
name: Optional[str] = None,
version: Optional[str] = None,
is_archive: Optional[bool] = True,
self,
target_path: Union[None, str, pathlib.Path] = None,
name: Optional[str] = None,
version: Optional[str] = None,
is_archive: Optional[bool] = True,
):
"""
Initialize either with target_path, or with name, version, archive.
BEWARE: whitespace is not allowed in the filename,
nor in the `name` or `version` arguments
"""
super().__init__()
if target_path is None:
Expand All @@ -36,6 +38,10 @@ def __init__(
)
self.target_path_str = str(target_path) # keep the original for reference
self.path = pathlib.Path(target_path)
if ' ' in self.filename:
logger.critical(
f'invalid filename "{self.filename}": whitespace not allowed'
)

def __str__(self):
return str(self.target_path_str)
Expand Down Expand Up @@ -85,7 +91,7 @@ def version(self) -> Optional[Version]:
version = Version(match_dict.get('version', ''))
except InvalidVersion:
version = None
logger.debug(f'No valid version in filename: {self.filename}')
logger.critical(f'No valid version in filename: {self.filename}')
return version

@property
Expand Down Expand Up @@ -125,7 +131,9 @@ def compose_filename(cls, name: str, version: str, is_archive: bool):

class Patcher(object):
@classmethod
def create_patch(cls, src_path: pathlib.Path, dst_path: pathlib.Path) -> pathlib.Path:
def create_patch(
cls, src_path: pathlib.Path, dst_path: pathlib.Path
) -> pathlib.Path:
"""
Create a binary patch file based on source and destination files.
Expand Down
Loading

0 comments on commit e7e5531

Please sign in to comment.