license
nexus
openstack
+ rtd
schema
sign
version
--- /dev/null
+***********
+ReadTheDocs
+***********
+
+.. program-output:: lftools rtd --help
+
+Commands
+========
+
+project-list
+------------
+
+.. program-output:: lftools rtd project-list --help
+
+project-details
+---------------
+
+.. program-output:: lftools rtd project-details --help
+
+
+project-version-list
+--------------------
+
+.. program-output:: lftools rtd project-version-list --help
+
+
+project-version-details
+-----------------------
+
+.. program-output:: lftools rtd project-version-details --help
+
+
+project-create
+--------------
+
+.. program-output:: lftools rtd project-create --help
+
+
+project-build-list
+------------------
+
+.. program-output:: lftools rtd project-build-list --help
+
+
+project-build-details
+---------------------
+
+.. program-output:: lftools rtd project-build-details --help
+
+
+project-build-trigger
+---------------------
+
+.. program-output:: lftools rtd project-build-trigger --help
+
+
+
+API requires a [rtd] section in ~/.config/lftools/lftools.ini:
+
+.. code-block:: bash
+
+ [rtd]
+ token = REDACTED
+ endpoint = https://readthedocs.org/api/v3/
+
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2019 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Initializes the REST API client."""
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2019 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""REST API interface using Requests."""
+
+import json
+
+import requests
+
+
+class RestApi(object):
+ """A generic REST API interface."""
+
+ def __init__(self, **params):
+ """Initialize the REST API class."""
+ self.params = params
+ self.endpoint = self.params['endpoint']
+ self.token = self.params['token']
+
+ if 'timeout' not in self.params:
+ self.timeout = None
+
+ self.r = requests.Session()
+ self.r.headers.update({'Authorization': 'Token {}'.format(self.token)})
+ self.r.headers.update({'Content-Type': 'application/json'})
+
+ def _request(self, url, method, data=None, timeout=10):
+ """Execute the requested request."""
+ resp = self.r.request(method, self.endpoint + url, data=data,
+ timeout=timeout)
+
+ if resp.text:
+ try:
+ if resp.headers['Content-Type'] == 'application/json':
+ body = json.loads(resp.text)
+ else:
+ body = resp.text
+ except ValueError:
+ body = None
+
+ else:
+ body = None
+ return resp
+
+ return resp, body
+
+ def get(self, url, **kwargs):
+ """HTTP GET request."""
+ return self._request(url, 'GET', **kwargs)
+
+ def patch(self, url, **kwargs):
+ """HTTP PATCH request."""
+ return self._request(url, 'PATCH', **kwargs)
+
+ def post(self, url, **kwargs):
+ """HTTP POST request."""
+ return self._request(url, 'POST', **kwargs)
+
+ def put(self, url, **kwargs):
+ """HTTP PUT request."""
+ return self._request(url, 'PUT', **kwargs)
+
+ def delete(self, url, **kwargs):
+ """HTTP DELETE request."""
+ return self._request(url, 'DELETE', **kwargs)
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2019 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Init for endpoints module."""
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2019 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+
+"""Read the Docs REST API interface."""
+
+import json
+
+from lftools import config
+import lftools.api.client as client
+
+
+class ReadTheDocs(client.RestApi):
+ """API endpoint wrapper for readthedocs.org.
+
+ Be sure to always include the trailing "/" when adding
+ new methods.
+ """
+
+ def __init__(self, **params):
+ """Initialize the class."""
+ if 'token' not in params:
+ params['token'] = config.get_setting('rtd', 'token')
+ if 'endpoint' not in params:
+ params['endpoint'] = config.get_setting('rtd', 'endpoint')
+ super(ReadTheDocs, self).__init__(**params)
+
+ def project_list(self):
+ """Return a list of projects.
+
+ This returns the list of projects by their slug name ['slug'],
+ not their pretty name ['name']. Since we use these for getting
+ details, triggering builds, etc., the pretty name is useless.
+
+ :param kwargs:
+ :return: [projects]
+ """
+ result = self.get('projects/')[1] # NOQA
+ more_results = None
+ data = result['results']
+ project_list = []
+
+ if result['next']:
+ more_results = result['next'].rsplit('/', 1)[-1]
+
+ if more_results:
+ while more_results is not None:
+ get_more_results = self.get('projects/' + more_results)[1]
+ data.append(get_more_results['results'])
+ more_results = get_more_results['next']
+
+ if more_results is not None:
+ more_results = more_results.rsplit('/', 1)[-1]
+
+ for project in data:
+ project_list.append(project['slug'])
+
+ return project_list
+
+ def project_details(self, project):
+ """Retrieve the details of a specific project.
+
+ :param project: The project's slug
+ :param kwargs:
+ :return: {result}
+ """
+ result = self.get('projects/{}/'.format(project))[1]
+ return result
+
+ def project_version_list(self, project):
+ """Retrieve a list of all ACTIVE versions of a project.
+
+ :param project: The project's slug
+ :return: {result}
+ """
+ result = self.get('projects/{}/versions/?active=True'
+ .format(project))[1]
+ more_results = None
+ versions = []
+
+ # I feel like there must be a better way...but, this works. -DWTalton
+ initial_versions = result['results']
+ for version in initial_versions:
+ versions.append(version['slug'])
+
+ if result['next']:
+ more_results = result['next'].rsplit('/', 1)[-1]
+
+ if more_results:
+ while more_results is not None:
+ get_more_results = self.get('projects/{}/versions/'
+ .format(project) + more_results)[1]
+ more_results = get_more_results['next']
+
+ for version in get_more_results['results']:
+ versions.append(version['slug'])
+
+ if more_results is not None:
+ more_results = more_results.rsplit('/', 1)[-1]
+
+ return versions
+
+ def project_version_details(self, project, version):
+ """Retrieve details of a single version.
+
+ :param project: The project's slug
+ :param version: The version's slug
+ :return: {result}
+ """
+ result = self.get('projects/{}/versions/{}/'
+ .format(project, version))[1]
+ return result
+
+ # This is implemented per their docs, however they do not appear to have
+ # it working yet as this always returns a 404
+ def project_version_update(self, project, version, active,
+ privacy_level):
+ """Edit a version.
+
+ :param project: The project slug
+ :param version: The version slug
+ :param active: 'true' or 'false'
+ :param privacy_level: 'public' or 'private'
+ :return: {result}
+ """
+ data = {
+ 'active': active,
+ 'privacy_level': privacy_level
+ }
+
+ json_data = json.dumps(data)
+ result = self.patch('projects/{}/version/{}/'.format(project, version),
+ data=json_data)
+ return result
+
+ def project_create(self, name, repository_url, repository_type, homepage,
+ programming_language, language, **kwargs):
+ """Create a new Read the Docs project.
+
+ :param name: Project name. Any spaces will convert to dashes for the
+ project slug
+ :param repository_url:
+ :param repository_type: Valid types are git, hg, bzr, and svn
+ :param homepage:
+ :param programming_language: valid programming language abbreviations
+ are py, java, js, cpp, ruby, php, perl, go, c, csharp,
+ swift, vb, r, objc, css, ts, scala, groovy, coffee,
+ lua, haskell, other, words
+ :param language: Most two letter language abbreviations: en, es, etc.
+ :param kwargs:
+ :return: {results}
+ """
+ data = {
+ 'name': name,
+ 'repository': {
+ 'url': repository_url,
+ 'type': repository_type
+ },
+ 'homepage': homepage,
+ 'programming_language': programming_language,
+ 'language': language
+ }
+
+ json_data = json.dumps(data)
+ result = self.post('projects/', data=json_data, **kwargs)
+ return result
+
+ def project_build_list(self, project, **kwargs):
+ """Retrieve the project's running build list.
+
+ For future expansion, the statuses are cloning,
+ installing, building.
+
+ :param project: The project's slug
+ :param kwargs:
+ :return: {result}
+ """
+ result = self.get('projects/{}/builds/?running=True'
+ .format(project), **kwargs)[1]
+
+ if result['count'] > 0:
+ return result
+ else:
+ return "There are no active builds."
+
+ def project_build_details(self, project, build_id, **kwargs):
+ """Retrieve the details of a specific build.
+
+ :param project: The project's slug
+ :param build_id: The build id
+ :param kwargs:
+ :return: {result}
+ """
+ result = self.get('projects/{}/builds/{}/'
+ .format(project, build_id))[1]
+ return result
+
+ def project_build_trigger(self, project, version):
+ """Trigger a project build.
+
+ :param project: The project's slug
+ :param version: The version of the project to build
+ (must be an active version)
+ :return: {result}
+ """
+ result = self.post('projects/{}/versions/{}/builds/'
+ .format(project, version))[1]
+ return result
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2019 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Exceptions for the API client."""
+
+
+class UnsupportedRequestType(Exception):
+ """Except on an unknown request."""
+
+ def __str__(self):
+ """Except unknown return type."""
+ return "Unknown request type"
from lftools.cli.lfidapi import lfidapi
from lftools.cli.license import license
from lftools.cli.nexus import nexus
+from lftools.cli.rtd import rtd
from lftools.cli.schema import schema
from lftools.cli.sign import sign
from lftools.cli.version import version
cli.add_command(jenkins_cli, name='jenkins')
cli.add_command(license)
cli.add_command(nexus)
+cli.add_command(rtd)
cli.add_command(schema)
cli.add_command(lfidapi)
cli.add_command(sign)
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2019 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+
+"""Read the Docs interface."""
+
+
+import logging
+from pprint import pformat
+
+import click
+
+from lftools.api.endpoints import readthedocs
+
+log = logging.getLogger(__name__)
+
+
+@click.group()
+@click.pass_context
+def rtd(ctx):
+ """Read the Docs interface."""
+ pass
+
+
+@click.command(name='project-list')
+@click.pass_context
+def project_list(ctx):
+ """Get a list of Read the Docs projects.
+
+ Returns a list of RTD projects for the account whose
+ token is configured in lftools.ini. This returns the
+ slug name, not the pretty name.
+ """
+ r = readthedocs.ReadTheDocs()
+ for project in r.project_list():
+ log.info(project)
+
+
+@click.command(name='project-details')
+@click.argument('project-slug')
+@click.pass_context
+def project_details(ctx, project_slug):
+ """Retrieve project details."""
+ r = readthedocs.ReadTheDocs()
+ data = r.project_details(project_slug)
+ log.info(pformat(data))
+
+
+@click.command(name='project-version-list')
+@click.argument('project-slug')
+@click.pass_context
+def project_version_list(ctx, project_slug):
+ """Retrieve project version list."""
+ r = readthedocs.ReadTheDocs()
+ data = r.project_version_list(project_slug)
+
+ for version in data:
+ log.info(version)
+
+
+@click.command(name='project-version-details')
+@click.argument('project-slug')
+@click.argument('version-slug')
+@click.pass_context
+def project_version_details(ctx, project_slug, version_slug):
+ """Retrieve project version details."""
+ r = readthedocs.ReadTheDocs()
+ data = r.project_version_details(project_slug, version_slug)
+ log.info(pformat(data))
+
+
+@click.command(name='project-create')
+@click.argument('project-name')
+@click.argument('repository-url')
+@click.argument('repository-type')
+@click.argument('homepage')
+@click.argument('programming-language')
+@click.argument('language')
+@click.pass_context
+def project_create(ctx, project_name, repository_url, repository_type,
+ homepage, programming_language, language):
+ """Create a new project."""
+ r = readthedocs.ReadTheDocs()
+ data = r.project_create(project_name, repository_url, repository_type,
+ homepage, programming_language, language)
+ log.info(pformat(data))
+
+
+@click.command(name='project-build-list')
+@click.argument('project-slug')
+@click.pass_context
+def project_build_list(ctx, project_slug):
+ """Retrieve a list of a project's builds."""
+ r = readthedocs.ReadTheDocs()
+ data = r.project_build_list(project_slug)
+ log.info(pformat(data))
+
+
+@click.command(name='project-build-details')
+@click.argument('project-slug')
+@click.argument('build-id')
+@click.pass_context
+def project_build_details(ctx, project_slug, build_id):
+ """Retrieve specific project build details."""
+ r = readthedocs.ReadTheDocs()
+ data = r.project_build_details(project_slug, build_id)
+ log.info(pformat(data))
+
+
+@click.command(name='project-build-trigger')
+@click.argument('project-slug')
+@click.argument('version-slug')
+@click.pass_context
+def project_build_trigger(ctx, project_slug, version_slug):
+ """Trigger a new build."""
+ r = readthedocs.ReadTheDocs()
+ data = r.project_build_trigger(project_slug, version_slug)
+ log.info(pformat(data))
+
+
+rtd.add_command(project_list)
+rtd.add_command(project_details)
+rtd.add_command(project_version_list)
+rtd.add_command(project_version_details)
+rtd.add_command(project_create)
+rtd.add_command(project_build_list)
+rtd.add_command(project_build_details)
+rtd.add_command(project_build_trigger)
--- /dev/null
+---
+features:
+ - |
+ Read the Docs CRUD operations.
+
+ Usage: Usage: lftools rtd [OPTIONS] COMMAND [ARGS]
+
+
+ .. code-block:: none
+
+ Commands:
+ project-list Get a list of Read the Docs projects.
+ project-details Retrieve project details.
+ project-version-list Retrieve project version list.
+ project-version-details Retrieve project version details.
+ project-create Create a new project.
+ project-build-list Retrieve a list of a project's builds.
+ project-build-details Retrieve specific project build details.
+ project-build-trigger Trigger a new build.
+
+ .. code-block:: none
+
+ Options:
+ --help Show this message and exit.
--- /dev/null
+{
+ "id": 9584913
+}
--- /dev/null
+{
+ "count": 1,
+ "results": [
+ {
+ "success": true
+ }
+ ]
+}
--- /dev/null
+{
+ "slug": "testproject1"
+}
--- /dev/null
+{
+ "next":null,
+ "results":[
+ {
+ "slug":"TestProject1"
+ }
+ ]
+}
--- /dev/null
+{
+ "slug": "latest"
+}
--- /dev/null
+{
+ "next": null,
+ "results": [
+ {
+ "slug": "test-trigger6"
+ }
+ ]
+}
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2018 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Test generic REST client."""
+
+import responses
+
+import lftools.api.client as client
+
+c = client.RestApi(endpoint='', token='xyz')
+
+
+@responses.activate
+def test_get():
+ responses.add(responses.GET, 'https://fakeurl/', json={'success': 'get'},
+ status=200, match_querystring=True)
+ resp = c.get('https://fakeurl/')
+ assert resp[1] == {'success': 'get'}
+
+
+@responses.activate
+def test_patch():
+ responses.add(responses.PATCH, url='https://fakeurl/',
+ json={'success': 'patch'}, status=204,
+ match_querystring=True)
+ resp = c.patch('https://fakeurl/')
+ assert resp[1] == {'success': 'patch'}
+
+
+@responses.activate
+def test_post():
+ responses.add(responses.POST, 'https://fakeurl/', json={'success': 'post'},
+ status=201, match_querystring=True)
+ resp = c.post('https://fakeurl/')
+ assert resp[1] == {'success': 'post'}
+
+
+@responses.activate
+def test_put():
+ responses.add(responses.PUT, 'https://fakeurl/', json={'success': 'put'},
+ status=200, match_querystring=True)
+ resp = c.put('https://fakeurl/')
+ assert resp[1] == {'success': 'put'}
+
+
+@responses.activate
+def test_delete():
+ responses.add(responses.DELETE, 'https://fakeurl/',
+ json={'success': 'delete'}, status=200,
+ match_querystring=True)
+ resp = c.delete('https://fakeurl/')
+ assert resp[1] == {'success': 'delete'}
--- /dev/null
+# SPDX-License-Identifier: EPL-1.0
+##############################################################################
+# Copyright (c) 2018 The Linux Foundation and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Eclipse Public License v1.0
+# which accompanies this distribution, and is available at
+# http://www.eclipse.org/legal/epl-v10.html
+##############################################################################
+"""Test rtd command."""
+
+import json
+import os
+import pytest
+import responses
+
+import lftools.api.endpoints.readthedocs as client
+
+rtd = client.ReadTheDocs(endpoint='https://readthedocs.org/api/v3/',
+ token='xyz')
+
+FIXTURE_DIR = os.path.join(os.path.dirname(
+ os.path.realpath(__file__)), 'fixtures',)
+
+
+@pytest.mark.datafiles(os.path.join(FIXTURE_DIR, 'rtd'),)
+@responses.activate
+def test_project_list(datafiles):
+ os.chdir(str(datafiles))
+ json_file = open('project_list.json', 'r')
+ json_data = json.loads(json_file.read())
+ responses.add(responses.GET,
+ url='https://readthedocs.org/api/v3/projects/',
+ json=json_data, status=200)
+ assert 'TestProject1' in rtd.project_list()
+
+
+@pytest.mark.datafiles(os.path.join(FIXTURE_DIR, 'rtd'),)
+@responses.activate
+def test_project_details(datafiles):
+ os.chdir(str(datafiles))
+ json_file = open('project_details.json', 'r')
+ json_data = json.loads(json_file.read())
+ responses.add(responses.GET,
+ url='https://readthedocs.org/api/v3/projects/TestProject1/',
+ json=json_data, status=200)
+ assert 'slug' in rtd.project_details('TestProject1')
+
+
+@pytest.mark.datafiles(os.path.join(FIXTURE_DIR, 'rtd'),)
+@responses.activate
+def test_project_version_list(datafiles):
+ os.chdir(str(datafiles))
+ json_file = open('project_version_list.json', 'r')
+ json_data = json.loads(json_file.read())
+ responses.add(responses.GET,
+ url='https://readthedocs.org/api/v3/projects/TestProject1/versions/?active=True', # noqa
+ json=json_data, status=200, match_querystring=True)
+ assert 'test-trigger6' in rtd.project_version_list('TestProject1')
+
+
+@pytest.mark.datafiles(os.path.join(FIXTURE_DIR, 'rtd'),)
+@responses.activate
+def test_project_version_details(datafiles):
+ os.chdir(str(datafiles))
+ json_file = open('project_version_details.json', 'r')
+ json_data = json.loads(json_file.read())
+ responses.add(responses.GET,
+ url='https://readthedocs.org/api/v3/projects/TestProject1/versions/latest/', # noqa
+ json=json_data, status=200)
+ assert 'slug' in rtd.project_version_details('TestProject1', 'latest')
+
+
+@responses.activate
+def test_project_version_update():
+ data = {
+ 'active': 'true',
+ 'privacy_level': 'public'
+ }
+ responses.add(responses.PATCH,
+ url='https://readthedocs.org/api/v3/projects/TestProject1/version/latest/', # noqa
+ json=data, status=204)
+ assert rtd.project_version_update('TestProject1', 'latest',
+ 'true', 'public')
+
+
+@responses.activate
+def test_project_create():
+ data = {
+ 'name': 'TestProject1',
+ 'repository': {
+ 'url': 'https://repository_url',
+ 'type': 'my_repo_type'
+ },
+ 'homepage': 'https://homepageurl',
+ 'programming_language': 'py',
+ 'language': 'en'
+ }
+ responses.add(responses.POST,
+ url='https://readthedocs.org/api/v3/projects/',
+ json=data, status=201)
+ assert rtd.project_create('TestProject1', 'https://repository_url',
+ 'my_repo_type', 'https://homepageurl',
+ 'py', 'en')
+
+
+@pytest.mark.datafiles(os.path.join(FIXTURE_DIR, 'rtd'),)
+@responses.activate
+def test_project_build_list(datafiles):
+ os.chdir(str(datafiles))
+ json_file = open('project_build_list.json', 'r')
+ json_data = json.loads(json_file.read())
+ responses.add(responses.GET,
+ url='https://readthedocs.org/api/v3/projects/testproject1/builds/?running=True', # noqa
+ json=json_data, status=200, match_querystring=True)
+ assert 'success' in rtd.project_build_list('testproject1')['results'][0]
+
+
+@pytest.mark.datafiles(os.path.join(FIXTURE_DIR, 'rtd'),)
+@responses.activate
+def test_project_build_details(datafiles):
+ os.chdir(str(datafiles))
+ json_file = open('project_build_details.json', 'r')
+ json_data = json.loads(json_file.read())
+ responses.add(responses.GET,
+ url='https://readthedocs.org/api/v3/projects/testproject1/builds/9584913/', # noqa
+ json=json_data, status=200)
+ assert 'id' in rtd.project_build_details('testproject1', 9584913)
+
+
+@responses.activate
+def test_project_build_trigger():
+ data = {
+ 'project': 'testproject1',
+ 'version': 'latest'
+ }
+ responses.add(responses.POST,
+ url='https://readthedocs.org/api/v3/projects/testproject1/versions/latest/builds/', # noqa
+ json=data, status=201)
+ assert rtd.project_build_trigger('testproject1', 'latest')