diff --git a/aptly_api/client.py b/aptly_api/client.py index a4930e0..bfce19b 100644 --- a/aptly_api/client.py +++ b/aptly_api/client.py @@ -12,6 +12,7 @@ from aptly_api.parts.repos import ReposAPISection from aptly_api.parts.files import FilesAPISection from aptly_api.parts.snapshots import SnapshotAPISection +from aptly_api.parts.mirrors import MirrorsAPISection class Client: @@ -31,6 +32,9 @@ def __init__(self, aptly_server_url: str, ssl_verify: Union[str, bool, None] = N ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) self.snapshots = SnapshotAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) + self.mirrors = MirrorsAPISection( + base_url=self.__aptly_server_url, ssl_verify=ssl_verify, + ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) @property def aptly_server_url(self) -> str: diff --git a/aptly_api/parts/mirrors.py b/aptly_api/parts/mirrors.py new file mode 100644 index 0000000..e9f7714 --- /dev/null +++ b/aptly_api/parts/mirrors.py @@ -0,0 +1,171 @@ +# -* encoding: utf-8 *- + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +from typing import NamedTuple, Sequence, Dict, cast, Optional, List, Union +from urllib.parse import quote + +from aptly_api.base import BaseAPIClient +from aptly_api.parts.packages import Package, PackageAPISection + + +Mirror = NamedTuple('Mirror', [ + ('uuid', Optional[str]), + ('name', str), + ('archiveurl', str), + ('distribution', Optional[str]), + ('components', Optional[Sequence[str]]), + ('architectures', Optional[Sequence[str]]), + ('meta', Optional[Sequence[Dict[str, str]]]), + ('downloaddate', Optional[str]), + ('filter', Optional[str]), + ('status', Optional[int]), + ('worker_pid', Optional[int]), + ('filter_with_deps', bool), + ('skip_component_check', bool), + ('skip_architecture_check', bool), + ('download_sources', bool), + ('download_udebs', bool), + ('download_installer', bool) +]) + +T_BodyDict = Dict[str, Union[str, bool, Sequence[Dict[str, str]], + Sequence[str], Dict[str, Union[bool, str]]]] + + +class MirrorsAPISection(BaseAPIClient): + @staticmethod + def mirror_from_response(api_response: Dict[str, str]) -> Mirror: + return Mirror( + uuid=cast(str, api_response["UUID"]) if "UUID" in api_response else None, + name=cast(str, api_response["Name"]), archiveurl=cast(str, api_response["ArchiveRoot"]), + distribution=cast(str, api_response["Distribution"]) if "Distribution" in api_response else None, + components=cast(List[str], api_response["Components"])if "Components" in api_response else None, + architectures=cast(List[str], api_response["Architectures"]) if "Architectures" in api_response else None, + meta=cast(List[Dict[str, str]], api_response["Meta"]) if "Meta" in api_response else None, + downloaddate=cast(str, api_response["LastDownloadDate"]) if "LastDownloadDate" in api_response else None, + filter=cast(str, api_response["Filter"]) if "Filter" in api_response else None, + status=cast(int, api_response["Status"])if "Status" in api_response else None, + worker_pid=cast(int, api_response["WorkerPID"]) if "WorkerPID" in api_response else None, + filter_with_deps=cast(bool, api_response["FilterWithDeps"]), + skip_component_check=cast(bool, api_response["SkipComponentCheck"]), + skip_architecture_check=cast(bool, api_response["SkipArchitectureCheck"]), + download_sources=cast(bool, api_response["DownloadSources"]), + download_udebs=cast(bool, api_response["DownloadUdebs"]), + download_installer=cast(bool, api_response["DownloadInstaller"]) + ) + + def list(self) -> Sequence[Mirror]: + resp = self.do_get("api/mirrors") + + mirrors = [] + for mirr in resp.json(): + mirrors.append(self.mirror_from_response(mirr)) + return mirrors + + def update(self, name: str, ignore_signatures: bool = False) -> None: + body = {} + if ignore_signatures: + body["IgnoreSignatures"] = ignore_signatures + self.do_put("api/mirrors/%s" % (quote(name)), json=body) + + def edit(self, name: str, newname: Optional[str] = None, archiveurl: Optional[str] = None, + filter: Optional[str] = None, architectures: Optional[List[str]] = None, + components: Optional[List[str]] = None, keyrings: Optional[List[str]] = None, + filter_with_deps: bool = False, skip_existing_packages: bool = False, + download_sources: bool = False, download_udebs: bool = False, + skip_component_check: bool = False, ignore_checksums: bool = False, + ignore_signatures: bool = False, force_update: bool = False) -> None: + + body = {} # type: T_BodyDict + if newname: + body["Name"] = newname + if archiveurl: + body["ArchiveURL"] = archiveurl + if filter: + body["Filter"] = filter + if architectures: + body["Architectures"] = architectures + if components: + body["Components"] = components + if keyrings: + body["Keyrings"] = keyrings + if filter_with_deps: + body["FilterWithDeps"] = filter_with_deps + if download_sources: + body["DownloadSources"] = download_sources + if download_udebs: + body["DownloadUdebs"] = download_udebs + if skip_component_check: + body["SkipComponentCheck"] = skip_component_check + if ignore_checksums: + body["IgnoreChecksums"] = ignore_checksums + if ignore_signatures: + body["IgnoreSignatures"] = ignore_signatures + if skip_existing_packages: + body["SkipExistingPackages"] = skip_existing_packages + if force_update: + body["ForceUpdate"] = force_update + + self.do_put("api/mirrors/%s" % (quote(name)), json=body) + + def show(self, name: str) -> Mirror: + resp = self.do_get("api/mirrors/%s" % (quote(name))) + return self.mirror_from_response(resp.json()) + + def list_packages(self, name: str, query: Optional[str] = None, with_deps: bool = False, + detailed: bool = False) -> Sequence[Package]: + params = {} + if query is not None: + params["q"] = query + if with_deps: + params["withDeps"] = "1" + if detailed: + params["format"] = "details" + + resp = self.do_get("api/mirrors/%s/packages" % + quote(name), params=params) + ret = [] + for rpkg in resp.json(): + ret.append(PackageAPISection.package_from_response(rpkg)) + return ret + + def delete(self, name: str) -> None: + self.do_delete("api/mirrors/%s" % quote(name)) + + def create(self, name: str, archiveurl: str, distribution: Optional[str] = None, + filter: Optional[str] = None, components: Optional[List[str]] = None, + architectures: Optional[List[str]] = None, keyrings: Optional[List[str]] = None, + download_sources: bool = False, download_udebs: bool = False, + download_installer: bool = False, filter_with_deps: bool = False, + skip_component_check: bool = False, ignore_signatures: bool = False) -> Mirror: + data = { + "Name": name, + "ArchiveURL": archiveurl + } # type: T_BodyDict + + if ignore_signatures: + data["IgnoreSignatures"] = ignore_signatures + if keyrings: + data["Keyrings"] = keyrings + if filter: + data["Filter"] = filter + if distribution: + data["Distribution"] = distribution + if components: + data["Components"] = components + if architectures: + data["Architectures"] = architectures + if download_sources: + data["DownloadSources"] = download_sources + if download_udebs: + data["DownloadUdebs"] = download_udebs + if download_installer: + data["DownloadInstaller"] = download_installer + if skip_component_check: + data["SkipComponentCheck"] = skip_component_check + + resp = self.do_post("api/mirrors", json=data) + + return self.mirror_from_response(resp.json()) diff --git a/aptly_api/parts/snapshots.py b/aptly_api/parts/snapshots.py index 7831d4d..0dba393 100644 --- a/aptly_api/parts/snapshots.py +++ b/aptly_api/parts/snapshots.py @@ -52,6 +52,14 @@ def create_from_repo(self, reponame: str, snapshotname: str, description: Option resp = self.do_post("api/repos/%s/snapshots" % quote(reponame), json=body) return self.snapshot_from_response(resp.json()) + def create_from_mirror(self, mirrorname: str, snapshotname: str, description: Optional[str] = None) -> Snapshot: + body = { + "Name": snapshotname + } + resp = self.do_post("api/mirrors/%s/snapshots" % + quote(mirrorname), json=body) + return self.snapshot_from_response(resp.json()) + def create_from_packages(self, snapshotname: str, description: Optional[str] = None, source_snapshots: Optional[Sequence[str]] = None, package_refs: Optional[Sequence[str]] = None) -> Snapshot: @@ -99,7 +107,8 @@ def list_packages(self, snapshotname: str, query: Optional[str] = None, with_dep if detailed: params["format"] = "details" - resp = self.do_get("api/snapshots/%s/packages" % quote(snapshotname), params=params) + resp = self.do_get("api/snapshots/%s/packages" % + quote(snapshotname), params=params) ret = [] for rpkg in resp.json(): ret.append(PackageAPISection.package_from_response(rpkg)) @@ -115,5 +124,6 @@ def delete(self, snapshotname: str, force: bool = False) -> None: self.do_delete("api/snapshots/%s" % quote(snapshotname), params=params) def diff(self, snapshot1: str, snapshot2: str) -> Sequence[Dict[str, str]]: - resp = self.do_get("api/snapshots/%s/diff/%s" % (quote(snapshot1), quote(snapshot2),)) + resp = self.do_get("api/snapshots/%s/diff/%s" % + (quote(snapshot1), quote(snapshot2),)) return cast(List[Dict[str, str]], resp.json()) diff --git a/aptly_api/tests/__init__.py b/aptly_api/tests/__init__.py index 83c4e19..e551fe6 100644 --- a/aptly_api/tests/__init__.py +++ b/aptly_api/tests/__init__.py @@ -12,3 +12,4 @@ from .test_publish import * # noqa from .test_repos import * # noqa from .test_snapshots import * # noqa +from .test_mirrors import * # noqa diff --git a/aptly_api/tests/test_mirrors.py b/aptly_api/tests/test_mirrors.py new file mode 100644 index 0000000..07c1103 --- /dev/null +++ b/aptly_api/tests/test_mirrors.py @@ -0,0 +1,284 @@ +# -* encoding: utf-8 *- + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +from typing import Any +from unittest.case import TestCase + +import requests_mock + +from aptly_api.parts.packages import Package +from aptly_api.parts.mirrors import MirrorsAPISection, Mirror + + +@requests_mock.Mocker(kw='rmock') +class MirrorsAPISectionTests(TestCase): + def __init__(self, *args: Any) -> None: + super().__init__(*args) + self.miapi = MirrorsAPISection("http://test/") + + def test_create(self, *, rmock: requests_mock.Mocker) -> None: + rmock.post("http://test/api/mirrors", + text="""{"UUID": "2cb5985a-a23f-4a1f-8eb6-d5409193b4eb", + "Name": "aptly-mirror", + "ArchiveRoot": "https://deb.nodesource.com/node_10.x/", + "Distribution": "bionic", "Components": ["main"], + "Architectures": ["amd64"], + "Meta": [{"Architectures": "i386 amd64 armhf arm64", + "Codename": "bionic", "Components": "main", + "Date": "Tue, 06 Apr 2021 21:05:41 UTC", + "Description": " Apt Repository for the Node.JS 10.x Branch", + "Label": "Node Source", "Origin": "Node Source"}], + "LastDownloadDate": "0001-01-01T00:00:00Z", + "Filter": "test", "Status": 0, "WorkerPID": 0, + "FilterWithDeps": true, "SkipComponentCheck": true, + "SkipArchitectureCheck": true, "DownloadSources": true, + "DownloadUdebs": true, "DownloadInstaller": true}""" + ) + self.assertSequenceEqual( + self.miapi.create(name="aptly-mirror", archiveurl='https://deb.nodesource.com/node_10.x/', + distribution='bionic', components=["main"], + architectures=["amd64"], + filter="test", download_udebs=True, + download_sources=True, download_installer=True, + skip_component_check=True, filter_with_deps=True, + keyrings=["/path/to/keyring"], ignore_signatures=True), + Mirror( + uuid='2cb5985a-a23f-4a1f-8eb6-d5409193b4eb', + name="aptly-mirror", + archiveurl="https://deb.nodesource.com/node_10.x/", + distribution='bionic', + components=["main"], + architectures=["amd64"], + downloaddate='0001-01-01T00:00:00Z', + meta=[{"Architectures": "i386 amd64 armhf arm64", + "Codename": "bionic", + "Components": "main", + "Date": "Tue, 06 Apr 2021 21:05:41 UTC", + "Description": " Apt Repository for the Node.JS 10.x Branch", + "Label": "Node Source", "Origin": "Node Source"}], + filter="test", + status=0, + worker_pid=0, + filter_with_deps=True, + skip_component_check=True, + skip_architecture_check=True, + download_sources=True, + download_udebs=True, + download_installer=True + + ) + ) + + def test_list(self, *, rmock: requests_mock.Mocker) -> None: + rmock.get("http://test/api/mirrors", + text="""[{"UUID": "2cb5985a-a23f-4a1f-8eb6-d5409193b4eb", + "Name": "aptly-mirror", + "ArchiveRoot": "https://deb.nodesource.com/node_10.x/", + "Distribution": "bionic", "Components": ["main"], + "Architectures": ["amd64"], + "Meta": [{"Architectures": "i386 amd64 armhf arm64", + "Codename": "bionic", "Components": "main", + "Date": "Tue, 06 Apr 2021 21:05:41 UTC", + "Description": " Apt Repository for the Node.JS 10.x Branch", + "Label": "Node Source", "Origin": "Node Source"}], + "LastDownloadDate": "0001-01-01T00:00:00Z", "Filter": "", + "Status": 0, "WorkerPID": 0, "FilterWithDeps": false, + "SkipComponentCheck": false, "SkipArchitectureCheck": false, + "DownloadSources": false, "DownloadUdebs": false, + "DownloadInstaller": false}]""" + ) + self.assertSequenceEqual( + self.miapi.list(), + [ + Mirror( + uuid='2cb5985a-a23f-4a1f-8eb6-d5409193b4eb', + name="aptly-mirror", + archiveurl="https://deb.nodesource.com/node_10.x/", + distribution='bionic', + components=["main"], + architectures=["amd64"], + downloaddate='0001-01-01T00:00:00Z', + meta=[{"Architectures": "i386 amd64 armhf arm64", + "Codename": "bionic", + "Components": "main", + "Date": "Tue, 06 Apr 2021 21:05:41 UTC", + "Description": " Apt Repository for the Node.JS 10.x Branch", + "Label": "Node Source", "Origin": "Node Source"}], + filter="", + status=0, + worker_pid=0, + filter_with_deps=False, + skip_component_check=False, + skip_architecture_check=False, + download_sources=False, + download_udebs=False, + download_installer=False + + ) + ] + ) + + def test_show(self, *, rmock: requests_mock.Mocker) -> None: + rmock.get("http://test/api/mirrors/aptly-mirror", + text="""{"UUID": "2cb5985a-a23f-4a1f-8eb6-d5409193b4eb", + "Name": "aptly-mirror", + "ArchiveRoot": "https://deb.nodesource.com/node_10.x/", + "Distribution": "bionic", "Components": ["main"], + "Architectures": ["amd64"], + "Meta": [{"Architectures": "i386 amd64 armhf arm64", + "Codename": "bionic", "Components": "main", + "Date": "Tue, 06 Apr 2021 21:05:41 UTC", + "Description": " Apt Repository for the Node.JS 10.x Branch", + "Label": "Node Source", "Origin": "Node Source"}], + "LastDownloadDate": "0001-01-01T00:00:00Z", "Filter": "", + "Status": 0, "WorkerPID": 0, "FilterWithDeps": false, + "SkipComponentCheck": false, "SkipArchitectureCheck": false, + "DownloadSources": false, "DownloadUdebs": false, + "DownloadInstaller": false}""" + ) + self.assertSequenceEqual( + self.miapi.show(name="aptly-mirror"), + Mirror( + uuid='2cb5985a-a23f-4a1f-8eb6-d5409193b4eb', + name="aptly-mirror", + archiveurl="https://deb.nodesource.com/node_10.x/", + distribution='bionic', + components=["main"], + architectures=["amd64"], + downloaddate='0001-01-01T00:00:00Z', + meta=[{"Architectures": "i386 amd64 armhf arm64", + "Codename": "bionic", + "Components": "main", + "Date": "Tue, 06 Apr 2021 21:05:41 UTC", + "Description": " Apt Repository for the Node.JS 10.x Branch", + "Label": "Node Source", "Origin": "Node Source"}], + filter="", + status=0, + worker_pid=0, + filter_with_deps=False, + skip_component_check=False, + skip_architecture_check=False, + download_sources=False, + download_udebs=False, + download_installer=False + + ) + ) + + def test_list_packages(self, *, rmock: requests_mock.Mocker) -> None: + rmock.get("http://test/api/mirrors/aptly-mirror/packages", + text='["Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572"]') + self.assertSequenceEqual( + self.miapi.list_packages( + name="aptly-mirror", query=("nodejs"), with_deps=True), + [ + Package( + key="Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572", + short_key=None, + files_hash=None, + fields=None, + ) + ], + ) + + def test_list_packages_details(self, *, rmock: requests_mock.Mocker) -> None: + rmock.get( + "http://test/api/mirrors/aptly-mirror/packages?format=details", + text="""[{ + "Architecture":"amd64", + "Conflicts": "nodejs-dev, nodejs-legacy, npm", + "Depends":"1libc6 (>= 2.9), libgcc1 (>= 1:3.4),""" + """ libstdc++6 (>= 4.4.0), python-minimal, ca-certificates", + "Description":" Node.js event-based server-side javascript engine\\n", + "Filename":"nodejs_10.24.1-1nodesource1_amd64.deb", + "FilesHash":"1f74a6abf6acc572", + "Homepage":"https://nodejs.org", + "Installed-Size":"78630", + "Key":"Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572", + "License":"unknown", + "MD5sum":"6d9f0e30396cb6c20945ff6de2f9f322", + "Maintainer":"Ivan Iguaran ", + "Package":"nodejs", + "Priority":"optional", + "Provides":"nodejs-dev, nodejs-legacy, npm", + "SHA1":"a3bc5a29614eab366bb3644abb1e602b5c8953d5", + "SHA256":"4b374d16b536cf1a3963ddc4575ed2b68b28b0b5ea6eefe93c942dfc0ed35177", + "SHA512":"bf203bb319de0c5f7ed3b6ba69de39b1ea8b5086b872561379bd462dd93f0796""" + """9ca64fa01ade01ff08fa13a4e5e28625b59292ba44bc01ba876ec95875630460", + "Section":"web", + "ShortKey":"Pamd64 nodejs 10.24.1-1nodesource1", + "Size":"15949164", + "Version":"10.24.1-1nodesource1" + }]""" + ) + self.assertSequenceEqual( + self.miapi.list_packages( + "aptly-mirror", detailed=True, with_deps=True, query="nodejs"), + [ + Package( + key='Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572', + short_key='Pamd64 nodejs 10.24.1-1nodesource1', + files_hash='1f74a6abf6acc572', + fields={ + "Architecture": "amd64", + 'Conflicts': 'nodejs-dev, nodejs-legacy, npm', + 'Depends': '1libc6 (>= 2.9), libgcc1 (>= 1:3.4), ' + 'libstdc++6 (>= 4.4.0), python-minimal, ca-certificates', + 'Description': ' Node.js event-based server-side javascript engine\n', + 'Filename': 'nodejs_10.24.1-1nodesource1_amd64.deb', + 'FilesHash': '1f74a6abf6acc572', + 'Homepage': 'https://nodejs.org', + 'Installed-Size': '78630', + 'Key': 'Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572', + 'License': 'unknown', + 'MD5sum': '6d9f0e30396cb6c20945ff6de2f9f322', + 'Maintainer': 'Ivan Iguaran ', + 'Package': 'nodejs', + 'Priority': 'optional', + 'Provides': 'nodejs-dev, nodejs-legacy, npm', + 'SHA1': 'a3bc5a29614eab366bb3644abb1e602b5c8953d5', + 'SHA256': '4b374d16b536cf1a3963ddc4575ed2b68b28b0b5ea6eefe93c942dfc0ed35177', + 'SHA512': 'bf203bb319de0c5f7ed3b6ba69de39b1ea8b5086b872561379bd462dd93f0796' + '9ca64fa01ade01ff08fa13a4e5e28625b59292ba44bc01ba876ec95875630460', + 'Section': 'web', + 'ShortKey': 'Pamd64 nodejs 10.24.1-1nodesource1', + 'Size': '15949164', + 'Version': '10.24.1-1nodesource1' + } + ) + ] + ) + + def test_delete(self, *, rmock: requests_mock.Mocker) -> None: + with self.assertRaises(requests_mock.NoMockAddress): + self.miapi.delete(name="aptly-mirror") + + def test_update(self, *, rmock: requests_mock.Mocker) -> None: + with self.assertRaises(requests_mock.NoMockAddress): + self.miapi.update(name="aptly-mirror", ignore_signatures=True) + + def test_edit(self, *, rmock: requests_mock.Mocker) -> None: + with self.assertRaises(requests_mock.NoMockAddress): + self.miapi.edit(name="aptly-mirror", newname="aptly-mirror-renamed", + archiveurl='https://deb.nodesource.com/node_10.x/', + architectures=["i386", "amd64"], filter="test", + components=["main"], keyrings=["/path/to/keyring"], + skip_existing_packages=True, ignore_checksums=True, + download_udebs=True, download_sources=True, + skip_component_check=True, filter_with_deps=True, + ignore_signatures=True, force_update=True) + + def test_delete_validation(self, *, rmock: requests_mock.Mocker) -> None: + rmock.delete("http://test/api/mirrors/aptly-mirror") + self.miapi.delete(name="aptly-mirror") + + def test_update_validation(self, *, rmock: requests_mock.Mocker) -> None: + rmock.put("http://test/api/mirrors/aptly-mirror") + self.miapi.update(name="aptly-mirror") + + def test_edit_validation(self, *, rmock: requests_mock.Mocker) -> None: + rmock.put("http://test/api/mirrors/aptly-mirror", + text='{"Name":"aptly-mirror-bla", "IgnoreSignatures": true}') + self.miapi.edit(name="aptly-mirror", newname="aptly-mirror-renamed") diff --git a/aptly_api/tests/test_snapshots.py b/aptly_api/tests/test_snapshots.py index 4ef06d4..507ad20 100644 --- a/aptly_api/tests/test_snapshots.py +++ b/aptly_api/tests/test_snapshots.py @@ -34,13 +34,15 @@ def test_list(self, *, rmock: requests_mock.Mocker) -> None: name='stretch-security-1', description='Snapshot from mirror [stretch-security]: http://security.debian.org/debian-security/ ' 'stretch/updates', - created_at=iso8601.parse_date('2017-06-03T21:36:22.2692213Z') + created_at=iso8601.parse_date( + '2017-06-03T21:36:22.2692213Z') ), Snapshot( name='stretch-updates-1', description='Snapshot from mirror [stretch-updates]: http://ftp-stud.hs-esslingen.de/debian/ ' 'stretch-updates', - created_at=iso8601.parse_date('2017-06-03T21:36:22.431767659Z') + created_at=iso8601.parse_date( + '2017-06-03T21:36:22.431767659Z') ) ] ) @@ -136,8 +138,10 @@ def test_list_packages_details(self, *, rmock: requests_mock.Mocker) -> None: self.assertIsNotNone(expected.fields) self.assertDictEqual( - parsed.fields if parsed.fields else {}, # make sure that mypy doesn't error on this being potentially None - expected.fields if expected.fields else {}, # this can't happen unless Package.__init__ is fubared + # make sure that mypy doesn't error on this being potentially None + parsed.fields if parsed.fields else {}, + # this can't happen unless Package.__init__ is fubared + expected.fields if expected.fields else {}, ) def test_show(self, *, rmock: requests_mock.Mocker) -> None: @@ -159,7 +163,8 @@ def test_update(self, *, rmock: requests_mock.Mocker) -> None: text='{"Name":"aptly-repo-2","CreatedAt":"2017-06-03T23:43:40.275605639Z",' '"Description":"test"}') self.assertEqual( - self.sapi.update("aptly-repo-1", newname="aptly-repo-2", newdescription="test"), + self.sapi.update( + "aptly-repo-1", newname="aptly-repo-2", newdescription="test"), Snapshot( name='aptly-repo-2', description='test', @@ -200,3 +205,17 @@ def test_create_from_packages(self, *, rmock: requests_mock.Mocker) -> None: created_at=iso8601.parse_date('2017-06-07T14:19:07.706408213Z') ) ) + + def test_create_from_mirror(self, *, rmock: requests_mock.Mocker) -> None: + rmock.post("http://test/api/mirrors/aptly-mirror/snapshots", + text='{"Name":"aptly-mirror-snap","CreatedAt":"2022-11-29T21:43:45.275605639Z",' + '"Description":"Snapshot from local mirror [aptly-mirror]"}') + self.assertEqual( + self.sapi.create_from_mirror(mirrorname="aptly-mirror", snapshotname="aptly-mirror-snap", + description='Snapshot from local repo [aptly-repo]'), + Snapshot( + name='aptly-mirror-snap', + description='Snapshot from local mirror [aptly-mirror]', + created_at=iso8601.parse_date('2022-11-29T21:43:45.275605639Z') + ) + )