diff --git a/jenkins_epo/jenkins.py b/jenkins_epo/jenkins.py index f28f28c7..695bd6aa 100644 --- a/jenkins_epo/jenkins.py +++ b/jenkins_epo/jenkins.py @@ -13,24 +13,21 @@ # jenkins-epo. If not, see . -import ast import asyncio from datetime import datetime, timedelta from itertools import product import logging import re -import aiohttp from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.jenkins import Jenkins, Requester from jenkins_yml import Job as JobSpec import yaml -from yarl import URL from .settings import SETTINGS from .utils import format_duration, match, parse_patterns, retry from .web import fullurl - +from . import rest logger = logging.getLogger(__name__) @@ -39,56 +36,17 @@ class NotOnJenkins(Exception): pass -class RESTClient(object): - def __init__(self, path=''): - self.path = path - - def __call__(self, arg): - return self.__class__(self.path.lstrip('/') + '/' + str(arg)) - - def __getattr__(self, name): - return self(name) - - @retry - def afetch(self, **kw): - session = aiohttp.ClientSession() - url = URL(self.path) - if kw: - url = url.with_query(**kw) - logger.debug("GET %s", url) - try: - response = yield from session.get(url, timeout=10) - payload = yield from response.read() - finally: - yield from session.close() - response.raise_for_status() - return payload.decode('utf-8') - - def aget(self, **kw): - payload = yield from self.api.python.afetch(**kw) - return ast.literal_eval(payload) - - @retry - def apost(self, **kw): - session = aiohttp.ClientSession() - url = URL(self.path) - if kw: - url = url.with_query(**kw) - logger.debug("POST %s", url) - try: - response = yield from session.post(url, timeout=10) - payload = yield from response.read() - finally: - yield from session.close() - response.raise_for_status() - return payload.decode('utf-8') - - class VerboseRequester(Requester): def get_url(self, url, *a, **kw): logger.debug("GET %s (sync)", url) return super(VerboseRequester, self).get_url(url, *a, **kw) + def post_url(self, url, *a, **kw): + logger.debug("POST %s (sync)", url) + return super(VerboseRequester, self).post_url( + url, *a, **kw + ) + # Monkey patch poll=True to avoid I/O in __init__ JenkinsBase.__init__.__defaults__ = (False,) @@ -109,7 +67,7 @@ def __getattr__(self, name): def load(self): if not self._instance: logger.debug("Connecting to Jenkins %s", SETTINGS.JENKINS_URL) - self.rest = RESTClient(SETTINGS.JENKINS_URL) + self.rest = rest.Client(SETTINGS.JENKINS_URL) self._instance = Jenkins( baseurl=SETTINGS.JENKINS_URL, requester=VerboseRequester(baseurl=SETTINGS.JENKINS_URL), @@ -118,7 +76,7 @@ def load(self): @asyncio.coroutine def is_queue_empty(self): - payload = yield from self.rest.queue.aget() + payload = yield from self.rest.queue.api.python.aget() items = [ i for i in payload['items'] if not i['stuck'] and match(i['task']['name'], self.queue_patterns) @@ -137,9 +95,10 @@ def get_job(self, name): def aget_job(self, name): self.load() instance = self._instance.get_job(name) - client = RESTClient(instance.baseurl) - instance._data = yield from client.aget() - instance._config = yield from client('config.xml').afetch() + client = rest.Client(instance.baseurl) + instance._data = yield from client.api.python.aget() + payload = yield from client('config.xml').aget() + instance._config = payload.data return Job.factory(instance) DESCRIPTION_TMPL = """\ @@ -234,7 +193,9 @@ def from_url(cls, url): if not url.startswith(SETTINGS.JENKINS_URL): raise NotOnJenkins("%s is not on this Jenkins." % url) - payload = yield from RESTClient(url).aget(tree=cls.jenkins_tree) + payload = yield from rest.Client(url).api.python.aget( + tree=cls.jenkins_tree, + ) return Build(None, payload) def __getattr__(self, name): @@ -310,7 +271,7 @@ def sha(self): @asyncio.coroutine def stop(self): - payload = yield from RESTClient(self.payload['url']).stop.apost() + payload = yield from rest.Client(self.payload['url']).stop.apost() return payload @@ -409,7 +370,9 @@ def node_param(self): @asyncio.coroutine def fetch_builds(self): tree = "builds[" + Build.jenkins_tree + "]" - payload = yield from RESTClient(self.baseurl).aget(tree=tree) + payload = yield from rest.Client(self.baseurl).api.python.aget( + tree=tree, + ) return payload['builds'] def process_builds(self, payload): diff --git a/jenkins_epo/rest.py b/jenkins_epo/rest.py new file mode 100644 index 00000000..b8c718b7 --- /dev/null +++ b/jenkins_epo/rest.py @@ -0,0 +1,102 @@ +# This file is part of jenkins-epo +# +# jenkins-epo is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or any later version. +# +# jenkins-epo is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# jenkins-epo. If not, see . + +import ast +import collections +import logging + +import aiohttp +from yarl import URL + +from .utils import retry + + +logger = logging.getLogger(__name__) + + +class Payload(object): + @classmethod + def factory(cls, status, headers, payload): + if isinstance(payload, list): + return PayloadList(status, headers, payload) + elif isinstance(payload, dict): + return PayloadDict(status, headers, payload) + elif isinstance(payload, str): + return PayloadString(status, headers, payload) + else: + raise Exception("Unhandled payload type") + + def __init__(self, status, headers, payload): + super(Payload, self).__init__(payload) + self.status = status + self.headers = headers + + +class PayloadList(Payload, collections.UserList): + pass + + +class PayloadDict(Payload, collections.UserDict): + pass + + +class PayloadString(Payload, collections.UserString): + pass + + +class Client(object): + def __init__(self, url=''): + self.url = url + + def __call__(self, url): + if not url.startswith('http://'): + url = self.url.rstrip('/') + '/' + str(url) + return self.__class__(url) + + def __getattr__(self, name): + return self(name) + + @retry + def aget(self, **kw): + session = aiohttp.ClientSession() + url = URL(self.url) + if kw: + url = url.with_query(**kw) + logger.debug("GET %s", url) + try: + response = yield from session.get(url, timeout=10) + payload = yield from response.read() + finally: + yield from session.close() + response.raise_for_status() + payload = payload.decode('utf-8') + if response.content_type == 'text/x-python': + payload = ast.literal_eval(payload) + return Payload.factory(response.status, response.headers, payload) + + @retry + def apost(self, **kw): + session = aiohttp.ClientSession() + url = URL(self.url) + if kw: + url = url.with_query(**kw) + logger.debug("POST %s", url) + try: + response = yield from session.post(url, timeout=10) + payload = yield from response.read() + finally: + yield from session.close() + response.raise_for_status() + payload = payload.decode('utf-8') + return Payload.factory(response.status, response.headers, payload) diff --git a/tests/test_build.py b/tests/test_build.py index 4368a994..df22a907 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -9,14 +9,14 @@ @asyncio.coroutine def test_from_url(SETTINGS, mocker): SETTINGS.JENKINS_URL = 'jenkins://' - RESTClient = mocker.patch('jenkins_epo.jenkins.RESTClient') + Client = mocker.patch('jenkins_epo.jenkins.rest.Client') from jenkins_epo.jenkins import Build, NotOnJenkins with pytest.raises(NotOnJenkins): yield from Build.from_url('circleci:///') - RESTClient().aget = CoroutineMock(return_value=dict(number=1)) + Client().api.python.aget = CoroutineMock(return_value=dict(number=1)) build = yield from Build.from_url('jenkins://job/1') @@ -112,13 +112,13 @@ def test_commit_status(): @pytest.mark.asyncio @asyncio.coroutine def test_stop(SETTINGS, mocker): - RESTClient = mocker.patch('jenkins_epo.jenkins.RESTClient') + Client = mocker.patch('jenkins_epo.jenkins.rest.Client') from jenkins_epo.jenkins import Build build = Build(Mock(), payload=dict(url='jenkins://')) - RESTClient().stop.apost = CoroutineMock() + Client().stop.apost = CoroutineMock() yield from build.stop() - assert RESTClient().stop.apost.mock_calls + assert Client().stop.apost.mock_calls diff --git a/tests/test_jenkins.py b/tests/test_jenkins.py index 96ba9499..b8a93ce9 100644 --- a/tests/test_jenkins.py +++ b/tests/test_jenkins.py @@ -1,6 +1,6 @@ import asyncio -from asynctest import patch, CoroutineMock, Mock +from asynctest import patch, CoroutineMock, Mock import pytest @@ -16,16 +16,20 @@ def test_lazy_load(mocker): def test_requester(mocker): mocker.patch('jenkins_epo.jenkins.Requester.get_url') + mocker.patch('jenkins_epo.jenkins.Requester.post_url') from jenkins_epo.jenkins import VerboseRequester VerboseRequester().get_url('url://') + VerboseRequester().post_url('url://') @pytest.mark.asyncio @asyncio.coroutine def test_fetch_builds(mocker): - RESTClient = mocker.patch('jenkins_epo.jenkins.RESTClient') - RESTClient().aget = aget = CoroutineMock(return_value=dict(builds=[])) + Client = mocker.patch('jenkins_epo.jenkins.rest.Client') + Client().api.python.aget = aget = CoroutineMock( + return_value=dict(builds=[]) + ) from jenkins_epo.jenkins import Job api_instance = Mock(_data=dict()) @@ -353,11 +357,13 @@ def test_get_job(factory, load, SETTINGS): def test_aget_job(mocker, SETTINGS): mocker.patch('jenkins_epo.jenkins.LazyJenkins.load') mocker.patch('jenkins_epo.jenkins.Job.factory') - RESTClient = mocker.patch('jenkins_epo.jenkins.RESTClient') - client = RESTClient.return_value - client.return_value.aget = CoroutineMock() + Client = mocker.patch('jenkins_epo.jenkins.rest.Client') + client = Client() + client.api.python.aget = CoroutineMock() + client().aget = CoroutineMock() from jenkins_epo.jenkins import LazyJenkins + my = LazyJenkins() my._instance = Mock() job = yield from my.aget_job('name') @@ -395,11 +401,13 @@ def test_queue_empty(mocker, SETTINGS): JENKINS = LazyJenkins(Mock()) JENKINS.rest = Mock() - JENKINS.rest.queue.aget = CoroutineMock(return_value=dict(items=[])) + JENKINS.rest.queue.api.python.aget = CoroutineMock( + return_value=dict(items=[]), + ) yield from JENKINS.is_queue_empty() - assert JENKINS.rest.queue.aget.mock_calls + assert JENKINS.rest.queue.api.python.aget.mock_calls @patch('jenkins_epo.jenkins.JobSpec') diff --git a/tests/test_rest.py b/tests/test_rest.py index 8c57441e..8900d373 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -1,21 +1,22 @@ import asyncio -from asynctest import CoroutineMock, Mock +from asynctest import CoroutineMock, Mock import pytest @pytest.mark.asyncio @asyncio.coroutine def test_get(mocker): - ClientSession = mocker.patch('jenkins_epo.jenkins.aiohttp.ClientSession') - from jenkins_epo.jenkins import RESTClient + ClientSession = mocker.patch('jenkins_epo.rest.aiohttp.ClientSession') + from jenkins_epo.rest import Client - client = RESTClient() + client = Client() client = client('http://jenkins/path').subpath session = ClientSession.return_value response = Mock(name='response') + response.content_type = 'text/x-python' session.get = CoroutineMock(return_value=response) response.read = CoroutineMock( return_value=repr(dict(unittest=True)).encode('utf-8') @@ -29,10 +30,10 @@ def test_get(mocker): @pytest.mark.asyncio @asyncio.coroutine def test_post(mocker): - ClientSession = mocker.patch('jenkins_epo.jenkins.aiohttp.ClientSession') - from jenkins_epo.jenkins import RESTClient + ClientSession = mocker.patch('jenkins_epo.rest.aiohttp.ClientSession') + from jenkins_epo.rest import Client - client = RESTClient() + client = Client() client = client('http://jenkins/path').subpath session = ClientSession.return_value @@ -46,3 +47,14 @@ def test_post(mocker): payload = yield from client.apost(param=1) assert ': True' in payload + + +def test_payload(): + from jenkins_epo.rest import Payload + + Payload.factory(Mock(), Mock(), 'string') + Payload.factory(Mock(), Mock(), list()) + Payload.factory(Mock(), Mock(), dict()) + + with pytest.raises(Exception): + Payload.factory(Mock(), Mock(), object())