├── .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 |
--------------------------------------------------------------------------------