Tapipy and TapisService

Tapipy and TapisService are powerful Python libraries for interacting with Tapis.

Tapipy User Guide

What is Tapipy

Tapipy is a Python library for interacting with an instance of the Tapis API framework. The package is hosted on PyPi - tapipy. You can install it with:

pip install tapipy

The library is automatically generated by referencing the OpenAPI spec files which a Tapis client built from the OpenAPI spec files from TACC’s Tapis services. With this functionality a user is able to authorize itself with the Tapis client and have a ‘live’ library in order to interact with all Tapis services. Usage of tapipy allows for automatic token refreshing, verbose error messages regarding endpoint inputs, and up to date syntax for all services.

Initializing Tapipy

To initialize a Tapis client object, t, you will replace your_tacc_username and your_tacc_password with your TACC username and password, preserving the quotation marks shown in the command below. This command will authenticate you with Tapis for your respective base_url and store your retrieved token for later tapipy calls.

from tapipy.tapis import Tapis

# Create python Tapis client for user
t = Tapis(base_url= "https://tacc.tapis.io",

# Get tokens now that you're initialized

These calls do not produce output. However you can use the following code to see the access token you just generated.


The output should look similar to the following; it describes the access token that was just created.

access_token: *very long string of alphanumeric characters*
claims: {
    'jti': '007fa9e6-f044-4817-a812-12292b2bdbe3',
    'iss': 'https://tacc.tapis.io/v3/tokens',
    'sub': 'your_tacc_username',
    'tapis/tenant_id': 'tacc',
    'tapis/token_type': 'access',
    'tapis/delegation': False,
    'tapis/delegation_sub': None,
    'tapis/username': 'your_tacc_username',
    'tapis/account_type': 'user', 'exp': 1657686889,
    'tapis/client_id': None,
    'tapis/grant_type': 'password'
expires_at: 2022-07-13 04:34:49+00:00
expires_in: <function Tapis.add_claims_to_token.<locals>._expires_in at 0x10a070280>
jti: 007fa9e6-f044-4817-a812-12292b2bdbe3
original_ttl: 14400

Where you will have your own access token and the placeholder your_tacc_username will be replaced with the username you used.

Using Tapipy

Now in order to use the Tapis client you can reference the Tapis Live Docs to browse all service specifications. Tapipy is generated off of these specific specifications, so this is your most up-to-date reference on tapipy functionality (if tapipy is fully updated).

For example, if you wanted to create a system using the Systems service you would navigate to the proper service in the top bar, browse the sidebar for functions and view the specification in the main pane. An example is displayed below:


For this example, creating a system, I can see I need to run the following:

t.systems.createSystem(id="id", host="host", systemType="LINUX")


Tapipy 1.3.2 introduces native support for t.files.insert(). Previous a workaround, t.upload(filePath, systemId, path) was required. Read more here.

You can specify any parameter identified by the spec. Tapipy will detail missing required parameters if they’re missing.

The live-docs are a helpful GUI on top of OpenAPI v3 specifications that each service writes to detail endpoints, attributes, proper content-types, return objects, etc. Depending on the verbosity of the specifications the site could detail what each function and attribute is, along with defaults and type restrictions.

Special User Query Parameters and Headers

For the most part, arguments that can or should be passed to a Tapis endpoint are described in the OpenAPI definition files and recognized automatically by tapipy. However, due to limitations in what can be expressed in OpenAPI, there are some paramaters that are not defined in the definition files; for example, the search parameters for various endpoints.

To accommodate these cases, tapipy recognizes two special keyword arguments to all of its methods that correspond to Tapis API calls (i.e., all of its “operations”). They are:

  • _tapis_headers - dictionary-like object of header names (keys) and vales.

  • _tapis_query_parameters - dictionary-like object of query parameter names (keys) and values.

Use the above two special arguments for passing headers (respectively, query parameters) that are not specified in the OpenAPI definition of an endpoint.

For example, I can issue a search using the following syntax:

    _tapis_query_parameters={'key': 'value'})

Additional Tapipy Parameters

The tapipy package allows for spec file customization in Tapis client initialization:

  • resource_set: str in [“tapipy”, “prod”, “staging”, “dev”]
    • Determines which set of resource to use, defaults to tapipy.

    • Important to note that if a custom_spec_dictionary is used, it is appended to this resource_set.
      • For example, you would set prod and then specify a custom specs that will be added on.

  • custom_spec_dict: {resource_name: str, resource_url: str}
    • Allows users to modify the base resource set urls.
      • i.e. I can specify actor as a resource name and change the url.

    • Also allows users to add new resources to the set.
      • i.e. I can add a new resource named “test” with a custom url.

      • Any new specs will be downloaded and added cached without an explicit recache.

    • Allows local resources.
      • Specify an absolute path in the dict with local: prefixing it and tapipy will load in a local OpenAPI v3 yml spec file.

      • custom_spec_dict={'cactus': 'local: /home/tapis/myfolder/cactusSpec.yml'}

  • download_latest_specs: bool
    • Allows users to re-download all specs regardless on if they already exist in the cache. Defaulted to False

    • This will happen every time the Tapis client is initialized, it’s a tad slower, and can cause live updates to specs.
      • As such, be warned. There are functions to update spec files below.

  • spec_dir: str
    • Allows users to specify folder to save specs to. Defaults to none which uses Tapipy’s install folder.

    • If you are updating specs it’s wise to use a different folder in order to not modify the base specs.


If your user account does not have permission to write to Tapipy’s install directory you will encounter an error due to file system permissions. You can use spec_dir as highlighted above and use a directory you have permission to write to. (Important for Docker containers)


The following is an example of some custom parameter setting. As you can see, the abaco resource will now use the spec at URL#1, overwriting the resource definition in the tapipy resource set, it’ll download it if it doesn’t exist. The same for the longhorn resource. This means that the Tapis client will now have access to all specs in tapipy like normal, but with a modified abaco and with a new longhorn resource. All of these are stored at the new spec_dir because you don’t want to accidentally overwrite any packaged specs when call update_spec_cache() later (talked about in the next section).

from tapipy.tapis import Tapis

t = Tapis(base_url='https://admin.develop.tapis.io',
          custom_spec_dict={'abaco': 'URL#1',
                            'longhorn': 'URL#2'},
                            'cactus': 'local: /home/tapis/myfolder/cactusSpec.yml'},

Tapipy Instantiation with only a Tapis access token

Tapipy can be instantiated with only a Tapis access token. This is useful when you have a created token and don’t want to provide username and password. The minimal instantiation requires the base_url and the token as follows:

from tapipy.tapis import Tapis

t = Tapis(base_url='https://admin.develop.tapis.io',

As the token is already created users do not have to run t.get_tokens(). You can use the client as normal at this point.

Refresh Tapipy’s Cached Specs

Tapipy uses a cache to organize spec dictionaries as pickled files and has the ability to accept custom spec files. By default Tapipy keeps a set of base spec files in the {tapipyInstallDir}/specs folder. These specs are pre-pickled at package build time. To update specs,


If your user account does not have permission to write to Tapipy’s install directory you will encounter an error due to file system permissions. You can use spec_dir as highlighted above and use a directory you have permission to write to. (Important for Docker containers)

Two Spec Refresh Methods

  1. t.update_spec_cache() - No inputs
    • Will refresh client’s t.resource_set resources and save to t.spec_dir.

    from tapipy.tapis import Tapis
    t = Tapis()
  2. tapipy.tapis.update_spec_cache() - Takes resources and spec_dir
    • Will refresh specified resources and cache to inputted spec_dir

    • Default to refreshing all resources from tapipy and saving to package install dir.

    from tapipy.tapis import update_spec_cache
    Resources = Dict[ResourceName, ResourceUrl]
    update_spec_cache(resources: Resources = None, spec_dir: str = None)

Creating and Using Tapipy Refresh tokens

Tapipy will authenticate you to Tapis and generate a token, however to activate automatic token refreshing via refresh token you must also have a Tapis client created in order to provide client_id and client_key at Tapis initialization.

The following code will show you how to create a client if you don’t already have one and then initialize a Tapis client with refresh capabilities by also providing client_id and client_key. If you already have a client created then you can jump to the Fourth step. Feel free to run just the code you need.

from tapipy.tapis import Tapis

# First - Create your normal Tapis client so we can run create_client.
t = Tapis(base_url = 'https://tacc.tapis.io',
          username = 'cgarcia',
          password = 'password')

# Second - List your clients (check if you already have a client created)

# Third - If you don't have a client, create one.

# Should result in:
active: True
client_id: my_client_id
client_key: my_client_key
create_time: Wed, 02 Nov 2022 20:18:01 GMT
display_name: my_display_name
last_update_time: Wed, 02 Nov 2022 20:18:01 GMT
owner: cgarcia
tenant_id: icicle]

# Fourth - Grab the client_id and client_key, add it to your new Tapis client initialization
t2 = Tapis(base_url='https://tapis.io',

# You should now be able to run refresh_tokens with no errors. The t2 object will refresh when needed.

Using t.files.insert with Tapipy for file upload.

Introduced in Tapipy 1.3.2, the library now has native support for multipart/form-data input data and headers. This allows us to natively support the t.files.insert operation. The following is an example of uploading a filepath.ext file to a specific path in a users Tapis system_id system.

with open("filepath.ext", "rb") as data_blob:
    res = t.files.insert(systemId=system_id, path=path, file = data_blob)


The previous static workaround, t.upload(filePath, systemId, local_file_path), is now deprecated and will be removed in a future release.

TapisService User Guide

What is TapisService

TapisService is a Python plugin for Tapipy which extends Tapipy with additional service oriented Python modules. This plugin is useful when creating a Tapis service or using a Tapis service account. The package is hosted on PyPi - tapisservice. You can install it with:

pip install tapisservice

The plugin adds systems for authentication, configs, logging, Tapis errors, TenantCache, alongside helper code for working in Flask, FastAPI, and Django, three popular Python web frameworks. Python based Tapis services should make use of tapisservice for at least the authentication in order to not unneccessary rereplicate code.

Writing a New Service

Getting Started

When creating a new Tapis v3 service, use the following guidelines:

Create a git repository with the following:
+ service: directory for API source code.
+ migrations: directory for migration scripts.
- config-local.json: API config values for local development
- configschema.json: jsonschema definition of the API config.
- Dockerfile: Build recipe for the API service Docker image.
- Dockerfile-migrations: Build recipe for the API service migrations Docker image.
- requirements.txt: Packages required for the API service or migrations scripts.
- docker-compose.yml: compose file for local development.
- CHANGELOG.md: Tracks releases for the service.
- README.md: High level description of the service.
- service.log: Mount into the API container to capture service logs during local development.
Service configuration

A description of all possible service configurations should be provided in the configschema.json file or example, if your service requires a SQL database, you may have a property, sql_db_url, with the following definition in your configschema.json:

"sql_db_url": {
    "type": "string",
    "description": "full URL, including protocol and database, to the SQL db.",
    "default": "postgres://myservice:mypassword@postgres:5432/mydb"

The fields you define in your service’s configschema.json file will complement those defined in the configschema.json file included in this repository. Any configs that will be used by all services, such as service_password, should be defined in the common configschema.json. It is currently possible to override the definition of a config provided in this repository with a new definition provided in your service’s configschema.json file, but this is not recommended.

Provide values for the configs in the config-local.json file. When deploying to a remote environment, such as the Tapis develop environment, the config-local.json can be replaced with file mounted from a ConfigMap with different values.

  • Add any packages required by your service to the requirements.txt file. Keep in mind that a number of packages are installed by this repository (such as jsonschema, pyjwt, etc.), so it is possible you will need to add few additional packages.

  • Create a Dockerfile to build your service. The image name for your service should be tapis/<service_name>-api; for example, tapis/tokens-api or tapis/tenants-api. Here is a general template for the Dockerfile for your service:

# inherit from the flaskbase image:
FROM: tapis/flaskbase

# set the name of the api, for use by some of the tapisservice modules.
ENV TAPIS_API <api_name>

# install additional requirements for the service
COPY requirements.txt /home/tapis/requirements.txt
RUN pip install -r /home/tapis/requirements.txt

# copy service source code
COPY configschema.json /home/tapis/configschema.json
COPY config-local.json /home/tapis/config.json
COPY service /home/tapis/service

# run service as non-root tapis user
RUN chown -R tapis:tapis /home/tapis
USER tapis
Migration skeleton

For services using Postgres, create migration skeleton. Migrations are based on the alembic package and must be initialized. Run the following commands from a terminal:

$ docker run -it --entrypoint=bash --network=<service>-api_<service> -v $(pwd):/home/tapis/mig tapis/tenants-api
# inside the container:
$ cd mig; flask db init
$ flask db migrate
$ flask db upgrade
$ exit
Migrations Dockerfile

Create Dockerfile-migrations to containerize your migrations code. For simple cases, you may be able to just use the following after change <service> to the name of your service.

# image: tapis/<service>-api-migrations
from tapis/<service>-api

COPY migrations /home/tapis/migrations

ENTRYPOINT ["flask", "db"]
Service Dockerfile

Write a docker-compose.yml file to facilitate local development.

TapisService Modules

Code for a number of common tasks has been packaged into tapisservice modules. In this section, we give an overview of how to use some of the functionality in your service.

Accessing TapisService Modules

You can install tapisservice with pip install tapisservice. Services can import modules directly from this package; for example:

from tapisservice import auth
from tapisservice.utils import ok

Service Configuration and Initialization

Most services will need to do some initialization tasks before they are ready to respond to requests. For example, they may need to connect to a database or make some requests to some other Tapis services. Initialization also usually involves reading configuration data. tapisservice audits the supplied config file and makes configration data available through a singelton object, conf, available from the tapisservice.config package. By importing the object, for instance, in the API’s __init__.py module, the config file will be read and validated, and the resulting configurations transformed to Python objects. For example, if a service requires a configuration, max_number_retries, then it could use the following entry in its configschema.json file:

    "max_number_retries": {
    "type": "integer",
    "description": "Maximum number of times the service should retry some complicated logic...",
    . . . # additional properties
"required": ["max_number_retries", . . .]

and then, place the following code in its __init__.py file:

from tapisservice.config import conf

print(f"We'll be trying at most {conf.max_number_retries} times."

By the time the import has completed, the service is guaranteed that conf contains all required fields and that they conform to the requirements specified in the configschema.json file. In particular, the types of the attributes are the same as that specified in the configchema.json file.

Making Service Requests

The tapisservice.auth package provides a function, get_service_tapis_client which can be used to get a pre-configured Tapis client for making service requests. A common technique is to fetch the service client in the __init__.py module so that it is created at service initialization and available via import throughout the rest of the service code.

Within __init__.py:

from tapisservice.auth import get_service_tapis_client
from tapisservice.tenants import TenantCache

Tenants = TenantCache()

t = get_service_tapis_client(tenants=Tenants)

From within any other service modules:

from __init__ import t
. . .
# use the client within some method or function:


The tapisservice.auth module provides functions for resolving the authentication data contained in a reqeust to your service.

JWT Authentication

The most common and straight-forward case is when an endpoint in your service requires a JWT. For this use case, the tapisservice.auth.authentication() function can be used. This function does the following:

  1. Checks for a JWT in the X-Tapis-Token header, and checks the other X-Tapis- headers.

  2. Validates the JWT, including checking the signature and expiration, and sets the following on the global thread-local, g:

    1. g.x_tapis_token - the raw JWT.

    2. g.token_claims - the claims object associated with the JWT, as a python dictionary.

    3. g.username - the username from the JWT username claim.

    4. g.tenant_id - the tenant id from the JWT tenant_id claim.

    5. g.account_type - the account type (either user or service) from the JWT.

    6. g.delegation - whether the token was a delegation token (True or False).

    7. g.x_tapis_tenant - the value of the X-Tapis-Tenant header.

    8. g.x_tapis_user - the value of the X-Tapis-User header.

    9. g.request_tenant_id - USE THIS JWT tenant_id or, if service, X-Tapis-Tenant.

    10. g.request_username - USE THIS JWT username or, if service, X-Tapis-User.


Use g.request_tenant_id and g.request_username. These both resolve to their respective values, obtained from the incoming request JWT Tapis token. However, the values change when service accounts send “on behalf of” (obo) requests. Only services within the same site may complete these request, this is checked by tapisservice.

This function raises the following exceptions:
  1. tapisservice.errors.NoTokenError - if no token was found.

  2. tapisservice.errors.AuthenticationError - the token was invalid.

Other Types of Authentication

Some services, such as the Authenticator, use other types of authentication, including HTTP Basic Auth. Several tapisservice.auth functions are provided to facilitate tasks related to alternative authentication methods. TODO


The tapisservice.logs module provides basic logger with a file and stdout handler. To use it, create an instance of the logger in each module where you want to add logs by calling the get_logger function with the module name; for example:

from tapisservice.logs import get_logger
logger = get_logger(__name__)

Then add log statements using the logger:

logger.debug(f"some debug message, x={x}."

Error Handling

For REST APIs, the tapisservice.util module provides the TapisApi class and the flask_errors_dict dict and handle_error() function. Use them by adding the following to your api.py module:

from flask import Flask
from tapisservice.utils import TapisApi, handle_error, flask_errors_dict

# basic flask "app" object:
app = Flask(__name__)

# TapisApi object; created with the app object and the flask_errors_dict to establish the 4 stanza structure of error respones:
api = TapisApi(app, errors=flask_errors_dict)

# Set up error handling to use the handle_error() function -
api.handle_error = handle_error
api.handle_exception = handle_error
api.handle_user_exception = handle_error

Now, from within a controller, raise an exception of type tapisservice.errors.BaseTapisError (or any child class). When instantiating the exception to be raised, set values for the msg and code attributed to set the message and HTTP response code. For example, in your controller, you could have:

raise errors.MyServiceError(msg='Invalid sprocket; too many widgets.', code=400)

Then, as long as the errors.MyServiceError class descended from the tapisservice.errors.BaseTapisError, the HTTP response returned to the user would be:

    "message": "Invalid sprocket; too many widgets.",
    "status": "error",
    "version": "conf.version",
    "result": null

And the HTTP status code would be 400.

Note that if your code (and, by extension, any code your code calls) raises an exception that does not descend from the tapisservice.errors.BaseTapisError then the HTTP response will still contain the 4-stanza JSON response above but the message field will contain “Unrecognized exception type..” to indicate that the exception was not a recognized exception. In general, your service should not raise exceptions of types other than tapisservice.errors.BaseTapisError and should instead handle all other exceptions and convert them to the appropriate Tapis Exceptions. This includes all exceptions from the Python standard library, such as KeyError, AttributeError, etc.

For example, if code you are writing could raise a KeyError, you should catch that and then translate it appropriately. Of course, different KeyErrors in different situations could translate into a user error, a service error, etc.

    app_id = post_data['app_id']
except KeyError:
    raise errors.BadInputError(msg='The app_id parameter was missing. Please be sure to pass app_id.')   # <- decends from BaseTapisError

Working with TapisService, Need to Knows

If you’re developing with TapisService then please ensure you understand the following topics.

Framework Talk

Flaskbase is the original implementation of a tapisservice like scheme where we provide neccessary helper functions for dealing with Tapis. It was a library packaged in a Docker container which services based (and still can) their containers off of for a working coding environment. As the name says, this library was originally intended for Flask and overtime it was generalized for more frameworks and to be used even without any specified framework. Alongside Flask, FastAPI has full support of all tapisservice features. Django is also a supported framework, however all functionality is not yet at 100%.

These three frameworks already have Tapis services already built off of them. These can be great as example implementations to base your own services off of. Information is as follows:

  • Flask:
    • Abaco. The repository’s /actors folder contains __init__.py along with auth.py.

    • Abaco was the original user of Flaskbase and fully implements all tapisservice functionality.

  • FastAPI:
    • Pod Service. The repository’s /service folder contains __init__.py along with auth.py.

    • Pods was the first implementation of FastAPI with tapisservice, it fully implements all tapisservice functionality.

  • Django:
    • PgREST. The repository’s /pgrest folder contains __init__.py along with views.py.

    • PgREST implemented most features, however it’s missing a full use of auth capabilities. Making use of only token resolution in the aforementioned views.py.

Framework Initialization


from __init__ import Tenants ## Expects "init.py" as described in "Making Service Requests"
from flask import Flask
from tapisservice.tapisflask.utils import TapisApi
from tapisservice.auth import authn_and_authz

def authentication():
    ## You can write your own auth code here
    ## This will run after tapisservice validates token and sets all valuable g object fields
    user = g.request_username
    tenant_id = g.request_tenant_id

def authorization():
    ## You can write your own auth code here
    ## This will run after tapisservice validates token and sets all valuable g object fields

app = Flask(__name__)
api = TapisApi(app)  ## Empty wrapper around "flask.Flask.api" for future use.

# tapisservice authn/authz
def auth():
    # Provide callbacks for authn and authz which will have access to g object fields.
    # authentication runs, followed by authorization


from __init__ import Tenants ## Expects "init.py" as described in "Making Service Requests"
from tapisservice.tapisfastapi.utils import GlobalsMiddleware
from tapisservice.tapisfastapi.auth import TapisMiddleware
from fastapi import FastAPI
from fastapi.middleware import Middleware

def authentication():
    ## You can write your own auth code here
    ## This will run after tapisservice validates token and sets all valuable g object fields

def authorization():
    ## You can write your own auth code here
    ## This will run after tapisservice validates token and sets all valuable g object fields

api = FastAPI(

Example of Usual API file imports

This applies most for FastAPI and Flask. Django can use some of this on a case by case basis.

from __init__ import t ## Expects "init.py" as described in "Making Service Requests"
from tapisservice.config import conf
from tapisservice.errors import BaseTapisError
from tapisservice.logs import get_logger
logger = get_logger(__name__)

# Use precreated t object with service
t.systems.get_systems(_tapis_set_x_headers_from_service = True)

# Get config.json site_id key value
site_id_to_use = conf.site_id

# Use logger and Tapis error
    logger.debug("Writing logs to file described in config.json and configschema.json.")
except BaseTapisError:
    logger.debug("Error found, will be given 400 status code and traditional Tapis 5 stanza output.")

Global g object - Flask and FastAPI

tapisservice uses request thread local g objects to expose data globally. This object is created per request and lasts the lifespan of said request. This allows tapisservice to write valuable information to it for reference by code. Key variables to use are: - g.request_tenant_id - Either incoming token tenant_id claim or “on behalf of” headers set by Tapis services. - g.request_username - Either incoming token username claim or “on behalf of” headers set by Tapis services. - g.token_claims - the claims object associated with the JWT, as a python dictionary.

Flask natively support a g object. tapisservice simply intercepts the object and writes to it during the authn_and_authz phase. FastAPI does not natively use a g object, but tapisservice introduces the variable. You can think of both versions of g to be the same in these docs, use it in both frameworks.

Use g.request_tenant_id and g.request_username

Hammering home the point, tapisservice exposes two sets of similar variables:

  • g.request_username - g.username if not service and g.x_tapis_user otherwise

  • g.username - incoming request token’s username claim

  • g.x_tapis_user - the value of the X-Tapis-Tenant header


  • g.request_tenant_id - g.tenant_id if not service and g.x_tapis_tenant otherwise

  • g.tenant_id - incoming request token’s tenant_id claim

  • g.x_tapis_tenant - the value of the X-Tapis-User header

Use g.request_tenant_id and g.request_username. These both resolve to the values obtained from the incoming request JWT Tapis token. However, the values change (to the x_tapis_* variants) when service accounts send “on behalf of” (obo) requests. This allows services to reference only one variable versus choosing to use g.username or g.x_tapis_user at runtime. To note, only services within the same site may complete these OBO requests, this is checked by tapisservice.

The remaining variables are useful for debugging or defining exactly who sent a request and exactly how.

Use Tapipy as a Service Account

A service account can still make use of the tapipy client, some changes will apply though.

from __init__ import t ## Expects "init.py" as described in "Making Service Requests"

# Uses tenant and username from client, meaning service's tenant and username.
t.systems.get_systems(_tapis_set_x_headers_from_service = True)

# Use specified _x_tapis_user and _x_tapis_tenant_id rather than service's values.
t.systems.get_systems(_x_tapis_user = "cgarcia",
                      _x_tapis_tenant_id = "tacc")