├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.rst ├── aptly_api ├── __init__.py ├── base.py ├── client.py ├── parts │ ├── __init__.py │ ├── files.py │ ├── mirrors.py │ ├── misc.py │ ├── packages.py │ ├── publish.py │ ├── repos.py │ └── snapshots.py └── tests │ ├── __init__.py │ ├── test_base.py │ ├── test_client.py │ ├── test_files.py │ ├── test_mirrors.py │ ├── test_misc.py │ ├── test_packages.py │ ├── test_publish.py │ ├── test_repos.py │ ├── test_snapshots.py │ └── testpkg.deb ├── requirements-test.txt └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = aptly_api 3 | 4 | [report] 5 | fail_under = 100 6 | show_missing = true 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | max-parallel: 4 14 | matrix: 15 | python-version: ["3.11", "3.12"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install Dependencies 24 | run: | 25 | python -m pip install --upgrade pip wheel 26 | pip install -r requirements-test.txt -e . 27 | - name: Run Tests 28 | run: | 29 | pytest --cov=aptly_api 30 | coverage report -m 31 | coverage lcov 32 | - name: Run Coveralls 33 | uses: coverallsapp/github-action@main 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | path-to-lcov: coverage.lcov 37 | - name: Run Flake8 38 | run: | 39 | flake8 --max-line-length=120 aptly_api setup.py 40 | - name: Run mypy 41 | run: | 42 | mypy --install-types --non-interactive \ 43 | --ignore-missing-imports --follow-imports=skip --disallow-untyped-calls \ 44 | --disallow-untyped-defs -p aptly_api 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | Lib 3 | lib 4 | *.db 5 | *.pyc 6 | *.pyd 7 | .project 8 | .pydevproject 9 | .settings 10 | .idea 11 | dist 12 | *.egg-info 13 | *.iml 14 | static 15 | build 16 | .idea 17 | .env 18 | __pycache__ 19 | .coverage 20 | .mypy_cache 21 | .tox 22 | .toxenv 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017, Jonas Maurus. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python 3 Aptly API client 2 | ========================= 3 | 4 | .. image:: https://coveralls.io/repos/github/gopythongo/aptly-api-client/badge.svg?branch=master 5 | :target: https://coveralls.io/github/gopythongo/aptly-api-client?branch=master 6 | 7 | .. image:: https://github.com/gopythongo/aptly-api-client/actions/workflows/test.yml/badge.svg 8 | :target: https://github.com/gopythongo/aptly-api-client/actions/workflows/test.yml 9 | 10 | This is a thin abstraction layer for interfacing with 11 | `Aptly's HTTP API `__. It's used by 12 | `GoPythonGo `__, but can be used as 13 | a standalone library from Pypi. 14 | 15 | .. code-block:: shell 16 | 17 | pip install aptly-api-client 18 | 19 | 20 | Usage 21 | ----- 22 | 23 | The library provides a direct abstraction of the published Aptly API, mostly 24 | using the same naming, only replacing it with pythonic naming where necessary. 25 | All code has full `PEP 484 `__ 26 | annotations, so if you're using a modern IDE, using this library should be 27 | especially straight-forward. 28 | 29 | Where appropriate, the library exposes the interface of the underlying 30 | ``requests`` library. This allows you to configure CA pinning, SSL client 31 | certificates, HTTP Basic authentication etc. 32 | 33 | .. code-block:: python 34 | 35 | # initialize a client 36 | from aptly_api import Client 37 | aptly = Client("http://aptly-endpoint.test/") 38 | 39 | # create a repository 40 | aptly.repos.create("myrepo", comment="a test repo", 41 | default_distribution="mydist", 42 | default_component="main") 43 | 44 | # upload a package 45 | aptly.files.upload("test_folder", "/tmp/mypkg_1.0_amd64.deb") 46 | 47 | # add the package to the repo 48 | aptly.repos.add_uploaded_file("myrepo", "test_folder") 49 | 50 | 51 | Contributors 52 | ============ 53 | 54 | * @findmyname666 55 | * Filip Křesťan 56 | * @mgusek 57 | * Samuel Bachmann 58 | * @agustinhenze 59 | 60 | 61 | License 62 | ======= 63 | 64 | Copyright (c) 2016-2019, Jonas Maurus and Contributors. 65 | All rights reserved. 66 | 67 | Redistribution and use in source and binary forms, with or without 68 | modification, are permitted provided that the following conditions are met: 69 | 70 | 1. Redistributions of source code must retain the above copyright notice, this 71 | list of conditions and the following disclaimer. 72 | 73 | 2. Redistributions in binary form must reproduce the above copyright notice, 74 | this list of conditions and the following disclaimer in the documentation 75 | and/or other materials provided with the distribution. 76 | 77 | 3. Neither the name of the copyright holder nor the names of its contributors 78 | may be used to endorse or promote products derived from this software 79 | without specific prior written permission. 80 | 81 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 82 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 83 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 84 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 85 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 86 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 87 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 88 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 89 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 90 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 91 | -------------------------------------------------------------------------------- /aptly_api/__init__.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | 8 | # explicit exports for mypy 9 | from aptly_api.client import Client as Client 10 | from aptly_api.base import AptlyAPIException as AptlyAPIException 11 | from aptly_api.parts.packages import Package as Package 12 | from aptly_api.parts.publish import PublishEndpoint as PublishEndpoint 13 | from aptly_api.parts.repos import Repo as Repo, FileReport as FileReport 14 | from aptly_api.parts.snapshots import Snapshot as Snapshot 15 | 16 | version = "0.3.0" 17 | 18 | 19 | __all__ = ['Client', 'AptlyAPIException', 'version', 'Package', 'PublishEndpoint', 'Repo', 'FileReport', 20 | 'Snapshot'] 21 | -------------------------------------------------------------------------------- /aptly_api/base.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from typing import IO, TextIO, BinaryIO, Sequence, Dict, Tuple, Optional, Union, List, Any, MutableMapping, Iterable, \ 6 | Mapping 7 | from urllib.parse import urljoin 8 | 9 | import requests 10 | from requests.auth import AuthBase 11 | 12 | _filetype = Optional[ 13 | Union[ 14 | Dict[ 15 | str, Union[ 16 | Union[TextIO, BinaryIO, str, bytes], 17 | Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes]], 18 | Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str], 19 | Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str, Dict[str, str]] 20 | ] 21 | ], 22 | Sequence[ 23 | Tuple[ 24 | str, Union[ 25 | Union[TextIO, BinaryIO, str, bytes], 26 | Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes]], 27 | Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str], 28 | Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str, Dict[str, str]] 29 | ] 30 | ] 31 | ], 32 | ] 33 | ] 34 | 35 | _datatype = Optional[ 36 | Union[ 37 | Iterable[bytes], 38 | str, 39 | bytes, 40 | Union[TextIO, BinaryIO], 41 | List[Tuple[Any, Any]], 42 | Tuple[Tuple[Any, Any], ...], 43 | Mapping[Any, Any] 44 | ] 45 | ] 46 | 47 | 48 | class AptlyAPIException(Exception): 49 | def __init__(self, *args: Any, status_code: int = 0) -> None: 50 | super().__init__(*args) 51 | self.status_code = status_code 52 | 53 | 54 | class BaseAPIClient: 55 | def __init__(self, base_url: str, ssl_verify: Union[str, bool, None] = None, 56 | ssl_cert: Optional[Tuple[str, str]] = None, http_auth: Optional[AuthBase] = None, 57 | timeout: int = 60) -> None: 58 | self.base_url = base_url 59 | self.ssl_verify = ssl_verify 60 | self.ssl_cert = ssl_cert 61 | self.http_auth = http_auth 62 | self.exc_class = AptlyAPIException 63 | self.timeout = timeout 64 | 65 | def _error_from_response(self, resp: requests.Response) -> str: 66 | if resp.status_code == 200: 67 | return "no error (status 200)" 68 | 69 | try: 70 | rcnt = resp.json() 71 | except ValueError: 72 | return "%s %s %s" % (resp.status_code, resp.reason, resp.text,) 73 | 74 | if isinstance(rcnt, dict): 75 | content = rcnt 76 | else: 77 | content = rcnt[0] 78 | 79 | ret = "%s - %s -" % (resp.status_code, resp.reason) 80 | if "error" in content: 81 | ret = "%s %s" % (ret, content["error"],) 82 | if "meta" in content: 83 | ret = "%s (%s)" % (ret, content["meta"],) 84 | return ret 85 | 86 | def _make_url(self, path: str) -> str: 87 | return urljoin(self.base_url, path) 88 | 89 | def do_get(self, urlpath: str, params: Optional[Dict[str, str]] = None) -> requests.Response: 90 | resp = requests.get(self._make_url(urlpath), params=params, verify=self.ssl_verify, 91 | cert=self.ssl_cert, auth=self.http_auth, timeout=self.timeout) 92 | 93 | if resp.status_code < 200 or resp.status_code >= 300: 94 | raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) 95 | 96 | return resp 97 | 98 | def do_post(self, urlpath: str, data: Union[bytes, MutableMapping[str, str], IO[Any], None] = None, 99 | params: Optional[Dict[str, str]] = None, 100 | files: _filetype = None, 101 | json: Optional[MutableMapping[Any, Any]] = None) -> requests.Response: 102 | resp = requests.post(self._make_url(urlpath), data=data, params=params, files=files, json=json, 103 | verify=self.ssl_verify, cert=self.ssl_cert, auth=self.http_auth, 104 | timeout=self.timeout) 105 | 106 | if resp.status_code < 200 or resp.status_code >= 300: 107 | raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) 108 | 109 | return resp 110 | 111 | def do_put(self, urlpath: str, data: Union[bytes, MutableMapping[str, str], IO[Any], None] = None, 112 | files: _filetype = None, 113 | json: Optional[MutableMapping[Any, Any]] = None) -> requests.Response: 114 | resp = requests.put(self._make_url(urlpath), data=data, files=files, json=json, 115 | verify=self.ssl_verify, cert=self.ssl_cert, auth=self.http_auth, 116 | timeout=self.timeout) 117 | 118 | if resp.status_code < 200 or resp.status_code >= 300: 119 | raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) 120 | 121 | return resp 122 | 123 | def do_delete(self, urlpath: str, params: Optional[Dict[str, str]] = None, 124 | data: _datatype = None, 125 | json: Union[List[Dict[str, Any]], Dict[str, Any], None] = None) -> requests.Response: 126 | resp = requests.delete(self._make_url(urlpath), params=params, data=data, json=json, 127 | verify=self.ssl_verify, cert=self.ssl_cert, auth=self.http_auth, 128 | timeout=self.timeout) 129 | 130 | if resp.status_code < 200 or resp.status_code >= 300: 131 | raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) 132 | 133 | return resp 134 | -------------------------------------------------------------------------------- /aptly_api/client.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from requests.auth import AuthBase 7 | from typing import Union, Optional, Tuple 8 | 9 | from aptly_api.parts.misc import MiscAPISection 10 | from aptly_api.parts.packages import PackageAPISection 11 | from aptly_api.parts.publish import PublishAPISection 12 | from aptly_api.parts.repos import ReposAPISection 13 | from aptly_api.parts.files import FilesAPISection 14 | from aptly_api.parts.snapshots import SnapshotAPISection 15 | from aptly_api.parts.mirrors import MirrorsAPISection 16 | 17 | 18 | class Client: 19 | def __init__(self, aptly_server_url: str, ssl_verify: Union[str, bool, None] = None, 20 | ssl_cert: Optional[Tuple[str, str]] = None, http_auth: Optional[AuthBase] = None, 21 | timeout: int = 60) -> None: 22 | self.__aptly_server_url = aptly_server_url 23 | self.files = FilesAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, 24 | ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) 25 | self.misc = MiscAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, 26 | ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) 27 | self.packages = PackageAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, 28 | ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) 29 | self.publish = PublishAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, 30 | ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) 31 | self.repos = ReposAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, 32 | ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) 33 | self.snapshots = SnapshotAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, 34 | ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) 35 | self.mirrors = MirrorsAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, 36 | ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) 37 | 38 | @property 39 | def aptly_server_url(self) -> str: 40 | return self.__aptly_server_url 41 | 42 | def __repr__(self) -> str: 43 | return "Client (Aptly API Client) <%s>" % self.aptly_server_url 44 | -------------------------------------------------------------------------------- /aptly_api/parts/__init__.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | -------------------------------------------------------------------------------- /aptly_api/parts/files.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | import os 7 | from typing import Sequence, List, Tuple, BinaryIO, cast, Optional # noqa: F401 8 | 9 | from aptly_api.base import BaseAPIClient, AptlyAPIException 10 | 11 | 12 | class FilesAPISection(BaseAPIClient): 13 | def list(self, directory: Optional[str] = None) -> Sequence[str]: 14 | if directory is None: 15 | resp = self.do_get("api/files") 16 | else: 17 | resp = self.do_get("api/files/%s" % directory) 18 | 19 | return cast(List[str], resp.json()) 20 | 21 | def upload(self, destination: str, *files: str) -> Sequence[str]: 22 | to_upload = [] # type: List[Tuple[str, BinaryIO]] 23 | for f in files: 24 | if not os.path.exists(f) or not os.access(f, os.R_OK): 25 | raise AptlyAPIException("File to upload %s can't be opened or read" % f) 26 | fh = open(f, mode="rb") 27 | to_upload.append((f, fh),) 28 | 29 | try: 30 | resp = self.do_post("api/files/%s" % destination, 31 | files=to_upload) 32 | except AptlyAPIException: 33 | raise 34 | finally: 35 | for fn, to_close in to_upload: 36 | if not to_close.closed: 37 | to_close.close() 38 | 39 | return cast(List[str], resp.json()) 40 | 41 | def delete(self, path: Optional[str] = None) -> None: 42 | self.do_delete("api/files/%s" % path) 43 | -------------------------------------------------------------------------------- /aptly_api/parts/mirrors.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import NamedTuple, Sequence, Dict, cast, Optional, List, Union 7 | from urllib.parse import quote 8 | 9 | from aptly_api.base import BaseAPIClient 10 | from aptly_api.parts.packages import Package, PackageAPISection 11 | 12 | 13 | Mirror = NamedTuple('Mirror', [ 14 | ('uuid', Optional[str]), 15 | ('name', str), 16 | ('archiveurl', str), 17 | ('distribution', Optional[str]), 18 | ('components', Optional[Sequence[str]]), 19 | ('architectures', Optional[Sequence[str]]), 20 | ('meta', Optional[Sequence[Dict[str, str]]]), 21 | ('downloaddate', Optional[str]), 22 | ('filter', Optional[str]), 23 | ('status', Optional[int]), 24 | ('worker_pid', Optional[int]), 25 | ('filter_with_deps', bool), 26 | ('skip_component_check', bool), 27 | ('skip_architecture_check', bool), 28 | ('download_sources', bool), 29 | ('download_udebs', bool), 30 | ('download_installer', bool) 31 | ]) 32 | 33 | T_BodyDict = Dict[str, Union[str, bool, Sequence[Dict[str, str]], 34 | Sequence[str], Dict[str, Union[bool, str]]]] 35 | 36 | 37 | class MirrorsAPISection(BaseAPIClient): 38 | @staticmethod 39 | def mirror_from_response(api_response: Dict[str, str]) -> Mirror: 40 | return Mirror( 41 | uuid=cast(str, api_response["UUID"]) if "UUID" in api_response else None, 42 | name=cast(str, api_response["Name"]), 43 | archiveurl=cast(str, api_response["ArchiveRoot"]), 44 | distribution=cast(str, api_response["Distribution"]) if "Distribution" in api_response else None, 45 | components=cast(List[str], api_response["Components"]) if "Components" in api_response else None, 46 | architectures=cast(List[str], api_response["Architectures"]) if "Architectures" in api_response else None, 47 | meta=cast(List[Dict[str, str]], api_response["Meta"]) if "Meta" in api_response else None, 48 | downloaddate=cast(str, api_response["LastDownloadDate"]) if "LastDownloadDate" in api_response else None, 49 | filter=cast(str, api_response["Filter"]) if "Filter" in api_response else None, 50 | status=cast(int, api_response["Status"]) if "Status" in api_response else None, 51 | worker_pid=cast(int, api_response["WorkerPID"]) if "WorkerPID" in api_response else None, 52 | filter_with_deps=cast(bool, api_response["FilterWithDeps"]) if "FilterWithDeps" in api_response else False, 53 | skip_component_check=cast(bool, api_response["SkipComponentCheck"] 54 | ) if "SkipComponentCheck" in api_response else False, 55 | skip_architecture_check=cast(bool, api_response["SkipArchitectureCheck"] 56 | ) if "SkipArchitectureCheck" in api_response else False, 57 | download_sources=cast(bool, api_response["DownloadSources"] 58 | ) if "DownloadSources" in api_response else False, 59 | download_udebs=cast(bool, api_response["DownloadUdebs"] 60 | ) if "DownloadUdebs" in api_response else False, 61 | download_installer=cast(bool, api_response["DownloadInstaller"] 62 | ) if "DownloadInstaller" in api_response else False, 63 | ) 64 | 65 | def list(self) -> Sequence[Mirror]: 66 | resp = self.do_get("api/mirrors") 67 | 68 | mirrors = [] 69 | for mirr in resp.json(): 70 | mirrors.append(self.mirror_from_response(mirr)) 71 | return mirrors 72 | 73 | def update(self, name: str, ignore_signatures: bool = False) -> None: 74 | body = {} 75 | if ignore_signatures: 76 | body["IgnoreSignatures"] = ignore_signatures 77 | self.do_put("api/mirrors/%s" % (quote(name)), json=body) 78 | 79 | def edit(self, name: str, newname: Optional[str] = None, archiveurl: Optional[str] = None, 80 | filter: Optional[str] = None, architectures: Optional[List[str]] = None, 81 | components: Optional[List[str]] = None, keyrings: Optional[List[str]] = None, 82 | filter_with_deps: bool = False, skip_existing_packages: bool = False, 83 | download_sources: bool = False, download_udebs: bool = False, 84 | skip_component_check: bool = False, ignore_checksums: bool = False, 85 | ignore_signatures: bool = False, force_update: bool = False) -> None: 86 | 87 | body = {} # type: T_BodyDict 88 | if newname: 89 | body["Name"] = newname 90 | if archiveurl: 91 | body["ArchiveURL"] = archiveurl 92 | if filter: 93 | body["Filter"] = filter 94 | if architectures: 95 | body["Architectures"] = architectures 96 | if components: 97 | body["Components"] = components 98 | if keyrings: 99 | body["Keyrings"] = keyrings 100 | if filter_with_deps: 101 | body["FilterWithDeps"] = filter_with_deps 102 | if download_sources: 103 | body["DownloadSources"] = download_sources 104 | if download_udebs: 105 | body["DownloadUdebs"] = download_udebs 106 | if skip_component_check: 107 | body["SkipComponentCheck"] = skip_component_check 108 | if ignore_checksums: 109 | body["IgnoreChecksums"] = ignore_checksums 110 | if ignore_signatures: 111 | body["IgnoreSignatures"] = ignore_signatures 112 | if skip_existing_packages: 113 | body["SkipExistingPackages"] = skip_existing_packages 114 | if force_update: 115 | body["ForceUpdate"] = force_update 116 | 117 | self.do_put("api/mirrors/%s" % (quote(name)), json=body) 118 | 119 | def show(self, name: str) -> Mirror: 120 | resp = self.do_get("api/mirrors/%s" % (quote(name))) 121 | return self.mirror_from_response(resp.json()) 122 | 123 | def list_packages(self, name: str, query: Optional[str] = None, with_deps: bool = False, 124 | detailed: bool = False) -> Sequence[Package]: 125 | params = {} 126 | if query is not None: 127 | params["q"] = query 128 | if with_deps: 129 | params["withDeps"] = "1" 130 | if detailed: 131 | params["format"] = "details" 132 | 133 | resp = self.do_get("api/mirrors/%s/packages" % 134 | quote(name), params=params) 135 | ret = [] 136 | for rpkg in resp.json(): 137 | ret.append(PackageAPISection.package_from_response(rpkg)) 138 | return ret 139 | 140 | def delete(self, name: str) -> None: 141 | self.do_delete("api/mirrors/%s" % quote(name)) 142 | 143 | def create(self, name: str, archiveurl: str, distribution: Optional[str] = None, 144 | filter: Optional[str] = None, components: Optional[List[str]] = None, 145 | architectures: Optional[List[str]] = None, keyrings: Optional[List[str]] = None, 146 | download_sources: bool = False, download_udebs: bool = False, 147 | download_installer: bool = False, filter_with_deps: bool = False, 148 | skip_component_check: bool = False, skip_architecture_check: bool = False, 149 | ignore_signatures: bool = False) -> Mirror: 150 | data = { 151 | "Name": name, 152 | "ArchiveURL": archiveurl 153 | } # type: T_BodyDict 154 | 155 | if distribution: 156 | data["Distribution"] = distribution 157 | if filter: 158 | data["Filter"] = filter 159 | if components: 160 | data["Components"] = components 161 | if architectures: 162 | data["Architectures"] = architectures 163 | if keyrings: 164 | data["Keyrings"] = keyrings 165 | if download_sources: 166 | data["DownloadSources"] = download_sources 167 | if download_udebs: 168 | data["DownloadUdebs"] = download_udebs 169 | if download_installer: 170 | data["DownloadInstaller"] = download_installer 171 | if filter_with_deps: 172 | data["FilterWithDeps"] = filter_with_deps 173 | if skip_component_check: 174 | data["SkipComponentCheck"] = skip_component_check 175 | if skip_architecture_check: 176 | data["SkipArchitectureCheck"] = skip_architecture_check 177 | if ignore_signatures: 178 | data["IgnoreSignatures"] = ignore_signatures 179 | 180 | resp = self.do_post("api/mirrors", json=data) 181 | 182 | return self.mirror_from_response(resp.json()) 183 | -------------------------------------------------------------------------------- /aptly_api/parts/misc.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import cast 7 | 8 | from aptly_api.base import AptlyAPIException, BaseAPIClient 9 | 10 | 11 | class MiscAPISection(BaseAPIClient): 12 | def graph(self, ext: str, layout: str = "horizontal") -> None: 13 | raise NotImplementedError("The Graph API is not yet supported") 14 | 15 | def version(self) -> str: 16 | resp = self.do_get("api/version") 17 | if "Version" in resp.json(): 18 | return cast(str, resp.json()["Version"]) 19 | else: 20 | raise AptlyAPIException("Aptly server didn't return a valid response object:\n%s" % resp.text) 21 | -------------------------------------------------------------------------------- /aptly_api/parts/packages.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | from typing import NamedTuple, Dict, Union, Optional 8 | from urllib.parse import quote 9 | 10 | from aptly_api.base import BaseAPIClient 11 | 12 | 13 | Package = NamedTuple('Package', [ 14 | ('key', str), 15 | ('short_key', Optional[str]), 16 | ('files_hash', Optional[str]), 17 | ('fields', Optional[Dict[str, str]]), 18 | ]) 19 | 20 | 21 | class PackageAPISection(BaseAPIClient): 22 | @staticmethod 23 | def package_from_response(api_response: Union[str, Dict[str, str]]) -> Package: 24 | if isinstance(api_response, str): 25 | return Package( 26 | key=api_response, 27 | short_key=None, 28 | files_hash=None, 29 | fields=None, 30 | ) 31 | else: 32 | return Package( 33 | key=api_response["Key"], 34 | short_key=api_response["ShortKey"] if "ShortKey" in api_response else None, 35 | files_hash=api_response["FilesHash"] if "FilesHash" in api_response else None, 36 | fields=api_response, 37 | ) 38 | 39 | def show(self, key: str) -> Package: 40 | resp = self.do_get("api/packages/%s" % quote(key)) 41 | return self.package_from_response(resp.json()) 42 | -------------------------------------------------------------------------------- /aptly_api/parts/publish.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import NamedTuple, Sequence, Dict, Union, List, cast, Optional 7 | from urllib.parse import quote 8 | 9 | from aptly_api.base import BaseAPIClient, AptlyAPIException 10 | 11 | PublishEndpoint = NamedTuple('PublishEndpoint', [ 12 | ('storage', str), 13 | ('prefix', str), 14 | ('distribution', str), 15 | ('source_kind', str), 16 | ('sources', Sequence[Dict[str, str]]), 17 | ('architectures', Sequence[str]), 18 | ('label', str), 19 | ('origin', str), 20 | ('acquire_by_hash', bool), 21 | ]) 22 | 23 | 24 | T_BodyDict = Dict[str, Union[str, bool, Sequence[Dict[str, str]], Sequence[str], Dict[str, Union[bool, str]]]] 25 | 26 | 27 | class PublishAPISection(BaseAPIClient): 28 | @staticmethod 29 | def endpoint_from_response(api_response: Union[Dict[str, str], Dict[str, List[str]], 30 | Dict[str, List[Dict[str, str]]]]) -> PublishEndpoint: 31 | return PublishEndpoint( 32 | storage=cast(str, api_response["Storage"]), 33 | prefix=cast(str, api_response["Prefix"]), 34 | distribution=cast(str, api_response["Distribution"]), 35 | source_kind=cast(str, api_response["SourceKind"]), 36 | sources=cast(List[Dict[str, str]], api_response["Sources"]), 37 | architectures=cast(List[str], api_response["Architectures"]), 38 | label=cast(str, api_response["Label"]), 39 | origin=cast(str, api_response["Origin"]), 40 | acquire_by_hash=cast(bool, api_response["AcquireByHash"]), 41 | ) 42 | 43 | @staticmethod 44 | def escape_prefix(prefix: str) -> str: 45 | if prefix == ".": 46 | return ":." 47 | if "/" in prefix: 48 | # prefix has not yet been quoted as described at 49 | # https://www.aptly.info/doc/api/publish/ 50 | if "_" in prefix: 51 | prefix = prefix.replace("_", "__") 52 | prefix = prefix.replace("/", "_") 53 | return prefix 54 | 55 | def list(self) -> Sequence[PublishEndpoint]: 56 | resp = self.do_get("api/publish") 57 | ret = [] 58 | for rpe in resp.json(): 59 | ret.append(self.endpoint_from_response(rpe)) 60 | return ret 61 | 62 | def publish(self, *, source_kind: str = "local", 63 | sources: Sequence[Dict[str, str]], 64 | architectures: Sequence[str], 65 | prefix: Optional[str] = None, distribution: Optional[str] = None, label: Optional[str] = None, 66 | origin: Optional[str] = None, force_overwrite: bool = False, 67 | sign_skip: bool = False, sign_batch: bool = True, sign_gpgkey: Optional[str] = None, 68 | sign_keyring: Optional[str] = None, sign_secret_keyring: Optional[str] = None, 69 | sign_passphrase: Optional[str] = None, sign_passphrase_file: Optional[str] = None, 70 | acquire_by_hash: Optional[bool] = None) -> PublishEndpoint: 71 | """ 72 | Example: 73 | 74 | .. code-block:: python 75 | p.publish( 76 | sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], 77 | prefix='s3:myendpoint:test/a_1', distribution='test', sign_batch=True, 78 | sign_gpgkey='A16BE921', sign_passphrase='*********' 79 | ) 80 | """ 81 | if sign_passphrase is not None and sign_passphrase_file is not None: 82 | raise AptlyAPIException("Can't use sign_passphrase and sign_passphrase_file at the same time") 83 | 84 | for source in sources: 85 | if "name" not in source and "Name" not in source: 86 | raise AptlyAPIException("Each source in publish() must contain the 'name' attribute") 87 | 88 | url = "api/publish" 89 | if prefix is not None and prefix != "": 90 | url = "api/publish/%s" % quote(self.escape_prefix(prefix)) 91 | 92 | body = { 93 | "SourceKind": source_kind, 94 | "Sources": sources, 95 | } # type: T_BodyDict 96 | 97 | if architectures is not None: 98 | body["Architectures"] = architectures 99 | if distribution is not None: 100 | body["Distribution"] = distribution 101 | if label is not None: 102 | body["Label"] = label 103 | if origin is not None: 104 | body["Origin"] = origin 105 | if force_overwrite: 106 | body["ForceOverwrite"] = True 107 | if acquire_by_hash is not None: 108 | body["AcquireByHash"] = acquire_by_hash 109 | 110 | sign_dict = {} # type: Dict[str, Union[bool,str]] 111 | if sign_skip: 112 | sign_dict["Skip"] = True 113 | else: 114 | sign_dict["Batch"] = sign_batch 115 | if sign_gpgkey is not None: 116 | sign_dict["GpgKey"] = sign_gpgkey 117 | if sign_keyring is not None: 118 | sign_dict["Keyring"] = sign_keyring 119 | if sign_secret_keyring is not None: 120 | sign_dict["SecretKeyring"] = sign_secret_keyring 121 | if sign_passphrase is not None: 122 | sign_dict["Passphrase"] = sign_passphrase 123 | if sign_passphrase_file is not None: 124 | sign_dict["PassphraseFile"] = sign_passphrase_file 125 | body["Signing"] = sign_dict 126 | 127 | resp = self.do_post(url, json=body) 128 | return self.endpoint_from_response(resp.json()) 129 | 130 | def update(self, *, prefix: str, distribution: str, 131 | snapshots: Optional[Sequence[Dict[str, str]]] = None, force_overwrite: bool = False, 132 | sign_skip: bool = False, sign_batch: bool = True, sign_gpgkey: Optional[str] = None, 133 | sign_keyring: Optional[str] = None, sign_secret_keyring: Optional[str] = None, 134 | sign_passphrase: Optional[str] = None, sign_passphrase_file: Optional[str] = None, 135 | skip_contents: Optional[bool] = False, 136 | skip_cleanup: Optional[bool] = False) -> PublishEndpoint: 137 | """ 138 | Example: 139 | 140 | .. code-block:: python 141 | p.update( 142 | prefix="s3:maurusnet:nightly/stretch", distribution="mn-nightly", 143 | sign_batch=True, sign_gpgkey='A16BE921', sign_passphrase='***********' 144 | ) 145 | """ 146 | if sign_passphrase is not None and sign_passphrase_file is not None: 147 | raise AptlyAPIException("Can't use sign_passphrase and sign_passphrase_file at the same time") 148 | 149 | body = {} # type: T_BodyDict 150 | 151 | if snapshots is not None: 152 | for source in snapshots: 153 | if "name" not in source and "Name" not in source: 154 | raise AptlyAPIException("Each source in update() must contain the 'name' attribute") 155 | body["Snapshots"] = snapshots 156 | 157 | if force_overwrite: 158 | body["ForceOverwrite"] = True 159 | if skip_cleanup: 160 | body["SkipCleanup"] = True 161 | if skip_contents: 162 | body["SkipContents"] = True 163 | 164 | sign_dict = {} # type: Dict[str, Union[bool,str]] 165 | if sign_skip: 166 | sign_dict["Skip"] = True 167 | else: 168 | sign_dict["Batch"] = sign_batch 169 | if sign_gpgkey is not None: 170 | sign_dict["GpgKey"] = sign_gpgkey 171 | if sign_keyring is not None: 172 | sign_dict["Keyring"] = sign_keyring 173 | if sign_secret_keyring is not None: 174 | sign_dict["SecretKeyring"] = sign_secret_keyring 175 | if sign_passphrase is not None: 176 | sign_dict["Passphrase"] = sign_passphrase 177 | if sign_passphrase_file is not None: 178 | sign_dict["PassphraseFile"] = sign_passphrase_file 179 | body["Signing"] = sign_dict 180 | 181 | resp = self.do_put("api/publish/%s/%s" % 182 | (quote(self.escape_prefix(prefix)), quote(distribution),), json=body) 183 | return self.endpoint_from_response(resp.json()) 184 | 185 | def drop(self, *, prefix: str, distribution: str, force_delete: bool = False) -> None: 186 | params = {} 187 | if force_delete: 188 | params["force"] = "1" 189 | self.do_delete("api/publish/%s/%s" % 190 | (quote(self.escape_prefix(prefix)), quote(distribution),), params=params) 191 | -------------------------------------------------------------------------------- /aptly_api/parts/repos.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import NamedTuple, Sequence, Dict, Union, cast, Optional 7 | from urllib.parse import quote 8 | 9 | from aptly_api.base import BaseAPIClient, AptlyAPIException 10 | from aptly_api.parts.packages import PackageAPISection, Package 11 | 12 | Repo = NamedTuple('Repo', [ 13 | ('name', str), 14 | ('comment', Optional[str]), 15 | ('default_distribution', Optional[str]), 16 | ('default_component', Optional[str]) 17 | ]) 18 | 19 | FileReport = NamedTuple('FileReport', [ 20 | ('failed_files', Sequence[str]), 21 | ('report', Dict[str, Sequence[str]]) 22 | ]) 23 | 24 | 25 | class ReposAPISection(BaseAPIClient): 26 | @staticmethod 27 | def repo_from_response(api_response: Dict[str, str]) -> Repo: 28 | return Repo( 29 | name=api_response["Name"], 30 | default_component=api_response["DefaultComponent"] if "DefaultComponent" in api_response else None, 31 | default_distribution=api_response["DefaultDistribution"] if "DefaultDistribution" in api_response else None, 32 | comment=api_response["Comment"] if "Comment" in api_response else None, 33 | ) 34 | 35 | @staticmethod 36 | def filereport_from_response(api_response: Dict[str, Union[Sequence[str], Dict[str, Sequence[str]]]]) -> FileReport: 37 | return FileReport( 38 | failed_files=cast(Sequence[str], api_response["FailedFiles"]), 39 | report=cast(Dict[str, Sequence[str]], api_response["Report"]), 40 | ) 41 | 42 | def create(self, reponame: str, comment: Optional[str] = None, default_distribution: Optional[str] = None, 43 | default_component: Optional[str] = None) -> Repo: 44 | data = { 45 | "Name": reponame, 46 | } 47 | 48 | if comment: 49 | data["Comment"] = comment 50 | if default_distribution: 51 | data["DefaultDistribution"] = default_distribution 52 | if default_component: 53 | data["DefaultComponent"] = default_component 54 | 55 | resp = self.do_post("api/repos", json=data) 56 | 57 | return self.repo_from_response(resp.json()) 58 | 59 | def show(self, reponame: str) -> Repo: 60 | resp = self.do_get("api/repos/%s" % quote(reponame)) 61 | return self.repo_from_response(resp.json()) 62 | 63 | def search_packages(self, reponame: str, query: Optional[str] = None, with_deps: bool = False, 64 | detailed: bool = False) -> Sequence[Package]: 65 | if query is None and with_deps: 66 | raise AptlyAPIException("search_packages can't include dependencies (with_deps==True) without" 67 | "a query") 68 | params = {} 69 | if query: 70 | params["q"] = query 71 | 72 | if with_deps: 73 | params["withDeps"] = "1" 74 | 75 | if detailed: 76 | params["format"] = "details" 77 | 78 | resp = self.do_get("api/repos/%s/packages" % quote(reponame), params=params) 79 | ret = [] 80 | for rpkg in resp.json(): 81 | ret.append(PackageAPISection.package_from_response(rpkg)) 82 | return ret 83 | 84 | def edit(self, reponame: str, comment: Optional[str] = None, default_distribution: Optional[str] = None, 85 | default_component: Optional[str] = None) -> Repo: 86 | if comment is None and default_component is None and default_distribution is None: 87 | raise AptlyAPIException("edit requires at least one of 'comment', 'default_distribution' or " 88 | "'default_component'.") 89 | 90 | body = {} 91 | if comment is not None: 92 | body["Comment"] = comment 93 | if default_distribution is not None: 94 | body["DefaultDistribution"] = default_distribution 95 | if default_component is not None: 96 | body["DefaultComponent"] = default_component 97 | 98 | resp = self.do_put("api/repos/%s" % quote(reponame), json=body) 99 | return self.repo_from_response(resp.json()) 100 | 101 | def list(self) -> Sequence[Repo]: 102 | resp = self.do_get("api/repos") 103 | 104 | repos = [] 105 | for rdesc in resp.json(): 106 | repos.append( 107 | self.repo_from_response(rdesc) 108 | ) 109 | return repos 110 | 111 | def delete(self, reponame: str, force: bool = False) -> None: 112 | self.do_delete("api/repos/%s" % quote(reponame), params={"force": "1" if force else "0"}) 113 | 114 | def add_uploaded_file(self, reponame: str, dir: str, filename: Optional[str] = None, 115 | remove_processed_files: bool = True, force_replace: bool = False) -> FileReport: 116 | params = { 117 | "noRemove": "0" if remove_processed_files else "1", 118 | } 119 | if force_replace: 120 | params["forceReplace"] = "1" 121 | 122 | if filename is None: 123 | resp = self.do_post("api/repos/%s/file/%s" % (quote(reponame), quote(dir),), params=params) 124 | else: 125 | resp = self.do_post("api/repos/%s/file/%s/%s" % (quote(reponame), quote(dir), quote(filename),), 126 | params=params) 127 | 128 | return self.filereport_from_response(resp.json()) 129 | 130 | def add_packages_by_key(self, reponame: str, *package_keys: str) -> Repo: 131 | resp = self.do_post("api/repos/%s/packages" % quote(reponame), json={ 132 | "PackageRefs": package_keys, 133 | }) 134 | return self.repo_from_response(resp.json()) 135 | 136 | def delete_packages_by_key(self, reponame: str, *package_keys: str) -> Repo: 137 | resp = self.do_delete("api/repos/%s/packages" % quote(reponame), json={ 138 | "PackageRefs": package_keys, 139 | }) 140 | return self.repo_from_response(resp.json()) 141 | -------------------------------------------------------------------------------- /aptly_api/parts/snapshots.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from datetime import datetime 7 | 8 | from typing import NamedTuple, Sequence, Optional, Dict, Union, cast, List 9 | from urllib.parse import quote 10 | 11 | import iso8601 12 | 13 | from aptly_api.base import BaseAPIClient, AptlyAPIException 14 | from aptly_api.parts.packages import Package, PackageAPISection 15 | 16 | Snapshot = NamedTuple('Snapshot', [ 17 | ('name', str), 18 | ('description', Optional[str]), 19 | ('created_at', Optional[datetime]) 20 | ]) 21 | 22 | 23 | class SnapshotAPISection(BaseAPIClient): 24 | @staticmethod 25 | def snapshot_from_response(api_response: Dict[str, Union[str, None]]) -> Snapshot: 26 | return Snapshot( 27 | # use a cast() here as `name` can never be None, but the `api_response` declaration can't handle that 28 | name=cast(str, api_response["Name"]), 29 | description=api_response["Description"] if "Description" in api_response else None, 30 | created_at=iso8601.parse_date( 31 | cast(str, api_response["CreatedAt"]) 32 | ) if "CreatedAt" in api_response else None, 33 | ) 34 | 35 | def list(self, sort: str = 'name') -> Sequence[Snapshot]: 36 | if sort not in ['name', 'time']: 37 | raise AptlyAPIException("Snapshot LIST only supports two sort modes: 'name' and 'time'. %s is not " 38 | "supported." % sort) 39 | resp = self.do_get("api/snapshots", params={"sort": sort}) 40 | ret = [] 41 | for rsnap in resp.json(): 42 | ret.append(self.snapshot_from_response(rsnap)) 43 | return ret 44 | 45 | def create_from_repo(self, reponame: str, snapshotname: str, description: Optional[str] = None) -> Snapshot: 46 | body = { 47 | "Name": snapshotname, 48 | } 49 | if description is not None: 50 | body["Description"] = description 51 | 52 | resp = self.do_post("api/repos/%s/snapshots" % quote(reponame), json=body) 53 | return self.snapshot_from_response(resp.json()) 54 | 55 | def create_from_mirror(self, mirrorname: str, snapshotname: str, description: Optional[str] = None) -> Snapshot: 56 | body = { 57 | "Name": snapshotname 58 | } 59 | if description is not None: 60 | body["Description"] = description 61 | 62 | resp = self.do_post("api/mirrors/%s/snapshots" % 63 | quote(mirrorname), json=body) 64 | return self.snapshot_from_response(resp.json()) 65 | 66 | def create_from_packages(self, snapshotname: str, description: Optional[str] = None, 67 | source_snapshots: Optional[Sequence[str]] = None, 68 | package_refs: Optional[Sequence[str]] = None) -> Snapshot: 69 | body = { 70 | "Name": snapshotname, 71 | } # type: Dict[str, Union[str, Sequence[str]]] 72 | if description is not None: 73 | body["Description"] = description 74 | 75 | if source_snapshots is not None: 76 | body["SourceSnapshots"] = source_snapshots 77 | 78 | if package_refs is not None: 79 | body["PackageRefs"] = package_refs 80 | 81 | resp = self.do_post("api/snapshots", json=body) 82 | return self.snapshot_from_response(resp.json()) 83 | 84 | def update(self, snapshotname: str, newname: Optional[str] = None, 85 | newdescription: Optional[str] = None) -> Snapshot: 86 | if newname is None and newdescription is None: 87 | raise AptlyAPIException("When updating a Snapshot you must at lease provide either a new name or a " 88 | "new description.") 89 | body = {} # type: Dict[str, Union[str, Sequence[str]]] 90 | if newname is not None: 91 | body["Name"] = newname 92 | 93 | if newdescription is not None: 94 | body["Description"] = newdescription 95 | 96 | resp = self.do_put("api/snapshots/%s" % quote(snapshotname), json=body) 97 | return self.snapshot_from_response(resp.json()) 98 | 99 | def show(self, snapshotname: str) -> Snapshot: 100 | resp = self.do_get("api/snapshots/%s" % quote(snapshotname)) 101 | return self.snapshot_from_response(resp.json()) 102 | 103 | def list_packages(self, snapshotname: str, query: Optional[str] = None, with_deps: bool = False, 104 | detailed: bool = False) -> Sequence[Package]: 105 | params = {} 106 | if query is not None: 107 | params["q"] = query 108 | if with_deps: 109 | params["withDeps"] = "1" 110 | if detailed: 111 | params["format"] = "details" 112 | 113 | resp = self.do_get("api/snapshots/%s/packages" % 114 | quote(snapshotname), params=params) 115 | ret = [] 116 | for rpkg in resp.json(): 117 | ret.append(PackageAPISection.package_from_response(rpkg)) 118 | return ret 119 | 120 | def delete(self, snapshotname: str, force: bool = False) -> None: 121 | params = None 122 | if force: 123 | params = { 124 | "force": "1", 125 | } 126 | 127 | self.do_delete("api/snapshots/%s" % quote(snapshotname), params=params) 128 | 129 | def diff(self, snapshot1: str, snapshot2: str) -> Sequence[Dict[str, str]]: 130 | resp = self.do_get("api/snapshots/%s/diff/%s" % 131 | (quote(snapshot1), quote(snapshot2),)) 132 | return cast(List[Dict[str, str]], resp.json()) 133 | -------------------------------------------------------------------------------- /aptly_api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | from .test_base import * # noqa 8 | from .test_client import * # noqa 9 | from .test_files import * # noqa 10 | from .test_misc import * # noqa 11 | from .test_packages import * # noqa 12 | from .test_publish import * # noqa 13 | from .test_repos import * # noqa 14 | from .test_snapshots import * # noqa 15 | from .test_mirrors import * # noqa 16 | -------------------------------------------------------------------------------- /aptly_api/tests/test_base.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | -------------------------------------------------------------------------------- /aptly_api/tests/test_client.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import Any, cast 7 | from unittest.case import TestCase 8 | 9 | import requests 10 | import requests_mock 11 | 12 | from aptly_api import Client as AptlyClient 13 | 14 | 15 | # as we're testing the individual parts, this is rather simple 16 | from aptly_api.base import AptlyAPIException 17 | 18 | 19 | class ClientTests(TestCase): 20 | def __init__(self, *args: Any) -> None: 21 | super().__init__(*args) 22 | self.client = AptlyClient("http://test/") 23 | 24 | def test_instantiate(self) -> None: 25 | cl = AptlyClient("http://test/") 26 | self.assertEqual( 27 | str(cl), 28 | "Client (Aptly API Client) " 29 | ) 30 | 31 | @requests_mock.Mocker(kw='rmock') 32 | def test_api_subdir_get(self, *, rmock: requests_mock.Mocker) -> None: 33 | # register mock:// scheme with urllib.parse 34 | import urllib.parse 35 | urllib.parse.uses_netloc += ['mock'] 36 | urllib.parse.uses_relative += ['mock'] 37 | urllib.parse.uses_fragment += ['mock'] 38 | urllib.parse.uses_params += ['mock'] 39 | 40 | cl = AptlyClient("mock://test/basedir/") 41 | rmock.get("mock://test/basedir/api/test", status_code=200, text='') 42 | cl.files.do_get("api/test") 43 | self.assertTrue(rmock.called) 44 | 45 | def test_error_no_error(self) -> None: 46 | class MockResponse: 47 | def __init__(self, status_code: int = 200) -> None: 48 | self.status_code = status_code 49 | 50 | self.assertEqual( 51 | self.client.files._error_from_response(cast(requests.Response, MockResponse())), 52 | "no error (status 200)" 53 | ) 54 | 55 | def test_error_no_json(self) -> None: 56 | adapter = requests_mock.Adapter() 57 | adapter.register_uri("GET", "mock://test/api", status_code=400, text="this is not json", reason="test") 58 | session = requests.session() 59 | session.mount("mock", adapter) 60 | resp = session.get("mock://test/api") 61 | 62 | self.assertEqual( 63 | self.client.files._error_from_response(resp), 64 | "400 test this is not json" 65 | ) 66 | 67 | def test_error_dict(self) -> None: 68 | adapter = requests_mock.Adapter() 69 | adapter.register_uri("GET", "mock://test/api", status_code=400, text='{"error": "error", "meta": "meta"}', 70 | reason="test") 71 | session = requests.session() 72 | session.mount("mock", adapter) 73 | resp = session.get("mock://test/api") 74 | self.assertEqual( 75 | self.client.files._error_from_response(resp), 76 | "400 - test - error (meta)" 77 | ) 78 | 79 | def test_error_list(self) -> None: 80 | adapter = requests_mock.Adapter() 81 | adapter.register_uri("GET", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', 82 | reason="test") 83 | session = requests.session() 84 | session.mount("mock", adapter) 85 | resp = session.get("mock://test/api") 86 | self.assertEqual( 87 | self.client.files._error_from_response(resp), 88 | "400 - test - error (meta)" 89 | ) 90 | 91 | @requests_mock.Mocker(kw='rmock') 92 | def test_error_get(self, *, rmock: requests_mock.Mocker) -> None: 93 | rmock.register_uri("GET", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', 94 | reason="test") 95 | with self.assertRaises(AptlyAPIException): 96 | self.client.files.do_get("mock://test/api") 97 | 98 | @requests_mock.Mocker(kw='rmock') 99 | def test_error_post(self, *, rmock: requests_mock.Mocker) -> None: 100 | rmock.register_uri("POST", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', 101 | reason="test") 102 | with self.assertRaises(AptlyAPIException): 103 | self.client.files.do_post("mock://test/api") 104 | 105 | @requests_mock.Mocker(kw='rmock') 106 | def test_error_put(self, *, rmock: requests_mock.Mocker) -> None: 107 | rmock.register_uri("PUT", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', 108 | reason="test") 109 | with self.assertRaises(AptlyAPIException): 110 | self.client.files.do_put("mock://test/api") 111 | 112 | @requests_mock.Mocker(kw='rmock') 113 | def test_error_delete(self, *, rmock: requests_mock.Mocker) -> None: 114 | rmock.register_uri("DELETE", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', 115 | reason="test") 116 | with self.assertRaises(AptlyAPIException): 117 | self.client.files.do_delete("mock://test/api") 118 | -------------------------------------------------------------------------------- /aptly_api/tests/test_files.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import Any 7 | 8 | import os 9 | from unittest.case import TestCase 10 | 11 | import requests_mock 12 | 13 | from aptly_api.base import AptlyAPIException 14 | from aptly_api.parts.files import FilesAPISection 15 | 16 | 17 | @requests_mock.Mocker(kw='rmock') 18 | class FilesAPISectionTests(TestCase): 19 | def __init__(self, *args: Any) -> None: 20 | super().__init__(*args) 21 | self.fapi = FilesAPISection("http://test/") 22 | 23 | def test_list(self, *, rmock: requests_mock.Mocker) -> None: 24 | rmock.get("http://test/api/files", text='["aptly-0.9"]') 25 | self.assertSequenceEqual(self.fapi.list(), ["aptly-0.9"]) 26 | 27 | def test_list_dir(self, *, rmock: requests_mock.Mocker) -> None: 28 | rmock.get("http://test/api/files/dir", text='["dir/aptly_0.9~dev+217+ge5d646c_i386.deb"]') 29 | self.assertSequenceEqual(self.fapi.list("dir"), ["dir/aptly_0.9~dev+217+ge5d646c_i386.deb"]) 30 | 31 | def test_upload_file(self, *, rmock: requests_mock.Mocker) -> None: 32 | rmock.post("http://test/api/files/test", text='["test/testpkg.deb"]') 33 | self.assertSequenceEqual( 34 | self.fapi.upload("test", os.path.join(os.path.dirname(__file__), "testpkg.deb")), 35 | ['test/testpkg.deb'], 36 | ) 37 | 38 | def test_upload_invalid(self, *, rmock: requests_mock.Mocker) -> None: 39 | with self.assertRaises(AptlyAPIException): 40 | self.fapi.upload("test", "noexistant") 41 | 42 | def test_upload_failed(self, *, rmock: requests_mock.Mocker) -> None: 43 | rmock.post("http://test/api/files/test", text='["test/testpkg.deb"]', 44 | status_code=500) 45 | with self.assertRaises(AptlyAPIException): 46 | self.fapi.upload("test", os.path.join(os.path.dirname(__file__), "testpkg.deb")) 47 | 48 | def test_delete(self, *, rmock: requests_mock.Mocker) -> None: 49 | rmock.delete("http://test/api/files/test", 50 | text='{}') 51 | self.fapi.delete("test") 52 | -------------------------------------------------------------------------------- /aptly_api/tests/test_mirrors.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import Any 7 | from unittest.case import TestCase 8 | from inspect import signature 9 | import json 10 | 11 | import requests_mock 12 | 13 | from aptly_api.parts.packages import Package 14 | from aptly_api.parts.mirrors import MirrorsAPISection, Mirror 15 | 16 | 17 | @requests_mock.Mocker(kw='rmock') 18 | class MirrorsAPISectionTests(TestCase): 19 | def __init__(self, *args: Any) -> None: 20 | super().__init__(*args) 21 | self.miapi = MirrorsAPISection("http://test/") 22 | 23 | def test_create(self, *, rmock: requests_mock.Mocker) -> None: 24 | expected = {"Name": "testname", "ArchiveURL": "http://randomurl.url"} 25 | 26 | rmock.post("http://test/api/mirrors", text="""{"Name":"nocheck", "ArchiveRoot":"nocheck"}""") 27 | self.miapi.create(expected["Name"], expected["ArchiveURL"]) 28 | 29 | self.assertEqual(rmock.request_history[0].method, "POST") 30 | self.assertEqual(len(rmock.request_history[0].json()), len(expected)) 31 | self.assertEqual(rmock.request_history[0].json(), expected) 32 | 33 | def test_create_all_args(self, *, rmock: requests_mock.Mocker) -> None: 34 | expected = { 35 | "Name": "aptly-mirror", 36 | "ArchiveURL": "https://deb.nodesource.com/node_10.x/", 37 | "Distribution": "bionic", 38 | "Filter": "test", 39 | "Components": ["main"], 40 | "Architectures": ["amd64"], 41 | "Keyrings": ["/path/to/keyring"], 42 | "DownloadSources": True, 43 | "DownloadUdebs": True, 44 | "DownloadInstaller": True, 45 | "FilterWithDeps": True, 46 | "SkipComponentCheck": True, 47 | "SkipArchitectureCheck": True, 48 | "IgnoreSignatures": True, 49 | } 50 | # Keep us from getting out of lockstep with the number of args to create 51 | self.assertEqual(len(signature(self.miapi.create).parameters), len(expected)) 52 | 53 | rmock.post("http://test/api/mirrors", text="""{"Name":"nocheck", "ArchiveRoot":"nocheck"}""") 54 | self.miapi.create( 55 | name="aptly-mirror", 56 | archiveurl="https://deb.nodesource.com/node_10.x/", 57 | distribution="bionic", 58 | filter="test", 59 | components=["main"], 60 | architectures=["amd64"], 61 | keyrings=["/path/to/keyring"], 62 | download_sources=True, 63 | download_udebs=True, 64 | download_installer=True, 65 | filter_with_deps=True, 66 | skip_component_check=True, 67 | skip_architecture_check=True, 68 | ignore_signatures=True, 69 | ) 70 | 71 | self.assertEqual(rmock.request_history[0].method, "POST") 72 | self.assertEqual(len(rmock.request_history[0].json()), len(expected)) 73 | self.assertEqual(rmock.request_history[0].json(), expected) 74 | 75 | def test_mirror_from_response(self, *, rmock: requests_mock.Mocker) -> None: 76 | self.assertSequenceEqual( 77 | self.miapi.mirror_from_response( 78 | json.loads("""{ 79 | "UUID": "2cb5985a-a23f-4a1f-8eb6-d5409193b4eb", 80 | "Name": "aptly-mirror", 81 | "ArchiveRoot": "https://deb.nodesource.com/node_10.x/", 82 | "Distribution": "bionic", 83 | "Components": ["main"], 84 | "Architectures": ["amd64"], 85 | "LastDownloadDate": "0001-01-01T00:00:00Z", 86 | "Meta": [{"Architectures": "i386 amd64 armhf arm64", 87 | "Codename": "bionic", 88 | "Components": "main", 89 | "Date": "Tue, 06 Apr 2021 21:05:41 UTC", 90 | "Description": " Apt Repository for the Node.JS 10.x Branch", 91 | "Label": "Node Source", 92 | "Origin": "Node Source"}], 93 | "Filter": "test", 94 | "Status": 0, 95 | "WorkerPID": 0, 96 | "FilterWithDeps": true, 97 | "SkipComponentCheck": true, 98 | "SkipArchitectureCheck": true, 99 | "DownloadSources": true, 100 | "DownloadUdebs": true, 101 | "DownloadInstaller": true 102 | }""") 103 | ), 104 | Mirror( 105 | uuid='2cb5985a-a23f-4a1f-8eb6-d5409193b4eb', 106 | name="aptly-mirror", 107 | archiveurl="https://deb.nodesource.com/node_10.x/", 108 | distribution='bionic', 109 | components=['main'], 110 | architectures=['amd64'], 111 | downloaddate='0001-01-01T00:00:00Z', 112 | meta=[{"Architectures": "i386 amd64 armhf arm64", 113 | "Codename": "bionic", 114 | "Components": "main", 115 | "Date": "Tue, 06 Apr 2021 21:05:41 UTC", 116 | "Description": " Apt Repository for the Node.JS 10.x Branch", 117 | "Label": "Node Source", "Origin": "Node Source"}], 118 | filter="test", 119 | status=0, 120 | worker_pid=0, 121 | filter_with_deps=True, 122 | skip_component_check=True, 123 | skip_architecture_check=True, 124 | download_sources=True, 125 | download_udebs=True, 126 | download_installer=True 127 | ) 128 | ) 129 | 130 | def test_list(self, *, rmock: requests_mock.Mocker) -> None: 131 | rmock.get("http://test/api/mirrors", 132 | text="""[{"UUID": "2cb5985a-a23f-4a1f-8eb6-d5409193b4eb", 133 | "Name": "aptly-mirror", 134 | "ArchiveRoot": "https://deb.nodesource.com/node_10.x/", 135 | "Distribution": "bionic", "Components": ["main"], 136 | "Architectures": ["amd64"], 137 | "Meta": [{"Architectures": "i386 amd64 armhf arm64", 138 | "Codename": "bionic", "Components": "main", 139 | "Date": "Tue, 06 Apr 2021 21:05:41 UTC", 140 | "Description": " Apt Repository for the Node.JS 10.x Branch", 141 | "Label": "Node Source", "Origin": "Node Source"}], 142 | "LastDownloadDate": "0001-01-01T00:00:00Z", "Filter": "", 143 | "Status": 0, "WorkerPID": 0, "FilterWithDeps": false, 144 | "SkipComponentCheck": false, "SkipArchitectureCheck": false, 145 | "DownloadSources": false, "DownloadUdebs": false, 146 | "DownloadInstaller": false}]""") 147 | self.assertSequenceEqual( 148 | self.miapi.list(), 149 | [ 150 | Mirror( 151 | uuid='2cb5985a-a23f-4a1f-8eb6-d5409193b4eb', 152 | name="aptly-mirror", 153 | archiveurl="https://deb.nodesource.com/node_10.x/", 154 | distribution='bionic', 155 | components=["main"], 156 | architectures=["amd64"], 157 | downloaddate='0001-01-01T00:00:00Z', 158 | meta=[{"Architectures": "i386 amd64 armhf arm64", 159 | "Codename": "bionic", 160 | "Components": "main", 161 | "Date": "Tue, 06 Apr 2021 21:05:41 UTC", 162 | "Description": " Apt Repository for the Node.JS 10.x Branch", 163 | "Label": "Node Source", "Origin": "Node Source"}], 164 | filter="", 165 | status=0, 166 | worker_pid=0, 167 | filter_with_deps=False, 168 | skip_component_check=False, 169 | skip_architecture_check=False, 170 | download_sources=False, 171 | download_udebs=False, 172 | download_installer=False 173 | 174 | ) 175 | ] 176 | ) 177 | 178 | def test_show(self, *, rmock: requests_mock.Mocker) -> None: 179 | rmock.get("http://test/api/mirrors/aptly-mirror", 180 | text="""{"UUID": "2cb5985a-a23f-4a1f-8eb6-d5409193b4eb", 181 | "Name": "aptly-mirror", 182 | "ArchiveRoot": "https://deb.nodesource.com/node_10.x/", 183 | "Distribution": "bionic", "Components": ["main"], 184 | "Architectures": ["amd64"], 185 | "Meta": [{"Architectures": "i386 amd64 armhf arm64", 186 | "Codename": "bionic", "Components": "main", 187 | "Date": "Tue, 06 Apr 2021 21:05:41 UTC", 188 | "Description": " Apt Repository for the Node.JS 10.x Branch", 189 | "Label": "Node Source", "Origin": "Node Source"}], 190 | "LastDownloadDate": "0001-01-01T00:00:00Z", "Filter": "", 191 | "Status": 0, "WorkerPID": 0, "FilterWithDeps": false, 192 | "SkipComponentCheck": false, "SkipArchitectureCheck": false, 193 | "DownloadSources": false, "DownloadUdebs": false, 194 | "DownloadInstaller": false}""") 195 | self.assertSequenceEqual( 196 | self.miapi.show(name="aptly-mirror"), 197 | Mirror( 198 | uuid='2cb5985a-a23f-4a1f-8eb6-d5409193b4eb', 199 | name="aptly-mirror", 200 | archiveurl="https://deb.nodesource.com/node_10.x/", 201 | distribution='bionic', 202 | components=["main"], 203 | architectures=["amd64"], 204 | downloaddate='0001-01-01T00:00:00Z', 205 | meta=[{"Architectures": "i386 amd64 armhf arm64", 206 | "Codename": "bionic", 207 | "Components": "main", 208 | "Date": "Tue, 06 Apr 2021 21:05:41 UTC", 209 | "Description": " Apt Repository for the Node.JS 10.x Branch", 210 | "Label": "Node Source", "Origin": "Node Source"}], 211 | filter="", 212 | status=0, 213 | worker_pid=0, 214 | filter_with_deps=False, 215 | skip_component_check=False, 216 | skip_architecture_check=False, 217 | download_sources=False, 218 | download_udebs=False, 219 | download_installer=False 220 | 221 | ) 222 | ) 223 | 224 | def test_list_packages(self, *, rmock: requests_mock.Mocker) -> None: 225 | rmock.get("http://test/api/mirrors/aptly-mirror/packages", 226 | text='["Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572"]') 227 | self.assertSequenceEqual( 228 | self.miapi.list_packages( 229 | name="aptly-mirror", query=("nodejs"), with_deps=True), 230 | [ 231 | Package( 232 | key="Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572", 233 | short_key=None, 234 | files_hash=None, 235 | fields=None, 236 | ) 237 | ], 238 | ) 239 | 240 | def test_list_packages_details(self, *, rmock: requests_mock.Mocker) -> None: 241 | rmock.get( 242 | "http://test/api/mirrors/aptly-mirror/packages?format=details", 243 | text='[{"Architecture":"amd64",' 244 | '"Conflicts":"nodejs-dev, nodejs-legacy, npm",' 245 | '"Depends":"1libc6 (>= 2.9), libgcc1 (>= 1:3.4), ' 246 | 'libstdc++6 (>= 4.4.0), python-minimal, ca-certificates",' 247 | '"Description":" Node.js event-based server-side javascript engine\\n",' 248 | '"Filename":"nodejs_10.24.1-1nodesource1_amd64.deb",' 249 | '"FilesHash":"1f74a6abf6acc572",' 250 | '"Homepage":"https://nodejs.org",' 251 | '"Installed-Size":"78630", "Key":"Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572",' 252 | '"License":"unknown",' 253 | '"MD5sum":"6d9f0e30396cb6c20945ff6de2f9f322","Maintainer":"Ivan Iguaran ",' 254 | '"Package":"nodejs",' 255 | '"Priority":"optional",' 256 | '"Provides":"nodejs-dev, nodejs-legacy, npm",' 257 | '"SHA1":"a3bc5a29614eab366bb3644abb1e602b5c8953d5",' 258 | '"SHA256":"4b374d16b536cf1a3963ddc4575ed2b68b28b0b5ea6eefe93c942dfc0ed35177",' 259 | '"SHA512":"bf203bb319de0c5f7ed3b6ba69de39b1ea8b5086b872561379bd462dd93f0796' 260 | '9ca64fa01ade01ff08fa13a4e5e28625b59292ba44bc01ba876ec95875630460",' 261 | '"Section":"web",' 262 | '"ShortKey":"Pamd64 nodejs 10.24.1-1nodesource1",' 263 | '"Size":"15949164",' 264 | '"Version":"10.24.1-1nodesource1"}]') 265 | self.assertSequenceEqual( 266 | self.miapi.list_packages( 267 | "aptly-mirror", detailed=True, with_deps=True, query="nodejs"), 268 | [ 269 | Package( 270 | key='Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572', 271 | short_key='Pamd64 nodejs 10.24.1-1nodesource1', 272 | files_hash='1f74a6abf6acc572', 273 | fields={ 274 | "Architecture": "amd64", 275 | 'Conflicts': 'nodejs-dev, nodejs-legacy, npm', 276 | 'Depends': '1libc6 (>= 2.9), libgcc1 (>= 1:3.4), ' 277 | 'libstdc++6 (>= 4.4.0), python-minimal, ca-certificates', 278 | 'Description': ' Node.js event-based server-side javascript engine\n', 279 | 'Filename': 'nodejs_10.24.1-1nodesource1_amd64.deb', 280 | 'FilesHash': '1f74a6abf6acc572', 281 | 'Homepage': 'https://nodejs.org', 282 | 'Installed-Size': '78630', 283 | 'Key': 'Pamd64 nodejs 10.24.1-1nodesource1 1f74a6abf6acc572', 284 | 'License': 'unknown', 285 | 'MD5sum': '6d9f0e30396cb6c20945ff6de2f9f322', 286 | 'Maintainer': 'Ivan Iguaran ', 287 | 'Package': 'nodejs', 288 | 'Priority': 'optional', 289 | 'Provides': 'nodejs-dev, nodejs-legacy, npm', 290 | 'SHA1': 'a3bc5a29614eab366bb3644abb1e602b5c8953d5', 291 | 'SHA256': '4b374d16b536cf1a3963ddc4575ed2b68b28b0b5ea6eefe93c942dfc0ed35177', 292 | 'SHA512': 'bf203bb319de0c5f7ed3b6ba69de39b1ea8b5086b872561379bd462dd93f0796' 293 | '9ca64fa01ade01ff08fa13a4e5e28625b59292ba44bc01ba876ec95875630460', 294 | 'Section': 'web', 295 | 'ShortKey': 'Pamd64 nodejs 10.24.1-1nodesource1', 296 | 'Size': '15949164', 297 | 'Version': '10.24.1-1nodesource1' 298 | } 299 | ) 300 | ] 301 | ) 302 | 303 | def test_delete(self, *, rmock: requests_mock.Mocker) -> None: 304 | with self.assertRaises(requests_mock.NoMockAddress): 305 | self.miapi.delete(name="aptly-mirror") 306 | 307 | def test_update(self, *, rmock: requests_mock.Mocker) -> None: 308 | with self.assertRaises(requests_mock.NoMockAddress): 309 | self.miapi.update(name="aptly-mirror", ignore_signatures=True) 310 | 311 | def test_edit(self, *, rmock: requests_mock.Mocker) -> None: 312 | with self.assertRaises(requests_mock.NoMockAddress): 313 | self.miapi.edit(name="aptly-mirror", newname="aptly-mirror-renamed", 314 | archiveurl='https://deb.nodesource.com/node_10.x/', 315 | architectures=["i386", "amd64"], filter="test", 316 | components=["main"], keyrings=["/path/to/keyring"], 317 | skip_existing_packages=True, ignore_checksums=True, 318 | download_udebs=True, download_sources=True, 319 | skip_component_check=True, filter_with_deps=True, 320 | ignore_signatures=True, force_update=True) 321 | 322 | def test_delete_validation(self, *, rmock: requests_mock.Mocker) -> None: 323 | rmock.delete("http://test/api/mirrors/aptly-mirror") 324 | self.miapi.delete(name="aptly-mirror") 325 | 326 | def test_update_validation(self, *, rmock: requests_mock.Mocker) -> None: 327 | rmock.put("http://test/api/mirrors/aptly-mirror") 328 | self.miapi.update(name="aptly-mirror") 329 | 330 | def test_edit_validation(self, *, rmock: requests_mock.Mocker) -> None: 331 | rmock.put("http://test/api/mirrors/aptly-mirror", 332 | text='{"Name":"aptly-mirror-bla", "IgnoreSignatures": true}') 333 | self.miapi.edit(name="aptly-mirror", newname="aptly-mirror-renamed") 334 | -------------------------------------------------------------------------------- /aptly_api/tests/test_misc.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import Any 7 | from unittest.case import TestCase 8 | 9 | import requests_mock 10 | 11 | from aptly_api.base import AptlyAPIException 12 | from aptly_api.parts.misc import MiscAPISection 13 | 14 | 15 | @requests_mock.Mocker(kw='rmock') 16 | class MiscAPISectionTests(TestCase): 17 | def __init__(self, *args: Any) -> None: 18 | super().__init__(*args) 19 | self.mapi = MiscAPISection("http://test/") 20 | 21 | def test_version(self, *, rmock: requests_mock.Mocker) -> None: 22 | rmock.get("http://test/api/version", text='{"Version":"1.0.1"}') 23 | self.assertEqual(self.mapi.version(), "1.0.1") 24 | 25 | def test_graph(self, *, rmock: requests_mock.Mocker) -> None: 26 | with self.assertRaises(NotImplementedError): 27 | self.mapi.graph("png") 28 | 29 | def test_version_error(self, *, rmock: requests_mock.Mocker) -> None: 30 | rmock.get("http://test/api/version", text='{"droenk": "blah"}') 31 | with self.assertRaises(AptlyAPIException): 32 | self.mapi.version() 33 | -------------------------------------------------------------------------------- /aptly_api/tests/test_packages.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import Any 7 | from unittest.case import TestCase 8 | 9 | import requests_mock 10 | 11 | from aptly_api.parts.packages import PackageAPISection, Package 12 | 13 | 14 | @requests_mock.Mocker(kw='rmock') 15 | class PackageAPISectionTests(TestCase): 16 | def __init__(self, *args: Any) -> None: 17 | super().__init__(*args) 18 | self.papi = PackageAPISection("http://test/") 19 | 20 | def test_show(self, *, rmock: requests_mock.Mocker) -> None: 21 | rmock.get( 22 | "http://test/api/packages/Pamd64%20authserver%200.1.14~dev0-1%201cc572a93625a9c9", 23 | text="""{"Architecture":"amd64", 24 | "Depends":"python3, python3-pip, python3-virtualenv, adduser, cron-daemon", 25 | "Description":" no description given\\n", 26 | "Filename":"authserver_0.1.14~dev0-1.deb", 27 | "FilesHash":"1cc572a93625a9c9", 28 | "Homepage":"http://example.com/no-uri-given", 29 | "Installed-Size":"74927", 30 | "Key":"Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9", 31 | "License":"unknown", 32 | "MD5sum":"03cca0794e63cf147b879e0a3695f523", 33 | "Maintainer":"Jonas Maurus", 34 | "Package":"authserver", 35 | "Priority":"extra", 36 | "Provides":"maurusnet-authserver", 37 | "SHA1":"9a77a31dba51f612ee08ee096381f0c7e8f97a42", 38 | "SHA256":"63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7", 39 | "SHA512":"01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65""" 40 | """cb6ac0f64b9bd88b507221a71825f5329fdda0e58728cd7", 41 | "Section":"default", 42 | "ShortKey":"Pamd64 authserver 0.1.14~dev0-1", 43 | "Size":"26623042", 44 | "Vendor":"root@test", 45 | "Version":"0.1.14~dev0-1"}""" 46 | ) 47 | pkg = self.papi.show("Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9") 48 | self.assertEqual( 49 | pkg, 50 | Package( 51 | key='Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', 52 | short_key='Pamd64 authserver 0.1.14~dev0-1', 53 | files_hash='1cc572a93625a9c9', 54 | fields={ 55 | 'Architecture': 'amd64', 56 | 'Depends': 'python3, python3-pip, python3-virtualenv, adduser, cron-daemon', 57 | 'Description': ' no description given\n', 58 | 'Filename': 'authserver_0.1.14~dev0-1.deb', 59 | 'FilesHash': '1cc572a93625a9c9', 60 | 'Homepage': 'http://example.com/no-uri-given', 61 | 'Installed-Size': '74927', 62 | 'Key': 'Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', 63 | 'License': 'unknown', 64 | 'MD5sum': '03cca0794e63cf147b879e0a3695f523', 65 | 'Maintainer': 'Jonas Maurus', 66 | 'Package': 'authserver', 67 | 'Priority': 'extra', 68 | 'Provides': 'maurusnet-authserver', 69 | 'SHA1': '9a77a31dba51f612ee08ee096381f0c7e8f97a42', 70 | 'SHA256': '63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7', 71 | 'SHA512': '01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65cb6ac' 72 | '0f64b9bd88b507221a71825f5329fdda0e58728cd7', 73 | 'Section': 'default', 74 | 'ShortKey': 'Pamd64 authserver 0.1.14~dev0-1', 75 | 'Size': '26623042', 76 | 'Vendor': 'root@test', 77 | 'Version': '0.1.14~dev0-1' 78 | } 79 | ) 80 | ) 81 | -------------------------------------------------------------------------------- /aptly_api/tests/test_publish.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from unittest.case import TestCase 7 | from typing import Any 8 | 9 | import requests_mock 10 | 11 | from aptly_api.base import AptlyAPIException 12 | from aptly_api.parts.publish import PublishEndpoint, PublishAPISection 13 | 14 | 15 | @requests_mock.Mocker(kw='rmock') 16 | class PublishAPISectionTests(TestCase): 17 | def __init__(self, *args: Any) -> None: 18 | super().__init__(*args) 19 | self.papi = PublishAPISection("http://test/") 20 | self.maxDiff = None 21 | 22 | def test_list(self, *, rmock: requests_mock.Mocker) -> None: 23 | rmock.get("http://test/api/publish", 24 | text='[{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"mn-nightly","Label":"",' 25 | '"Origin":"","Prefix":"nightly/stretch","SkipContents":false,' 26 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"maurusnet"}],' 27 | '"Storage":"s3:maurusnet"}]') 28 | self.assertSequenceEqual( 29 | self.papi.list(), 30 | [ 31 | PublishEndpoint( 32 | storage='s3:maurusnet', 33 | prefix='nightly/stretch', 34 | distribution='mn-nightly', 35 | source_kind='local', 36 | sources=[{ 37 | 'Name': 'maurusnet', 38 | 'Component': 'main' 39 | }], 40 | architectures=['amd64'], 41 | label='', 42 | origin='', 43 | acquire_by_hash=False 44 | ) 45 | ] 46 | ) 47 | 48 | def test_update(self, *, rmock: requests_mock.Mocker) -> None: 49 | rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", 50 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' 51 | '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' 52 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' 53 | '"Storage":"s3:aptly-repo"}') 54 | self.assertEqual( 55 | self.papi.update( 56 | prefix="s3:aptly-repo:test/xyz_1", 57 | distribution="test", 58 | sign_batch=True, 59 | sign_gpgkey="A16BE921", 60 | sign_passphrase="123456", 61 | ), 62 | PublishEndpoint( 63 | storage='s3:aptly-repo', 64 | prefix='test/xyz_1', 65 | distribution='test', 66 | source_kind='local', 67 | sources=[{ 68 | 'Name': 'aptly-repo', 69 | 'Component': 'main' 70 | }], 71 | architectures=['amd64'], 72 | label='', 73 | origin='', 74 | acquire_by_hash=False 75 | ) 76 | ) 77 | 78 | def test_update_passphrase_file(self, *, rmock: requests_mock.Mocker) -> None: 79 | rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", 80 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' 81 | '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' 82 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' 83 | '"Storage":"s3:aptly-repo"}') 84 | self.assertEqual( 85 | self.papi.update( 86 | prefix="s3:aptly-repo:test/xyz_1", 87 | distribution="test", 88 | sign_batch=True, 89 | sign_gpgkey="A16BE921", 90 | sign_passphrase_file="/root/passphrase.txt", 91 | ), 92 | PublishEndpoint( 93 | storage='s3:aptly-repo', 94 | prefix='test/xyz_1', 95 | distribution='test', 96 | source_kind='local', 97 | sources=[{ 98 | 'Name': 'aptly-repo', 99 | 'Component': 'main' 100 | }], 101 | architectures=['amd64'], 102 | label='', 103 | origin='', 104 | acquire_by_hash=False 105 | ) 106 | ) 107 | 108 | def test_update_no_sign(self, *, rmock: requests_mock.Mocker) -> None: 109 | rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", 110 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' 111 | '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' 112 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' 113 | '"Storage":"s3:aptly-repo"}') 114 | self.assertEqual( 115 | self.papi.update( 116 | prefix="s3:aptly-repo:test/xyz_1", 117 | distribution="test", 118 | sign_skip=True, 119 | ), 120 | PublishEndpoint( 121 | storage='s3:aptly-repo', 122 | prefix='test/xyz_1', 123 | distribution='test', 124 | source_kind='local', 125 | sources=[{ 126 | 'Name': 'aptly-repo', 127 | 'Component': 'main' 128 | }], 129 | architectures=['amd64'], 130 | label='', 131 | origin='', 132 | acquire_by_hash=False 133 | ) 134 | ) 135 | 136 | def test_update_snapshots(self, *, rmock: requests_mock.Mocker) -> None: 137 | rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", 138 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' 139 | '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' 140 | '"SourceKind":"snapshot","Sources":[{"Component":"main","Name":"aptly-repo-1"}],' 141 | '"Storage":"s3:aptly-repo"}') 142 | self.assertEqual( 143 | self.papi.update( 144 | prefix="s3:aptly-repo:test/xyz_1", 145 | distribution="test", 146 | snapshots=[{"Name": "aptly-repo-1"}], 147 | force_overwrite=True, 148 | sign_batch=True, 149 | sign_gpgkey="A16BE921", 150 | sign_passphrase="123456", 151 | sign_keyring="/etc/gpg-managed-keyring/pubring.pub", 152 | sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg" 153 | ), 154 | PublishEndpoint( 155 | storage='s3:aptly-repo', 156 | prefix='test/xyz_1', 157 | distribution='test', 158 | source_kind='snapshot', 159 | sources=[{ 160 | 'Name': 'aptly-repo-1', 161 | 'Component': 'main', 162 | }], 163 | architectures=['amd64'], 164 | label='', 165 | origin='', 166 | acquire_by_hash=False 167 | ) 168 | ) 169 | 170 | def test_publish(self, *, rmock: requests_mock.Mocker) -> None: 171 | rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", 172 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' 173 | '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' 174 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' 175 | '"Storage":"s3:myendpoint"}') 176 | self.assertEqual( 177 | self.papi.publish( 178 | sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], 179 | prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', 180 | sign_batch=True, sign_gpgkey='A16BE921', sign_passphrase='*********', 181 | force_overwrite=True, sign_keyring="/etc/gpg-managed-keyring/pubring.pub", 182 | sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", 183 | acquire_by_hash=False 184 | ), 185 | PublishEndpoint( 186 | storage='s3:myendpoint', 187 | prefix='test/a_1', 188 | distribution='test', 189 | source_kind='local', 190 | sources=[{'Component': 'main', 'Name': 'aptly-repo'}], 191 | architectures=['amd64'], 192 | label='test', 193 | origin='origin', 194 | acquire_by_hash=False 195 | ) 196 | ) 197 | 198 | def test_publish_passphrase_file(self, *, rmock: requests_mock.Mocker) -> None: 199 | rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", 200 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' 201 | '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' 202 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' 203 | '"Storage":"s3:myendpoint"}') 204 | self.assertEqual( 205 | self.papi.publish( 206 | sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], 207 | prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', 208 | sign_batch=True, sign_gpgkey='A16BE921', sign_passphrase_file='/root/passphrase.txt', 209 | force_overwrite=True, sign_keyring="/etc/gpg-managed-keyring/pubring.pub", 210 | sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", 211 | acquire_by_hash=False 212 | ), 213 | PublishEndpoint( 214 | storage='s3:myendpoint', 215 | prefix='test/a_1', 216 | distribution='test', 217 | source_kind='local', 218 | sources=[{'Component': 'main', 'Name': 'aptly-repo'}], 219 | architectures=['amd64'], 220 | label='test', 221 | origin='origin', 222 | acquire_by_hash=False 223 | ) 224 | ) 225 | 226 | def test_publish_no_sign(self, *, rmock: requests_mock.Mocker) -> None: 227 | rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", 228 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' 229 | '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' 230 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' 231 | '"Storage":"s3:myendpoint"}') 232 | self.assertEqual( 233 | self.papi.publish( 234 | sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], 235 | prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', 236 | sign_skip=True, 237 | acquire_by_hash=False 238 | ), 239 | PublishEndpoint( 240 | storage='s3:myendpoint', 241 | prefix='test/a_1', 242 | distribution='test', 243 | source_kind='local', 244 | sources=[{'Component': 'main', 'Name': 'aptly-repo'}], 245 | architectures=['amd64'], 246 | label='test', 247 | origin='origin', 248 | acquire_by_hash=False 249 | ) 250 | ) 251 | 252 | def test_publish_default_key(self, *, rmock: requests_mock.Mocker) -> None: 253 | rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", 254 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' 255 | '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' 256 | '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' 257 | '"Storage":"s3:myendpoint"}') 258 | self.assertEqual( 259 | self.papi.publish( 260 | sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], 261 | prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', 262 | sign_batch=True, sign_passphrase='*********', 263 | force_overwrite=True, sign_keyring="/etc/gpg-managed-keyring/pubring.pub", 264 | sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", 265 | ), 266 | PublishEndpoint( 267 | storage='s3:myendpoint', 268 | prefix='test/a_1', 269 | distribution='test', 270 | source_kind='local', 271 | sources=[{'Component': 'main', 'Name': 'aptly-repo'}], 272 | architectures=['amd64'], 273 | label='test', 274 | origin='origin', 275 | acquire_by_hash=False 276 | ) 277 | ) 278 | 279 | def test_update_snapshot_default_key(self, *, rmock: requests_mock.Mocker) -> None: 280 | rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", 281 | text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' 282 | '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' 283 | '"SourceKind":"snapshot","Sources":[{"Component":"main","Name":"aptly-repo-1"}],' 284 | '"Storage":"s3:aptly-repo"}') 285 | self.assertEqual( 286 | self.papi.update( 287 | prefix="s3:aptly-repo:test/xyz_1", 288 | distribution="test", 289 | snapshots=[{"Name": "aptly-repo-1"}], 290 | force_overwrite=True, 291 | sign_batch=True, 292 | sign_passphrase="123456", 293 | sign_keyring="/etc/gpg-managed-keyring/pubring.pub", 294 | sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", 295 | skip_contents=True, 296 | skip_cleanup=True 297 | ), 298 | PublishEndpoint( 299 | storage='s3:aptly-repo', 300 | prefix='test/xyz_1', 301 | distribution='test', 302 | source_kind='snapshot', 303 | sources=[{ 304 | 'Name': 'aptly-repo-1', 305 | 'Component': 'main', 306 | }], 307 | architectures=['amd64'], 308 | label='', 309 | origin='', 310 | acquire_by_hash=False 311 | ) 312 | ) 313 | 314 | def test_double_passphrase(self, *, rmock: requests_mock.Mocker) -> None: 315 | with self.assertRaises(AptlyAPIException): 316 | self.papi.publish(sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], 317 | prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, 318 | sign_gpgkey='A16BE921', sign_passphrase="*******", sign_passphrase_file="****") 319 | with self.assertRaises(AptlyAPIException): 320 | self.papi.update(prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, 321 | sign_gpgkey='A16BE921', sign_passphrase="*******", sign_passphrase_file="****") 322 | 323 | def test_no_name(self, *, rmock: requests_mock.Mocker) -> None: 324 | with self.assertRaises(AptlyAPIException): 325 | self.papi.publish(sources=[{'nope': 'nope'}], architectures=['amd64'], 326 | prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, 327 | sign_gpgkey='A16BE921', sign_passphrase="*******") 328 | with self.assertRaises(AptlyAPIException): 329 | self.papi.update(snapshots=[{'nope': 'nope'}], 330 | prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, 331 | sign_gpgkey='A16BE921', sign_passphrase="*******") 332 | 333 | def test_drop(self, *, rmock: requests_mock.Mocker) -> None: 334 | rmock.delete("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1/test?force=1", text='{}') 335 | self.papi.drop(prefix='s3:myendpoint:test/a_1', distribution='test', force_delete=True) 336 | 337 | def test_escape_prefix(self, *args: Any, **kwargs: Any) -> None: 338 | self.assertEqual( 339 | self.papi.escape_prefix("test/a_1"), 340 | "test_a__1", 341 | ) 342 | self.assertEqual( 343 | self.papi.escape_prefix("test-a-1"), 344 | "test-a-1" 345 | ) 346 | self.assertEqual( 347 | self.papi.escape_prefix("test/a"), 348 | "test_a" 349 | ) 350 | self.assertEqual( 351 | self.papi.escape_prefix("."), 352 | ":." 353 | ) 354 | -------------------------------------------------------------------------------- /aptly_api/tests/test_repos.py: -------------------------------------------------------------------------------- 1 | # -* encoding: utf-8 *- 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | from typing import Any 7 | from unittest.case import TestCase 8 | 9 | import requests_mock 10 | 11 | from aptly_api.base import AptlyAPIException 12 | from aptly_api.parts.packages import Package 13 | from aptly_api.parts.repos import ReposAPISection, Repo, FileReport 14 | 15 | 16 | @requests_mock.Mocker(kw='rmock') 17 | class ReposAPISectionTests(TestCase): 18 | def __init__(self, *args: Any) -> None: 19 | super().__init__(*args) 20 | self.rapi = ReposAPISection("http://test/") 21 | 22 | def test_create(self, *, rmock: requests_mock.Mocker) -> None: 23 | rmock.post("http://test/api/repos", 24 | text='{"Name":"aptly-repo","Comment":"test","DefaultDistribution":"test","DefaultComponent":"test"}') 25 | self.assertEqual( 26 | self.rapi.create("aptly-repo", comment="test", default_component="test", default_distribution="test"), 27 | Repo( 28 | name="aptly-repo", 29 | default_distribution="test", 30 | default_component="test", 31 | comment="test", 32 | ) 33 | ) 34 | 35 | def test_show(self, *, rmock: requests_mock.Mocker) -> None: 36 | rmock.get("http://test/api/repos/aptly-repo", 37 | text='{"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}') 38 | self.assertEqual( 39 | self.rapi.show("aptly-repo"), 40 | Repo( 41 | name="aptly-repo", 42 | default_distribution="", 43 | default_component="", 44 | comment="", 45 | ) 46 | ) 47 | 48 | def test_search(self, *, rmock: requests_mock.Mocker) -> None: 49 | rmock.get("http://test/api/repos/aptly-repo/packages", 50 | text='["Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9"]') 51 | self.assertSequenceEqual( 52 | self.rapi.search_packages("aptly-repo"), 53 | [ 54 | Package( 55 | key="Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9", 56 | short_key=None, 57 | files_hash=None, 58 | fields=None, 59 | ) 60 | ], 61 | ) 62 | 63 | def test_search_details(self, *, rmock: requests_mock.Mocker) -> None: 64 | rmock.get( 65 | "http://test/api/repos/aptly-repo/packages?format=details", 66 | text="""[{ 67 | "Architecture":"amd64", 68 | "Depends":"python3, python3-pip, python3-virtualenv, adduser, cron-daemon", 69 | "Description":" no description given\\n", 70 | "Filename":"authserver_0.1.14~dev0-1.deb", 71 | "FilesHash":"1cc572a93625a9c9", 72 | "Homepage":"http://example.com/no-uri-given", 73 | "Installed-Size":"74927", 74 | "Key":"Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9", 75 | "License":"unknown", 76 | "MD5sum":"03cca0794e63cf147b879e0a3695f523", 77 | "Maintainer":"Jonas Maurus", 78 | "Package":"authserver", 79 | "Priority":"extra", 80 | "Provides":"maurusnet-authserver", 81 | "SHA1":"9a77a31dba51f612ee08ee096381f0c7e8f97a42", 82 | "SHA256":"63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7", 83 | "SHA512":"01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65cb""" 84 | """6ac0f64b9bd88b507221a71825f5329fdda0e58728cd7", 85 | "Section":"default", 86 | "ShortKey":"Pamd64 authserver 0.1.14~dev0-1", 87 | "Size":"26623042", 88 | "Vendor":"root@test", 89 | "Version":"0.1.14~dev0-1" 90 | }]""" 91 | ) 92 | self.assertSequenceEqual( 93 | self.rapi.search_packages("aptly-repo", detailed=True, with_deps=True, query="Name (authserver)"), 94 | [ 95 | Package( 96 | key='Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', 97 | short_key='Pamd64 authserver 0.1.14~dev0-1', 98 | files_hash='1cc572a93625a9c9', 99 | fields={ 100 | 'Architecture': 'amd64', 101 | 'Depends': 'python3, python3-pip, python3-virtualenv, adduser, cron-daemon', 102 | 'Description': ' no description given\n', 103 | 'Filename': 'authserver_0.1.14~dev0-1.deb', 104 | 'FilesHash': '1cc572a93625a9c9', 105 | 'Homepage': 'http://example.com/no-uri-given', 106 | 'Installed-Size': '74927', 107 | 'Key': 'Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', 108 | 'License': 'unknown', 109 | 'MD5sum': '03cca0794e63cf147b879e0a3695f523', 110 | 'Maintainer': 'Jonas Maurus', 111 | 'Package': 'authserver', 112 | 'Priority': 'extra', 113 | 'Provides': 'maurusnet-authserver', 114 | 'SHA1': '9a77a31dba51f612ee08ee096381f0c7e8f97a42', 115 | 'SHA256': '63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7', 116 | 'SHA512': '01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65' 117 | 'cb6ac0f64b9bd88b507221a71825f5329fdda0e58728cd7', 118 | 'Section': 'default', 119 | 'ShortKey': 'Pamd64 authserver 0.1.14~dev0-1', 120 | 'Size': '26623042', 121 | 'Vendor': 'root@test', 122 | 'Version': '0.1.14~dev0-1' 123 | } 124 | ) 125 | ] 126 | ) 127 | 128 | def test_repo_edit_validation(self, *, rmock: requests_mock.Mocker) -> None: 129 | with self.assertRaises(AptlyAPIException): 130 | self.rapi.edit("aptly-repo") 131 | 132 | def test_repo_edit(self, *, rmock: requests_mock.Mocker) -> None: 133 | rmock.put("http://test/api/repos/aptly-repo", 134 | text='{"Name":"aptly-repo","Comment":"comment",' 135 | '"DefaultDistribution":"stretch","DefaultComponent":"main"}') 136 | self.assertEqual( 137 | self.rapi.edit("aptly-repo", comment="comment", default_distribution="stretch", default_component="main"), 138 | Repo(name='aptly-repo', comment='comment', default_distribution='stretch', default_component='main') 139 | ) 140 | 141 | def test_list(self, *, rmock: requests_mock.Mocker) -> None: 142 | rmock.get("http://test/api/repos", 143 | text='[{"Name":"maurusnet","Comment":"","DefaultDistribution":"",' 144 | '"DefaultComponent":"main"},{"Name":"aptly-repo","Comment":"comment",' 145 | '"DefaultDistribution":"stretch","DefaultComponent":"main"}]') 146 | self.assertSequenceEqual( 147 | self.rapi.list(), 148 | [ 149 | Repo(name='maurusnet', comment='', default_distribution='', default_component='main'), 150 | Repo(name='aptly-repo', comment='comment', default_distribution='stretch', default_component='main'), 151 | ] 152 | ) 153 | 154 | def test_delete(self, *, rmock: requests_mock.Mocker) -> None: 155 | with self.assertRaises(requests_mock.NoMockAddress): 156 | self.rapi.delete("aptly-repo", force=True) 157 | 158 | def test_add_file(self, *, rmock: requests_mock.Mocker) -> None: 159 | rmock.post("http://test/api/repos/aptly-repo/file/test/dirmngr_2.1.18-6_amd64.deb", 160 | text='{"FailedFiles":[],"Report":{"Warnings":[],' 161 | '"Added":["dirmngr_2.1.18-6_amd64 added"],"Removed":[]}}') 162 | self.assertEqual( 163 | self.rapi.add_uploaded_file("aptly-repo", "test", "dirmngr_2.1.18-6_amd64.deb", force_replace=True), 164 | FileReport(failed_files=[], 165 | report={'Added': ['dirmngr_2.1.18-6_amd64 added'], 166 | 'Removed': [], 'Warnings': []}) 167 | ) 168 | 169 | def test_add_dir(self, *, rmock: requests_mock.Mocker) -> None: 170 | rmock.post("http://test/api/repos/aptly-repo/file/test", 171 | text='{"FailedFiles":[],"Report":{"Warnings":[],' 172 | '"Added":["dirmngr_2.1.18-6_amd64 added"],"Removed":[]}}') 173 | self.assertEqual( 174 | self.rapi.add_uploaded_file("aptly-repo", "test", force_replace=True), 175 | FileReport(failed_files=[], 176 | report={'Added': ['dirmngr_2.1.18-6_amd64 added'], 177 | 'Removed': [], 'Warnings': []}) 178 | ) 179 | 180 | def test_add_package(self, *, rmock: requests_mock.Mocker) -> None: 181 | rmock.post("http://test/api/repos/aptly-repo/packages", 182 | text='{"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}') 183 | self.assertEqual( 184 | self.rapi.add_packages_by_key("aptly-repo", "Pamd64 dirmngr 2.1.18-6 4c7412c5f0d7b30a"), 185 | Repo(name='aptly-repo', comment='', default_distribution='', default_component='') 186 | ) 187 | 188 | def test_delete_package(self, *, rmock: requests_mock.Mocker) -> None: 189 | rmock.delete( 190 | "http://test/api/repos/aptly-repo/packages", 191 | text='{"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}' 192 | ) 193 | self.assertEqual( 194 | self.rapi.delete_packages_by_key("aptly-repo", "Pamd64 dirmngr 2.1.18-6 4c7412c5f0d7b30a"), 195 | Repo(name='aptly-repo', comment='', default_distribution='', default_component=''), 196 | ) 197 | 198 | def test_search_invalid_params(self, *, rmock: requests_mock.Mocker) -> None: 199 | with self.assertRaises(AptlyAPIException): 200 | self.rapi.search_packages("aptly-repo", with_deps=True) 201 | -------------------------------------------------------------------------------- /aptly_api/tests/test_snapshots.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | from typing import Any 5 | from unittest.case import TestCase 6 | 7 | import iso8601 8 | import requests_mock 9 | 10 | from aptly_api.base import AptlyAPIException 11 | from aptly_api.parts.packages import Package 12 | from aptly_api.parts.snapshots import SnapshotAPISection, Snapshot 13 | 14 | 15 | @requests_mock.Mocker(kw='rmock') 16 | class SnapshotAPISectionTests(TestCase): 17 | def __init__(self, *args: Any) -> None: 18 | super().__init__(*args) 19 | self.sapi = SnapshotAPISection("http://test/") 20 | self.maxDiff = None 21 | 22 | def test_list(self, *, rmock: requests_mock.Mocker) -> None: 23 | rmock.get("http://test/api/snapshots", 24 | text='[{"Name":"stretch-security-1","CreatedAt":"2017-06-03T21:36:22.2692213Z",' 25 | '"Description":"Snapshot from mirror [stretch-security]: ' 26 | 'http://security.debian.org/debian-security/ stretch/updates"},' 27 | '{"Name":"stretch-updates-1","CreatedAt":"2017-06-03T21:36:22.431767659Z",' 28 | '"Description":"Snapshot from mirror [stretch-updates]: ' 29 | 'http://ftp-stud.hs-esslingen.de/debian/ stretch-updates"}]') 30 | self.assertSequenceEqual( 31 | self.sapi.list(), 32 | [ 33 | Snapshot( 34 | name='stretch-security-1', 35 | description='Snapshot from mirror [stretch-security]: http://security.debian.org/debian-security/ ' 36 | 'stretch/updates', 37 | created_at=iso8601.parse_date( 38 | '2017-06-03T21:36:22.2692213Z') 39 | ), 40 | Snapshot( 41 | name='stretch-updates-1', 42 | description='Snapshot from mirror [stretch-updates]: http://ftp-stud.hs-esslingen.de/debian/ ' 43 | 'stretch-updates', 44 | created_at=iso8601.parse_date( 45 | '2017-06-03T21:36:22.431767659Z') 46 | ) 47 | ] 48 | ) 49 | 50 | def test_list_invalid(self, *, rmock: requests_mock.Mocker) -> None: 51 | with self.assertRaises(AptlyAPIException): 52 | self.sapi.list("snoepsort") 53 | 54 | def test_update_noparams(self, *, rmock: requests_mock.Mocker) -> None: 55 | with self.assertRaises(AptlyAPIException): 56 | self.sapi.update("test") 57 | 58 | def test_create(self, *, rmock: requests_mock.Mocker) -> None: 59 | rmock.post("http://test/api/repos/aptly-repo/snapshots", 60 | text='{"Name":"aptly-repo-1","CreatedAt":"2017-06-03T23:43:40.275605639Z",' 61 | '"Description":"Snapshot from local repo [aptly-repo]"}') 62 | self.assertEqual( 63 | self.sapi.create_from_repo("aptly-repo", "aptly-repo-1", 64 | description='Snapshot from local repo [aptly-repo]'), 65 | Snapshot( 66 | name='aptly-repo-1', 67 | description='Snapshot from local repo [aptly-repo]', 68 | created_at=iso8601.parse_date('2017-06-03T23:43:40.275605639Z') 69 | ) 70 | ) 71 | 72 | def test_list_packages(self, *, rmock: requests_mock.Mocker) -> None: 73 | rmock.get("http://test/api/snapshots/aptly-repo-1/packages", 74 | text='["Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d"]') 75 | self.assertEqual( 76 | self.sapi.list_packages("aptly-repo-1"), 77 | [ 78 | Package( 79 | key='Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d', 80 | short_key=None, 81 | files_hash=None, 82 | fields=None 83 | ), 84 | ] 85 | ) 86 | 87 | def test_list_packages_details(self, *, rmock: requests_mock.Mocker) -> None: 88 | rmock.get("http://test/api/snapshots/aptly-repo-1/packages", 89 | text='[{"Architecture":"all","Depends":"postgresql-9.6-postgis-2.3-scripts",' 90 | '"Description":" transitional dummy package\\n This is a transitional dummy package. ' 91 | 'It can safely be removed.\\n",' 92 | '"Filename":"postgresql-9.6-postgis-scripts_2.3.2+dfsg-1~exp2.pgdg90+1_all.deb",' 93 | '"FilesHash":"5f70af798690300d",' 94 | '"Homepage":"http://postgis.net/",' 95 | '"Installed-Size":"491",' 96 | '"Key":"Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d",' 97 | '"MD5sum":"56de7bac497e4ac34017f4d11e75fffb",' 98 | '"Maintainer":"Debian GIS Project \u003cpkg-grass-devel@lists.alioth.debian.org\u003e",' 99 | '"Package":"postgresql-9.6-postgis-scripts",' 100 | '"Priority":"extra",' 101 | '"SHA1":"61bb9250e7a35be9b78808944e8cfbae1e70f67d",' 102 | '"SHA256":"01c0c4645e9100f7ddb6d05a9d36ad3866ac8d2e412b7c04163a9e65397ce05e",' 103 | '"Section":"oldlibs",' 104 | '"ShortKey":"Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1",' 105 | '"Size":"468824","Source":"postgis","Version":"2.3.2+dfsg-1~exp2.pgdg90+1"}]') 106 | parsed = self.sapi.list_packages("aptly-repo-1", query="Name (% postgresql-9.6.-postgis-sc*)", detailed=True, 107 | with_deps=True)[0] 108 | expected = Package( 109 | key='Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d', 110 | short_key='Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1', 111 | files_hash='5f70af798690300d', 112 | fields={ 113 | 'Maintainer': 'Debian GIS Project ', 114 | 'Size': '468824', 115 | 'MD5sum': '56de7bac497e4ac34017f4d11e75fffb', 116 | 'ShortKey': 'Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1', 117 | 'FilesHash': '5f70af798690300d', 118 | 'Filename': 'postgresql-9.6-postgis-scripts_2.3.2+dfsg-1~exp2.pgdg90+1_all.deb', 119 | 'Section': 'oldlibs', 120 | 'Homepage': 'http://postgis.net/', 121 | 'Description': ' transitional dummy package\n This is a transitional dummy package. ' 122 | 'It can safely be removed.\n', 123 | 'Architecture': 'all', 124 | 'Priority': 'extra', 125 | 'Source': 'postgis', 126 | 'SHA1': '61bb9250e7a35be9b78808944e8cfbae1e70f67d', 127 | 'Installed-Size': '491', 128 | 'Version': '2.3.2+dfsg-1~exp2.pgdg90+1', 129 | 'Depends': 'postgresql-9.6-postgis-2.3-scripts', 130 | 'Key': 'Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d', 131 | 'SHA256': '01c0c4645e9100f7ddb6d05a9d36ad3866ac8d2e412b7c04163a9e65397ce05e', 132 | 'Package': 'postgresql-9.6-postgis-scripts' 133 | } 134 | ) 135 | 136 | # mypy should detect this as ensuring that parsed.fields is not None, but it doesn't 137 | self.assertIsNotNone(parsed.fields) 138 | self.assertIsNotNone(expected.fields) 139 | 140 | self.assertDictEqual( 141 | # make sure that mypy doesn't error on this being potentially None 142 | parsed.fields if parsed.fields else {}, 143 | # this can't happen unless Package.__init__ is fubared 144 | expected.fields if expected.fields else {}, 145 | ) 146 | 147 | def test_show(self, *, rmock: requests_mock.Mocker) -> None: 148 | rmock.get("http://test/api/snapshots/aptly-repo-1", 149 | text='{"Name":"aptly-repo-1",' 150 | '"CreatedAt":"2017-06-03T23:43:40.275605639Z",' 151 | '"Description":"Snapshot from local repo [aptly-repo]"}') 152 | self.assertEqual( 153 | self.sapi.show("aptly-repo-1"), 154 | Snapshot( 155 | name='aptly-repo-1', 156 | description='Snapshot from local repo [aptly-repo]', 157 | created_at=iso8601.parse_date('2017-06-03T23:43:40.275605639Z') 158 | ) 159 | ) 160 | 161 | def test_update(self, *, rmock: requests_mock.Mocker) -> None: 162 | rmock.put("http://test/api/snapshots/aptly-repo-1", 163 | text='{"Name":"aptly-repo-2","CreatedAt":"2017-06-03T23:43:40.275605639Z",' 164 | '"Description":"test"}') 165 | self.assertEqual( 166 | self.sapi.update( 167 | "aptly-repo-1", newname="aptly-repo-2", newdescription="test"), 168 | Snapshot( 169 | name='aptly-repo-2', 170 | description='test', 171 | created_at=iso8601.parse_date('2017-06-03T23:43:40.275605639Z') 172 | ) 173 | ) 174 | 175 | def test_delete(self, *, rmock: requests_mock.Mocker) -> None: 176 | rmock.delete("http://test/api/snapshots/aptly-repo-1", 177 | text='{}') 178 | self.sapi.delete("aptly-repo-1", force=True) 179 | 180 | def test_diff(self, *, rmock: requests_mock.Mocker) -> None: 181 | rmock.get("http://test/api/snapshots/aptly-repo-1/diff/aptly-repo-2", 182 | text='[{"Left":null,"Right":"Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9"},' 183 | '{"Left":"Pamd64 radicale 1.1.1 fbc974fa526f14e9","Right":null}]') 184 | self.assertSequenceEqual( 185 | self.sapi.diff("aptly-repo-1", "aptly-repo-2"), 186 | [ 187 | {'Left': None, 'Right': 'Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9'}, 188 | {'Left': 'Pamd64 radicale 1.1.1 fbc974fa526f14e9', 'Right': None} 189 | ] 190 | ) 191 | 192 | def test_create_from_packages(self, *, rmock: requests_mock.Mocker) -> None: 193 | rmock.post("http://test/api/snapshots", 194 | text='{"Name":"aptly-repo-2","CreatedAt":"2017-06-07T14:19:07.706408213Z","Description":"test"}') 195 | self.assertEqual( 196 | self.sapi.create_from_packages( 197 | "aptly-repo-2", 198 | description="test", 199 | package_refs=["Pamd64 dirmngr 2.1.18-6 4c7412c5f0d7b30a"], 200 | source_snapshots=["aptly-repo-1"] 201 | ), 202 | Snapshot( 203 | name='aptly-repo-2', 204 | description='test', 205 | created_at=iso8601.parse_date('2017-06-07T14:19:07.706408213Z') 206 | ) 207 | ) 208 | 209 | def test_create_from_mirror(self, *, rmock: requests_mock.Mocker) -> None: 210 | expected = {'Name': 'aptly-mirror-snap', 'Description': 'Snapshot from local repo [aptly-repo]'} 211 | rmock.post("http://test/api/mirrors/aptly-mirror/snapshots", 212 | text='{"Name":"aptly-mirror-snap","CreatedAt":"2022-11-29T21:43:45.275605639Z",' 213 | '"Description":"Snapshot from local mirror [aptly-mirror]"}') 214 | self.assertEqual( 215 | self.sapi.create_from_mirror(mirrorname="aptly-mirror", snapshotname="aptly-mirror-snap", 216 | description='Snapshot from local repo [aptly-repo]'), 217 | Snapshot( 218 | name='aptly-mirror-snap', 219 | description='Snapshot from local mirror [aptly-mirror]', 220 | created_at=iso8601.parse_date('2022-11-29T21:43:45.275605639Z') 221 | ) 222 | ) 223 | self.assertEqual(rmock.request_history[0].json(), expected) 224 | -------------------------------------------------------------------------------- /aptly_api/tests/testpkg.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopythongo/aptly-api-client/e6200156fb8becf747969be8a451b7348c9d5d35/aptly_api/tests/testpkg.deb -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | requests-mock==1.12.1 2 | coverage==7.6.7 3 | coveralls==4.0.1 4 | flake8==7.1.1 5 | pep257==0.7.0 6 | doc8==1.1.2 7 | Pygments==2.18.0 8 | mypy==1.13.0 9 | pytest==8.3.3 10 | pytest-cov==6.0.0 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -* encoding: utf-8 *- 3 | 4 | # This Source Code Form is subject to the terms of the Mozilla Public 5 | # License, v. 2.0. If a copy of the MPL was not distributed with this 6 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | import os 9 | import re 10 | from setuptools import setup, find_packages 11 | 12 | _package_root = "." 13 | _root_package = 'aptly_api' 14 | _HERE = os.path.abspath(os.path.dirname(__file__)) 15 | 16 | with open("aptly_api/__init__.py", "rt", encoding="utf-8") as vf: 17 | lines = vf.readlines() 18 | 19 | _version = "0.0.0+local" 20 | for line in lines: 21 | m = re.match("version = \"(.*?)\"", line) 22 | if m: 23 | _version = m.group(1) 24 | 25 | _packages = find_packages(_package_root, exclude=["*.tests", "*.tests.*", "tests.*", "tests"]) 26 | 27 | _requirements = [ 28 | # intentionally unpinned. We're a library, so we don't need to conflict with others by pinning versions 29 | # and we don't depend on a specific minimum version. 30 | 'requests', 31 | 'iso8601', 32 | ] 33 | 34 | try: 35 | long_description = open(os.path.join(_HERE, 'README.rst')).read() 36 | except IOError: 37 | long_description = None 38 | 39 | setup( 40 | name='aptly-api-client', 41 | version=_version, 42 | packages=_packages, 43 | package_dir={ 44 | '': _package_root, 45 | }, 46 | install_requires=_requirements, 47 | classifiers=[ 48 | "Development Status :: 4 - Beta", 49 | "Intended Audience :: Developers", 50 | "Intended Audience :: System Administrators", 51 | "Environment :: Console", 52 | "Programming Language :: Python :: 3 :: Only", 53 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 54 | "Operating System :: POSIX", 55 | ], 56 | author="Jonas Maurus (@jdelic)", 57 | author_email="jonas@gopythongo.com", 58 | maintainer="GoPythonGo.com", 59 | maintainer_email="info@gopythongo.com", 60 | description="A Python 3 client for the Aptly API", 61 | long_description=long_description, 62 | ) 63 | --------------------------------------------------------------------------------