Skip to content

Commit 53b8619

Browse files
committed
feat: add support for debian packages using tar.zst
1 parent 8f0c990 commit 53b8619

File tree

8 files changed

+60
-18
lines changed

8 files changed

+60
-18
lines changed

.github/workflows/cicd.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ jobs:
3838
- name: Run ruff format
3939
run: poetry run ruff format --diff src tests
4040

41-
- name: Run ruff
42-
run: poetry run ruff src tests
41+
- name: Run ruff check
42+
run: poetry run ruff check src tests
4343

4444
- name: Run mypy
4545
run: poetry run mypy --show-error-codes src

.pre-commit-config.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ repos:
1515
language: system
1616
types: [python]
1717
pass_filenames: false
18-
- id: ruff
19-
name: ruff
20-
entry: poetry run ruff src tests --fix
18+
- id: ruff-check
19+
name: ruff check
20+
entry: poetry run ruff check src tests --fix
2121
language: system
2222
types: [python]
2323
pass_filenames: false

pyproject.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,16 @@ build-backend = "poetry.masonry.api"
7979

8080
[tool.ruff]
8181
line-length = 90
82+
src = ["src", "tests"]
83+
84+
[tool.ruff.lint]
8285
select = [
8386
"E",
8487
"F",
8588
"W",
8689
"I001",
8790
]
88-
src = ["src", "tests"]
8991

90-
[tool.ruff.per-file-ignores]
92+
[tool.ruff.lint.per-file-ignores]
9193
"__init__.py" = ["F401"]
9294
"src/ops2deb/templates.py" = ["E501", "W191"]

src/ops2deb/extracter.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shutil
66
import tarfile
77
from pathlib import Path
8+
from typing import BinaryIO, Literal, Optional
89

910
import unix_ar
1011
import zstandard
@@ -14,6 +15,47 @@
1415
from ops2deb.utils import log_and_raise
1516

1617

18+
# https://github.com/python/cpython/issues/81276
19+
class TarFile(tarfile.TarFile):
20+
"""Subclass of tarfile.TarFile that can read and write zstd compressed archives."""
21+
22+
OPEN_METH = {"zst": "zstopen"} | tarfile.TarFile.OPEN_METH # type: ignore
23+
24+
@classmethod
25+
def zstopen(
26+
cls,
27+
name: str,
28+
mode: Literal["r", "w", "x"] = "r",
29+
fileobj: Optional[BinaryIO] = None,
30+
) -> tarfile.TarFile:
31+
if mode not in ("r", "w", "x"):
32+
raise NotImplementedError(f"mode `{mode}' not implemented for zst")
33+
34+
if mode == "r":
35+
zfobj = zstandard.open(fileobj or name, "rb")
36+
else:
37+
zfobj = zstandard.open(
38+
fileobj or name,
39+
mode + "b",
40+
cctx=zstandard.ZstdCompressor(write_checksum=True, threads=-1),
41+
)
42+
try:
43+
tarobj = cls.taropen(name, mode, zfobj)
44+
except (OSError, EOFError, zstandard.ZstdError) as exc:
45+
zfobj.close()
46+
if mode == "r":
47+
raise tarfile.ReadError("not a zst file") from exc
48+
raise
49+
except:
50+
zfobj.close()
51+
raise
52+
# Setting the _extfileobj attribute is important to signal a need to
53+
# close this object and thus flush the compressed stream.
54+
# Unfortunately, tarfile.pyi doesn't know about it.
55+
tarobj._extfileobj = False # type: ignore
56+
return tarobj
57+
58+
1759
def _unpack_gz(file_path: str, extract_path: str) -> None:
1860
output_path = Path(extract_path) / Path(file_path).stem
1961
with output_path.open("wb") as output:
@@ -35,7 +77,7 @@ def _unpack_deb(file_path: str, extract_path: str) -> None:
3577
if file_name.startswith("debian-binary"):
3678
continue
3779
tarball = ar_file.open(file_name)
38-
tar_file = tarfile.open(fileobj=tarball)
80+
tar_file = TarFile.open(fileobj=tarball)
3981
try:
4082
tar_file.extractall(Path(extract_path) / file_name.split(".")[0])
4183
finally:

src/ops2deb/updater.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,7 @@ async def _find_latest_version(
180180
if version in blueprint.versions():
181181
return None
182182
logger.info(
183-
f"{blueprint.name} can be bumped "
184-
f"from {blueprint.version} to {version}"
183+
f"{blueprint.name} can be bumped from {blueprint.version} to {version}"
185184
)
186185
return LatestRelease(blueprint, version)
187186
except Ops2debUpdaterError as e:

tests/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def mock_httpx_client():
128128

129129
def async_client_mock(**kwargs):
130130
kwargs.pop("transport", None)
131-
return real_async_client(app=app, **kwargs)
131+
return real_async_client(transport=httpx.ASGITransport(app=app), **kwargs)
132132

133133
httpx.AsyncClient = async_client_mock
134134
yield

tests/test_ops2deb.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,7 @@ def test_generate__should_fail_gracefully_when_server_returns_a_404(call_ops2deb
177177

178178
# Then
179179
expected_error = (
180-
"Failed to download http://testserver/1.0.0/404.zip. "
181-
"Server responded with 404."
180+
"Failed to download http://testserver/1.0.0/404.zip. Server responded with 404."
182181
)
183182
assert expected_error in result.stderr
184183
assert result.exit_code == 77

tests/test_updater.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ async def test_generic_update_strategy_finds_latest_release_version(
9393
app = app_factory(versions)
9494

9595
# When
96-
async with AsyncClient(app=app) as client:
96+
async with AsyncClient(transport=httpx.ASGITransport(app=app)) as client:
9797
update_strategy = GenericUpdateStrategy(client)
9898
latest_version = await update_strategy(blueprint)
9999

@@ -112,7 +112,7 @@ async def test_generic_update_strategy_finds_latest_release_version_when_version
112112
app = app_factory(["2.0.0"])
113113

114114
# When
115-
async with AsyncClient(app=app) as client:
115+
async with AsyncClient(transport=httpx.ASGITransport(app=app)) as client:
116116
update_strategy = GenericUpdateStrategy(client)
117117
latest_version = await update_strategy(blueprint)
118118

@@ -133,7 +133,7 @@ async def test_github_update_strategy_should_find_expected_blueprint_release(
133133
):
134134
app = github_app_factory(tag_name)
135135
blueprint = blueprint_factory(fetch=fetch_url)
136-
async with AsyncClient(app=app) as client:
136+
async with AsyncClient(transport=httpx.ASGITransport(app=app)) as client:
137137
update_strategy = GithubUpdateStrategy(client)
138138
assert await update_strategy(blueprint) == "2.3.0"
139139

@@ -144,7 +144,7 @@ async def test_github_update_strategy_should_not_return_an_older_version_than_cu
144144
app = github_app_factory("0.1.0", versions=["1.0.0"])
145145
url = "https://github.com/owner/name/releases/{{version}}/some-app.tar.gz"
146146
blueprint = blueprint_factory(fetch=url)
147-
async with AsyncClient(app=app) as client:
147+
async with AsyncClient(transport=httpx.ASGITransport(app=app)) as client:
148148
update_strategy = GithubUpdateStrategy(client)
149149
assert await update_strategy(blueprint) == "1.0.0"
150150

@@ -155,7 +155,7 @@ async def test_github_update_strategy_should_fail_gracefully_when_asset_not_foun
155155
app = github_app_factory("someapp-v2.3")
156156
url = "https://github.com/owner/name/releases/someapp-v{{version}}/some-app.tar.gz"
157157
blueprint = blueprint_factory(fetch=url)
158-
async with AsyncClient(app=app) as client:
158+
async with AsyncClient(transport=httpx.ASGITransport(app=app)) as client:
159159
with pytest.raises(Ops2debUpdaterError) as e:
160160
await GithubUpdateStrategy(client)(blueprint)
161161
assert "Failed to determine latest release URL" in str(e)

0 commit comments

Comments
 (0)