Add support for Read the Docs API v3 23/61523/14
authorDW Talton <dtalton@contractor.linuxfoundation.org>
Thu, 29 Aug 2019 01:29:37 +0000 (18:29 -0700)
committerDW Talton <dtalton@contractor.linuxfoundation.org>
Tue, 24 Sep 2019 20:34:58 +0000 (13:34 -0700)
Add support for RTD APIv3, which allows for PUT and POST
operations (v2 was read-only). Support added via modular
API client with RTD module

Issue: RELENG-2239
Signed-off-by: DW Talton <dtalton@contractor.linuxfoundation.org>
Change-Id: Iaad90ed360eaac0de0e7a6c5a4c6fa77ab349f70

18 files changed:
docs/commands/index.rst
docs/commands/rtd.rst [new file with mode: 0644]
lftools/api/__init__.py [new file with mode: 0644]
lftools/api/client.py [new file with mode: 0644]
lftools/api/endpoints/__init__.py [new file with mode: 0644]
lftools/api/endpoints/readthedocs.py [new file with mode: 0644]
lftools/api/exceptions.py [new file with mode: 0644]
lftools/cli/__init__.py
lftools/cli/rtd.py [new file with mode: 0644]
releasenotes/notes/readthedocs-1c75ba657986dc40.yaml [new file with mode: 0644]
tests/fixtures/rtd/project_build_details.json [new file with mode: 0644]
tests/fixtures/rtd/project_build_list.json [new file with mode: 0644]
tests/fixtures/rtd/project_details.json [new file with mode: 0644]
tests/fixtures/rtd/project_list.json [new file with mode: 0644]
tests/fixtures/rtd/project_version_details.json [new file with mode: 0644]
tests/fixtures/rtd/project_version_list.json [new file with mode: 0644]
tests/test_api_client.py [new file with mode: 0644]
tests/test_rtd.py [new file with mode: 0644]

index a0032c7..3aea343 100644 (file)
@@ -20,6 +20,7 @@ It supports the following commands:
     license
     nexus
     openstack
+    rtd
     schema
     sign
     version
diff --git a/docs/commands/rtd.rst b/docs/commands/rtd.rst
new file mode 100644 (file)
index 0000000..4055e76
--- /dev/null
@@ -0,0 +1,65 @@
+***********
+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/
+
diff --git a/lftools/api/__init__.py b/lftools/api/__init__.py
new file mode 100644 (file)
index 0000000..3ce3ee7
--- /dev/null
@@ -0,0 +1,10 @@
+# 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."""
diff --git a/lftools/api/client.py b/lftools/api/client.py
new file mode 100644 (file)
index 0000000..9677888
--- /dev/null
@@ -0,0 +1,71 @@
+# 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)
diff --git a/lftools/api/endpoints/__init__.py b/lftools/api/endpoints/__init__.py
new file mode 100644 (file)
index 0000000..51b4d1f
--- /dev/null
@@ -0,0 +1,10 @@
+# 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."""
diff --git a/lftools/api/endpoints/readthedocs.py b/lftools/api/endpoints/readthedocs.py
new file mode 100644 (file)
index 0000000..da66325
--- /dev/null
@@ -0,0 +1,214 @@
+# 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
diff --git a/lftools/api/exceptions.py b/lftools/api/exceptions.py
new file mode 100644 (file)
index 0000000..cbef4e9
--- /dev/null
@@ -0,0 +1,18 @@
+# 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"
index b1fd934..2b732ef 100644 (file)
@@ -29,6 +29,7 @@ from lftools.cli.jenkins import jenkins_cli
 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
@@ -86,6 +87,7 @@ cli.add_command(infofile)
 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)
diff --git a/lftools/cli/rtd.py b/lftools/cli/rtd.py
new file mode 100644 (file)
index 0000000..e50a9c6
--- /dev/null
@@ -0,0 +1,134 @@
+# 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)
diff --git a/releasenotes/notes/readthedocs-1c75ba657986dc40.yaml b/releasenotes/notes/readthedocs-1c75ba657986dc40.yaml
new file mode 100644 (file)
index 0000000..7216fe0
--- /dev/null
@@ -0,0 +1,24 @@
+---
+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.
diff --git a/tests/fixtures/rtd/project_build_details.json b/tests/fixtures/rtd/project_build_details.json
new file mode 100644 (file)
index 0000000..5acf62d
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "id": 9584913
+}
diff --git a/tests/fixtures/rtd/project_build_list.json b/tests/fixtures/rtd/project_build_list.json
new file mode 100644 (file)
index 0000000..91c1f6f
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "count": 1,
+  "results": [
+    {
+      "success": true
+    }
+  ]
+}
diff --git a/tests/fixtures/rtd/project_details.json b/tests/fixtures/rtd/project_details.json
new file mode 100644 (file)
index 0000000..2afccbf
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "slug": "testproject1"
+}
diff --git a/tests/fixtures/rtd/project_list.json b/tests/fixtures/rtd/project_list.json
new file mode 100644 (file)
index 0000000..a8d7937
--- /dev/null
@@ -0,0 +1,8 @@
+{
+   "next":null,
+   "results":[
+      {
+         "slug":"TestProject1"
+      }
+   ]
+}
diff --git a/tests/fixtures/rtd/project_version_details.json b/tests/fixtures/rtd/project_version_details.json
new file mode 100644 (file)
index 0000000..c539c3d
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "slug": "latest"
+}
diff --git a/tests/fixtures/rtd/project_version_list.json b/tests/fixtures/rtd/project_version_list.json
new file mode 100644 (file)
index 0000000..7d08092
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "next": null,
+  "results": [
+    {
+      "slug": "test-trigger6"
+    }
+  ]
+}
diff --git a/tests/test_api_client.py b/tests/test_api_client.py
new file mode 100644 (file)
index 0000000..55d7e2b
--- /dev/null
@@ -0,0 +1,58 @@
+# 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'}
diff --git a/tests/test_rtd.py b/tests/test_rtd.py
new file mode 100644 (file)
index 0000000..df04c38
--- /dev/null
@@ -0,0 +1,140 @@
+# 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')