Package: thoughtspot-rest-api-v1
A simple and pythonic Python implementation of ThoughtSpot's REST APIs (both V1 and V2).thoughtspot_rest_api_v1library implements the ThoughtSpot public REST APIs as directly as possible. It is not a "full SDK" with representation of each request and response type, but rather uses Python's Lists and Dicts for input and output.Each API endpoint is represented by a single method within theTSRestApiV1orTSRestApiV2class.Method and argument names closely match to the documented API endpoints, with a few minor changes are taken to make certain parameters more obvious for an end user, or to align with Pythonic conventions ('id' parameters in the API itself are written as 'guid' or '{object-name}_id' for example)TSRestApiV2is implemented with an openrequestparameter for most endpoints, allowing you to construct any variation of a request based on what you build in the V2 REST API playground (a few simple endpoints have all their parameters implemented in the Python method).->Learning from the Source Code->Getting Started->Usingthoughtspot_rest_api_v1->Additional LibrariesLearning from the source codeIf you want to use the library as a reference for how a REST API endpoint is called correctly, look at the/src/thoughtspot_rest_api_v1/tsrestapiv1.pyfile. It contains the definition of all the ENUMs and theTSRestApiV1class. Similarly, theTSRestApiV2class is defined in the/src/thoughtspot_rest_api_v1/tsrestapiv2.pyfile.TheTSRestApiV1class uses therequestslibrary to create an internal requests.Session object when the REST API sign-in command is run. This fulfils the ThoughtSpot REST API V1 requirement for session cookie details to be passed in every request.Each method is implemented to be as self-contained as possible (other than using the shared session object), so you can use the library as an additional reference along with the V1 Playground (Swagger) to see exactly how any given call is implemented.The library is designed to work with the latest version of ThoughtSpot Cloud. It should also work with Software versions 7.1.1 and later, but the library is not versioned, so please check your documentation for available endpoints within the release you use if on a Software release.Getting StartedTo install thoughtspot_rest_api_v1, simply run this simple command in your terminal of choice:$ python3 -m pip install thoughtspot_rest_api_v1Getting the source code$ git clone https://github.com/thoughtspot/thoughtspot_rest_api_v1_python.gitOnce you have a copy of the source, you can embed it in your own Python package, or install it into your site-packages easily:$ cd thoughtspot_rest_api_v1_python
$ python3 -m pip install --upgradeImporting the libraryfrom thoughtspot_rest_api_v1 import *This will bring theTSRestApiV1class, as well as the following enumerations:TSTypes,MetadataNames,MetadataSorts,MetadataSubtypes,MetadataCategories,ShareModes,Privileges.There is also aTSRestApiV2class, with enumerations:TSTypesV2,ReportTypes, to use the subset of REST API V2 capabilities that do not exist in the V1 API.TheUserDetails,GroupsDetailsandLiveboardDetailsclasses are also imported automatically fromdetails_objects.pyto provide easy access to useful properties from the very complex responses from themetadata/detailsendpoint.Modifying the TSRestApiV1 requests.Session object (SSL errors, etc.)The REST API commands are all handled via therequestsmodule, using arequests.Sessionobject.The session object used by all methods is accessible via:TSRestApiV1.requests_sessionA common issue within organizations is SSL certificates not being available / included within the certificate store used by Python. One way around this issue is to use theverify=Falseargument in requests (this is not recommended, but may be the only easy path forward. Check with your security team and never use with ThoughtSpot Cloud or from outside your protected network).This will set the Session object toverify=Falsefor all calls:ts: TSRestApiV1 = TSRestApiV1(server_url=server)
ts.requests_session.verify = FalseIf you find there are other options you need to set on the Session object for your particular situation, you can use the same technique to apply other changes.Logging into the REST APIYou create a TSRestApiV1 object with theserver_urlargument, then use thesession_login()method with username and password to log in. After login succeeds, the TSRestApiV1 object has an open requests.Session object which maintains the necessary cookies to use the REST API continuously .username = os.getenv('username')  # or type in yourself
password = os.getenv('password')  # or type in yourself
server = os.getenv('server')      # or type in yourself

ts: TSRestApiV1 = TSRestApiV1(server_url=server)
try:
    ts.session_login(username=username, password=password)
except requests.exceptions.HTTPError as e:
    print(e)
    print(e.response.content)In all further examples, thetsvariable represents aTSRestApiV1object after thesession_login()method has been called successfully.Logging out of the REST APIThesession_logout()method of the ThoughtSpot class will send the API request to end your TS session. After you do this, you may want to del the ThoughtSpot object if you are doing a lot of other work to end the request.Session object:ts.session_logout()
del tsENUM data structuresThe ThoughtSpot API has internal namings for many features, which require looking up in the reference guide. To help out, the tsrestapiv1.py file defines several ENUM style classes:TSTypes: Combines MetadataNames and MetadataSubtypes into a single set of unique values, so that you don't need to worry about when to use Subtypes. The implementations of the /metadata/ endpoints read the values from this ENUM and issue the correct REST API commandMetadataCategories: Contains the options for the argument called 'category' in metadata callsMetadataSorts: Contains the available sort options for the argument called 'sort' in metadata callsShareModes: The modes used in the JSON of the /security/share endpointPrivileges: The name of the Privileges that Groups can have (and users can inherit) in ThoughtSpotMetadataTypes: The namings used in the 'type' parameter of the /metadata/ endpoint calls, with simplified names that matches the names in the UI and standard ThoughtSpot documentation. For example, MetadataTypes.GROUP = 'USER_GROUP'MetadataSubtypes: The available options for the 'subtype' argument used for certain metadata callsLogging into REST API V2REST API V2 allows for Bearer Token authentication, which is the preferred method rather than session cookie sign-in. You create a TSRestApiV2 object with theserver_urlargument.
Next request a Full Access token usingauth_token_full(). Get thetokenvalue from the response, then set thebearer_tokenproperty of the TSRestApiV2 object with the token. The object will keep the bearer token and use it in the headers of any subsequent call.from thoughtspot_rest_api_v1 import *

username = os.getenv('username')  # or type in yourself
password = os.getenv('password')  # or type in yourself
server = os.getenv('server')      # or type in yourself

ts: TSRestApiV2 = TSRestApiV2(server_url=server)
try:
    auth_token_response = ts.auth_token_full(username=username, password=password, validity_time_in_sec=3000)
except requests.exceptions.HTTPError as e:
    print(e)
    print(e.response.content)
ts.bearer_token = auth_token_response['token']V2 MethodsREST API V2 exclusively uses JSON for the request format. Because Python Dicts map nearly directly to JSON, many of the methods for endpoints simply have a 'request=' argument, with the expectation that you form the request per the Documentation / Playground however you see fit:search_request = {'org_identifiers': ['My Org']}
users_response = ts.users_search(request=search_request)Other methods, where the potential request arguments are very small / simple, do have the full set of arguments represented in the Python method:users_reset_password(user_identifier='bill.guy@company.com', new_password='agreatnewpassword')Implementing new V2 methodsThe TSRestApiV2 class includes three 'base' methods, one for each HTTP request verb one might make to the V2.0 REST API:TSRestApiV2.get_request(endpoint)
TSRestApiV2.post_request(endpoint, request=None)
TSRestApiV2.post_request_binary(endpoint, request=None)  # for binary responsesTheendpointargument only takes the unique part of the endpoint, likeusers/create, not the entire URL.All of the methods for particular endpoints call these base methods like:def users_update(self, user_identifier: str, request: Dict):
    endpoint = 'users/{}/update'.format(user_identifier)
    return self.post_request(endpoint=endpoint, request=request)If a method is not implemented in the library (a new version has rolled out before a maintainer has added the new endpoint to the library), you can simply implement it "externally" using these 'base methods'.You can also reference theTSRestApiV2.requests_sessionobject directly if you want to issue something directly using the Pythonrequestslibrary.V2 ExamplesThe /examples_v2/ directory of this repository contains examples of using the V2 API, often as a parallel to a script with the same name in the V1 /examples/ directory.TML operationsOne primary use case of the REST APIs is to import and export ThoughtSpot Modeling Language (TML) files.Disabling FQN exportThoughtSpot 8.9 adds the 'export_fqn' option to the/metadata/tml/exportendpoint, which includes the GUID references to all related objects in an exported TML file. Because this is very useful, the library defaults to including the argument set to true in all TML Export methods.Older versions of ThoughtSpot will not support this option, so there is a globalcan_export_fqnflag to disable it. Set it to False if you are encountering errors and are on a version prior to 8.9:ts: TSRestApiV1 = TSRestApiV1(server_url=server)
ts.can_export_fqn = FalseRetrieving the TML as a Python OrderedDict from REST APIIf you want to use the TML classes to programmatically adjust the returned TML, there is aexport_tml(guid)method which retrieves the TML from the API in JSON format and then returns it as a Python OrderedDict.This method is designed to be the input of thethoughtspot_tmllibrarylb_tml = ts.metadata_tml_export(guid=lb_guid, export_associated=False)Downloading the TML directly as stringIf you want the TML as you would see it within the TML editor, usets.metadata_tml_export_string(guid, formattype, export_associated=False)formattype defaults to 'YAML' but can be set to 'JSON'.This method returns a Python str object, which can then be editing in memory or saved directly to disk.Download TML with Name-GUID map from export_associated=trueTML files don't include the GUIDs of associated objects, but they are available in the headers of the response whenexport_associatedis set totrue.There are two functions that export both the TML and a Dict of structure{'object_name' : 'guid' }, which can be used to add theFQNproperty to the TML.tml_od, name_guid_map = ts.metadata_tml_export_with_associations_map(guid=lb_guid)
tml_str, name_guid_map = ts.metadata_tml_export_string_with_associations_map(guid=lb_guid)To add theFQNproperties from the dict mapping, you'll need to modify the TML as an object, so the export_string method is less useful than the version that exports the OrderedDict.Importing/Publishing TML back to ThoughtSpot ServerSimilar tometadata_tml_export(),metadata_tml_import()uses a Python OrderedDict as input (or a List containing OrderedDict objects for multiple import). The method converts the OrderedDict to JSON format internally for use in the REST API request body.ts.metadata_tml_import(tml_ordereddict, create_new_on_server=False, validate_only=False))You can also import multiple using a List:ts.metadata_tml_import([tml_od_1, tml_od_2], create_new_on_server=True)There are a few optional arguments:create_new_on_server- you must set this to True, otherwise it will update the existing object with the same GUID.validate_only- If set to True, this only runs through validation and returns the response with any errors listed'There is a static method for pulling the GUIDs from the import command responsets.guids_from_imported_tml(), which returns the GUIDs in a list in the order they were sent to the import.The following example uses thethoughtspot_tmllibrary, where each object has a.tmlproperty containing the OrderedDict:# Get create Worksheet object
ws_obj = Worksheet(ts.metaddata_export_tml(guid=lb_guid))
# Send the .tml property, not the whole object
import_response = ts.metadata_import_tml(ws_obj.tml, create_new_on_server=False)
new_guids = ts.guids_from_imported_tml(import_response)
new_guid = new_guids[0]  # when only a single TML importedMetadata operationsDoing any actions with the REST APIs requires knowing the GUIDs of the objects. The/metadata/endpoints are incredibly flexible, allowing you to retrieve details about almost any object type from the same endpoints. This flexibility means you must set a number of arguments with each call, including using the internal names of the object types.TSTypes, MetadataTypes and MetadataSubtypes and other ENUMsBecause the internal API identifiers of the object types in V1 often do not reflect the names used in the current versions of ThoughtSpot, there are ENUMs provided within the library to map the 'product name' to the REST API string value:TSTypes,MetadataTypes, andMetadataSubtypes.The REST API has a single object_type for Tables, Views, and Worksheets, and uses an additional 'sub_type' property if you need to distinguish between those objects.Because of the complexity of remembering to add the sub-type, theTSTypesENUM should be used - it uses the sub-types of the data objects rather than the type. The implementation of all of themetadataendpoints works even if a sub-type is passed into theobject_typeargument.This allows you to do:objs = ts.metadata_listobjectheaders(object_type=TSTypes.WORKSHEET, sort='MODIFIED', sort_ascending=False, category=category_filter, fetchids=object_guid_list)whereas otherwise you would need to specify both type and sub-type for a Worksheet or other data objectobjs = ts.metadata_listobjectheaders(object_type=MetadataTypes.WORKSHEET, subtypes=[MetadataSubtypes.WORKSHEET], sort='MODIFIED', sort_ascending=False, category=category_filter, fetchids=object_guid_list)Other ENUMs include:Sorts,Categories,ShareModes,Privileges,PermissionTypes, andGroupVisibility. Often the keys for these enums is no different than the string values themselves, but they provide easy access to the full set of values when using auto-complete in your IDE.metadata_listobjectheaders and metadata_listThemetadata/listobjectheadersandmetadata/listREST API endpoints do relatively the same thing, although there are slight differences in the responses from each call.The implementation is exactly as described in the documentation (https://developers.thoughtspot.com/docs/?pageid=metadata-api#object-header) with these changes for ease of use and clarity:'object_type' argument will accept the sub_types for the data object types and automatically structure the REST API request appropriately'filter' argument is the name given to the 'pattern' argument in the REST API command, as the name pattern seemed unclear to usersmetadata/listreturns a slightly more complex object thanmetadata/listobjectheaders. To get the same list of objects frommetadata/list, use theheaderskey from the response:objs = ts.metadata_listobjectheaders(object_type=TSTypes.WORKSHEET, sort='MODIFIED', sort_ascending=False)

for obj in objs:
    # parse the objectsvs.response = ts.metadata_list(object_type=TSTypes.WORKSHEET, sort='MODIFIED', sort_ascending=False)
objs = response['headers']
for obj in objs:
    # parse the objectsmetadata_details and Details classesmetadata_detailsreturns the full internal object model of a given object, which is typically a very large and complex response to parse.The Details classes defined indetails_objects.pycan wrap around the response with easy methods to retrieve useful properties from this response.How to use:details_response = ts.metadata_details(object_type=TSTypes.GROUP, object_guids=['{guid}'])
group_details = GroupDetails(details_obj=details_response[0])
privs = group_details.privileges()

details_response = ts.metadata_details(object_type=TSTypes.USER, object_guids=['{guid}'])
user_details = UserDetails(details_obj=details_response[0])
user_created = user_details.created_timestamp()
user_inherited_groups = user_details.inherited_groups()Some of the properties that were previously only accessible from themetadata/detailsresponse may be available from endpoints in the V2 REST API, so it is worth checking there first before working with the details responses.Connection detailsConnections, representing basic connection details of the external data sources ThoughtSpot connects to, have their own set of endpoints in/connection/outside of the/metadata/endpoints.There are few of these endpoints used by ThoughtSpot's UI but not currently ported and documented to the/v1/public/endpoints that have been implemented as part of this library to help bridge customers until public endpoints with the details have been made available.Dependent Objects API callsUser and Group operationsThe/user/and/group/endpoints contain CRUD operation endpoints for these two object types (while other objects are created via TML import and export). They allow you to sync users and groups to other systems.Following REST API principles, many of the endpoints themselves are the same but use different HTTP verbs to achieve different actions. The TSRestApiV1 methods distinguish these following the naming pattern of {fixed_endpoint_separated_by_underscores}_{http_verb} i.e.POST /group/{groupid}/usersbecomesgroup_users_post()while the PUT version of the same endpoint becomesgroup_users_put()Metadata operationsGET /group/andGET /user/are equivalent to usingmetadata/listobjectheaders, they are just convenience methods to get the same exact result, with ability to filter directly by name. However, there are additional endpoints that provide more specific information that is difficult to determine otherwise.GET /group/listuser/{groupid},GET /group/{groupid}/users/,GET /group/{groupid}/groups, andGET /user/{userid}/groupseach give a specific insight into the relationship between groups and users, starting from an individual group or user's perspective. They can save a huge amount of time when auditing how your system is configured or why certain privileges / access control is working in a particular way.For example, if you want to know the set of users assigned to a group, you would do:group_name = 'Group A'
group = ts.group_get(name=group_name)
group_guid = group['header']['id']
users_in_group = ts.group_users_get(group_guid=group_guid)
for user in users_in_group:
    # do something with user headersCRUD operationsAs much as possible, the parameters within the endpoints or as part of the POST/PUT bodies have been mapped directly to methods with arguments matching the definitions in the documentation. Differences may occur such as usingguidin place ofidper Python's spec, or making arguments plural or singular when the API argument is named the opposite way.The response is always the full JSON response from the API unless the API does not return a JSON response. You will need to parse the response to get essential details (such as the new GUID assigned to the newly created object)Example:new_group = ts.group_post(group_name='group_1', display_name='Group 1', privileges=[Privileges.CAN_DOWNLOAD_DATA, Privileges.HAS_SPOTIQ_PRIVILEGE])
new_group_guid = new_group['headers']['id']Sharing / object access : /security/ endpointsObject access in ThoughtSpot is controlled by Sharing. Objects are owned by theirauthor(this is different from anyownerproperty you might see in API responses) and can only be accessed by others if they are shared.Sharing an objectThe/security/shareendpoint is used to share objects with users and groups, which becomessecurity_share()of theTSRestApiV11class.You can specify if the user or group has READ_ONLY or EDIT access through thepermissions=argument, but it is relatively complex object format. For that reason, the library provides thecreate_shared_permissions()helper method to get the object with the right format to send into the permissions= argument:perms = ts.create_share_permissions(read_only_users_or_groups_guids=read_only_guids, 
                                    edit_access_users_or_groups_guids=edit_guids)
ts.security_share(shared_object_type=TSTypes.LIVEBOARD, shared_object_guids=[lb_guid], permissions=perms)You can also remove sharing from an object, using the third optional argument ofcreate_share_permissions(remove_access_users_or_groups=):guids_to_remove_sharing_from = ['{groupGuid6}', '{groupGuid7}']
remove_perms = ts.create_share_permissions(remove_access_users_or_groups_groups=guids_to_remove_sharing_from)
ts.security_share(shared_object_type=TSTypes.LIVEBOARD, shared_object_guids=[lb_guid],
                  permissions=remove_perms)You can share an individual viz from a Liveboard rather than the whole Liveboard usingsecurity_shareviz(). There is not multiple levels of permissions on a shared viz:viz_guid = '{vizGuid}'
ts.security_shareviz(shared_object_type=TSTypes.LIVEBOARD, pinboard_guid=lb_guid, viz_guid=viz_guid, 
                     principal_ids=read_only_guids)Auditing permissions on an objectThe set of sharing set on an object is referred to as permissions. There are several similar endpoints in the V1 REST API for determining permissions:security_metadata_permissions(),security_metadata_permissions_by_id()andsecurity_effectivepermissionbulk().Please see the REST API documentation to understand the main differences, but in essencesecurity_metadata_permissions()can retrieve multiple objects of the same type, whilesecurity_metadata_permissions_by_id()only brings back one object at a time. Both have apermission_type=argument that can take the values in thePermissionTypesENUM, eitherEFFECTIVEorDEFINEDts.security_metadata_permissions_by_id(object_type=TSTypes.LIVEBOARD, object_guid=lb_guid, dependent_share=False,
                                       permission_type=PermissionTypes.EFFECTIVE)security_effectivepermissionbulk()allows you to specify objects of multiple types using a more complex Dict for theids_by_type=argument, but it does not let you choose between the PermissionTypes the way the other endpoints do:ts.security_effectivepermissionbulk(ids_by_type={MetadataTypes.LIVEBOARD: ['{lbGuid1}'], 
                                                 MetadataTypes.ANSWER: ['{aGuid1}']}
                                    )These endpoints have been simplified in REST API V2 intosecurity_permission_tsobject()to get the set of users and groups who can see/edit the object, andts_security_permission_principal()to see what content of a given type the user or group has access to:ts2.security_permission_principal(username_or_group_name='auser@domain.com')  # alternatively username_or_group_guid=
ts2.security_permission_tsobject(guid=lb_guid, object_type=TSTypesV2.LIVEBOARD, include_dependents=False)/session/ endpointsThe/session/REST API endpoints are mixed between those intended to be used from the end user's browser (like/session/login/token) and others used in back-end processes to help facilitate SSO and session management.In general, anything intended as part of the Trusted Authentication workflow from the browser should be handled by the Visual Embed SDK, and once that session is established, other methods likePOST /session/homepinboardorPOST /session/logoutcan be sent from the browser to achieve integration with the embedding application.Trusted authentication token requestsThe methodts.session_auth_token()is used to request the login token for a trusted auth flow. This should only be used by a back-end service, with thesecret_keyfrom the ThoughtSpot instance stored and accessed via a secure method (up to you how best to do this). You also must develop the process to securely determine the username of the user logged into the embedding application (seehttps://developers.thoughtspot.com/docs/?pageid=trusted-authfor full documentation):user_from_request = get_username_from_secure_request(request)
token = ts.session_auth_token(secret_key=securely_accessed_secret_key, username=user_from_request)The example scriptexamples/trusted_authentication_with_authorization.pyshows how to combine various metadata and CRUD operations with the request for the trusted authorization login token.Data and export APIsIn the V1 REST API, the Liveboard PDF Export endpoint lives under/export/while the Liveboard Data and Search Data endpoints which export data in JSON format live in the top level. In V2, the Data endpoints live under the/data/higher level endpoint.Thus the methods arets.pinboarddata()andts.searchdata()on theTSRestApiV1class, while the equivalent in V2 for saved Answers isdata_answer_data().These same features have been relocated in the V2 REST API under/report/and/data/endpoints.try:
    liveboard_pdf = ts.export_pinboard_pdf(pinboard_id=first_liveboard_id, footer_text="Viz by the foot",
                                           cover_page=False, filter_page=False, landscape_or_portrait='PORTRAIT')
    new_file_name = "../Test PDF.pdf"
    with open(new_file_name, 'bw') as fh:
        fh.write(liveboard_pdf)
except requests.exceptions.HTTPError as e:
    print(e)examples/liveboard_pdf_export.pyshows how to use to get binary object exports and save them to disk, although you could do further processing of those objects in memory.The Data APIs return back the data from a saved object or an arbitrary TML search string in a JSON format. Please seeexamples/data_exports.pyfor the various options and how to work with the result sets.lb_guid = '{lbGuid}'
viz_on_lb_guid = '{vizGuid}'  # For retrieving from one specific viz

lb_data_response = ts.pinboarddata(pinboard_guid=lb_guid)

viz_data_response = ts.pinboarddata(pinboard_guid=lb_guid, vizids=[viz_on_lb_guid])Additional librariesthoughtspot_tmlis a library for processing the ThoughtSpot Modeling Language (TML) files. You can usethoughtspot_tmlto manipulate TML files from disk or exported via the REST API.cs_toolsis a package of command-line tools built by the ThoughtSpot Professional Services team, aimed at ThoughtSpot administrators.