pax_global_header00006660000000000000000000000064143277453010014520gustar00rootroot0000000000000052 comment=9e5e13e29c741929d85e4714350f00900cf145a8 aptly-api-client-0.2.4/000077500000000000000000000000001432774530100146775ustar00rootroot00000000000000aptly-api-client-0.2.4/.coveragerc000066400000000000000000000001101432774530100170100ustar00rootroot00000000000000[run] source = aptly_api [report] fail_under = 100 show_missing = true aptly-api-client-0.2.4/.github/000077500000000000000000000000001432774530100162375ustar00rootroot00000000000000aptly-api-client-0.2.4/.github/dependabot.yml000066400000000000000000000001641432774530100210700ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" aptly-api-client-0.2.4/.github/workflows/000077500000000000000000000000001432774530100202745ustar00rootroot00000000000000aptly-api-client-0.2.4/.github/workflows/test.yml000066400000000000000000000026531432774530100220040ustar00rootroot00000000000000name: Linters on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: python-version: ["3.10"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies run: | python -m pip install --upgrade pip wheel pip install -r requirements-test.txt -e . - name: Run Tests run: | pytest --cov=aptly_api coverage report -m coverage lcov - name: Run Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coverage.lcov - name: Run Flake8 run: | flake8 --max-line-length=120 aptly_api setup.py - name: Run mypy run: | mypy --install-types --non-interactive \ --ignore-missing-imports --follow-imports=skip --disallow-untyped-calls \ --disallow-untyped-defs -p aptly_apiaptly-api-client-0.2.4/.gitignore000066400000000000000000000002521432774530100166660ustar00rootroot00000000000000syntax: glob Lib lib *.db *.pyc *.pyd .project .pydevproject .settings .idea dist *.egg-info *.iml static build .idea .env __pycache__ .coverage .mypy_cache .tox .toxenv aptly-api-client-0.2.4/LICENSE000066400000000000000000000027401432774530100157070ustar00rootroot00000000000000Copyright (c) 2016-2017, Jonas Maurus. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. aptly-api-client-0.2.4/README.rst000066400000000000000000000070031432774530100163660ustar00rootroot00000000000000Python 3 Aptly API client ========================= .. image:: https://coveralls.io/repos/github/gopythongo/aptly-api-client/badge.svg?branch=master :target: https://coveralls.io/github/gopythongo/aptly-api-client?branch=master .. image:: https://github.com/gopythongo/aptly-api-client/actions/workflows/test.yml/badge.svg :target: https://github.com/gopythongo/aptly-api-client/actions/workflows/test.yml This is a thin abstraction layer for interfacing with `Aptly's HTTP API `__. It's used by `GoPythonGo `__, but can be used as a standalone library from Pypi. .. code-block:: shell pip install aptly-api-client Usage ----- The library provides a direct abstraction of the published Aptly API, mostly using the same naming, only replacing it with pythonic naming where necessary. All code has full `PEP 484 `__ annotations, so if you're using a modern IDE, using this library should be especially straight-forward. Where appropriate, the library exposes the interface of the underlying ``requests`` library. This allows you to configure CA pinning, SSL client certificates, HTTP Basic authentication etc. .. code-block:: python # initialize a client from aptly_api import Client aptly = Client("http://aptly-endpoint.test/") # create a repository aptly.repos.create("myrepo", comment="a test repo", default_distribution="mydist", default_component="main") # upload a package aptly.files.upload("test_folder", "/tmp/mypkg_1.0_amd64.deb") # add the package to the repo aptly.repos.add_uploaded_file("myrepo", "test_folder") Contributors ============ * @findmyname666 * Filip Křesťan * @mgusek * Samuel Bachmann * @agustinhenze License ======= Copyright (c) 2016-2019, Jonas Maurus and Contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. aptly-api-client-0.2.4/aptly_api/000077500000000000000000000000001432774530100166615ustar00rootroot00000000000000aptly-api-client-0.2.4/aptly_api/__init__.py000066400000000000000000000014061432774530100207730ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # explicit exports for mypy from aptly_api.client import Client as Client from aptly_api.base import AptlyAPIException as AptlyAPIException from aptly_api.parts.packages import Package as Package from aptly_api.parts.publish import PublishEndpoint as PublishEndpoint from aptly_api.parts.repos import Repo as Repo, FileReport as FileReport from aptly_api.parts.snapshots import Snapshot as Snapshot version = "0.2.4" __all__ = ['Client', 'AptlyAPIException', 'version', 'Package', 'PublishEndpoint', 'Repo', 'FileReport', 'Snapshot'] aptly-api-client-0.2.4/aptly_api/base.py000066400000000000000000000123141432774530100201460ustar00rootroot00000000000000# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import IO, TextIO, BinaryIO, Sequence, Dict, Tuple, Optional, Union, List, Any, MutableMapping, Iterable, \ Mapping from urllib.parse import urljoin import requests from requests.auth import AuthBase _filetype = Optional[ Union[ Dict[ str, Union[ Union[TextIO, BinaryIO, str, bytes], Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes]], Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str], Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str, Dict[str, str]] ] ], Sequence[ Tuple[ str, Union[ Union[TextIO, BinaryIO, str, bytes], Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes]], Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str], Tuple[Optional[str], Union[TextIO, BinaryIO, str, bytes], str, Dict[str, str]] ] ] ], ] ] _datatype = Optional[ Union[ Iterable[bytes], str, bytes, Union[TextIO, BinaryIO], List[Tuple[Any, Any]], Tuple[Tuple[Any, Any], ...], Mapping[Any, Any] ] ] class AptlyAPIException(Exception): def __init__(self, *args: Any, status_code: int = 0) -> None: super().__init__(*args) self.status_code = status_code class BaseAPIClient: def __init__(self, base_url: str, ssl_verify: Union[str, bool, None] = None, ssl_cert: Optional[Tuple[str, str]] = None, http_auth: Optional[AuthBase] = None, timeout: int = 60) -> None: self.base_url = base_url self.ssl_verify = ssl_verify self.ssl_cert = ssl_cert self.http_auth = http_auth self.exc_class = AptlyAPIException self.timeout = timeout def _error_from_response(self, resp: requests.Response) -> str: if resp.status_code == 200: return "no error (status 200)" try: rcnt = resp.json() except ValueError: return "%s %s %s" % (resp.status_code, resp.reason, resp.text,) if isinstance(rcnt, dict): content = rcnt else: content = rcnt[0] ret = "%s - %s -" % (resp.status_code, resp.reason) if "error" in content: ret = "%s %s" % (ret, content["error"],) if "meta" in content: ret = "%s (%s)" % (ret, content["meta"],) return ret def _make_url(self, path: str) -> str: return urljoin(self.base_url, path) def do_get(self, urlpath: str, params: Optional[Dict[str, str]] = None) -> requests.Response: resp = requests.get(self._make_url(urlpath), params=params, verify=self.ssl_verify, cert=self.ssl_cert, auth=self.http_auth, timeout=self.timeout) if resp.status_code < 200 or resp.status_code >= 300: raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) return resp def do_post(self, urlpath: str, data: Union[bytes, MutableMapping[str, str], IO[Any], None] = None, params: Optional[Dict[str, str]] = None, files: _filetype = None, json: Optional[MutableMapping[Any, Any]] = None) -> requests.Response: resp = requests.post(self._make_url(urlpath), data=data, params=params, files=files, json=json, verify=self.ssl_verify, cert=self.ssl_cert, auth=self.http_auth, timeout=self.timeout) if resp.status_code < 200 or resp.status_code >= 300: raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) return resp def do_put(self, urlpath: str, data: Union[bytes, MutableMapping[str, str], IO[Any]] = None, files: _filetype = None, json: Optional[MutableMapping[Any, Any]] = None) -> requests.Response: resp = requests.put(self._make_url(urlpath), data=data, files=files, json=json, verify=self.ssl_verify, cert=self.ssl_cert, auth=self.http_auth, timeout=self.timeout) if resp.status_code < 200 or resp.status_code >= 300: raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) return resp def do_delete(self, urlpath: str, params: Optional[Dict[str, str]] = None, data: _datatype = None, json: Union[List[Dict[str, Any]], Dict[str, Any], None] = None) -> requests.Response: resp = requests.delete(self._make_url(urlpath), params=params, data=data, json=json, verify=self.ssl_verify, cert=self.ssl_cert, auth=self.http_auth, timeout=self.timeout) if resp.status_code < 200 or resp.status_code >= 300: raise AptlyAPIException(self._error_from_response(resp), status_code=resp.status_code) return resp aptly-api-client-0.2.4/aptly_api/client.py000066400000000000000000000043351432774530100205160ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from requests.auth import AuthBase from typing import Union, Optional, Tuple from aptly_api.parts.misc import MiscAPISection from aptly_api.parts.packages import PackageAPISection from aptly_api.parts.publish import PublishAPISection from aptly_api.parts.repos import ReposAPISection from aptly_api.parts.files import FilesAPISection from aptly_api.parts.snapshots import SnapshotAPISection class Client: def __init__(self, aptly_server_url: str, ssl_verify: Union[str, bool, None] = None, ssl_cert: Optional[Tuple[str, str]] = None, http_auth: Optional[AuthBase] = None, timeout: int = 60) -> None: self.__aptly_server_url = aptly_server_url self.files = FilesAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) self.misc = MiscAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) self.packages = PackageAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) self.publish = PublishAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) self.repos = ReposAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) self.snapshots = SnapshotAPISection(base_url=self.__aptly_server_url, ssl_verify=ssl_verify, ssl_cert=ssl_cert, http_auth=http_auth, timeout=timeout) @property def aptly_server_url(self) -> str: return self.__aptly_server_url def __repr__(self) -> str: return "Client (Aptly API Client) <%s>" % self.aptly_server_url aptly-api-client-0.2.4/aptly_api/parts/000077500000000000000000000000001432774530100200125ustar00rootroot00000000000000aptly-api-client-0.2.4/aptly_api/parts/__init__.py000066400000000000000000000003401432774530100221200ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. aptly-api-client-0.2.4/aptly_api/parts/files.py000066400000000000000000000027671432774530100215020ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os from typing import Sequence, List, Tuple, BinaryIO, cast, Optional # noqa: F401 from aptly_api.base import BaseAPIClient, AptlyAPIException class FilesAPISection(BaseAPIClient): def list(self, directory: Optional[str] = None) -> Sequence[str]: if directory is None: resp = self.do_get("api/files") else: resp = self.do_get("api/files/%s" % directory) return cast(List[str], resp.json()) def upload(self, destination: str, *files: str) -> Sequence[str]: to_upload = [] # type: List[Tuple[str, BinaryIO]] for f in files: if not os.path.exists(f) or not os.access(f, os.R_OK): raise AptlyAPIException("File to upload %s can't be opened or read" % f) fh = open(f, mode="rb") to_upload.append((f, fh),) try: resp = self.do_post("api/files/%s" % destination, files=to_upload) except AptlyAPIException: raise finally: for fn, to_close in to_upload: if not to_close.closed: to_close.close() return cast(List[str], resp.json()) def delete(self, path: Optional[str] = None) -> None: self.do_delete("api/files/%s" % path) aptly-api-client-0.2.4/aptly_api/parts/misc.py000066400000000000000000000014031432774530100213150ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import cast from aptly_api.base import AptlyAPIException, BaseAPIClient class MiscAPISection(BaseAPIClient): def graph(self, ext: str, layout: str = "horizontal") -> None: raise NotImplementedError("The Graph API is not yet supported") def version(self) -> str: resp = self.do_get("api/version") if "Version" in resp.json(): return cast(str, resp.json()["Version"]) else: raise AptlyAPIException("Aptly server didn't return a valid response object:\n%s" % resp.text) aptly-api-client-0.2.4/aptly_api/parts/packages.py000066400000000000000000000025101432774530100221400ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import NamedTuple, Dict, Union, Optional from urllib.parse import quote from aptly_api.base import BaseAPIClient Package = NamedTuple('Package', [ ('key', str), ('short_key', Optional[str]), ('files_hash', Optional[str]), ('fields', Optional[Dict[str, str]]), ]) class PackageAPISection(BaseAPIClient): @staticmethod def package_from_response(api_response: Union[str, Dict[str, str]]) -> Package: if isinstance(api_response, str): return Package( key=api_response, short_key=None, files_hash=None, fields=None, ) else: return Package( key=api_response["Key"], short_key=api_response["ShortKey"] if "ShortKey" in api_response else None, files_hash=api_response["FilesHash"] if "FilesHash" in api_response else None, fields=api_response, ) def show(self, key: str) -> Package: resp = self.do_get("api/packages/%s" % quote(key)) return self.package_from_response(resp.json()) aptly-api-client-0.2.4/aptly_api/parts/publish.py000066400000000000000000000176231432774530100220430ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import NamedTuple, Sequence, Dict, Union, List, cast, Optional from urllib.parse import quote from aptly_api.base import BaseAPIClient, AptlyAPIException PublishEndpoint = NamedTuple('PublishEndpoint', [ ('storage', str), ('prefix', str), ('distribution', str), ('source_kind', str), ('sources', Sequence[Dict[str, str]]), ('architectures', Sequence[str]), ('label', str), ('origin', str), ('acquire_by_hash', bool), ]) T_BodyDict = Dict[str, Union[str, bool, Sequence[Dict[str, str]], Sequence[str], Dict[str, Union[bool, str]]]] class PublishAPISection(BaseAPIClient): @staticmethod def endpoint_from_response(api_response: Union[Dict[str, str], Dict[str, List[str]], Dict[str, List[Dict[str, str]]]]) -> PublishEndpoint: return PublishEndpoint( storage=cast(str, api_response["Storage"]), prefix=cast(str, api_response["Prefix"]), distribution=cast(str, api_response["Distribution"]), source_kind=cast(str, api_response["SourceKind"]), sources=cast(List[Dict[str, str]], api_response["Sources"]), architectures=cast(List[str], api_response["Architectures"]), label=cast(str, api_response["Label"]), origin=cast(str, api_response["Origin"]), acquire_by_hash=cast(bool, api_response["AcquireByHash"]), ) @staticmethod def escape_prefix(prefix: str) -> str: if prefix == ".": return ":." if "/" in prefix: # prefix has not yet been quoted as described at # https://www.aptly.info/doc/api/publish/ if "_" in prefix: prefix = prefix.replace("_", "__") prefix = prefix.replace("/", "_") return prefix def list(self) -> Sequence[PublishEndpoint]: resp = self.do_get("api/publish") ret = [] for rpe in resp.json(): ret.append(self.endpoint_from_response(rpe)) return ret def publish(self, *, source_kind: str = "local", sources: Sequence[Dict[str, str]], architectures: Sequence[str], prefix: Optional[str] = None, distribution: Optional[str] = None, label: Optional[str] = None, origin: Optional[str] = None, force_overwrite: bool = False, sign_skip: bool = False, sign_batch: bool = True, sign_gpgkey: Optional[str] = None, sign_keyring: Optional[str] = None, sign_secret_keyring: Optional[str] = None, sign_passphrase: Optional[str] = None, sign_passphrase_file: Optional[str] = None, acquire_by_hash: Optional[bool] = None) -> PublishEndpoint: """ Example: .. code-block:: python p.publish( sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], prefix='s3:myendpoint:test/a_1', distribution='test', sign_batch=True, sign_gpgkey='A16BE921', sign_passphrase='*********' ) """ if sign_passphrase is not None and sign_passphrase_file is not None: raise AptlyAPIException("Can't use sign_passphrase and sign_passphrase_file at the same time") for source in sources: if "name" not in source and "Name" not in source: raise AptlyAPIException("Each source in publish() must contain the 'name' attribute") url = "api/publish" if prefix is not None and prefix != "": url = "api/publish/%s" % quote(self.escape_prefix(prefix)) body = { "SourceKind": source_kind, "Sources": sources, } # type: T_BodyDict if architectures is not None: body["Architectures"] = architectures if distribution is not None: body["Distribution"] = distribution if label is not None: body["Label"] = label if origin is not None: body["Origin"] = origin if force_overwrite: body["ForceOverwrite"] = True if acquire_by_hash is not None: body["AcquireByHash"] = acquire_by_hash sign_dict = {} # type: Dict[str, Union[bool,str]] if sign_skip: sign_dict["Skip"] = True else: sign_dict["Batch"] = sign_batch if sign_gpgkey is not None: sign_dict["GpgKey"] = sign_gpgkey if sign_keyring is not None: sign_dict["Keyring"] = sign_keyring if sign_secret_keyring is not None: sign_dict["SecretKeyring"] = sign_secret_keyring if sign_passphrase is not None: sign_dict["Passphrase"] = sign_passphrase if sign_passphrase_file is not None: sign_dict["PassphraseFile"] = sign_passphrase_file body["Signing"] = sign_dict resp = self.do_post(url, json=body) return self.endpoint_from_response(resp.json()) def update(self, *, prefix: str, distribution: str, snapshots: Optional[Sequence[Dict[str, str]]] = None, force_overwrite: bool = False, sign_skip: bool = False, sign_batch: bool = True, sign_gpgkey: Optional[str] = None, sign_keyring: Optional[str] = None, sign_secret_keyring: Optional[str] = None, sign_passphrase: Optional[str] = None, sign_passphrase_file: Optional[str] = None, skip_contents: Optional[bool] = False, skip_cleanup: Optional[bool] = False) -> PublishEndpoint: """ Example: .. code-block:: python p.update( prefix="s3:maurusnet:nightly/stretch", distribution="mn-nightly", sign_batch=True, sign_gpgkey='A16BE921', sign_passphrase='***********' ) """ if sign_passphrase is not None and sign_passphrase_file is not None: raise AptlyAPIException("Can't use sign_passphrase and sign_passphrase_file at the same time") body = {} # type: T_BodyDict if snapshots is not None: for source in snapshots: if "name" not in source and "Name" not in source: raise AptlyAPIException("Each source in update() must contain the 'name' attribute") body["Snapshots"] = snapshots if force_overwrite: body["ForceOverwrite"] = True if skip_cleanup: body["SkipCleanup"] = True if skip_contents: body["SkipContents"] = True sign_dict = {} # type: Dict[str, Union[bool,str]] if sign_skip: sign_dict["Skip"] = True else: sign_dict["Batch"] = sign_batch if sign_gpgkey is not None: sign_dict["GpgKey"] = sign_gpgkey if sign_keyring is not None: sign_dict["Keyring"] = sign_keyring if sign_secret_keyring is not None: sign_dict["SecretKeyring"] = sign_secret_keyring if sign_passphrase is not None: sign_dict["Passphrase"] = sign_passphrase if sign_passphrase_file is not None: sign_dict["PassphraseFile"] = sign_passphrase_file body["Signing"] = sign_dict resp = self.do_put("api/publish/%s/%s" % (quote(self.escape_prefix(prefix)), quote(distribution),), json=body) return self.endpoint_from_response(resp.json()) def drop(self, *, prefix: str, distribution: str, force_delete: bool = False) -> None: params = {} if force_delete: params["force"] = "1" self.do_delete("api/publish/%s/%s" % (quote(self.escape_prefix(prefix)), quote(distribution),), params=params) aptly-api-client-0.2.4/aptly_api/parts/repos.py000066400000000000000000000127531432774530100215240ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import NamedTuple, Sequence, Dict, Union, cast, Optional from urllib.parse import quote from aptly_api.base import BaseAPIClient, AptlyAPIException from aptly_api.parts.packages import PackageAPISection, Package Repo = NamedTuple('Repo', [ ('name', str), ('comment', Optional[str]), ('default_distribution', Optional[str]), ('default_component', Optional[str]) ]) FileReport = NamedTuple('FileReport', [ ('failed_files', Sequence[str]), ('report', Dict[str, Sequence[str]]) ]) class ReposAPISection(BaseAPIClient): @staticmethod def repo_from_response(api_response: Dict[str, str]) -> Repo: return Repo( name=api_response["Name"], default_component=api_response["DefaultComponent"] if "DefaultComponent" in api_response else None, default_distribution=api_response["DefaultDistribution"] if "DefaultDistribution" in api_response else None, comment=api_response["Comment"] if "Comment" in api_response else None, ) @staticmethod def filereport_from_response(api_response: Dict[str, Union[Sequence[str], Dict[str, Sequence[str]]]]) -> FileReport: return FileReport( failed_files=cast(Sequence[str], api_response["FailedFiles"]), report=cast(Dict[str, Sequence[str]], api_response["Report"]), ) def create(self, reponame: str, comment: Optional[str] = None, default_distribution: Optional[str] = None, default_component: Optional[str] = None) -> Repo: data = { "Name": reponame, } if comment: data["Comment"] = comment if default_distribution: data["DefaultDistribution"] = default_distribution if default_component: data["DefaultComponent"] = default_component resp = self.do_post("api/repos", json=data) return self.repo_from_response(resp.json()) def show(self, reponame: str) -> Repo: resp = self.do_get("api/repos/%s" % quote(reponame)) return self.repo_from_response(resp.json()) def search_packages(self, reponame: str, query: Optional[str] = None, with_deps: bool = False, detailed: bool = False) -> Sequence[Package]: if query is None and with_deps: raise AptlyAPIException("search_packages can't include dependencies (with_deps==True) without" "a query") params = {} if query: params["q"] = query if with_deps: params["withDeps"] = "1" if detailed: params["format"] = "details" resp = self.do_get("api/repos/%s/packages" % quote(reponame), params=params) ret = [] for rpkg in resp.json(): ret.append(PackageAPISection.package_from_response(rpkg)) return ret def edit(self, reponame: str, comment: Optional[str] = None, default_distribution: Optional[str] = None, default_component: Optional[str] = None) -> Repo: if comment is None and default_component is None and default_distribution is None: raise AptlyAPIException("edit requires at least one of 'comment', 'default_distribution' or " "'default_component'.") body = {} if comment is not None: body["Comment"] = comment if default_distribution is not None: body["DefaultDistribution"] = default_distribution if default_component is not None: body["DefaultComponent"] = default_component resp = self.do_put("api/repos/%s" % quote(reponame), json=body) return self.repo_from_response(resp.json()) def list(self) -> Sequence[Repo]: resp = self.do_get("api/repos") repos = [] for rdesc in resp.json(): repos.append( self.repo_from_response(rdesc) ) return repos def delete(self, reponame: str, force: bool = False) -> None: self.do_delete("api/repos/%s" % quote(reponame), params={"force": "1" if force else "0"}) def add_uploaded_file(self, reponame: str, dir: str, filename: Optional[str] = None, remove_processed_files: bool = True, force_replace: bool = False) -> FileReport: params = { "noRemove": "0" if remove_processed_files else "1", } if force_replace: params["forceReplace"] = "1" if filename is None: resp = self.do_post("api/repos/%s/file/%s" % (quote(reponame), quote(dir),), params=params) else: resp = self.do_post("api/repos/%s/file/%s/%s" % (quote(reponame), quote(dir), quote(filename),), params=params) return self.filereport_from_response(resp.json()) def add_packages_by_key(self, reponame: str, *package_keys: str) -> Repo: resp = self.do_post("api/repos/%s/packages" % quote(reponame), json={ "PackageRefs": package_keys, }) return self.repo_from_response(resp.json()) def delete_packages_by_key(self, reponame: str, *package_keys: str) -> Repo: resp = self.do_delete("api/repos/%s/packages" % quote(reponame), json={ "PackageRefs": package_keys, }) return self.repo_from_response(resp.json()) aptly-api-client-0.2.4/aptly_api/parts/snapshots.py000066400000000000000000000113361432774530100224120ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from datetime import datetime from typing import NamedTuple, Sequence, Optional, Dict, Union, cast, List from urllib.parse import quote import iso8601 from aptly_api.base import BaseAPIClient, AptlyAPIException from aptly_api.parts.packages import Package, PackageAPISection Snapshot = NamedTuple('Snapshot', [ ('name', str), ('description', Optional[str]), ('created_at', Optional[datetime]) ]) class SnapshotAPISection(BaseAPIClient): @staticmethod def snapshot_from_response(api_response: Dict[str, Union[str, None]]) -> Snapshot: return Snapshot( # use a cast() here as `name` can never be None, but the `api_response` declaration can't handle that name=cast(str, api_response["Name"]), description=api_response["Description"] if "Description" in api_response else None, created_at=iso8601.parse_date( cast(str, api_response["CreatedAt"]) ) if "CreatedAt" in api_response else None, ) def list(self, sort: str = 'name') -> Sequence[Snapshot]: if sort not in ['name', 'time']: raise AptlyAPIException("Snapshot LIST only supports two sort modes: 'name' and 'time'. %s is not " "supported." % sort) resp = self.do_get("api/snapshots", params={"sort": sort}) ret = [] for rsnap in resp.json(): ret.append(self.snapshot_from_response(rsnap)) return ret def create_from_repo(self, reponame: str, snapshotname: str, description: Optional[str] = None) -> Snapshot: body = { "Name": snapshotname, } if description is not None: body["Description"] = description resp = self.do_post("api/repos/%s/snapshots" % quote(reponame), json=body) return self.snapshot_from_response(resp.json()) def create_from_packages(self, snapshotname: str, description: Optional[str] = None, source_snapshots: Optional[Sequence[str]] = None, package_refs: Optional[Sequence[str]] = None) -> Snapshot: body = { "Name": snapshotname, } # type: Dict[str, Union[str, Sequence[str]]] if description is not None: body["Description"] = description if source_snapshots is not None: body["SourceSnapshots"] = source_snapshots if package_refs is not None: body["PackageRefs"] = package_refs resp = self.do_post("api/snapshots", json=body) return self.snapshot_from_response(resp.json()) def update(self, snapshotname: str, newname: Optional[str] = None, newdescription: Optional[str] = None) -> Snapshot: if newname is None and newdescription is None: raise AptlyAPIException("When updating a Snapshot you must at lease provide either a new name or a " "new description.") body = {} # type: Dict[str, Union[str, Sequence[str]]] if newname is not None: body["Name"] = newname if newdescription is not None: body["Description"] = newdescription resp = self.do_put("api/snapshots/%s" % quote(snapshotname), json=body) return self.snapshot_from_response(resp.json()) def show(self, snapshotname: str) -> Snapshot: resp = self.do_get("api/snapshots/%s" % quote(snapshotname)) return self.snapshot_from_response(resp.json()) def list_packages(self, snapshotname: str, query: Optional[str] = None, with_deps: bool = False, detailed: bool = False) -> Sequence[Package]: params = {} if query is not None: params["q"] = query if with_deps: params["withDeps"] = "1" if detailed: params["format"] = "details" resp = self.do_get("api/snapshots/%s/packages" % quote(snapshotname), params=params) ret = [] for rpkg in resp.json(): ret.append(PackageAPISection.package_from_response(rpkg)) return ret def delete(self, snapshotname: str, force: bool = False) -> None: params = None if force: params = { "force": "1", } self.do_delete("api/snapshots/%s" % quote(snapshotname), params=params) def diff(self, snapshot1: str, snapshot2: str) -> Sequence[Dict[str, str]]: resp = self.do_get("api/snapshots/%s/diff/%s" % (quote(snapshot1), quote(snapshot2),)) return cast(List[Dict[str, str]], resp.json()) aptly-api-client-0.2.4/aptly_api/tests/000077500000000000000000000000001432774530100200235ustar00rootroot00000000000000aptly-api-client-0.2.4/aptly_api/tests/__init__.py000066400000000000000000000007711432774530100221410ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from .test_base import * # noqa from .test_client import * # noqa from .test_files import * # noqa from .test_misc import * # noqa from .test_packages import * # noqa from .test_publish import * # noqa from .test_repos import * # noqa from .test_snapshots import * # noqa aptly-api-client-0.2.4/aptly_api/tests/test_base.py000066400000000000000000000003401432774530100223430ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. aptly-api-client-0.2.4/aptly_api/tests/test_client.py000066400000000000000000000111511432774530100227110ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import Any, cast from unittest.case import TestCase import requests import requests_mock from aptly_api import Client as AptlyClient # as we're testing the individual parts, this is rather simple from aptly_api.base import AptlyAPIException class ClientTests(TestCase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.client = AptlyClient("http://test/") def test_instantiate(self) -> None: cl = AptlyClient("http://test/") self.assertEqual( str(cl), "Client (Aptly API Client) " ) @requests_mock.Mocker(kw='rmock') def test_api_subdir_get(self, *, rmock: requests_mock.Mocker) -> None: # register mock:// scheme with urllib.parse import urllib.parse urllib.parse.uses_netloc += ['mock'] urllib.parse.uses_relative += ['mock'] urllib.parse.uses_fragment += ['mock'] urllib.parse.uses_params += ['mock'] cl = AptlyClient("mock://test/basedir/") rmock.get("mock://test/basedir/api/test", status_code=200, text='') cl.files.do_get("api/test") self.assertTrue(rmock.called) def test_error_no_error(self) -> None: class MockResponse: def __init__(self, status_code: int = 200) -> None: self.status_code = status_code self.assertEqual( self.client.files._error_from_response(cast(requests.Response, MockResponse())), "no error (status 200)" ) def test_error_no_json(self) -> None: adapter = requests_mock.Adapter() adapter.register_uri("GET", "mock://test/api", status_code=400, text="this is not json", reason="test") session = requests.session() session.mount("mock", adapter) resp = session.get("mock://test/api") self.assertEqual( self.client.files._error_from_response(resp), "400 test this is not json" ) def test_error_dict(self) -> None: adapter = requests_mock.Adapter() adapter.register_uri("GET", "mock://test/api", status_code=400, text='{"error": "error", "meta": "meta"}', reason="test") session = requests.session() session.mount("mock", adapter) resp = session.get("mock://test/api") self.assertEqual( self.client.files._error_from_response(resp), "400 - test - error (meta)" ) def test_error_list(self) -> None: adapter = requests_mock.Adapter() adapter.register_uri("GET", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', reason="test") session = requests.session() session.mount("mock", adapter) resp = session.get("mock://test/api") self.assertEqual( self.client.files._error_from_response(resp), "400 - test - error (meta)" ) @requests_mock.Mocker(kw='rmock') def test_error_get(self, *, rmock: requests_mock.Mocker) -> None: rmock.register_uri("GET", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', reason="test") with self.assertRaises(AptlyAPIException): self.client.files.do_get("mock://test/api") @requests_mock.Mocker(kw='rmock') def test_error_post(self, *, rmock: requests_mock.Mocker) -> None: rmock.register_uri("POST", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', reason="test") with self.assertRaises(AptlyAPIException): self.client.files.do_post("mock://test/api") @requests_mock.Mocker(kw='rmock') def test_error_put(self, *, rmock: requests_mock.Mocker) -> None: rmock.register_uri("PUT", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', reason="test") with self.assertRaises(AptlyAPIException): self.client.files.do_put("mock://test/api") @requests_mock.Mocker(kw='rmock') def test_error_delete(self, *, rmock: requests_mock.Mocker) -> None: rmock.register_uri("DELETE", "mock://test/api", status_code=400, text='[{"error": "error", "meta": "meta"}]', reason="test") with self.assertRaises(AptlyAPIException): self.client.files.do_delete("mock://test/api") aptly-api-client-0.2.4/aptly_api/tests/test_files.py000066400000000000000000000040621432774530100225400ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import Any import os from unittest.case import TestCase import requests_mock from aptly_api.base import AptlyAPIException from aptly_api.parts.files import FilesAPISection @requests_mock.Mocker(kw='rmock') class FilesAPISectionTests(TestCase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.fapi = FilesAPISection("http://test/") def test_list(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/files", text='["aptly-0.9"]') self.assertSequenceEqual(self.fapi.list(), ["aptly-0.9"]) def test_list_dir(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/files/dir", text='["dir/aptly_0.9~dev+217+ge5d646c_i386.deb"]') self.assertSequenceEqual(self.fapi.list("dir"), ["dir/aptly_0.9~dev+217+ge5d646c_i386.deb"]) def test_upload_file(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/files/test", text='["test/testpkg.deb"]') self.assertSequenceEqual( self.fapi.upload("test", os.path.join(os.path.dirname(__file__), "testpkg.deb")), ['test/testpkg.deb'], ) def test_upload_invalid(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(AptlyAPIException): self.fapi.upload("test", "noexistant") def test_upload_failed(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/files/test", text='["test/testpkg.deb"]', status_code=500) with self.assertRaises(AptlyAPIException): self.fapi.upload("test", os.path.join(os.path.dirname(__file__), "testpkg.deb")) def test_delete(self, *, rmock: requests_mock.Mocker) -> None: rmock.delete("http://test/api/files/test", text='{}') self.fapi.delete("test") aptly-api-client-0.2.4/aptly_api/tests/test_misc.py000066400000000000000000000022341432774530100223700ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import Any from unittest.case import TestCase import requests_mock from aptly_api.base import AptlyAPIException from aptly_api.parts.misc import MiscAPISection @requests_mock.Mocker(kw='rmock') class MiscAPISectionTests(TestCase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.mapi = MiscAPISection("http://test/") def test_version(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/version", text='{"Version":"1.0.1"}') self.assertEqual(self.mapi.version(), "1.0.1") def test_graph(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(NotImplementedError): self.mapi.graph("png") def test_version_error(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/version", text='{"droenk": "blah"}') with self.assertRaises(AptlyAPIException): self.mapi.version() aptly-api-client-0.2.4/aptly_api/tests/test_packages.py000066400000000000000000000075011432774530100232150ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import Any from unittest.case import TestCase import requests_mock from aptly_api.parts.packages import PackageAPISection, Package @requests_mock.Mocker(kw='rmock') class PackageAPISectionTests(TestCase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.papi = PackageAPISection("http://test/") def test_show(self, *, rmock: requests_mock.Mocker) -> None: rmock.get( "http://test/api/packages/Pamd64%20authserver%200.1.14~dev0-1%201cc572a93625a9c9", text="""{"Architecture":"amd64", "Depends":"python3, python3-pip, python3-virtualenv, adduser, cron-daemon", "Description":" no description given\\n", "Filename":"authserver_0.1.14~dev0-1.deb", "FilesHash":"1cc572a93625a9c9", "Homepage":"http://example.com/no-uri-given", "Installed-Size":"74927", "Key":"Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9", "License":"unknown", "MD5sum":"03cca0794e63cf147b879e0a3695f523", "Maintainer":"Jonas Maurus", "Package":"authserver", "Priority":"extra", "Provides":"maurusnet-authserver", "SHA1":"9a77a31dba51f612ee08ee096381f0c7e8f97a42", "SHA256":"63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7", "SHA512":"01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65""" """cb6ac0f64b9bd88b507221a71825f5329fdda0e58728cd7", "Section":"default", "ShortKey":"Pamd64 authserver 0.1.14~dev0-1", "Size":"26623042", "Vendor":"root@test", "Version":"0.1.14~dev0-1"}""" ) pkg = self.papi.show("Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9") self.assertEqual( pkg, Package( key='Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', short_key='Pamd64 authserver 0.1.14~dev0-1', files_hash='1cc572a93625a9c9', fields={ 'Architecture': 'amd64', 'Depends': 'python3, python3-pip, python3-virtualenv, adduser, cron-daemon', 'Description': ' no description given\n', 'Filename': 'authserver_0.1.14~dev0-1.deb', 'FilesHash': '1cc572a93625a9c9', 'Homepage': 'http://example.com/no-uri-given', 'Installed-Size': '74927', 'Key': 'Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', 'License': 'unknown', 'MD5sum': '03cca0794e63cf147b879e0a3695f523', 'Maintainer': 'Jonas Maurus', 'Package': 'authserver', 'Priority': 'extra', 'Provides': 'maurusnet-authserver', 'SHA1': '9a77a31dba51f612ee08ee096381f0c7e8f97a42', 'SHA256': '63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7', 'SHA512': '01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65cb6ac' '0f64b9bd88b507221a71825f5329fdda0e58728cd7', 'Section': 'default', 'ShortKey': 'Pamd64 authserver 0.1.14~dev0-1', 'Size': '26623042', 'Vendor': 'root@test', 'Version': '0.1.14~dev0-1' } ) ) aptly-api-client-0.2.4/aptly_api/tests/test_publish.py000066400000000000000000000373431432774530100231140ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from unittest.case import TestCase from typing import Any import requests_mock from aptly_api.base import AptlyAPIException from aptly_api.parts.publish import PublishEndpoint, PublishAPISection @requests_mock.Mocker(kw='rmock') class PublishAPISectionTests(TestCase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.papi = PublishAPISection("http://test/") self.maxDiff = None def test_list(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/publish", text='[{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"mn-nightly","Label":"",' '"Origin":"","Prefix":"nightly/stretch","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"maurusnet"}],' '"Storage":"s3:maurusnet"}]') self.assertSequenceEqual( self.papi.list(), [ PublishEndpoint( storage='s3:maurusnet', prefix='nightly/stretch', distribution='mn-nightly', source_kind='local', sources=[{ 'Name': 'maurusnet', 'Component': 'main' }], architectures=['amd64'], label='', origin='', acquire_by_hash=False ) ] ) def test_update(self, *, rmock: requests_mock.Mocker) -> None: rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' '"Storage":"s3:aptly-repo"}') self.assertEqual( self.papi.update( prefix="s3:aptly-repo:test/xyz_1", distribution="test", sign_batch=True, sign_gpgkey="A16BE921", sign_passphrase="123456", ), PublishEndpoint( storage='s3:aptly-repo', prefix='test/xyz_1', distribution='test', source_kind='local', sources=[{ 'Name': 'aptly-repo', 'Component': 'main' }], architectures=['amd64'], label='', origin='', acquire_by_hash=False ) ) def test_update_passphrase_file(self, *, rmock: requests_mock.Mocker) -> None: rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' '"Storage":"s3:aptly-repo"}') self.assertEqual( self.papi.update( prefix="s3:aptly-repo:test/xyz_1", distribution="test", sign_batch=True, sign_gpgkey="A16BE921", sign_passphrase_file="/root/passphrase.txt", ), PublishEndpoint( storage='s3:aptly-repo', prefix='test/xyz_1', distribution='test', source_kind='local', sources=[{ 'Name': 'aptly-repo', 'Component': 'main' }], architectures=['amd64'], label='', origin='', acquire_by_hash=False ) ) def test_update_no_sign(self, *, rmock: requests_mock.Mocker) -> None: rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' '"Storage":"s3:aptly-repo"}') self.assertEqual( self.papi.update( prefix="s3:aptly-repo:test/xyz_1", distribution="test", sign_skip=True, ), PublishEndpoint( storage='s3:aptly-repo', prefix='test/xyz_1', distribution='test', source_kind='local', sources=[{ 'Name': 'aptly-repo', 'Component': 'main' }], architectures=['amd64'], label='', origin='', acquire_by_hash=False ) ) def test_update_snapshots(self, *, rmock: requests_mock.Mocker) -> None: rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' '"SourceKind":"snapshot","Sources":[{"Component":"main","Name":"aptly-repo-1"}],' '"Storage":"s3:aptly-repo"}') self.assertEqual( self.papi.update( prefix="s3:aptly-repo:test/xyz_1", distribution="test", snapshots=[{"Name": "aptly-repo-1"}], force_overwrite=True, sign_batch=True, sign_gpgkey="A16BE921", sign_passphrase="123456", sign_keyring="/etc/gpg-managed-keyring/pubring.pub", sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg" ), PublishEndpoint( storage='s3:aptly-repo', prefix='test/xyz_1', distribution='test', source_kind='snapshot', sources=[{ 'Name': 'aptly-repo-1', 'Component': 'main', }], architectures=['amd64'], label='', origin='', acquire_by_hash=False ) ) def test_publish(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' '"Storage":"s3:myendpoint"}') self.assertEqual( self.papi.publish( sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', sign_batch=True, sign_gpgkey='A16BE921', sign_passphrase='*********', force_overwrite=True, sign_keyring="/etc/gpg-managed-keyring/pubring.pub", sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", acquire_by_hash=False ), PublishEndpoint( storage='s3:myendpoint', prefix='test/a_1', distribution='test', source_kind='local', sources=[{'Component': 'main', 'Name': 'aptly-repo'}], architectures=['amd64'], label='test', origin='origin', acquire_by_hash=False ) ) def test_publish_passphrase_file(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' '"Storage":"s3:myendpoint"}') self.assertEqual( self.papi.publish( sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', sign_batch=True, sign_gpgkey='A16BE921', sign_passphrase_file='/root/passphrase.txt', force_overwrite=True, sign_keyring="/etc/gpg-managed-keyring/pubring.pub", sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", acquire_by_hash=False ), PublishEndpoint( storage='s3:myendpoint', prefix='test/a_1', distribution='test', source_kind='local', sources=[{'Component': 'main', 'Name': 'aptly-repo'}], architectures=['amd64'], label='test', origin='origin', acquire_by_hash=False ) ) def test_publish_no_sign(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' '"Storage":"s3:myendpoint"}') self.assertEqual( self.papi.publish( sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', sign_skip=True, acquire_by_hash=False ), PublishEndpoint( storage='s3:myendpoint', prefix='test/a_1', distribution='test', source_kind='local', sources=[{'Component': 'main', 'Name': 'aptly-repo'}], architectures=['amd64'], label='test', origin='origin', acquire_by_hash=False ) ) def test_publish_default_key(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"test",' '"Origin":"origin","Prefix":"test/a_1","SkipContents":false,' '"SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],' '"Storage":"s3:myendpoint"}') self.assertEqual( self.papi.publish( sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], prefix='s3:myendpoint:test/a_1', distribution='test', label='test', origin='origin', sign_batch=True, sign_passphrase='*********', force_overwrite=True, sign_keyring="/etc/gpg-managed-keyring/pubring.pub", sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", ), PublishEndpoint( storage='s3:myendpoint', prefix='test/a_1', distribution='test', source_kind='local', sources=[{'Component': 'main', 'Name': 'aptly-repo'}], architectures=['amd64'], label='test', origin='origin', acquire_by_hash=False ) ) def test_update_snapshot_default_key(self, *, rmock: requests_mock.Mocker) -> None: rmock.put("http://test/api/publish/s3%3Aaptly-repo%3Atest_xyz__1/test", text='{"AcquireByHash":false,"Architectures":["amd64"],"Distribution":"test","Label":"",' '"Origin":"","Prefix":"test/xyz_1","SkipContents":false,' '"SourceKind":"snapshot","Sources":[{"Component":"main","Name":"aptly-repo-1"}],' '"Storage":"s3:aptly-repo"}') self.assertEqual( self.papi.update( prefix="s3:aptly-repo:test/xyz_1", distribution="test", snapshots=[{"Name": "aptly-repo-1"}], force_overwrite=True, sign_batch=True, sign_passphrase="123456", sign_keyring="/etc/gpg-managed-keyring/pubring.pub", sign_secret_keyring="/etc/gpg-managed-keyring/secring.gpg", skip_contents=True, skip_cleanup=True ), PublishEndpoint( storage='s3:aptly-repo', prefix='test/xyz_1', distribution='test', source_kind='snapshot', sources=[{ 'Name': 'aptly-repo-1', 'Component': 'main', }], architectures=['amd64'], label='', origin='', acquire_by_hash=False ) ) def test_double_passphrase(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(AptlyAPIException): self.papi.publish(sources=[{'Name': 'aptly-repo'}], architectures=['amd64'], prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, sign_gpgkey='A16BE921', sign_passphrase="*******", sign_passphrase_file="****") with self.assertRaises(AptlyAPIException): self.papi.update(prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, sign_gpgkey='A16BE921', sign_passphrase="*******", sign_passphrase_file="****") def test_no_name(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(AptlyAPIException): self.papi.publish(sources=[{'nope': 'nope'}], architectures=['amd64'], prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, sign_gpgkey='A16BE921', sign_passphrase="*******") with self.assertRaises(AptlyAPIException): self.papi.update(snapshots=[{'nope': 'nope'}], prefix='s3:myendpoint:test/a_1', distribution='test', sign_skip=False, sign_gpgkey='A16BE921', sign_passphrase="*******") def test_drop(self, *, rmock: requests_mock.Mocker) -> None: rmock.delete("http://test/api/publish/s3%3Amyendpoint%3Atest_a__1/test?force=1", text='{}') self.papi.drop(prefix='s3:myendpoint:test/a_1', distribution='test', force_delete=True) def test_escape_prefix(self, *args: Any, **kwargs: Any) -> None: self.assertEqual( self.papi.escape_prefix("test/a_1"), "test_a__1", ) self.assertEqual( self.papi.escape_prefix("test-a-1"), "test-a-1" ) self.assertEqual( self.papi.escape_prefix("test/a"), "test_a" ) self.assertEqual( self.papi.escape_prefix("."), ":." ) aptly-api-client-0.2.4/aptly_api/tests/test_repos.py000066400000000000000000000232721432774530100225720ustar00rootroot00000000000000# -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import Any from unittest.case import TestCase import requests_mock from aptly_api.base import AptlyAPIException from aptly_api.parts.packages import Package from aptly_api.parts.repos import ReposAPISection, Repo, FileReport @requests_mock.Mocker(kw='rmock') class ReposAPISectionTests(TestCase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.rapi = ReposAPISection("http://test/") def test_create(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/repos", text='{"Name":"aptly-repo","Comment":"test","DefaultDistribution":"test","DefaultComponent":"test"}') self.assertEqual( self.rapi.create("aptly-repo", comment="test", default_component="test", default_distribution="test"), Repo( name="aptly-repo", default_distribution="test", default_component="test", comment="test", ) ) def test_show(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/repos/aptly-repo", text='{"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}') self.assertEqual( self.rapi.show("aptly-repo"), Repo( name="aptly-repo", default_distribution="", default_component="", comment="", ) ) def test_search(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/repos/aptly-repo/packages", text='["Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9"]') self.assertSequenceEqual( self.rapi.search_packages("aptly-repo"), [ Package( key="Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9", short_key=None, files_hash=None, fields=None, ) ], ) def test_search_details(self, *, rmock: requests_mock.Mocker) -> None: rmock.get( "http://test/api/repos/aptly-repo/packages?format=details", text="""[{ "Architecture":"amd64", "Depends":"python3, python3-pip, python3-virtualenv, adduser, cron-daemon", "Description":" no description given\\n", "Filename":"authserver_0.1.14~dev0-1.deb", "FilesHash":"1cc572a93625a9c9", "Homepage":"http://example.com/no-uri-given", "Installed-Size":"74927", "Key":"Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9", "License":"unknown", "MD5sum":"03cca0794e63cf147b879e0a3695f523", "Maintainer":"Jonas Maurus", "Package":"authserver", "Priority":"extra", "Provides":"maurusnet-authserver", "SHA1":"9a77a31dba51f612ee08ee096381f0c7e8f97a42", "SHA256":"63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7", "SHA512":"01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65cb""" """6ac0f64b9bd88b507221a71825f5329fdda0e58728cd7", "Section":"default", "ShortKey":"Pamd64 authserver 0.1.14~dev0-1", "Size":"26623042", "Vendor":"root@test", "Version":"0.1.14~dev0-1" }]""" ) self.assertSequenceEqual( self.rapi.search_packages("aptly-repo", detailed=True, with_deps=True, query="Name (authserver)"), [ Package( key='Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', short_key='Pamd64 authserver 0.1.14~dev0-1', files_hash='1cc572a93625a9c9', fields={ 'Architecture': 'amd64', 'Depends': 'python3, python3-pip, python3-virtualenv, adduser, cron-daemon', 'Description': ' no description given\n', 'Filename': 'authserver_0.1.14~dev0-1.deb', 'FilesHash': '1cc572a93625a9c9', 'Homepage': 'http://example.com/no-uri-given', 'Installed-Size': '74927', 'Key': 'Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9', 'License': 'unknown', 'MD5sum': '03cca0794e63cf147b879e0a3695f523', 'Maintainer': 'Jonas Maurus', 'Package': 'authserver', 'Priority': 'extra', 'Provides': 'maurusnet-authserver', 'SHA1': '9a77a31dba51f612ee08ee096381f0c7e8f97a42', 'SHA256': '63555a135bf0aa1762d09fc622881aaf352cdb3b244da5d78278c7efa2dba8b7', 'SHA512': '01f9ca888014599374bf7a2c8c46f895d7ef0dfea99dfd092007f9fc5d5fe57a2755b843eda296b65' 'cb6ac0f64b9bd88b507221a71825f5329fdda0e58728cd7', 'Section': 'default', 'ShortKey': 'Pamd64 authserver 0.1.14~dev0-1', 'Size': '26623042', 'Vendor': 'root@test', 'Version': '0.1.14~dev0-1' } ) ] ) def test_repo_edit_validation(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(AptlyAPIException): self.rapi.edit("aptly-repo") def test_repo_edit(self, *, rmock: requests_mock.Mocker) -> None: rmock.put("http://test/api/repos/aptly-repo", text='{"Name":"aptly-repo","Comment":"comment",' '"DefaultDistribution":"stretch","DefaultComponent":"main"}') self.assertEqual( self.rapi.edit("aptly-repo", comment="comment", default_distribution="stretch", default_component="main"), Repo(name='aptly-repo', comment='comment', default_distribution='stretch', default_component='main') ) def test_list(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/repos", text='[{"Name":"maurusnet","Comment":"","DefaultDistribution":"",' '"DefaultComponent":"main"},{"Name":"aptly-repo","Comment":"comment",' '"DefaultDistribution":"stretch","DefaultComponent":"main"}]') self.assertSequenceEqual( self.rapi.list(), [ Repo(name='maurusnet', comment='', default_distribution='', default_component='main'), Repo(name='aptly-repo', comment='comment', default_distribution='stretch', default_component='main'), ] ) def test_delete(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(requests_mock.NoMockAddress): self.rapi.delete("aptly-repo", force=True) def test_add_file(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/repos/aptly-repo/file/test/dirmngr_2.1.18-6_amd64.deb", text='{"FailedFiles":[],"Report":{"Warnings":[],' '"Added":["dirmngr_2.1.18-6_amd64 added"],"Removed":[]}}') self.assertEqual( self.rapi.add_uploaded_file("aptly-repo", "test", "dirmngr_2.1.18-6_amd64.deb", force_replace=True), FileReport(failed_files=[], report={'Added': ['dirmngr_2.1.18-6_amd64 added'], 'Removed': [], 'Warnings': []}) ) def test_add_dir(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/repos/aptly-repo/file/test", text='{"FailedFiles":[],"Report":{"Warnings":[],' '"Added":["dirmngr_2.1.18-6_amd64 added"],"Removed":[]}}') self.assertEqual( self.rapi.add_uploaded_file("aptly-repo", "test", force_replace=True), FileReport(failed_files=[], report={'Added': ['dirmngr_2.1.18-6_amd64 added'], 'Removed': [], 'Warnings': []}) ) def test_add_package(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/repos/aptly-repo/packages", text='{"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}') self.assertEqual( self.rapi.add_packages_by_key("aptly-repo", "Pamd64 dirmngr 2.1.18-6 4c7412c5f0d7b30a"), Repo(name='aptly-repo', comment='', default_distribution='', default_component='') ) def test_delete_package(self, *, rmock: requests_mock.Mocker) -> None: rmock.delete( "http://test/api/repos/aptly-repo/packages", text='{"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}' ) self.assertEqual( self.rapi.delete_packages_by_key("aptly-repo", "Pamd64 dirmngr 2.1.18-6 4c7412c5f0d7b30a"), Repo(name='aptly-repo', comment='', default_distribution='', default_component=''), ) def test_search_invalid_params(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(AptlyAPIException): self.rapi.search_packages("aptly-repo", with_deps=True) aptly-api-client-0.2.4/aptly_api/tests/test_snapshots.py000066400000000000000000000241621432774530100234630ustar00rootroot00000000000000# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from typing import Any from unittest.case import TestCase import iso8601 import requests_mock from aptly_api.base import AptlyAPIException from aptly_api.parts.packages import Package from aptly_api.parts.snapshots import SnapshotAPISection, Snapshot @requests_mock.Mocker(kw='rmock') class SnapshotAPISectionTests(TestCase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.sapi = SnapshotAPISection("http://test/") self.maxDiff = None def test_list(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/snapshots", text='[{"Name":"stretch-security-1","CreatedAt":"2017-06-03T21:36:22.2692213Z",' '"Description":"Snapshot from mirror [stretch-security]: ' 'http://security.debian.org/debian-security/ stretch/updates"},' '{"Name":"stretch-updates-1","CreatedAt":"2017-06-03T21:36:22.431767659Z",' '"Description":"Snapshot from mirror [stretch-updates]: ' 'http://ftp-stud.hs-esslingen.de/debian/ stretch-updates"}]') self.assertSequenceEqual( self.sapi.list(), [ Snapshot( name='stretch-security-1', description='Snapshot from mirror [stretch-security]: http://security.debian.org/debian-security/ ' 'stretch/updates', created_at=iso8601.parse_date('2017-06-03T21:36:22.2692213Z') ), Snapshot( name='stretch-updates-1', description='Snapshot from mirror [stretch-updates]: http://ftp-stud.hs-esslingen.de/debian/ ' 'stretch-updates', created_at=iso8601.parse_date('2017-06-03T21:36:22.431767659Z') ) ] ) def test_list_invalid(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(AptlyAPIException): self.sapi.list("snoepsort") def test_update_noparams(self, *, rmock: requests_mock.Mocker) -> None: with self.assertRaises(AptlyAPIException): self.sapi.update("test") def test_create(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/repos/aptly-repo/snapshots", text='{"Name":"aptly-repo-1","CreatedAt":"2017-06-03T23:43:40.275605639Z",' '"Description":"Snapshot from local repo [aptly-repo]"}') self.assertEqual( self.sapi.create_from_repo("aptly-repo", "aptly-repo-1", description='Snapshot from local repo [aptly-repo]'), Snapshot( name='aptly-repo-1', description='Snapshot from local repo [aptly-repo]', created_at=iso8601.parse_date('2017-06-03T23:43:40.275605639Z') ) ) def test_list_packages(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/snapshots/aptly-repo-1/packages", text='["Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d"]') self.assertEqual( self.sapi.list_packages("aptly-repo-1"), [ Package( key='Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d', short_key=None, files_hash=None, fields=None ), ] ) def test_list_packages_details(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/snapshots/aptly-repo-1/packages", text='[{"Architecture":"all","Depends":"postgresql-9.6-postgis-2.3-scripts",' '"Description":" transitional dummy package\\n This is a transitional dummy package. ' 'It can safely be removed.\\n",' '"Filename":"postgresql-9.6-postgis-scripts_2.3.2+dfsg-1~exp2.pgdg90+1_all.deb",' '"FilesHash":"5f70af798690300d",' '"Homepage":"http://postgis.net/",' '"Installed-Size":"491",' '"Key":"Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d",' '"MD5sum":"56de7bac497e4ac34017f4d11e75fffb",' '"Maintainer":"Debian GIS Project \u003cpkg-grass-devel@lists.alioth.debian.org\u003e",' '"Package":"postgresql-9.6-postgis-scripts",' '"Priority":"extra",' '"SHA1":"61bb9250e7a35be9b78808944e8cfbae1e70f67d",' '"SHA256":"01c0c4645e9100f7ddb6d05a9d36ad3866ac8d2e412b7c04163a9e65397ce05e",' '"Section":"oldlibs",' '"ShortKey":"Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1",' '"Size":"468824","Source":"postgis","Version":"2.3.2+dfsg-1~exp2.pgdg90+1"}]') parsed = self.sapi.list_packages("aptly-repo-1", query="Name (% postgresql-9.6.-postgis-sc*)", detailed=True, with_deps=True)[0] expected = Package( key='Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d', short_key='Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1', files_hash='5f70af798690300d', fields={ 'Maintainer': 'Debian GIS Project ', 'Size': '468824', 'MD5sum': '56de7bac497e4ac34017f4d11e75fffb', 'ShortKey': 'Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1', 'FilesHash': '5f70af798690300d', 'Filename': 'postgresql-9.6-postgis-scripts_2.3.2+dfsg-1~exp2.pgdg90+1_all.deb', 'Section': 'oldlibs', 'Homepage': 'http://postgis.net/', 'Description': ' transitional dummy package\n This is a transitional dummy package. ' 'It can safely be removed.\n', 'Architecture': 'all', 'Priority': 'extra', 'Source': 'postgis', 'SHA1': '61bb9250e7a35be9b78808944e8cfbae1e70f67d', 'Installed-Size': '491', 'Version': '2.3.2+dfsg-1~exp2.pgdg90+1', 'Depends': 'postgresql-9.6-postgis-2.3-scripts', 'Key': 'Pall postgresql-9.6-postgis-scripts 2.3.2+dfsg-1~exp2.pgdg90+1 5f70af798690300d', 'SHA256': '01c0c4645e9100f7ddb6d05a9d36ad3866ac8d2e412b7c04163a9e65397ce05e', 'Package': 'postgresql-9.6-postgis-scripts' } ) # mypy should detect this as ensuring that parsed.fields is not None, but it doesn't self.assertIsNotNone(parsed.fields) self.assertIsNotNone(expected.fields) self.assertDictEqual( parsed.fields if parsed.fields else {}, # make sure that mypy doesn't error on this being potentially None expected.fields if expected.fields else {}, # this can't happen unless Package.__init__ is fubared ) def test_show(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/snapshots/aptly-repo-1", text='{"Name":"aptly-repo-1",' '"CreatedAt":"2017-06-03T23:43:40.275605639Z",' '"Description":"Snapshot from local repo [aptly-repo]"}') self.assertEqual( self.sapi.show("aptly-repo-1"), Snapshot( name='aptly-repo-1', description='Snapshot from local repo [aptly-repo]', created_at=iso8601.parse_date('2017-06-03T23:43:40.275605639Z') ) ) def test_update(self, *, rmock: requests_mock.Mocker) -> None: rmock.put("http://test/api/snapshots/aptly-repo-1", text='{"Name":"aptly-repo-2","CreatedAt":"2017-06-03T23:43:40.275605639Z",' '"Description":"test"}') self.assertEqual( self.sapi.update("aptly-repo-1", newname="aptly-repo-2", newdescription="test"), Snapshot( name='aptly-repo-2', description='test', created_at=iso8601.parse_date('2017-06-03T23:43:40.275605639Z') ) ) def test_delete(self, *, rmock: requests_mock.Mocker) -> None: rmock.delete("http://test/api/snapshots/aptly-repo-1", text='{}') self.sapi.delete("aptly-repo-1", force=True) def test_diff(self, *, rmock: requests_mock.Mocker) -> None: rmock.get("http://test/api/snapshots/aptly-repo-1/diff/aptly-repo-2", text='[{"Left":null,"Right":"Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9"},' '{"Left":"Pamd64 radicale 1.1.1 fbc974fa526f14e9","Right":null}]') self.assertSequenceEqual( self.sapi.diff("aptly-repo-1", "aptly-repo-2"), [ {'Left': None, 'Right': 'Pamd64 authserver 0.1.14~dev0-1 1cc572a93625a9c9'}, {'Left': 'Pamd64 radicale 1.1.1 fbc974fa526f14e9', 'Right': None} ] ) def test_create_from_packages(self, *, rmock: requests_mock.Mocker) -> None: rmock.post("http://test/api/snapshots", text='{"Name":"aptly-repo-2","CreatedAt":"2017-06-07T14:19:07.706408213Z","Description":"test"}') self.assertEqual( self.sapi.create_from_packages( "aptly-repo-2", description="test", package_refs=["Pamd64 dirmngr 2.1.18-6 4c7412c5f0d7b30a"], source_snapshots=["aptly-repo-1"] ), Snapshot( name='aptly-repo-2', description='test', created_at=iso8601.parse_date('2017-06-07T14:19:07.706408213Z') ) ) aptly-api-client-0.2.4/aptly_api/tests/testpkg.deb000066400000000000000000000000001432774530100221460ustar00rootroot00000000000000aptly-api-client-0.2.4/requirements-test.txt000066400000000000000000000002341432774530100211370ustar00rootroot00000000000000requests-mock==1.10.0 coverage==6.5.0 coveralls==3.3.1 flake8==5.0.4 pep257==0.7.0 doc8==1.0.0 Pygments==2.13.0 mypy==0.982 pytest==7.2.0 pytest-cov==4.0.0 aptly-api-client-0.2.4/setup.py000066400000000000000000000034741432774530100164210ustar00rootroot00000000000000#!/usr/bin/python # -* encoding: utf-8 *- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os import re from setuptools import setup, find_packages _package_root = "." _root_package = 'aptly_api' _HERE = os.path.abspath(os.path.dirname(__file__)) with open("aptly_api/__init__.py", "rt", encoding="utf-8") as vf: lines = vf.readlines() _version = "0.0.0+local" for line in lines: m = re.match("version = \"(.*?)\"", line) if m: _version = m.group(1) _packages = find_packages(_package_root, exclude=["*.tests", "*.tests.*", "tests.*", "tests"]) _requirements = [ # intentionally unpinned. We're a library, so we don't need to conflict with others by pinning versions # and we don't depend on a specific minimum version. 'requests', 'iso8601', ] try: long_description = open(os.path.join(_HERE, 'README.rst')).read() except IOError: long_description = None setup( name='aptly-api-client', version=_version, packages=_packages, package_dir={ '': _package_root, }, install_requires=_requirements, classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Environment :: Console", "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Operating System :: POSIX", ], author="Jonas Maurus (@jdelic)", author_email="jonas@gopythongo.com", maintainer="GoPythonGo.com", maintainer_email="info@gopythongo.com", description="A Python 3 client for the Aptly API", long_description=long_description, )