diff --git a/docs/index.rst b/docs/index.rst
index 01b5904..7a50843 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -21,6 +21,12 @@ Builds
.. autoclass:: quickbuild.endpoints.builds.Builds
:members:
+Tokens
+~~~~~~
+
+.. autoclass:: quickbuild.endpoints.tokens.Tokens
+ :members:
+
Users
~~~~~
diff --git a/quickbuild/core.py b/quickbuild/core.py
index ac4078a..35ae6d8 100644
--- a/quickbuild/core.py
+++ b/quickbuild/core.py
@@ -4,6 +4,7 @@
from typing import Any, Callable, NamedTuple, Optional
from quickbuild.endpoints.builds import Builds
+from quickbuild.endpoints.tokens import Tokens
from quickbuild.endpoints.users import Users
from quickbuild.exceptions import (
QBError,
@@ -23,6 +24,7 @@ class QuickBuild(ABC):
def __init__(self):
self.builds = Builds(self)
+ self.tokens = Tokens(self)
self.users = Users(self)
@staticmethod
diff --git a/quickbuild/endpoints/tokens.py b/quickbuild/endpoints/tokens.py
new file mode 100644
index 0000000..3e94b48
--- /dev/null
+++ b/quickbuild/endpoints/tokens.py
@@ -0,0 +1,81 @@
+from typing import List, Optional
+
+import xmltodict
+
+
+class Tokens:
+ """With tokens, one can authorize/unauthorize agents, or access agent
+ details including token value and latest usage information.
+ """
+
+ def __init__(self, quickbuild):
+ self.quickbuild = quickbuild
+
+ def get(self, agent_address: Optional[str] = None) -> List[dict]:
+ """
+ Get token value and latest used information of agents.
+
+ Args:
+ agent_address (Optional[str]):
+ Build agent address, eg. my-agent:8811.
+ If param address is set to None, details of all agents will be returned.
+
+ Returns:
+ List[dict]: List of token and agent details
+ """
+ def callback(response: str) -> List[dict]:
+ root = xmltodict.parse(response)
+
+ tokens = []
+ if root['list'] is not None:
+ tokens = root['list']['com.pmease.quickbuild.model.Token']
+ if isinstance(tokens, list) is False:
+ tokens = [tokens]
+ return tokens
+
+ params_agent_address = dict(address=agent_address) if agent_address else {}
+
+ return self.quickbuild._request(
+ 'GET',
+ 'tokens',
+ callback,
+ params=params_agent_address,
+ )
+
+ def authorize(self, agent_ip: str, agent_port: int = 8811) -> str:
+ """
+ Authorize a build agent to join the build grid.
+
+ Args:
+ agent_ip (str): The build agent IP address.
+ agent_port (int): The build agent port (default: 8811).
+
+ Returns:
+ str: identifier of the newly created token for the build agent
+ """
+ response = self.quickbuild._request(
+ 'GET',
+ 'tokens/authorize',
+ params=dict(ip=agent_ip, port=agent_port),
+ )
+
+ return response
+
+ def unauthorize(self, agent_ip: str, agent_port: int = 8811) -> str:
+ """
+ Unauthorize an already authorized build agent.
+
+ Args:
+ agent_ip (str): The build agent IP address.
+ agent_port (int): The build agent port (default: 8811).
+
+ Returns:
+ str: identifier of the removed token representing the build agent.
+ """
+ response = self.quickbuild._request(
+ 'GET',
+ 'tokens/unauthorize',
+ params=dict(ip=agent_ip, port=agent_port),
+ )
+
+ return response
diff --git a/tests/test_tokens.py b/tests/test_tokens.py
new file mode 100644
index 0000000..f653613
--- /dev/null
+++ b/tests/test_tokens.py
@@ -0,0 +1,189 @@
+
+import re
+
+import pytest
+import responses
+
+from quickbuild import AsyncQBClient, QBClient
+
+
+REGEX_IP = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
+REGEX_PORT = '\d+'
+
+TOKEN_XML = r"""
+
+
+
+ 120204
+ 84858611-a1fe-4f88-a49c-f600cf0ecf11
+ 192.168.1.100
+ 8811
+ false
+ 2021-02-08T20:01:09.426Z
+ Run step (configuration: root/pipelineC, build: B.50906, step: master>build)
+ quickbuild-agent-192-168-1-100
+ true
+
+
+"""
+
+TOKENS_XML = r"""
+
+
+
+ 117554
+ bee7e8ff-fd9a-475a-8cf5-42a5353b8875
+ 192.168.1.100
+ 8811
+ false
+ 2021-02-08T20:08:29.360Z
+ Check build condition (configuration: root/pipelineA)
+ quickbuild-agent-192-168-1-100
+ true
+
+
+ 115672
+ 27350640-d9f9-4a10-96ae-b6ec8fee998b
+ 192.168.1.101
+ 8811
+ false
+ 2021-02-08T20:01:10.175Z
+ Run step (configuration: root/pipelineA, build: B.1234, step: master>finalize)
+ quickbuild-agent-192-168-1-101
+ true
+
+
+ 116545
+ 8f604c48-b9f4-4bbe-847c-c073b2aebc81
+ 192.168.1.102
+ 8811
+ false
+ 2021-02-08T20:01:10.013Z
+ Run step (configuration: root/pipelineB, build: B.123, step: master>publish)
+ quickbuild-agent-192-168-1-102
+ true
+
+
+"""
+
+EMPTY_TOKEN_XML = r"""
+
+
+"""
+
+
+@responses.activate
+def test_authorize():
+ RESPONSE_DATA = '120123'
+
+ responses.add(
+ responses.GET,
+ re.compile(r'.*/rest/tokens/authorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
+ content_type='text/plain',
+ body=RESPONSE_DATA,
+ match_querystring=True,
+ )
+
+ response = QBClient('http://server').tokens.authorize('192.168.1.100', 8811)
+ assert response == '120123'
+
+ response = QBClient('http://server').tokens.authorize('192.168.1.100')
+ assert response == '120123'
+
+
+@responses.activate
+def test_unauthorize():
+ RESPONSE_DATA = '120123'
+
+ responses.add(
+ responses.GET,
+ re.compile(r'.*/rest/tokens/unauthorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
+ content_type='text/plain',
+ body=RESPONSE_DATA,
+ match_querystring=True,
+ )
+
+ response = QBClient('http://server').tokens.unauthorize('192.168.1.100', 8811)
+ assert response == '120123'
+
+ response = QBClient('http://server').tokens.unauthorize('192.168.1.100')
+ assert response == '120123'
+
+
+@responses.activate
+def test_token_and_agent_details():
+ responses.add(
+ responses.GET,
+ re.compile(r'.*/rest/tokens\?address=quickbuild-agent-192-168-1-100%3A8811'),
+ content_type='application/xml',
+ body=TOKEN_XML
+ )
+
+ response = QBClient('http://server').tokens.get('quickbuild-agent-192-168-1-100:8811')
+ assert len(response) == 1
+ assert response[0]['id'] == '120204'
+
+
+@responses.activate
+def test_tokens_and_agent_details():
+ responses.add(
+ responses.GET,
+ re.compile(r'.*/rest/tokens'),
+ content_type='application/xml',
+ body=TOKENS_XML,
+ )
+
+ response = QBClient('http://server').tokens.get()
+ assert len(response) == 3
+ assert response[0]['id'] == '117554'
+ assert response[1]['id'] == '115672'
+ assert response[2]['id'] == '116545'
+
+
+@responses.activate
+def test_tokens_and_agent_details_with_unknown_address():
+ responses.add(
+ responses.GET,
+ re.compile(r'.*/rest/tokens\?address=unknown'),
+ content_type='application/xml',
+ body=EMPTY_TOKEN_XML,
+ match_querystring=True,
+ )
+
+ response = QBClient('http://server').tokens.get('unknown')
+ assert len(response) == 0
+ assert response == []
+
+
+@pytest.mark.asyncio
+async def test_authorize_async(aiohttp_mock):
+ RESPONSE_DATA = '120123'
+
+ client = AsyncQBClient('http://server')
+ try:
+ aiohttp_mock.get(
+ re.compile(r'.*/rest/tokens/authorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
+ body=RESPONSE_DATA,
+ )
+
+ response = await client.tokens.authorize('192.168.1.100')
+ assert response == '120123'
+ finally:
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_unauthorize_async(aiohttp_mock):
+ RESPONSE_DATA = '120123'
+
+ client = AsyncQBClient('http://server')
+ try:
+ aiohttp_mock.get(
+ re.compile(r'.*/rest/tokens/unauthorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
+ body=RESPONSE_DATA,
+ )
+
+ response = await client.tokens.unauthorize('192.168.1.100')
+ assert response == '120123'
+ finally:
+ await client.close()