Skip to content
This repository has been archived by the owner on Oct 3, 2018. It is now read-only.

Commit

Permalink
Merge pull request #279 from novafloss/rest
Browse files Browse the repository at this point in the history
Extract rest client
  • Loading branch information
bersace committed Feb 22, 2017
2 parents f77e98a + 09e335c commit 132d802
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 77 deletions.
77 changes: 20 additions & 57 deletions jenkins_epo/jenkins.py
Expand Up @@ -13,24 +13,21 @@
# jenkins-epo. If not, see <http://www.gnu.org/licenses/>.


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__)

Expand All @@ -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,)
Expand All @@ -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),
Expand All @@ -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)
Expand All @@ -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 = """\
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down
102 changes: 102 additions & 0 deletions 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 <http://www.gnu.org/licenses/>.

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)
10 changes: 5 additions & 5 deletions tests/test_build.py
Expand Up @@ -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')

Expand Down Expand Up @@ -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
24 changes: 16 additions & 8 deletions tests/test_jenkins.py
@@ -1,6 +1,6 @@
import asyncio
from asynctest import patch, CoroutineMock, Mock

from asynctest import patch, CoroutineMock, Mock
import pytest


Expand All @@ -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())
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down

0 comments on commit 132d802

Please sign in to comment.