Skip to content

Client User Guide

Sumio Kiyooka edited this page May 13, 2021 · 26 revisions

Introduction

The Client class is a binding to Blackduck's REST API that provides a robust connection backed by a Requests session object. The user specifies a base URL, timeout, retries, proxies, and TLS verification upon initialization and these attributes are persisted across all requests.

At the REST API level, the Client class provides a consistent way to discover and traverse public resources, uses a generator to fetch all items using pagination, and automatically renews the bearer token.

Note that unlike HubInstance (from HubRestApi.py), Client does not provide a method for each and every endpoint. Instead it only provides a handful of core methods (Client API Reference) and then encourages the user to leverage the functionality provided by the underlying Requests session and to be comfortable working at that level. See Client versus HubInstance Comparison.

It is designed to provide a solid foundation especially suited for long-running scripts.

Table of Contents

Python 3 is required. Use pip to install the library from PyPI.

pip3 install blackduck

First generate an access token from Blackduck's UI: My Access Tokens

Save the token and provide it to Client. In the following example, the token is saved to an environment variable: blackduck_token

from blackduck import Client
import logging
import os
from pprint import pprint

logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
)

bd = Client(
    token=os.environ.get('blackduck_token'),
    base_url="https://your.blackduck.url",
    # verify=False  # TLS certificate verification
)

Blackduck's REST API provides a way to discover and traverse available resources identified by name. To take advantage of this mechanism, Client provides two key methods: list_resources() and get_resource()

To fetch what public resources are available, use the method list_resources() which returns a Python dict.

pprint(bd.list_resources())  # resources available at the root level

For an object returned from get_resource(), one can invoke list_resources() on it to see what sub-resources are available.

for project in bd.get_resource('projects'):
    pprint(bd.list_resources(project))
    quit(0)

To see the URL for the resource itself use the 'href' key:

resources_dict = bd.list_resources(project)
print(resources_dict['href'])

To fetch a resource pass in the name (str) provided by an earlier list_resources() call.

If the resource is paginated it will automatically return a generator that will fetch items as required and eventually will return all items. This generator can naturally be used in a for loop:

for project in bd.get_resource('projects'):
    print(project['name'])

To fetch a sub-resource, pass in the parent. To get the versions for each project:

for project in bd.get_resource('projects'):
    print(project['name'])
    for version in bd.get_resource('versions', project):
        print(version['versionName'])

If the resource is not paginated pass items=False and a dict will be returned:

bd.get_resource('maxSnippetFileSize', items=False)

This covers advanced topics that are relevant to a user more familiar with the REST API.

Parameters are set using 'params' of session.request. To fetch all users sorted by firstname:

params = {
    'sort': 'firstname'
}
bd.get_resource('users', params=params)  # constructs url /api/users?sort=firstname

It is possible to provide multiple values for the same key. To fetch all users with the letters 't' AND 'e' in the 'userName':

params = {
    'q': ["userName:t", "userName:e"]
}
bd.get_resource('users', params=params)  # constructs url: /api/users?q=userName:t&q=userName:e

A more complicated example to get all users with 'e' in the userName AND 'test' in the email, sorting the results by firstname:

params = {
    'q': ["userName:e", "email:test"],
    'sort': 'firstname',
}
bd.get_resource('users', params=params)  # constructs url: /api/users?q=userName:e&q=email:test&sort=firstname

By default, if no 'accept' nor 'content-type' headers are specified with a GET request, the following default media type headers will be inserted:

headers = {
    'accept': "application/json",
    'content-type': "application/json"
}

Generally "application/json" instructs the REST API to use the most recent external version of an end-point. These headers will work with most (but unfortunately not all) endpoints.

To use a specific media type header specify one of 'accept', 'content-type' (or some combination of both) as required. For example:

headers = {
    'accept': "application/vnd.blackducksoftware.user-4+json"
}
items = bd.get_resource('dormantUsers', headers=headers)

In addition to list_resources() and get_resource(), the Client purposely exposes the underlying requests layer so that the user can comfortably work with the request(), get()*, put(), post(), and delete() methods.

* While you can certainly use get() we recommend get_json() instead. See explanation further below.

Accessing the underlying session directly allows a user to customize the URL to send an arbitrary request. It is expected that the user provides any headers and handles the response object. See the very excellent Requests documentation. Requests is the #1 downloaded and used Python library for good reason and we encourage you to spend a few hours to learn more about it (if you haven't done so already) and leverage it when working with the REST API.

response = bd.session.get("/api/projects?offset=0&limit=10")

response.raise_for_status()
json_data = response.json()
items = json_data['items']

As the above construct of http get, check response, and decode the json to a dict is ubiquitous when fetching data, there is a helper method get_json() to streamline to a single call but preserve underlying error handling of HTTPErrors and JSONDecodeErrors.

json_data = bd.get_json("/api/projects?offset=0&limit=10")
items = json_data['items']

To see the request responses from the server, set the logging level to DEBUG

logging.basicConfig(
    level=logging.DEBUG,
    ...
)

# DEBUG - https://your.blackduck.url:443 "GET /api/projects?offset=0&limit=100 HTTP/1.1" 200 None

Use the underlying session to POST data. The dict passed to session.post must use the named parameter 'json'.

For example:

project_data = {
    'name': "test project",
    'description': "some description",
    'projectLevelAdjustments': True,
}

try:
    r = bd.session.post("/api/projects", json=project_data)
    r.raise_for_status()
    print(f"created project {r.links['project']['url']}")
except requests.HTTPError as err:
    # more fine grained error handling here; otherwise:
    bd.http_error_handler(err)

or something like this:

r = bd.session.post("/api/projects", json=project_data)
if r.status_code == 201:
    print(f"created project {r.links['project']['url']}")
else:
    bd.http_error_handler(r)

Use the underlying session to PUT data. The dict passed to session.put must use the named parameter 'json'.

For example:

project_url = "/api/projects/c177cb7d-8942-484a-a203-9c2b0582c186"  # for a specific project

project_data = {
    'name': "test project",
    'description': "some other description",
}

try:
    r = bd.session.put(project_url, json=project_data)
    r.raise_for_status()
    print("updated project")
except requests.HTTPError as err:
    if err.response.status_code == 404:
        print("not found")
    else:
        bd.http_error_handler(err)

or something like this:

r = bd.session.put(project_url, json=project_data)
if r.status_code == 200:
    print("updated project")
elif r.status_code == 404:
    print("not found")
else:
    bd.http_error_handler(r)

Use the underlying session to DELETE data. For example use this construct:

project_url = "/api/projects/c177cb7d-8942-484a-a203-9c2b0582c186"  # for a specific project

try:
    r = bd.session.delete(project_url)
    r.raise_for_status()
    print("deleted project")
except requests.HTTPError as err:
    if err.response.status_code == 404:
        print("not found")
    else:
        bd.http_error_handler(err)

or something like this:

r = bd.session.delete(project_url)
if r.status_code == 204:
    print("deleted project")
elif r.status_code == 404:
    print("not found")
else:
    bd.http_error_handler(r)

If your code is already using a class named Client, use Python's import as to avoid a name clash:

from blackduck import Client as HubClient

bd = HubClient(
   ...
)

Username/password authentication is not recommended and it may be deprecated in the future. It is recommended to use the more secure access token authentication instead.

However, it is still possible to authenticate via username/password if required:

from blackduck import Client
from blackduck.Client import HubSession
from blackduck.Authentication import CookieAuth
import logging

logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] {%(module)s:%(lineno)d} %(levelname)s - %(message)s"
)

verify = False  # TLS certificate verification
base_url = "https://your.blackduck.url"
session = HubSession(base_url, timeout=15.0, retries=3, verify=verify)
auth = CookieAuth(session, "sysadmin", "PASSWORD")

bd = Client(base_url=base_url, session=session, auth=auth)