From e64023dde8a37cb0eaa80086111dc258f9d6ef37 Mon Sep 17 00:00:00 2001 From: Leonardo Rossetti Date: Jun 24 2022 16:42:33 +0000 Subject: [PATCH 1/2] adding custom error class --- diff --git a/src/kojid_cloud_scheduler/errors.py b/src/kojid_cloud_scheduler/errors.py new file mode 100644 index 0000000..fbcfacc --- /dev/null +++ b/src/kojid_cloud_scheduler/errors.py @@ -0,0 +1,23 @@ +class KCSError(Exception): + """ + Project base exception class. + + This error should used to raise errors + or inherited by other exception classes. + """ + + def __init__(self, message, code=1): + """ + Creates a new KCSError object instance. + + Parameters + ---------- + message: str + Error message + code: int + Error code, defaults to 1. + """ + super(KCSError, self).__init__(message) + + self.message = message + self.code = code From 0b5cc6949805908b0f0ea6cc831a189064ae9d68 Mon Sep 17 00:00:00 2001 From: Leonardo Rossetti Date: Jun 28 2022 10:37:44 +0000 Subject: [PATCH 2/2] adding kojiclient code --- diff --git a/poetry.lock b/poetry.lock index 6cc4b5f..4170e6b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,6 +21,25 @@ tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)" tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] +name = "certifi" +version = "2022.6.15" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." @@ -29,6 +48,33 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "gssapi" +version = "1.7.3" +description = "Python GSSAPI Wrapper" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +decorator = "*" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" @@ -37,6 +83,20 @@ optional = false python-versions = "*" [[package]] +name = "koji" +version = "1.29.0" +description = "Koji is a system for building and tracking RPMS. The base package contains shared libraries and the command-line interface." +category = "main" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +python-dateutil = "*" +requests = "*" +requests-gssapi = "*" +six = "*" + +[[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" @@ -100,6 +160,20 @@ tomli = ">=1.0.0" testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] +name = "pytest-mock" +version = "3.7.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] name = "pytest-sugar" version = "0.9.4" description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." @@ -113,6 +187,55 @@ pytest = ">=2.9" termcolor = ">=1.1.0" [[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "requests" +version = "2.28.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2.0.0,<2.1.0" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "requests-gssapi" +version = "1.2.3" +description = "A GSSAPI authentication handler for python-requests" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +gssapi = "*" +requests = ">=1.1.0" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] name = "termcolor" version = "1.1.0" description = "ANSII Color formatting for output in terminal." @@ -128,10 +251,23 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "f3749f8acd88278dca034f733bbfef3ea3c3a4efa56daffe2e5c914f6b71bc47" +content-hash = "fad70e809d8180abbaec8db51b1abf838778fde8b8c6f70c5da6301daa9dffcd" [metadata.files] atomicwrites = [ @@ -142,14 +278,56 @@ attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] +certifi = [ + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +decorator = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] +gssapi = [ + {file = "gssapi-1.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:69104cb87205ab6b6ac1e265b2df1c94b661140fdd5db223fefc564f958bcd44"}, + {file = "gssapi-1.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fd3546303db6ae238764d61b4b639ffb74d615d0eb4735389887af804ff336"}, + {file = "gssapi-1.7.3-cp310-cp310-win32.whl", hash = "sha256:ee76851059aecfcf2927ae991d74bd8c635de3e958d8e1cd3cd213d566ec7911"}, + {file = "gssapi-1.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:2788648b614ac10fdf3df71a15ac04ec256e1721dd0c84ac8e104b71cd5fbac5"}, + {file = "gssapi-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a5ba691f66e554d9adaf7d6aa8c82f384494d954059bdfcc6be560e7f9122ed3"}, + {file = "gssapi-1.7.3-cp36-cp36m-win32.whl", hash = "sha256:35e864942c50d0e19608d12052a721745c8f28221c229cba05e534669d7b6225"}, + {file = "gssapi-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2b9a47aa3d9c267c9c3677131f305fbf3c3c544450d2999f113af6e3ba738d2c"}, + {file = "gssapi-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0eb28405cf3d5171b37796fa7cf5c06bb48eefd6d854b587aafe31b4b4d068db"}, + {file = "gssapi-1.7.3-cp37-cp37m-win32.whl", hash = "sha256:6e5a0aae3be78fa5747120f51baa9d070dbc188c105064669ca6b53b75a08c80"}, + {file = "gssapi-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:35dcddb6915dc7c5ae73afacaa6bb1e2da79c24668ad5a3cf2d7890d9c149d3e"}, + {file = "gssapi-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e62981c96b4c138684e8b9dd932a4d52dcc7cc185cf8ac10aae9dd3db23d7e2"}, + {file = "gssapi-1.7.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:56ae0564140ef3cd53034bdb0e4236e00f69dd2b0e3680c41263025d40d02c9d"}, + {file = "gssapi-1.7.3-cp38-cp38-win32.whl", hash = "sha256:a7a9c9af5c96bfb95daa9b7166ae071549c8e6a01fbbdf9a589856d60677a823"}, + {file = "gssapi-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:a2d272d6df0cdbc934d06a0e299fcfb8d7dff211b63d43ce291dcc8055a598ac"}, + {file = "gssapi-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec25bfa506dbf82e0471f9ffce198b10da8f4b787f1779d52dbe514eb2cda999"}, + {file = "gssapi-1.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13971279ccad7df0847d6be33ed2ea1c643bd58f065735c4954cd37c6f85d333"}, + {file = "gssapi-1.7.3-cp39-cp39-win32.whl", hash = "sha256:c65221f5bc91f1fa317b48866d1a6da6248195d5c58e743f6c512a80c7c98119"}, + {file = "gssapi-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:8bb1603b24a1f21a0da19df9f2544c122af896b1f28a17b0d45cf87773de58c4"}, + {file = "gssapi-1.7.3.tar.gz", hash = "sha256:c69b9f633a0c03c1b84ba14c73b0ec132f6323056e675702c1a5f75f316e06fb"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +koji = [ + {file = "koji-1.29.0-py2-none-any.whl", hash = "sha256:df7395dfc4157af4ee8ddb6c6498c762a98adeb77fb43d72cb7ce204857c367d"}, + {file = "koji-1.29.0-py3-none-any.whl", hash = "sha256:7a24bc9e9c7508ded2e174f243780c57562422b8c7320714cff839b841b79b7a"}, + {file = "koji-1.29.0.tar.gz", hash = "sha256:7db1ea4f68dd94d556c612e40bbfc2268164bfebc4a4cd08b63971bddaa686bc"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -170,9 +348,28 @@ pytest = [ {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, ] +pytest-mock = [ + {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, + {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, +] pytest-sugar = [ {file = "pytest-sugar-0.9.4.tar.gz", hash = "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3"}, ] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +requests = [ + {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, + {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, +] +requests-gssapi = [ + {file = "requests-gssapi-1.2.3.tar.gz", hash = "sha256:20784508981401f7153c933eed095338933a40818da65a259dbf2d80dccb150e"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, ] @@ -180,3 +377,7 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] diff --git a/pyproject.toml b/pyproject.toml index edcbbc2..f4d6e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,10 +10,12 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" +koji = "^1.29.0" [tool.poetry.dev-dependencies] pytest = "^7.1.2" pytest-sugar = "^0.9.4" +pytest-mock = "^3.7.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/src/kojid_cloud_scheduler/kojiclient.py b/src/kojid_cloud_scheduler/kojiclient.py new file mode 100644 index 0000000..0c16f68 --- /dev/null +++ b/src/kojid_cloud_scheduler/kojiclient.py @@ -0,0 +1,89 @@ +import koji +import requests + +from .errors import KCSError + + +class KojiClient: + """ + Koji client class which handles Koji Hub API integration. + + This client doesn't require authentication as it only uses + read API calls. + """ + + def __init__(self, hub_url): + """ + Creates a new KojiClient instance. + + Parameters + ---------- + hub_url: str + The base hub api endpoint to use, example: https://koji.fedoraproject.org/kojihub + """ + self.session = None + self.hub_url = hub_url + + def connect(self): + """ + Creates a new client session to the hub api server. + + Raises + ------ + errors.KCSError + """ + try: + self.session = koji.ClientSession(self.hub_url) + except AssertionError: + raise KCSError('"hub_url" must be set') + + def ping(self): + """ + Test the client connection. + + Raises + ------ + errors.KSCError + + Return + ------ + str + A 'Hello World' string message from the server + """ + try: + return self.session.hello() + except requests.exceptions.RequestException as e: + raise KCSError(str(e)) + + def list_tasks(self, **kwargs): + """ + List tasks based on the provided kwargs. + + The provided kwargs will be forwarded to + `self.session.listTasks`. + + Check the upstream Koji documentation for + a detailed explanation about what can be + used in kwargs. + + Parameters + ---------- + kwargs: dict[str, any] + Extra key word arguments to be used as a dictionary + by `self.session.listTasks`. + + Return + ------ + list + A list of tasks or an empty list if none are found. + """ + opts = { + 'arch': ['x86_64'], + 'state': [koji.TASK_STATES['OPEN']], + } + opts.update(**kwargs) + + try: + return self.session.listTasks(opts) + except requests.exceptions.RequestException as e: + raise KCSError(str(e)) diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..e684a42 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,32 @@ +import pytest + + +@pytest.fixture +def list_tasks_result(): + return [ + { + 'arch': 'x86_64', + 'awaited': True, + 'channel_id': 1, + 'completion_time': None, + 'completion_ts': None, + 'create_time': '2022-06-23 21:29:13.877050+00:00', + 'create_ts': 1656019753.87705, + 'host_id': 377, + 'id': 88661672, + 'label': 'x86_64', + 'method': 'buildArch', + 'owner': 4760, + 'owner_name': 'koschei/koschei-backend01.iad2.fedoraproject.org', + 'owner_type': 0, + 'parent': 88661591, + 'priority': 49, + 'request': "", + 'result': None, + 'start_time': '2022-06-23 21:29:46.766581+00:00', + 'start_ts': 1656019786.766581, + 'state': 1, + 'waiting': None, + 'weight': 1.5653944408333333 + } + ] diff --git a/test/test_kojiclient.py b/test/test_kojiclient.py new file mode 100644 index 0000000..ac734e2 --- /dev/null +++ b/test/test_kojiclient.py @@ -0,0 +1,79 @@ +import koji +import pytest +import requests + +from kojid_cloud_scheduler import kojiclient, errors + + +def test_connect_ok(): + url = 'https://koji-hub:443' + client = kojiclient.KojiClient(url) + client.connect() + + assert client.session is not None + + +def test_connect_error(): + url = None + client = kojiclient.KojiClient(url) + + with pytest.raises(errors.KCSError): + client.connect() + + +def test_ping_ok(mocker): + url = 'https://koji.fedoraproject.org/kojihub' + client = kojiclient.KojiClient(url) + client.connect() + + fn = mocker.Mock(return_value='Hello World') + mocker.patch.object(client.session, 'hello', fn) + + assert 'Hello World' == client.ping() + + +def test_ping_error(mocker): + url = 'https://koji-hub:8443' + client = kojiclient.KojiClient(url) + client.connect() + + fn = mocker.Mock(side_effect=requests.exceptions.RequestException('fail')) + mocker.patch.object(client.session, 'hello', fn) + + with pytest.raises(errors.KCSError): + client.ping() + + +def test_list_tasks_ok(mocker, list_tasks_result): + url = 'https://koji.fedoraproject.org/kojihub' + client = kojiclient.KojiClient(url) + client.connect() + + fn = mocker.Mock(return_value=list_tasks_result) + mocker.patch.object(client.session, 'listTasks', fn) + + assert client.list_tasks() == list_tasks_result + + +def test_list_tasks_error_notfound(mocker): + url = 'https://koji.fedoraproject.org/kojihub' + client = kojiclient.KojiClient(url) + client.connect() + + fn = mocker.Mock(return_value=[]) + mocker.patch.object(client.session, 'listTasks', fn) + + assert [] == client.list_tasks(channel_id=[-1]) + + +def test_list_tasks_error_connection(mocker): + url = 'https://koji-hub:8443' + client = kojiclient.KojiClient(url) + client.connect() + + fn = mocker.Mock(side_effect=requests.exceptions.RequestException('fail')) + mocker.patch.object(client.session, 'listTasks', fn) + + with pytest.raises(errors.KCSError): + client.list_tasks() +