Repository: google/turbinia (language: python)

Issue:
Turbinia API client library and tool
Create a Python client library for the API server. 

Use the API library to create a command-line tool that replaces most turbiniactl functionality that can now be provided by the API server.
API Server logging paths
Per our past chat around this creating this as reference, ideally would like the api server to log into the `LOG_DIR` directory for easier reference, currently seems to be done in `/var/log/supervisor`. Also it doesn't seem like the log line for mounting the Web UI gets recorded anywhere which makes it a little difficult during testing/seeing issues with web mount if its also possible to record this as well/may be affecting any other log lines we have outside of FastAPI request logging.

Also the other part from chat is to have new Turbinia variable with Web mount path rather than trying to look for it relatively. I think best to set it by default to `/tmp/web` and then I can make adjustments in Docker container to write Web dist to `/tmp/dir` . I used `os.path.join` in other parts of Turbinia code as an FYI, `pathlib` seems similar but if we wanted one less library small nit. Also a log line indicating that the Web UI was successfully mounted would be helpful.


Discussion / hints:

Fix patch:
diff --git a/.pylintrc b/.pylintrc
index 4e89b0c10..0913992d9 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -85,6 +85,7 @@ disable=abstract-method,
         intern-builtin,
         invalid-str-codec,
         locally-disabled,
+        logging-fstring-interpolation,
         long-builtin,
         long-suffix,
         map-builtin-not-iterating,
diff --git a/docker/api_server/Dockerfile b/docker/api_server/Dockerfile
index 577b92927..3489015d7 100644
--- a/docker/api_server/Dockerfile
+++ b/docker/api_server/Dockerfile
@@ -41,4 +41,4 @@ USER turbinia
 CMD ["/home/turbinia/start.sh"]
 # Expose Prometheus and API endpoints.
 EXPOSE 9200/tcp
-EXPOSE 8000/tcp
\ No newline at end of file
+EXPOSE 8000/tcp
diff --git a/docker/api_server/oauth2_web.cfg b/docker/api_server/oauth2_web.cfg
deleted file mode 100644
index 2b6858a80..000000000
--- a/docker/api_server/oauth2_web.cfg
+++ /dev/null
@@ -1,103 +0,0 @@
-## OAuth2 Proxy Config File
-## https://github.com/oauth2-proxy/oauth2-proxy
-
-## <addr>:<port> to listen on for HTTP/HTTPS clients
-http_address = "0.0.0.0:8080"
-
-## Are we running behind a reverse proxy? Will not accept headers like X-Real-Ip unless this is set.
-#reverse_proxy = true
-
-## TLS Settings
-# tls_cert_file = ""
-# tls_key_file = ""
-
-## the OAuth Redirect URL.
-# defaults to the "https://" + requested host header + "/oauth2/callback"
-# redirect_url = "https://internalapp.yourcompany.com/oauth2/callback"
-
-## the http url(s) of the upstream endpoint. If multiple, routing is based on path
-upstreams = [
-    "http://127.0.0.1:8000/"
-]
-
-## Logging configuration
-logging_filename = "oauth2proxy.log"
-#logging_max_size = 100
-#logging_max_age = 7
-#logging_local_time = true
-#logging_compress = false
-#standard_logging = true
-#standard_logging_format = "[{{.Timestamp}}] [{{.File}}] {{.Message}}"
-#request_logging = true
-#request_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}"
-#auth_logging = true
-#auth_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}"
-## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
-# pass_basic_auth = true
-pass_user_headers=true
-pass_authorization_header=true
-
-## pass the request Host Header to upstream
-## when disabled the upstream Host is used as the Host Header
-pass_host_header=true
-
-## Email Domains to allow authentication for (this authorizes any email on this domain)
-## for more granular authorization use `authenticated_emails_file`
-## To authorize any email addresses use "*"
-email_domains = [
-    "domain.com"
-]
-
-## The OAuth Client ID, Secret
-# web client id
-client_id = "<client_id_here>"
-client_secret = "<client_secret_here>"
-provider="google"
-oidc_issuer_url="https://accounts.google.com"
-skip_jwt_bearer_tokens=true
-
-## Pass OAuth Access token to upstream via "X-Forwarded-Access-Token"
-pass_access_token=true
-
-## Authenticated Email Addresses File (one email per line)
-# authenticated_emails_file = ""
-
-## Htpasswd File (optional)
-## Additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -B" for bcrypt encryption
-## enabling exposes a username/login signin form
-# htpasswd_file = ""
-
-## bypass authentication for requests that match the method & path. Format: method=path_regex OR path_regex alone for all methods
-#skip_auth_routes = [
-#  "^/"
-#]
-## Templates
-## optional directory with custom sign_in.html and error.html
-# custom_templates_dir = ""
-
-## skip SSL checking for HTTPS requests
-# ssl_insecure_skip_verify = false
-
-## Cookie Settings
-## Name     - the cookie name
-## Secret   - the seed string for secure cookies; should be 16, 24, or 32 bytes
-##            for use with an AES cipher when cookie_refresh or pass_access_token
-##            is set
-## Domain   - (optional) cookie domain to force cookies to (ie: .yourcompany.com)
-## Expire   - (duration) expire timeframe for cookie
-## Refresh  - (duration) refresh the cookie when duration has elapsed after cookie was initially set.
-##            Should be less than cookie_expire; set to 0 to disable.
-##            On refresh, OAuth token is re-validated.
-##            (ie: 1h means tokens are refreshed on request 1hr+ after it was set)
-## Secure   - secure cookies are only sent by the browser of a HTTPS connection (recommended)
-## HttpOnly - httponly cookies are not readable by javascript (recommended)
-# cookie_name = "_oauth2_proxy"
-cookie_secret = "<random_32_byte_string>"
-# cookie_domains = ""
-# cookie_expire = "168h"
-# cookie_refresh = ""
-# cookie_secure = true
-# cookie_httponly = true
-
-#session_store_type = "redis"
-#redis_connection_url = "redis://127.0.0.1:6379/1"
\ No newline at end of file
diff --git a/docker/api_server/start.sh b/docker/api_server/start.sh
index 96990bcac..dedd0db70 100755
--- a/docker/api_server/start.sh
+++ b/docker/api_server/start.sh
@@ -10,4 +10,4 @@ fi
 /usr/local/bin/turbiniactl api_server
 
 # Don't exit
-while sleep 1000; do :; done
+while sleep 1000; do :; done
\ No newline at end of file
diff --git a/docker/api_server/turbinia_services.conf b/docker/api_server/turbinia_services.conf
deleted file mode 100644
index f90bd0dd6..000000000
--- a/docker/api_server/turbinia_services.conf
+++ /dev/null
@@ -1,17 +0,0 @@
-[program:turbinia-api-server]
-command=/usr/local/bin/turbiniactl api_server
-process_name=%(program_name)s
-directory=/tmp
-user=turbinia
-autostart=false
-autorestart=true
-redirect_stderr=true
-
-[program:oauth2-proxy]
-command=/usr/local/bin/oauth2-proxy --config /oauth2-proxy/oauth2_web.cfg
-process_name=%(program_name)s
-directory=/tmp
-user=turbinia
-autostart=false
-autorestart=true
-redirect_stderr=true
diff --git a/docker/oauth2_proxy/Dockerfile b/docker/oauth2_proxy/Dockerfile
index ce938e2a3..7aa1f2a30 100644
--- a/docker/oauth2_proxy/Dockerfile
+++ b/docker/oauth2_proxy/Dockerfile
@@ -27,4 +27,4 @@ USER turbinia
 CMD ["/home/turbinia/start.sh"]
 # Expose Prometheus and Oauth2 endpoints.
 EXPOSE 9200/tcp
-EXPOSE 8080/tcp
\ No newline at end of file
+EXPOSE 8080/tcp
diff --git a/docker/oauth2_proxy/oauth2_proxy.cfg b/docker/oauth2_proxy/oauth2_proxy.cfg
new file mode 100644
index 000000000..7349d9607
--- /dev/null
+++ b/docker/oauth2_proxy/oauth2_proxy.cfg
@@ -0,0 +1,47 @@
+# OAuth2-proxy ip/port
+http_address = "0.0.0.0:8080"
+
+# Turbinia API server URL
+upstreams = [
+    "http://127.0.0.1:8000/"
+]
+
+# Turbinia OIDC client id and secret from your provider
+client_id = "<CLIENT_ID>"
+client_secret = "<CLIENT_SECRET>"
+provider="oidc"
+
+# Do not change the redirect URI - this should be the OAuth2-proxy callback URL
+redirect_url = "http://localhost:8080/oauth2/callback"
+
+# Set oidc_issuer_url, oidc_jwks_url and scope to your provider specific URLs
+oidc_issuer_url="https://accounts.google.com"
+oidc_jwks_url="https://accounts.google.com/.well-known/openid-configuration"
+scope="https://www.googleapis.com/auth/userinfo.email"
+# Uncomment the line below and add any extra Client IDs if needed
+#oidc_extra_audiences = "<additional_client_ids>"
+
+# A local file with a list of authorized email addresses
+# One email address per line
+authenticated_emails_file = "/etc/turbinia/auth.txt"
+
+# Alternative authorization per domain instead of user emails
+#email_domains = [ "google.com"]
+
+# Secret used with the oauth2-proxy session cookies
+cookie_secret="<random_32_bytes_string>"
+
+# DO NOT EDIT BELOW THIS LINE
+skip_jwt_bearer_tokens=true
+force_code_challenge_method='S256'
+logging_filename = "/tmp/oauth2proxy.log"
+standard_logging = true
+standard_logging_format = "[{{.Timestamp}}] [{{.File}}] {{.Message}}"
+request_logging = true
+request_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}"
+auth_logging = true
+auth_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}"
+show_debug_on_error=true
+#pass_user_headers=true
+#pass_authorization_header=true
+#pass_access_token=true
diff --git a/docker/oauth2_proxy/start.sh b/docker/oauth2_proxy/start.sh
index 2f0471300..ab0c0ac60 100644
--- a/docker/oauth2_proxy/start.sh
+++ b/docker/oauth2_proxy/start.sh
@@ -15,4 +15,4 @@ fi
 oauth2-proxy --config /etc/turbinia/oauth2.conf 
 
 # Don't exit
-while sleep 1000; do :; done
\ No newline at end of file
+while sleep 1000; do :; done
diff --git a/docker/vscode/Dockerfile b/docker/vscode/Dockerfile
index 8e5364401..048b78c9c 100644
--- a/docker/vscode/Dockerfile
+++ b/docker/vscode/Dockerfile
@@ -25,15 +25,15 @@ RUN sed -i "s/bind .*/bind 127.0.0.1/g" /etc/redis/redis.conf
 
 # Install Google Cloud SDK
 RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list \
-&& curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg  add - \
-&& apt-get update -y \
-&& apt-get install google-cloud-sdk -y
+  && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg  add - \
+  && apt-get update -y \
+  && apt-get install google-cloud-sdk -y
 
 # Install NodeJS for frontend development
 RUN curl -sL https://deb.nodesource.com/setup_18.x -o nodesource_setup.sh
 RUN bash nodesource_setup.sh
 RUN apt-get update && apt-get install -y --no-install-recommends \
-    nodejs \
+  nodejs \
   && rm -rf /var/lib/apt/lists/*
 
 # Insert dummy command to override base image CMD
diff --git a/docker/vscode/vscode-launch.json b/docker/vscode/vscode-launch.json
index ed930b6bf..2cf319b03 100644
--- a/docker/vscode/vscode-launch.json
+++ b/docker/vscode/vscode-launch.json
@@ -14,6 +14,19 @@
         "PYTHONPATH": "${workspaceFolder}"
       }
     },
+    {
+      "name": "Turbinia API server",
+      "type": "python",
+      "request": "launch",
+      "program": "turbinia/turbiniactl.py",
+      "args": [
+        "api_server"
+      ],
+      "console": "integratedTerminal",
+      "env": {
+        "PYTHONPATH": "${workspaceFolder}"
+      }
+    },
     {
       "name": "Turbinia Celery Worker",
       "type": "python",
diff --git a/docs/user/install.md b/docs/user/install.md
index b11170adf..e1e4f3177 100644
--- a/docs/user/install.md
+++ b/docs/user/install.md
@@ -66,7 +66,7 @@ to get a shell with access to your GCP resources.
     Alternatively, if you wish to store this information in GCS instead, you
     can edit `main.tf` and change the `bucket` parameter to the GCS bucket
     you wish to keep this state information in. See the
-    [Terraform documentation](https://www.terraform.io/docs/commands/index.html)
+    [Terraform documentation](https://developer.hashicorp.com/terraform/cli/commands)
     for more information.
   - The current configuration does not enable alert notifications by default.
     Please see [here](#grafana-smtp-setup) for the instructions
diff --git a/requirements.txt b/requirements.txt
index 3e132193e..b1e199003 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -25,7 +25,6 @@ pyyaml>=5.4.1
 pyOpenSSL<=21.0.0
 redis<4.2
 six>=1.15.0
-starlette-oauth2-api
 urllib3[secure]
 uvicorn>=0.17.6
 vine>=5.0.0
diff --git a/turbinia/api/api_server.py b/turbinia/api/api_server.py
index fba04bebb..55b98f1b9 100644
--- a/turbinia/api/api_server.py
+++ b/turbinia/api/api_server.py
@@ -16,37 +16,38 @@
 
 import io
 import logging
-import pathlib
-import uvicorn
 import yaml
+import uvicorn
+import pathlib
 
 from fastapi import FastAPI
+from fastapi.responses import Response
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.routing import APIRoute
-from fastapi.responses import Response
-from fastapi.staticfiles import StaticFiles
-
-from starlette_oauth2_api import AuthenticateMiddleware
 
 from turbinia import config
-from turbinia.api.routes.router import router
+from turbinia.api.routes.router import api_router
+from turbinia.api.routes.ui import ui_router
+
+from turbinia.config import logger
 
-log = logging.getLogger('turbinia:api_server')
+logger.setup()
+log = logging.getLogger('turbinia')
 
 
-def get_application():
+def get_application() -> FastAPI:
   """Returns a FastAPI application object."""
-  description = '''Turbinia API server'''
-  _app = FastAPI(
+  description: str = 'Turbinia API server'
+  fastapi_app = FastAPI(
       title='Turbinia API Server', description=description, version='1.0.0',
       license_info={
           'name': 'Apache License 2.0',
           'url': 'https://www.apache.org/licenses/LICENSE-2.0.html'
-      }, routes=router.routes)
-  return _app
+      })
+  return fastapi_app
 
 
-def set_operation_ids(app: FastAPI):
+def set_operation_ids(app: FastAPI) -> None:
   """Simplify operation ID names to be used by client generator.
 
  This method must only be called after all routes have been initialized.
@@ -56,53 +57,6 @@ def set_operation_ids(app: FastAPI):
       route.operation_id = route.name
 
 
-def serve_static_content(app: FastAPI):
-  """Configure the application to serve static content.
-
-  This method must be called after all routes have been initialized.
-  """
-  this_path = pathlib.Path(__file__).parent.resolve()
-  web_content_path = this_path.parent.parent.joinpath('web/dist')
-  css_content_path = web_content_path.joinpath('css')
-  js_content_path = web_content_path.joinpath('js')
-  if web_content_path.exists():
-    try:
-      app.mount(
-          "/web", StaticFiles(directory=web_content_path, html=True), name="/")
-      app.mount("/css", StaticFiles(directory=css_content_path), name="/css")
-      app.mount("/js", StaticFiles(directory=js_content_path), name="/js")
-    except RuntimeError as exception:
-      log.error(
-          'Unable to serve Web UI static content: {0!s}'.format(exception))
-  else:
-    log.info(
-        'Web UI path {0!s} could not be found. Will not serve Web UI.'.format(
-            web_content_path))
-
-
-def configure_authentication_providers(app: FastAPI):
-  """Configure the application's authentication providers.
-
-  Example provider configuration using starlette-oauth2-pai:
-  """
-  app.add_middleware(
-      AuthenticateMiddleware,
-      providers={
-          'web-ui': {
-              'keys': 'https://www.googleapis.com/oauth2/v3/certs',
-              'issuer': 'https://accounts.google.com',
-              'audience': 'GOOGLE_WEB_CLIENT_ID',
-          },
-          'cli-client': {
-              'keys': 'https://www.googleapis.com/oauth2/v3/certs',
-              'issuer': 'https://accounts.google.com',
-              'audience': 'GOOGLE_NATIVE_CLIENT_ID',
-          }
-      },
-      public_paths={'/'},
-  )
-
-
 app = get_application()
 
 app.add_middleware(
@@ -113,20 +67,27 @@ def configure_authentication_providers(app: FastAPI):
     allow_headers=["*"],
 )
 
-if config.API_AUTHENTICATION_ENABLED:
-  configure_authentication_providers(app)
+app.include_router(api_router)
+app.include_router(ui_router)
 
 set_operation_ids(app)
-serve_static_content(app)
+
+
+@app.get(
+    '/openapi.yaml', tags=['OpenAPI Specification'], include_in_schema=False)
+def read_openapi_yaml():
+  """Serve the OpenAPI specification in YAML format."""
+  openapi_json = app.openapi()
+  yaml_s = io.StringIO()
+  yaml.dump(openapi_json, yaml_s)
+  return Response(yaml_s.getvalue(), media_type='text/yaml')
 
 
 class TurbiniaAPIServer:
   """Turbinia API server."""
 
-  def __init__(self, app=None, router=None):
-    self.app = app if app else get_application()
-    self.router = router
-    self.openapi_spec = self.app.openapi()
+  def __init__(self, app=None):
+    self.app: FastAPI = app if app else get_application()
 
   def start(self, app_name: str):
     """Runs the Turbinia API server
@@ -135,21 +96,12 @@ def start(self, app_name: str):
       app_name (str): module:app string used by Uvicorn
           to start the HTTP server.
     """
-    _config = config.LoadConfig()
+    _config: config = config.LoadConfig()
     uvicorn.run(
         app_name, host=_config.API_SERVER_ADDRESS, port=_config.API_SERVER_PORT,
-        log_level="info", reload=True)
-
-
-@app.get('/docs/openapi.yaml', tags=['OpenAPI Specification'])
-def read_openapi_yaml():
-  """Serve the OpenAPI specification in YAML format."""
-  openapi_json = app.openapi()
-  yaml_s = io.StringIO()
-  yaml.dump(openapi_json, yaml_s)
-  return Response(yaml_s.getvalue(), media_type='text/yaml')
+        log_level="info", reload=False, workers=4)
 
 
 if __name__ == '__main__':
-  api_server = TurbiniaAPIServer(app=app, router=router)
+  api_server = TurbiniaAPIServer(app=app)
   api_server.start('api_server:app')
diff --git a/turbinia/api/cli/.turbinia_api_config.json b/turbinia/api/cli/.turbinia_api_config.json
new file mode 100644
index 000000000..1df78775d
--- /dev/null
+++ b/turbinia/api/cli/.turbinia_api_config.json
@@ -0,0 +1,19 @@
+{
+	"default": {
+		"description": "This file is used by turbiniamgmt to determine the location of the API server and if authentication will be used. These options should match your Turbinia deployment.",
+		"comments": "By default, the credentials and client secrets files are located in the user's home directory.",
+		"API_SERVER_ADDRESS": "http://localhost",
+		"API_SERVER_PORT": 8000,
+		"API_AUTHENTICATION_ENABLED": false,
+		"CLIENT_SECRETS_FILENAME": ".client_secrets.json",
+		"CREDENTIALS_FILENAME": ".credentials_default.json"
+	},
+	"development": {
+		"description": "Development environment.",
+		"API_SERVER_ADDRESS": "http://localhost",
+		"API_SERVER_PORT": 8001,
+		"API_AUTHENTICATION_ENABLED": false,
+		"CLIENT_SECRETS_FILENAME": ".client_secrets_dev.json",
+		"CREDENTIALS_FILENAME": ".credentials_dev.json"
+	}
+}
\ No newline at end of file
diff --git a/turbinia/api/cli/LICENSE b/turbinia/api/cli/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/turbinia/api/cli/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/turbinia/api/cli/README.md b/turbinia/api/cli/README.md
new file mode 100644
index 000000000..5d4851e7e
--- /dev/null
+++ b/turbinia/api/cli/README.md
@@ -0,0 +1,1 @@
+# turbinia-client
diff --git a/turbinia/api/cli/requirements.txt b/turbinia/api/cli/requirements.txt
new file mode 100644
index 000000000..b03cb4c3c
--- /dev/null
+++ b/turbinia/api/cli/requirements.txt
@@ -0,0 +1,2 @@
+turbinia-api-client
+click
diff --git a/turbinia/api/cli/setup.py b/turbinia/api/cli/setup.py
new file mode 100644
index 000000000..127092f4a
--- /dev/null
+++ b/turbinia/api/cli/setup.py
@@ -0,0 +1,42 @@
+"""
+Turbinia client command-line tool.
+
+Install via 'pip install turbinia-client'
+"""
+from setuptools import setup, find_packages
+from pathlib import Path
+
+NAME = "turbinia-client"
+VERSION = "1.0.0"
+
+REQUIRES = [
+    "click",
+    "turbinia-api-client",
+]
+
+this_directory = Path(__file__).parent
+README = (this_directory / "README.md").read_text()
+
+setup(
+    name=NAME, 
+    version=VERSION,
+    description="Turbinia API Client command-line tool.",
+    long_description_content_type="text/markdown",
+    long_description=README,
+    keywords=[
+        "Turbinia Client", "Turbinia", "Turbinia API Server"
+    ], 
+    python_requires=">=3.6", 
+    install_requires=REQUIRES,
+    packages=find_packages(),
+    include_package_data=True,
+    entry_points={
+        'console_scripts': [
+            'turbinia-client=turbinia_client.turbiniacli_tool:main'
+        ]
+    },
+    license='Apache License, Version 2.0',
+    url='http://turbinia.plumbing/',
+    maintainer='Turbinia development team',
+    maintainer_email='turbinia-dev@googlegroups.com'
+)
diff --git a/turbinia/api/cli/turbinia_client/__init__.py b/turbinia/api/cli/turbinia_client/__init__.py
new file mode 100644
index 000000000..9b2deb267
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client / management tool."""
diff --git a/turbinia/api/cli/turbinia_client/core/__init__.py b/turbinia/api/cli/turbinia_client/core/__init__.py
new file mode 100644
index 000000000..736e8bbff
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/core/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
diff --git a/turbinia/api/cli/turbinia_client/core/commands.py b/turbinia/api/cli/turbinia_client/core/commands.py
new file mode 100644
index 000000000..6572653f2
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/core/commands.py
@@ -0,0 +1,288 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
+
+import os
+import logging
+import click
+import base64
+import tarfile
+
+from turbinia_api_client import exceptions
+from turbinia_api_client import api_client
+from turbinia_api_client.api import turbinia_requests_api
+from turbinia_api_client.api import turbinia_tasks_api
+from turbinia_api_client.api import turbinia_configuration_api
+from turbinia_api_client.api import turbinia_jobs_api
+from turbinia_api_client.api import turbinia_request_results_api
+
+from turbinia_client.core import groups
+from turbinia_client.helpers import formatter
+
+log = logging.getLogger('turbinia')
+
+
+@groups.config_group.command('list')
+@click.pass_context
+def get_config(ctx: click.Context) -> None:
+  """Gets Turbinia server configuration."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(client)
+  try:
+    api_response = api_instance.read_config()
+    formatter.echo_json(api_response)
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling get_config: {exception.body}')
+
+
+@groups.result_group.command('request')
+@click.pass_context
+@click.argument('request_id')
+def get_request_result(ctx: click.Context, request_id: str) -> None:
+  """Gets Turbinia request results / output files."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_request_results_api.TurbiniaRequestResultsApi(client)
+  try:
+    api_response = api_instance.get_request_output(
+        request_id, _preload_content=False, _request_timeout=(30, 30))
+    filename = f'{request_id}.tgz'
+    click.echo(f'Saving output for request {request_id} to: {filename}')
+    # Read the response and save into a local file.
+    with open(filename, 'wb') as file:
+      for chunk in api_response.read_chunked():
+        file.write(chunk)
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling get_request_output: {exception.body}')
+  except OSError as exception:
+    log.error(f'Unable to save file: {exception}')
+  except (ValueError, tarfile.ReadError, tarfile.CompressionError) as exception:
+    log.error(f'Error reading saved results file {filename}: {exception}')
+
+
+@groups.result_group.command('task')
+@click.pass_context
+@click.argument('task_id')
+def get_task_result(ctx: click.Context, task_id: str) -> None:
+  """Gets Turbinia task results / output files."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_request_results_api.TurbiniaRequestResultsApi(client)
+  try:
+    api_response = api_instance.get_task_output(
+        task_id, _preload_content=False, _request_timeout=(30, 30))
+    filename = f'{task_id}.tgz'
+    click.echo(f'Saving output for task {task_id} to: {filename}')
+
+    # Read the response and save into a local file.
+    with open(filename, 'wb') as file:
+      for chunk in api_response.read_chunked():
+        file.write(chunk)
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling get_task_output: {exception.body}')
+  except OSError as exception:
+    log.error(f'Unable to save file: {exception}')
+  except (ValueError, tarfile.ReadError, tarfile.CompressionError) as exception:
+    log.error(f'Error reading saved results file {filename}: {exception}')
+
+
+@groups.jobs_group.command('list')
+@click.pass_context
+def get_jobs(ctx: click.Context) -> None:
+  """Gets Turbinia jobs list."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_jobs_api.TurbiniaJobsApi(client)
+  try:
+    api_response = api_instance.read_jobs()
+    click.echo(api_response)
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling read_jobs: {exception.body}')
+
+
+@groups.status_group.command('request')
+@click.pass_context
+@click.argument('request_id')
+@click.option(
+    '--json_dump', '-j', help='Generates JSON output.', is_flag=True,
+    required=False)
+def get_request(ctx: click.Context, request_id: str, json_dump: bool) -> None:
+  """Gets Turbinia request status."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_requests_api.TurbiniaRequestsApi(client)
+  if request_id == 'summary':
+    click.echo(
+        'Oops! "summary" is not a valid request identifier. '
+        'Did you mean to run "turbinia-client status summary" instead?')
+    return
+  try:
+    api_response = api_instance.get_request_status(request_id)
+    if json_dump:
+      formatter.echo_json(api_response)
+    else:
+      report = formatter.RequestMarkdownReport(api_response).generate_markdown()
+      click.echo(report)
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling get_request_status: {exception.body}')
+
+
+@groups.status_group.command('workers')
+@click.pass_context
+@click.option(
+    '--json_dump', '-j', help='Generates JSON output.', is_flag=True,
+    required=False)
+def get_workers(ctx: click.Context, json_dump: bool) -> None:
+  click.echo('Not implemented yet.')
+
+
+@groups.status_group.command('summary')
+@click.pass_context
+@click.option(
+    '--json_dump', '-j', help='Generates JSON output.', is_flag=True,
+    required=False)
+def get_requests_summary(ctx: click.Context, json_dump: bool) -> None:
+  """Gets a summary of all Turbinia requests."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_requests_api.TurbiniaRequestsApi(client)
+  try:
+    api_response = api_instance.get_requests_summary()
+    if json_dump:
+      formatter.echo_json(api_response)
+    else:
+      report = formatter.SummaryMarkdownReport(api_response).generate_markdown()
+      click.echo(report)
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling get_requests_summary: {exception.body}')
+
+
+@groups.status_group.command('task')
+@click.pass_context
+@click.argument('task_id')
+@click.option(
+    '--json_dump', '-j', help='Generates JSON output.', is_flag=True,
+    required=False)
+def get_task(ctx: click.Context, task_id: str, json_dump: bool) -> None:
+  """Gets Turbinia task status."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_tasks_api.TurbiniaTasksApi(client)
+  try:
+    api_response = api_instance.get_task_status(task_id)
+    if json_dump:
+      formatter.echo_json(api_response)
+    else:
+      report = formatter.TaskMarkdownReport(api_response).generate_markdown()
+      click.echo(report)
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling get_task_status: {exception.body}')
+
+
+@click.pass_context
+def create_request(ctx: click.Context, *args: int, **kwargs: int) -> None:
+  """Creates and submits a new Turbinia request."""
+  client: api_client.ApiClient = ctx.obj.api_client
+  api_instance = turbinia_requests_api.TurbiniaRequestsApi(client)
+  evidence_name = ctx.command.name
+
+  # Normalize the evidence class name from lowercase to the original name.
+  evidence_name = ctx.obj.normalize_evidence_name(evidence_name)
+  # Build request and reqeust_options objects to send to the API server.
+  request_options = list(ctx.obj.request_options.keys())
+  request = {'evidence': {'type': evidence_name}, 'request_options': {}}
+
+  if 'googlecloud' in evidence_name:
+    api_instance_config = turbinia_configuration_api.TurbiniaConfigurationApi(
+        client)
+    cloud_provider = api_instance_config.read_config()['CLOUD_PROVIDER']
+    if cloud_provider != 'GCP':
+      log.error(
+          f'The evidence type {evidence_name} is Google Cloud only and '
+          f'the configured provider for this Turbinia instance is '
+          f'{cloud_provider}.')
+      return
+
+  for key, value in kwargs.items():
+    # If the value is not empty, add it to the request.
+    if kwargs.get(key):
+      # Check if the key is for evidence or request_options
+      if not key in request_options:
+        request['evidence'][key] = value
+      elif key in ('jobs_allowlist', 'jobs_denylist'):
+        jobs_list = value.split(',')
+        request['request_options'][key] = jobs_list
+      else:
+        request['request_options'][key] = value
+
+  if all(key in request['request_options']
+         for key in ('recipe_name', 'recipe_data')):
+    log.error('You can only provide one of recipe_data or recipe_name')
+    return
+
+  recipe_name = request['request_options'].get('recipe_name')
+  if recipe_name:
+    if not recipe_name.endswith('.yaml'):
+      recipe_name = f'{recipe_name}.yaml'
+    # Fallback path for the recipe would be TURBINIA_CLI_CONFIG_PATH/recipe_name
+    # This is the same path where the client configuration is loaded from.
+    recipe_path_fallback = os.path.expanduser(ctx.obj.config_path)
+    recipe_path_fallback = os.path.join(recipe_path_fallback, recipe_name)
+
+    if os.path.isfile(recipe_name):
+      recipe_path = recipe_name
+    elif os.path.isfile(recipe_path_fallback):
+      recipe_path = recipe_path_fallback
+    else:
+      log.error(f'Unable to load recipe {recipe_name}.')
+      return
+
+    try:
+      with open(recipe_path, 'r', encoding='utf-8') as recipe_file:
+        # Read the file and convert to base64 encoded bytes.
+        recipe_bytes = recipe_file.read().encode('utf-8')
+        recipe_data = base64.b64encode(recipe_bytes)
+    except OSError as exception:
+      log.error(f'Error opening recipe file {recipe_path}: {exception}')
+      return
+    except TypeError as exception:
+      log.error(f'Error converting recipe data to Base64: {exception}')
+      return
+    # We found the recipe file, so we will send it to the API server
+    # via the recipe_data parameter. To do so, we need to pop recipe_name
+    # from the request so that we only have recipe_data.
+    request['request_options'].pop('recipe_name')
+    # recipe_data should be a UTF-8 encoded string.
+    request['request_options']['recipe_data'] = recipe_data.decode('utf-8')
+
+  # Send the request to the API server.
+  try:
+    log.info(f'Sending request: {request}')
+    api_response = api_instance.create_request(request)
+    log.info(f'Received response: {api_response}')
+  except exceptions.ApiException as exception:
+    log.error(
+        f'Received status code {exception.status} '
+        f'when calling create_request: {exception.body}')
+  except (TypeError, exceptions.ApiTypeError) as exception:
+    log.error(f'The request object is invalid. {exception}')
diff --git a/turbinia/api/cli/turbinia_client/core/groups.py b/turbinia/api/cli/turbinia_client/core/groups.py
new file mode 100644
index 000000000..9521cd26b
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/core/groups.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
+
+import click
+
+
+@click.group('config')
+def config_group():
+  """Get Turbinia configuration."""
+
+
+@click.group('status')
+def status_group():
+  """Get Turbinia request/task status."""
+
+
+@click.group('result')
+def result_group():
+  """Get Turbinia task or request results."""
+
+
+@click.group('jobs')
+def jobs_group():
+  """Get a list of enabled Turbinia jobs."""
+
+
+@click.group('submit')
+def submit_group():
+  """Submit new requests to the Turbinia API server."""
diff --git a/turbinia/api/cli/turbinia_client/factory/__init__.py b/turbinia/api/cli/turbinia_client/factory/__init__.py
new file mode 100644
index 000000000..9b2deb267
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/factory/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client / management tool."""
diff --git a/turbinia/api/cli/turbinia_client/factory/factory.py b/turbinia/api/cli/turbinia_client/factory/factory.py
new file mode 100644
index 000000000..322868527
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/factory/factory.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client / management tool."""
+
+from abc import ABC, abstractmethod
+from typing import TypeVar, Type, Tuple, Any, List
+
+import logging
+import click
+
+from turbinia_client.helpers import click_helper
+from turbinia_client.core.commands import create_request
+
+T = TypeVar('T', bound='FactoryInterface')
+
+log = logging.getLogger('turbinia')
+
+
+class FactoryInterface(ABC):
+  """Factory Interface."""
+
+  @classmethod
+  def get_evidence_attributes(
+      cls: Type[T], evidence_mapping: dict) -> List[Tuple[str, Any, dict]]:
+    """Retrieves a list of evidence attribute metadata.
+
+    Args:
+      evidence_mapping (dict): A dictionary of Evidence types
+          and attributes retrieved from the Turbinia API server.
+
+    Returns:
+      list: A list of 3-tuples comprising the necessary parameters to create
+          new click.Option objects.
+    """
+    map_types = dict(str=str, int=int)
+    options_meta = []
+    for evidence_name, options in evidence_mapping.items():
+      for option_name, option_parameters in options.items():
+        param_decls = (['--' + option_name], option_name)
+        param_type = map_types.get(option_parameters.get('type'))
+        attrs = dict(
+            required=option_parameters.get('required'), type=param_type)
+        options_meta.append((evidence_name, param_decls, attrs))
+    return options_meta
+
+  @classmethod
+  @abstractmethod
+  def create_dynamic_objects(
+      cls: Type[T], name: str, evidence_mapping: dict,
+      request_options: dict) -> None:
+    """Creates one or more click.Command or click.Options objects.
+    
+    Args:
+      evidence_mapping (dict): A dictionary of Evidence types
+          and attributes retrieved from the Turbinia API server.
+      request_options (dict): A dictionary of BaseRequestOptions
+          attributes and types retrieved from the Turbinia APi server.
+    """
+    raise NotImplementedError
+
+  @classmethod
+  @abstractmethod
+  def create_object(
+      cls: Type[T], name: str, params: Tuple[Any, dict], callback: Any):
+    """Creates a click.Command or click.Options object.
+
+    Args:
+      name (str): Object name.
+      params (tuple): A tuple comprised of arguments needed to create
+          click.Option or click.Command objects.
+      callback: A callback function.
+    """
+    raise NotImplementedError
+
+
+class OptionFactory(FactoryInterface):
+  """Command line options factory class."""
+
+  @classmethod
+  def append_request_option_objects(
+      cls: Type[T], params: List[click.Option],
+      request_options: dict = None) -> None:
+    """Appends options to a list of parameters for a click.Command object."""
+    for request_option in list(request_options.keys()):
+      request_parameters = click_helper.generate_option_parameters(
+          request_option)
+      params.append(OptionFactory.create_object(params=request_parameters))
+
+  @classmethod
+  def create_dynamic_objects(
+      cls, name: str = None, evidence_mapping: dict = None,
+      request_options: dict = None) -> List[click.Option]:
+    """Creates a list of click.Option objects."""
+    option_objects = []
+    if not name:
+      log.info('An evidence type was not provided.')
+      return []
+    options = OptionFactory.get_evidence_attributes(evidence_mapping)
+    for evidence_name, param_decls, attrs in options:
+      if name == evidence_name:
+        log.debug('Creating option for {0:s}'.format(name))
+        click_option = OptionFactory.create_object(params=(param_decls, attrs))
+        option_objects.append(click_option)
+    return option_objects
+
+  @classmethod
+  def create_object(
+      cls: Type[T], name: str = None, params: Tuple[Any, dict] = None,
+      callback=None) -> click.Option:
+    """Creates a click.Option object with specified parameters."""
+    option = None
+    if params:
+      (param_decls, attrs) = params
+      option = click.Option(*param_decls, **attrs)
+    return option
+
+
+class CommandFactory(FactoryInterface):
+  """Commands factory class."""
+
+  @classmethod
+  def create_dynamic_objects(
+      cls, name: str = None, evidence_mapping: dict = None,
+      request_options: dict = None) -> List[click.Command]:
+    """Creates a list of click.Command objects."""
+
+    command_objects = []
+    for evidence_name in list(evidence_mapping.keys()):
+      log.debug('Creating command for {0:s}'.format(evidence_name))
+      # Use lowercase evidence names.
+      evidence_name_lower = evidence_name.lower()
+      params = OptionFactory.create_dynamic_objects(
+          name=evidence_name, evidence_mapping=evidence_mapping,
+          request_options=request_options)
+      OptionFactory.append_request_option_objects(params, request_options)
+      cmd = CommandFactory.create_object(
+          name=evidence_name_lower, params=params, callback=create_request)
+      command_objects.append(cmd)
+    return command_objects
+
+  @classmethod
+  def create_object(
+      cls: Type[T], name: str = None, params: Tuple[Any, dict] = None,
+      callback=None) -> click.Command:
+    """Creates a click.Command object with specified parameters."""
+    return click.Command(name=name, params=params, callback=callback)
diff --git a/turbinia/api/cli/turbinia_client/helpers/__init__.py b/turbinia/api/cli/turbinia_client/helpers/__init__.py
new file mode 100644
index 000000000..736e8bbff
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/helpers/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
diff --git a/turbinia/api/cli/turbinia_client/helpers/auth_helper.py b/turbinia/api/cli/turbinia_client/helpers/auth_helper.py
new file mode 100644
index 000000000..6f111c4e5
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/helpers/auth_helper.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
+
+import logging
+import os
+import sys
+
+from google_auth_oauthlib import flow
+from google.oauth2.credentials import Credentials
+from google.auth.transport.requests import Request
+from google.auth import exceptions as google_exceptions
+
+log = logging.getLogger('turbinia')
+
+
+def get_oauth2_credentials(credentials_path, client_secrets_path):
+  """Authenticates the user using Google OAuth services."""
+  scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email']
+  credentials = None
+
+  # Load credentials file if it exists
+  if os.path.exists(credentials_path):
+    try:
+      credentials = Credentials.from_authorized_user_file(
+          credentials_path, scopes)
+    except ValueError as exception:
+      log.error(f'Error loading credentials: {exception!s}')
+    # Refresh credentials using existing refresh_token
+    if credentials and credentials.refresh_token:
+      log.debug('Found a refresh token. Requesting new id_token...')
+      try:
+        credentials.refresh(Request())
+      except google_exceptions.RefreshError as exception:
+        log.error(f'Error refreshing credentials: {exception!s}')
+  else:
+    # No credentials file, acquire new credentials from secrets file.
+    log.info('Could not find existing credentials. Requesting new tokens.')
+    try:
+      appflow = flow.InstalledAppFlow.from_client_secrets_file(
+          client_secrets_path, scopes)
+    except FileNotFoundError as exception:
+      log.error(f'Client secrets file not found: {exception!s}')
+      sys.exit(1)
+
+    log.info(
+        'Starting local HTTP server on localhost:8888 for OAUTH flow. '
+        'If running turbinia-client remotely over SSH you will need to tunnel '
+        'port 8888.')
+    appflow.run_local_server(host='localhost', port=8888)
+    credentials = appflow.credentials
+
+    # Save credentials
+    with open(credentials_path, 'w', encoding='utf-8') as token:
+      token.write(credentials.to_json())
+
+  return credentials.id_token
diff --git a/turbinia/api/cli/turbinia_client/helpers/click_helper.py b/turbinia/api/cli/turbinia_client/helpers/click_helper.py
new file mode 100644
index 000000000..c4556b7bc
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/helpers/click_helper.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
+
+from typing import Tuple, Sequence
+
+
+def generate_option_parameters(
+    option_name: str) -> Tuple[Tuple[Sequence[str], str], dict]:
+  """Builds click.Option or click.Command arguments based on a given parameter 
+      name.
+  """
+  return ((['--' + option_name], option_name), {'required': False, 'type': str})
diff --git a/turbinia/api/cli/turbinia_client/helpers/formatter.py b/turbinia/api/cli/turbinia_client/helpers/formatter.py
new file mode 100644
index 000000000..b5b7fa5c0
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/helpers/formatter.py
@@ -0,0 +1,316 @@
+#!/usr/bin/env python
+#
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Methods for formatting text."""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from click import echo as click_echo
+
+import logging
+import json
+
+log = logging.getLogger('turbinia')
+
+
+def echo_json(json_data: dict) -> None:
+  """Pretty print JSON data."""
+  if isinstance(json_data, dict):
+    click_echo(json.dumps(json_data, indent=2))
+
+
+class MarkdownReportComponent(ABC):
+  """Components for generating Turbinia request/task
+      markdown reports.
+  """
+
+  def __init__(self):
+    """Instantiates a MarkdownReportComponent object."""
+    self._components: list(MarkdownReportComponent) = []
+    self._parent: MarkdownReportComponent = None
+    self._report: str = None
+
+  @property
+  def components(self):
+    """Returns the components list."""
+    return self._components
+
+  @property
+  def report(self):
+    """Returns the markdown report text."""
+    return self._report
+
+  @report.setter
+  def report(self, report):
+    self._report = report
+
+  @property
+  def parent(self) -> MarkdownReportComponent:
+    """Returns the parent object."""
+    return self._parent
+
+  @parent.setter
+  def parent(self, parent: MarkdownReportComponent):
+    self._parent = parent
+
+  def bold(self, text):
+    """Formats text as bold in Markdown format.
+
+    Args:
+        text(string): Text to format
+
+    Return:
+        string: Formatted text.
+    """
+    return f'**{text.strip():s}**'
+
+  def heading1(self, text):
+    """Formats text as heading 1 in Markdown format.
+
+    Args:
+        text(string): Text to format
+
+    Return:
+        string: Formatted text.
+    """
+    return f'# {text.strip():s}'
+
+  def heading2(self, text):
+    """Formats text as heading 2 in Markdown format.
+
+    Args:
+        text(string): Text to format
+
+    Return:
+        string: Formatted text.
+    """
+    return f'## {text.strip():s}'
+
+  def heading3(self, text):
+    """Formats text as heading 3 in Markdown format.
+
+    Args:
+        text(string): Text to format
+
+    Return:
+        string: Formatted text.
+    """
+    return f'### {text.strip():s}'
+
+  def heading4(self, text):
+    """Formats text as heading 4 in Markdown format.
+
+    Args:
+        text(string): Text to format
+
+    Return:
+        string: Formatted text.
+    """
+    return f'#### {text.strip():s}'
+
+  def heading5(self, text):
+    """Formats text as heading 5 in Markdown format.
+     Args:
+        text(string): Text to format
+     Return:
+        string: Formatted text.
+    """
+    return f'##### {text.strip():s}'
+
+  def bullet(self, text, level=1):
+    """Formats text as a bullet in Markdown format.
+
+      Args:
+        text(string): Text to format
+      Return:
+        string: Formatted text.
+    """
+    return f"{'    ' * (level - 1):s}* {text.strip():s}"
+
+  def code(self, text):
+    """Formats text as code in Markdown format.
+
+      Args:
+          text(string): Text to format
+
+     Return:
+          string: Formatted text.
+    """
+    return f'`{text.strip():s}`'
+
+  def add(self, component: MarkdownReportComponent) -> None:
+    """Adds a MarkdownReportComponent object to the components list.
+
+    This method should additionally set the parent object.
+    """
+    pass
+
+  def add_components(self, components: list[MarkdownReportComponent]) -> None:
+    """Adds multiple MarkdownReportComponent objects to the components list."""
+    pass
+
+  def remove(self, component: MarkdownReportComponent) -> None:
+    """Removes a MarkdownReportComponent object from the components list.
+
+    This method should set the component's object to None.
+    """
+    pass
+
+  @abstractmethod
+  def generate_markdown(self) -> str:
+    pass
+
+
+class TaskMarkdownReport(MarkdownReportComponent):
+  """Turbinia Task markdown report."""
+
+  def __init__(self, request_data: dict = None):
+    """Initialize TaskMarkdownReport"""
+    super().__init__()
+    self._request_data: dict = request_data
+
+  def generate_markdown(self) -> str:
+    """Generate a markdown report."""
+    report: list[str] = []
+    task: dict = self._request_data
+    if not task:
+      return ''
+
+    try:
+      report.append(self.heading2(task.get('name')))
+      line = f"{self.bold('Evidence:'):s} {task.get('evidence_name')!s}"
+      report.append(self.bullet(line))
+      line = f"{self.bold('Status:'):s} {task.get('status'):s}"
+      report.append(self.bullet(line))
+      report.append(self.bullet(f"Task Id: {task.get('id')!s}"))
+      report.append(
+          self.bullet(f"Executed on worker {task.get('worker_name')!s}"))
+      if task.get('report_data'):
+        report.append('')
+        report.append(self.heading3('Task Reported Data'))
+        report.extend(task.get('report_data').splitlines())
+      report.append('')
+      report.append(self.heading3('Saved Task Files:'))
+
+      saved_paths = task.get('saved_paths')
+      if saved_paths:
+        for path in saved_paths:
+          report.append(self.bullet(self.code(path)))
+          report.append('')
+    except TypeError as exception:
+      log.warning(f'Error formatting the Markdown report: {exception!s}')
+
+    self.report = '\n'.join(report)
+    return self.report
+
+
+class RequestMarkdownReport(MarkdownReportComponent):
+  """Turbinia Request Markdown report."""
+
+  def __init__(self, request_data: dict):
+    """Initializes a RequestMarkdownReport object."""
+    super().__init__()
+    self._request_data: dict = request_data
+
+    tasks = [TaskMarkdownReport(task) for task in request_data.get('tasks')]
+    self.add_components(tasks)
+
+  def add(self, component: MarkdownReportComponent) -> None:
+    if component:
+      self.components.append(component)
+      component.parent = self
+
+  def remove(self, component: MarkdownReportComponent) -> None:
+    self.components.remove(component)
+    component.parent = None
+
+  def add_components(self, components: list[MarkdownReportComponent]) -> None:
+    if components:
+      for component in components:
+        self.components.append(component)
+        component.parent = self
+
+  def generate_markdown(self) -> str:
+    """Generates a Markdown version of Requests results."""
+    report: list[str] = []
+    request_dict: dict = self._request_data
+    if not request_dict:
+      return ''
+
+    try:
+      report.append(
+          self.heading2(f"Request ID: {request_dict.get('request_id')!s}"))
+      report.append(
+          self.bullet(
+              f"Last Update: {request_dict.get('last_task_update_time')!s}"))
+      report.append(
+          self.bullet(f"Requester: {request_dict.get('requester')!s}"))
+      report.append(self.bullet(f"Reason: {request_dict.get('reason')!s}"))
+      report.append(self.bullet(f"Status: {request_dict.get('status')!s}"))
+      report.append(
+          self.bullet(f"Failed tasks: {request_dict.get('failed_tasks'):d}"))
+      report.append(
+          self.bullet(f"Running tasks: {request_dict.get('running_tasks'):d}"))
+      report.append(
+          self.bullet(
+              f"Successful tasks: {request_dict.get('successful_tasks'):d}"))
+      report.append(
+          self.bullet(f"Task Count: {request_dict.get('task_count'):d}"))
+      report.append(
+          self.bullet(f"Queued tasks: {request_dict.get('queued_tasks'):d}"))
+      report.append('')
+    except TypeError as exception:
+      log.warning(f'Error formatting the Markdown report: {exception!s}')
+
+    for task in self.components:
+      report.append(task.generate_markdown())
+
+    self.report = '\n'.join(report)
+    return self.report
+
+
+class SummaryMarkdownReport(MarkdownReportComponent):
+  """A markdown report summary of all Turbinia Requests."""
+
+  def __init__(self, requests_summary: list[dict]):
+    """Initialize SummaryMarkdownReport."""
+    super().__init__()
+    self._requests_summary = requests_summary
+
+  def generate_markdown(self) -> str:
+    """Generate a Markdown version of Requests summary results."""
+    if not self._requests_summary:
+      return ''
+    report: list[str] = []
+    requests_status_list = self._requests_summary.get('requests_status')
+    for request_dict in requests_status_list:
+      request_report = RequestMarkdownReport(request_dict).generate_markdown()
+      report.append(request_report)
+
+    self.report = '\n'.join(report)
+    return self.report
+
+
+class WorkersMarkdownReport(MarkdownReportComponent):
+  """A markdown report of all tasks for a specific worker."""
+
+  def __init__(self, request_data: dict):
+    super().__init__()
+    self._request_data: dict = request_data
+
+  def generate_markdown(self) -> str:
+    """Generates a Markdown version of tasks per worker."""
+    raise NotImplementedError
diff --git a/turbinia/api/cli/turbinia_client/turbiniacli.py b/turbinia/api/cli/turbinia_client/turbiniacli.py
new file mode 100644
index 000000000..244970957
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/turbiniacli.py
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
+
+import os
+import sys
+import logging
+import json
+import turbinia_api_client
+from typing import Union
+
+from turbinia_api_client.api import turbinia_configuration_api
+
+from turbinia_client.helpers import auth_helper
+
+log = logging.getLogger('turbinia')
+
+
+class TurbiniaCli:
+  """Turbinia API client tool.
+
+  Attributes:
+    _api_client (turbinia_api_client.ApiClient): An instance of the ApiClient
+        class.
+    _config (turbinia_api_client.Configuration): An instance of the
+        Configuration class.
+    _config_dict (dict): Contains all the cli configuration keys and values
+        from the config file.
+    _evidence_mapping (dict): Internal dictionary used to map Evidence object
+        names to Click commands.
+    _request_options (dict): Internal dictinoary used to map Request options
+        to Click options.
+    api_server_address (str): URL for the API server.
+    api_server_port (int): Port for the API server.
+    api_authentication_enabled (bool): If set to True, the client will attempt
+        to authenticate.
+    client_secrets_path (str): Full path to the client secrets file used for
+        authentication.
+    config_instance (str): A name defined in the configuration file that holds
+        all the configuration options for a specific Turbinia deployment.
+    config_path (str): Full path to the directory containing the cli tool's
+        configuration file.
+    credentials_path (str): Full path to the credentials cache file used for
+        re-authentication.
+  """
+
+  def __init__(self, config_instance=None, config_path=None):
+    self._api_client: turbinia_api_client.ApiClient = None
+    self._config: turbinia_api_client.Configuration = None
+    self._config_dict: dict = {}
+    self._evidence_mapping: dict = {}
+    self._request_options: dict = {}
+    self.api_server_address: str = None
+    self.api_server_port: int = None
+    self.api_authentication_enabled: str = None
+    self.client_secrets_path: str = None
+    self.config_instance: str = config_instance
+    self.config_path: str = config_path
+    self.credentials_path: str = None
+
+  def setup(self) -> None:
+    """Sets up necessary attributes and preflight requests to the API server.
+
+    The preflight requests get_evidence_arguments and get_request_options
+    are used to dynamically build command options.
+    """
+
+    # Initialize attributes from config file.
+    self.read_api_configuration()
+
+    if not self.config:
+      host = f'{self.api_server_address:s}:{self.api_server_port:d}'
+      self.config = self.default_config(host)
+    if not self.api_client:
+      self.api_client = self.default_api_client(self.config)
+
+    if self.api_authentication_enabled:
+      log.info(
+          f'Authentication is enabled. Using client_secrets file at: '
+          f'{self.client_secrets_path:s} and caching credentials at: '
+          f'{self.credentials_path:s}')
+      self.config.access_token = auth_helper.get_oauth2_credentials(
+          self.credentials_path, self.client_secrets_path)
+
+    log.info(
+        f'Using configuration instance name -> {self.config_instance:s}'
+        f' with host {self.api_server_address:s}:{self.api_server_port:d}')
+    try:
+      self.evidence_mapping = self.get_evidence_arguments()
+      self.request_options = self.get_request_options()
+    except turbinia_api_client.ApiException as exception:
+      log.error(
+          'Error while attempting to contact the API server during setup: %s',
+          exception)
+      sys.exit(-1)
+
+  @property
+  def api_client(self):
+    """Returns an API client object."""
+    return self._api_client
+
+  @api_client.setter
+  def api_client(self, api_client):
+    self._api_client = api_client
+
+  @property
+  def config(self):
+    """Returns an API client configuration object."""
+    return self._config
+
+  @config.setter
+  def config(self, config):
+    self._config = config
+
+  @property
+  def config_dict(self):
+    """Returns an API client configuration object."""
+    return self._config_dict
+
+  @config_dict.setter
+  def config_dict(self, config_dict):
+    self._config_dict = config_dict
+
+  @property
+  def evidence_mapping(self):
+    """Returns a dictionary that contains Evidence object data."""
+    return self._evidence_mapping
+
+  @evidence_mapping.setter
+  def evidence_mapping(self, evidence_mapping):
+    self._evidence_mapping = evidence_mapping
+
+  @property
+  def request_options(self):
+    """Returns a dictionary that contains RequestOptions object data."""
+    return self._request_options
+
+  @request_options.setter
+  def request_options(self, request_options):
+    self._request_options = request_options
+
+  def default_api_client(
+      self, config: turbinia_api_client.Configuration
+  ) -> turbinia_api_client.ApiClient:
+    """Default value for API client instance."""
+    return turbinia_api_client.ApiClient(configuration=config)
+
+  def default_config(self, host: str) -> turbinia_api_client.Configuration:
+    """Default value for API client configuration."""
+    return turbinia_api_client.Configuration(host=host)
+
+  def get_evidence_arguments(self, evidence_name=None) -> dict:
+    """Gets arguments for Evidence types."""
+    api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(
+        self.api_client)
+    api_response = None
+    if evidence_name:
+      api_response = api_instance.get_evidence_attributes_by_name(evidence_name)
+    else:
+      api_response = api_instance.get_evidence_types()
+
+    self.evidence_mapping: dict = api_response
+    return api_response
+
+  def get_request_options(self) -> dict:
+    """Gets BaseRequestOptions attributes."""
+    api_response = None
+    api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(
+        self.api_client)
+    api_response = api_instance.get_request_options()
+
+    return api_response
+
+  def read_api_configuration(self) -> None:
+    """Reads the configuration file to obtain the API server URI."""
+    if self.config_path == '~':
+      client_config_path = os.path.expanduser('~')
+      client_config_path = os.path.join(
+          client_config_path, '.turbinia_api_config.json')
+    else:
+      client_config_path = os.path.join(
+          self.config_path, '.turbinia_api_config.json')
+    try:
+      with open(client_config_path, encoding='utf-8') as config:
+        config_data = json.loads(config.read())
+        config_dict = config_data.get(self.config_instance)
+        if not config_dict:
+          log.error(f'Error reading configuration key {self.config_instance:s}')
+          sys.exit(-1)
+        self.api_server_address = config_dict['API_SERVER_ADDRESS']
+        self.api_server_port = config_dict['API_SERVER_PORT']
+        self.api_authentication_enabled = config_dict[
+            'API_AUTHENTICATION_ENABLED']
+        credentials_filename = config_dict['CREDENTIALS_FILENAME']
+        client_secrets_filename = config_dict['CLIENT_SECRETS_FILENAME']
+        home_path = os.path.expanduser('~')
+        self.credentials_path = os.path.join(home_path, credentials_filename)
+        self.client_secrets_path = os.path.join(
+            home_path, client_secrets_filename)
+        self.config_dict = config_dict
+    except (IOError, FileNotFoundError) as exception:
+      log.error(f'Unable to read the configuration file {exception!s}')
+      sys.exit(-1)
+    except json.JSONDecodeError as exception:
+      log.error(f'Error decoding configuration file: {exception!s}')
+      sys.exit(-1)
+    except KeyError as exception:
+      log.error(f'Required configuration key not found: {exception!s}')
+      sys.exit(-1)
+
+  def normalize_evidence_name(self, evidence_name_low: str) -> Union[str, None]:
+    """Converts a lowercase evidence name into the proper class name."""
+    evidence_name = None
+    evidence_name_low = evidence_name_low.lower()
+    if evidence_name_low:
+      for name in self.evidence_mapping.keys():
+        if evidence_name_low == name.lower():
+          evidence_name = name
+          break
+
+    if not evidence_name:
+      log.error(f'Unable to map {evidence_name_low} to a valid Evidence name.')
+      sys.exit(-1)
+
+    return evidence_name
diff --git a/turbinia/api/cli/turbinia_client/turbiniacli_tool.py b/turbinia/api/cli/turbinia_client/turbiniacli_tool.py
new file mode 100644
index 000000000..8bee66c8f
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/turbiniacli_tool.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
+
+import os
+import sys
+import logging
+import click
+
+from urllib3 import exceptions as urllib3_exceptions
+
+from turbinia_client.turbiniacli import TurbiniaCli
+from turbinia_client.core import groups
+from turbinia_client.factory import factory
+
+_LOGGER_FORMAT = '%(asctime)s %(levelname)s %(name)s - %(message)s'
+logging.basicConfig(format=_LOGGER_FORMAT, level=logging.INFO)
+log = logging.getLogger('turbinia')
+
+
+@click.group(context_settings={
+    'help_option_names': ['-h', '--help'],
+})
+@click.option(
+    '--config_instance', '-c', help='A Turbinia instance configuration name.',
+    show_default=True, show_envvar=True, type=str,
+    default=lambda: os.environ.get('TURBINIA_CONFIG_INSTANCE', 'default'))
+@click.option(
+    '--config_path', '-p', help='Path to the .turbinia_api_config.json file..',
+    show_default=True, show_envvar=True, type=str,
+    default=lambda: os.environ.get('TURBINIA_API_CONFIG_PATH', '~'))
+@click.pass_context
+def cli(ctx: click.Context, config_instance: str, config_path: str) -> None:
+  """Turbinia API command-line tool (turbinia-client).
+  
+  \b                         ***    ***                                       
+  \b                          *          *                                      
+  \b                     ***             ******                                 
+  \b                    *                      *                                
+  \b                    **      *   *  **     ,*                                
+  \b                      *******  * ********                                  
+  \b                             *  * *                                         
+  \b                             *  * *                                         
+  \b                             %%%%%%                                         
+  \b                             %%%%%%                                         
+  \b                    %%%%%%%%%%%%%%%       %%%%%%                            
+  \b              %%%%%%%%%%%%%%%%%%%%%      %%%%%%%                            
+  \b %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  ** *******       
+  \b %%                                                   %%  ***************   
+  \b %%                                (%%%%%%%%%%%%%%%%%%%  *****  **          
+  \b   %%%%%        %%%%%%%%%%%%%%%                                             
+                                              
+  \b   %%%%%%%%%%                     %%          **             ***              
+  \b      %%%                         %%  %%             %%%           %%%%,      
+  \b      %%%      %%%   %%%   %%%%%  %%%   %%%   %%  %%%   %%%  %%%       (%%    
+  \b      %%%      %%%   %%%  %%%     %%     %%/  %%  %%%   %%%  %%%  %%%%%%%%    
+  \b      %%%      %%%   %%%  %%%     %%%   %%%   %%  %%%   %%%  %%% %%%   %%%    
+  \b      %%%        %%%%%    %%%       %%%%%     %%  %%%    %%  %%%   %%%%%      
+
+  This command-line tool interacts with Turbinia's API server.
+
+  You can specify the API server location in ~/.turbinia_api_config.json
+  """
+  ctx.obj = TurbiniaCli(
+      config_instance=config_instance, config_path=config_path)
+
+  # Set up the tool based on the configuration file parameters.
+  ctx.obj.setup()
+
+  # Build all the commands based on responses from the API server.
+  request_commands = factory.CommandFactory.create_dynamic_objects(
+      evidence_mapping=ctx.obj.evidence_mapping,
+      request_options=ctx.obj.request_options)
+  for command in request_commands:
+    groups.submit_group.add_command(command)
+
+
+def main():
+  """Initialize the cli application."""
+
+  # Register all command groups.
+  cli.add_command(groups.submit_group)
+  cli.add_command(groups.config_group)
+  cli.add_command(groups.jobs_group)
+  cli.add_command(groups.result_group)
+  cli.add_command(groups.status_group)
+
+  try:
+    cli.main()
+  except (ConnectionRefusedError, urllib3_exceptions.MaxRetryError,
+          urllib3_exceptions.NewConnectionError) as exception:
+    log.error(f'Error connecting to the Turbinia API server: {exception!s}')
+    sys.exit(-1)
+
+
+main()
diff --git a/turbinia/api/client/.gitignore b/turbinia/api/client/.gitignore
new file mode 100644
index 000000000..43995bd42
--- /dev/null
+++ b/turbinia/api/client/.gitignore
@@ -0,0 +1,66 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis/
+venv/
+.venv/
+.python-version
+.pytest_cache
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+#Ipython Notebook
+.ipynb_checkpoints
diff --git a/turbinia/api/client/.gitlab-ci.yml b/turbinia/api/client/.gitlab-ci.yml
new file mode 100644
index 000000000..023e6dc82
--- /dev/null
+++ b/turbinia/api/client/.gitlab-ci.yml
@@ -0,0 +1,24 @@
+# ref: https://docs.gitlab.com/ee/ci/README.html
+
+stages:
+  - test
+
+.tests:
+  stage: test
+  script:
+   - pip install -r requirements.txt
+   - pip install -r test-requirements.txt
+   - pytest --cov=turbinia_api_client
+
+test-3.6:
+  extends: .tests
+  image: python:3.6-alpine
+test-3.7:
+  extends: .tests
+  image: python:3.7-alpine
+test-3.8:
+  extends: .tests
+  image: python:3.8-alpine
+test-3.9:
+  extends: .tests
+  image: python:3.9-alpine
diff --git a/turbinia/api/client/.openapi-generator-ignore b/turbinia/api/client/.openapi-generator-ignore
new file mode 100644
index 000000000..7484ee590
--- /dev/null
+++ b/turbinia/api/client/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/turbinia/api/client/.travis.yml b/turbinia/api/client/.travis.yml
new file mode 100644
index 000000000..9aa2dafe8
--- /dev/null
+++ b/turbinia/api/client/.travis.yml
@@ -0,0 +1,13 @@
+# ref: https://docs.travis-ci.com/user/languages/python
+language: python
+python:
+  - "3.6"
+  - "3.7"
+  - "3.8"
+  - "3.9"
+# command to install dependencies
+install:
+  - "pip install -r requirements.txt"
+  - "pip install -r test-requirements.txt"
+# command to run tests
+script: pytest --cov=turbinia_api_client
diff --git a/turbinia/api/client/LICENSE b/turbinia/api/client/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/turbinia/api/client/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/turbinia/api/client/README.md b/turbinia/api/client/README.md
new file mode 100644
index 000000000..9ad2895c9
--- /dev/null
+++ b/turbinia/api/client/README.md
@@ -0,0 +1,149 @@
+# turbinia-api-client
+Turbinia API server
+
+This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
+
+- API version: 1.0.0
+- Package version: 1.0.0
+- Build package: org.openapitools.codegen.languages.PythonClientCodegen
+
+## Requirements.
+
+Python >=3.6
+
+## Installation & Usage
+### pip install
+
+If the python package is hosted on a repository, you can install directly using:
+
+```sh
+pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git
+```
+(you may need to run `pip` with root permission: `sudo pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git`)
+
+Then import the package:
+```python
+import turbinia_api_client
+```
+
+### Setuptools
+
+Install via [Setuptools](http://pypi.python.org/pypi/setuptools).
+
+```sh
+python setup.py install --user
+```
+(or `sudo python setup.py install` to install the package for all users)
+
+Then import the package:
+```python
+import turbinia_api_client
+```
+
+## Getting Started
+
+Please follow the [installation procedure](#installation--usage) and then run the following:
+
+```python
+
+import time
+import turbinia_api_client
+from pprint import pprint
+from turbinia_api_client.api import turbinia_configuration_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(api_client)
+    evidence_name = None # bool, date, datetime, dict, float, int, list, str, none_type | 
+
+    try:
+        # Get Evidence Attributes By Name
+        api_response = api_instance.get_evidence_attributes_by_name(evidence_name)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaConfigurationApi->get_evidence_attributes_by_name: %s\n" % e)
+```
+
+## Documentation for API Endpoints
+
+All URIs are relative to *http://localhost*
+
+Class | Method | HTTP request | Description
+------------ | ------------- | ------------- | -------------
+*TurbiniaConfigurationApi* | [**get_evidence_attributes_by_name**](docs/TurbiniaConfigurationApi.md#get_evidence_attributes_by_name) | **GET** /api/config/evidence/{evidence_name} | Get Evidence Attributes By Name
+*TurbiniaConfigurationApi* | [**get_evidence_types**](docs/TurbiniaConfigurationApi.md#get_evidence_types) | **GET** /api/config/evidence | Get Evidence Types
+*TurbiniaConfigurationApi* | [**get_request_options**](docs/TurbiniaConfigurationApi.md#get_request_options) | **GET** /api/config/request_options | Get Request Options
+*TurbiniaConfigurationApi* | [**read_config**](docs/TurbiniaConfigurationApi.md#read_config) | **GET** /api/config/ | Read Config
+*TurbiniaJobsApi* | [**read_jobs**](docs/TurbiniaJobsApi.md#read_jobs) | **GET** /api/jobs/ | Read Jobs
+*TurbiniaLogsApi* | [**get_logs**](docs/TurbiniaLogsApi.md#get_logs) | **GET** /api/logs/{query} | Get Logs
+*TurbiniaRequestResultsApi* | [**get_request_output**](docs/TurbiniaRequestResultsApi.md#get_request_output) | **GET** /api/result/request/{request_id} | Get Request Output
+*TurbiniaRequestResultsApi* | [**get_task_output**](docs/TurbiniaRequestResultsApi.md#get_task_output) | **GET** /api/result/task/{task_id} | Get Task Output
+*TurbiniaRequestsApi* | [**create_request**](docs/TurbiniaRequestsApi.md#create_request) | **POST** /api/request/ | Create Request
+*TurbiniaRequestsApi* | [**get_request_status**](docs/TurbiniaRequestsApi.md#get_request_status) | **GET** /api/request/{request_id} | Get Request Status
+*TurbiniaRequestsApi* | [**get_requests_summary**](docs/TurbiniaRequestsApi.md#get_requests_summary) | **GET** /api/request/summary | Get Requests Summary
+*TurbiniaTasksApi* | [**get_task_status**](docs/TurbiniaTasksApi.md#get_task_status) | **GET** /api/task/{task_id} | Get Task Status
+
+
+## Documentation For Models
+
+ - [BaseRequestOptions](docs/BaseRequestOptions.md)
+ - [HTTPValidationError](docs/HTTPValidationError.md)
+ - [Request](docs/Request.md)
+ - [ValidationError](docs/ValidationError.md)
+ - [ValidationErrorLocInner](docs/ValidationErrorLocInner.md)
+
+
+## Documentation For Authorization
+
+
+## oAuth2
+
+- **Type**: OAuth
+- **Flow**: accessCode
+- **Authorization URL**: https://accounts.google.com/o/oauth2/v2/auth
+- **Scopes**: N/A
+
+
+## Author
+
+
+
+
+## Notes for Large OpenAPI documents
+If the OpenAPI document is large, imports in turbinia_api_client.apis and turbinia_api_client.models may fail with a
+RecursionError indicating the maximum recursion limit has been exceeded. In that case, there are a couple of solutions:
+
+Solution 1:
+Use specific imports for apis and models like:
+- `from turbinia_api_client.api.default_api import DefaultApi`
+- `from turbinia_api_client.model.pet import Pet`
+
+Solution 2:
+Before importing the package, adjust the maximum recursion limit as shown below:
+```
+import sys
+sys.setrecursionlimit(1500)
+import turbinia_api_client
+from turbinia_api_client.apis import *
+from turbinia_api_client.models import *
+```
+
diff --git a/turbinia/api/client/docs/BaseRequestOptions.md b/turbinia/api/client/docs/BaseRequestOptions.md
new file mode 100644
index 000000000..327c3c67f
--- /dev/null
+++ b/turbinia/api/client/docs/BaseRequestOptions.md
@@ -0,0 +1,23 @@
+# BaseRequestOptions
+
+Base Request Options class to be extended by other option types. 
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**filter_patterns** | **[str]** |  | [optional] 
+**group_id** | **str** |  | [optional] 
+**jobs_allowlist** | **[str]** |  | [optional] 
+**jobs_denylist** | **[str]** |  | [optional] 
+**reason** | **str** |  | [optional] 
+**recipe_data** | **str** |  | [optional] 
+**recipe_name** | **str** |  | [optional] 
+**request_id** | **str** |  | [optional] 
+**requester** | **str** |  | [optional] 
+**sketch_id** | **int** |  | [optional] 
+**yara_rules** | **str** |  | [optional] 
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/turbinia/api/client/docs/HTTPValidationError.md b/turbinia/api/client/docs/HTTPValidationError.md
new file mode 100644
index 000000000..8e62aa78d
--- /dev/null
+++ b/turbinia/api/client/docs/HTTPValidationError.md
@@ -0,0 +1,12 @@
+# HTTPValidationError
+
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**detail** | [**[ValidationError]**](ValidationError.md) |  | [optional] 
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/turbinia/api/client/docs/LogsApi.md b/turbinia/api/client/docs/LogsApi.md
new file mode 100644
index 000000000..2c7c7d696
--- /dev/null
+++ b/turbinia/api/client/docs/LogsApi.md
@@ -0,0 +1,88 @@
+# turbinia_api_client.LogsApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**get_logs**](LogsApi.md#get_logs) | **GET** /api/logs/{query} | Get Logs
+
+
+# **get_logs**
+> bool, date, datetime, dict, float, int, list, str, none_type get_logs(query)
+
+Get Logs
+
+Retrieve log data.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import logs_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = logs_api.LogsApi(api_client)
+    query = "query_example" # str | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Get Logs
+        api_response = api_instance.get_logs(query)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling LogsApi->get_logs: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **query** | **str**|  |
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/OpenAPISpecificationApi.md b/turbinia/api/client/docs/OpenAPISpecificationApi.md
new file mode 100644
index 000000000..6dc404ff8
--- /dev/null
+++ b/turbinia/api/client/docs/OpenAPISpecificationApi.md
@@ -0,0 +1,82 @@
+# turbinia_api_client.OpenAPISpecificationApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**read_openapi_yaml_docs_openapi_yaml_get**](OpenAPISpecificationApi.md#read_openapi_yaml_docs_openapi_yaml_get) | **GET** /docs/openapi.yaml | Read Openapi Yaml
+
+
+# **read_openapi_yaml_docs_openapi_yaml_get**
+> bool, date, datetime, dict, float, int, list, str, none_type read_openapi_yaml_docs_openapi_yaml_get()
+
+Read Openapi Yaml
+
+Serve the OpenAPI specification in YAML format.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import open_api_specification_api
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = open_api_specification_api.OpenAPISpecificationApi(api_client)
+
+    # example, this endpoint has no required or optional parameters
+    try:
+        # Read Openapi Yaml
+        api_response = api_instance.read_openapi_yaml_docs_openapi_yaml_get()
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling OpenAPISpecificationApi->read_openapi_yaml_docs_openapi_yaml_get: %s\n" % e)
+```
+
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/Request.md b/turbinia/api/client/docs/Request.md
new file mode 100644
index 000000000..7aa404650
--- /dev/null
+++ b/turbinia/api/client/docs/Request.md
@@ -0,0 +1,15 @@
+# Request
+
+Base request object. 
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**evidence** | **{str: (bool, date, datetime, dict, float, int, list, str, none_type)}** |  | 
+**request_options** | [**BaseRequestOptions**](BaseRequestOptions.md) |  | 
+**description** | **str** |  | [optional]  if omitted the server will use the default value of "Turbinia request object"
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/turbinia/api/client/docs/RequestStatus.md b/turbinia/api/client/docs/RequestStatus.md
new file mode 100644
index 000000000..8471cd960
--- /dev/null
+++ b/turbinia/api/client/docs/RequestStatus.md
@@ -0,0 +1,23 @@
+# RequestStatus
+
+Represents a Turbinia request status object.
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**failed_tasks** | **int** |  | [optional]  if omitted the server will use the default value of 0
+**last_task_update_time** | **str** |  | [optional] 
+**queued_tasks** | **int** |  | [optional]  if omitted the server will use the default value of 0
+**reason** | **str** |  | [optional] 
+**request_id** | **str** |  | [optional] 
+**requester** | **str** |  | [optional] 
+**running_tasks** | **int** |  | [optional]  if omitted the server will use the default value of 0
+**status** | **str** |  | [optional] 
+**successful_tasks** | **int** |  | [optional]  if omitted the server will use the default value of 0
+**task_count** | **int** |  | [optional]  if omitted the server will use the default value of 0
+**tasks** | **[{str: (bool, date, datetime, dict, float, int, list, str, none_type)}]** |  | [optional]  if omitted the server will use the default value of []
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/turbinia/api/client/docs/RequestsSummary.md b/turbinia/api/client/docs/RequestsSummary.md
new file mode 100644
index 000000000..ae0ebda0b
--- /dev/null
+++ b/turbinia/api/client/docs/RequestsSummary.md
@@ -0,0 +1,13 @@
+# RequestsSummary
+
+Represents a summary view of multiple Turbinia requests.
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**requests_status** | [**[RequestStatus]**](RequestStatus.md) |  | [optional]  if omitted the server will use the default value of []
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/turbinia/api/client/docs/TurbiniaConfigurationApi.md b/turbinia/api/client/docs/TurbiniaConfigurationApi.md
new file mode 100644
index 000000000..bad46acfc
--- /dev/null
+++ b/turbinia/api/client/docs/TurbiniaConfigurationApi.md
@@ -0,0 +1,310 @@
+# turbinia_api_client.TurbiniaConfigurationApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**get_evidence_attributes_by_name**](TurbiniaConfigurationApi.md#get_evidence_attributes_by_name) | **GET** /api/config/evidence/{evidence_name} | Get Evidence Attributes By Name
+[**get_evidence_types**](TurbiniaConfigurationApi.md#get_evidence_types) | **GET** /api/config/evidence | Get Evidence Types
+[**get_request_options**](TurbiniaConfigurationApi.md#get_request_options) | **GET** /api/config/request_options | Get Request Options
+[**read_config**](TurbiniaConfigurationApi.md#read_config) | **GET** /api/config/ | Read Config
+
+
+# **get_evidence_attributes_by_name**
+> bool, date, datetime, dict, float, int, list, str, none_type get_evidence_attributes_by_name(evidence_name)
+
+Get Evidence Attributes By Name
+
+Returns supported Evidence object types and required parameters.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_configuration_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(api_client)
+    evidence_name = None # bool, date, datetime, dict, float, int, list, str, none_type | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Get Evidence Attributes By Name
+        api_response = api_instance.get_evidence_attributes_by_name(evidence_name)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaConfigurationApi->get_evidence_attributes_by_name: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **evidence_name** | **bool, date, datetime, dict, float, int, list, str, none_type**|  |
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **get_evidence_types**
+> bool, date, datetime, dict, float, int, list, str, none_type get_evidence_types()
+
+Get Evidence Types
+
+Returns supported Evidence object types and required parameters.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_configuration_api
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(api_client)
+
+    # example, this endpoint has no required or optional parameters
+    try:
+        # Get Evidence Types
+        api_response = api_instance.get_evidence_types()
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaConfigurationApi->get_evidence_types: %s\n" % e)
+```
+
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **get_request_options**
+> bool, date, datetime, dict, float, int, list, str, none_type get_request_options()
+
+Get Request Options
+
+Returns a list BaseRequestOptions attributes.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_configuration_api
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(api_client)
+
+    # example, this endpoint has no required or optional parameters
+    try:
+        # Get Request Options
+        api_response = api_instance.get_request_options()
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaConfigurationApi->get_request_options: %s\n" % e)
+```
+
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **read_config**
+> bool, date, datetime, dict, float, int, list, str, none_type read_config()
+
+Read Config
+
+Retrieve turbinia config.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_configuration_api
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_configuration_api.TurbiniaConfigurationApi(api_client)
+
+    # example, this endpoint has no required or optional parameters
+    try:
+        # Read Config
+        api_response = api_instance.read_config()
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaConfigurationApi->read_config: %s\n" % e)
+```
+
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/TurbiniaJobsApi.md b/turbinia/api/client/docs/TurbiniaJobsApi.md
new file mode 100644
index 000000000..0363e925f
--- /dev/null
+++ b/turbinia/api/client/docs/TurbiniaJobsApi.md
@@ -0,0 +1,82 @@
+# turbinia_api_client.TurbiniaJobsApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**read_jobs**](TurbiniaJobsApi.md#read_jobs) | **GET** /api/jobs/ | Read Jobs
+
+
+# **read_jobs**
+> bool, date, datetime, dict, float, int, list, str, none_type read_jobs()
+
+Read Jobs
+
+Return enabled jobs from the current Turbinia config.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_jobs_api
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_jobs_api.TurbiniaJobsApi(api_client)
+
+    # example, this endpoint has no required or optional parameters
+    try:
+        # Read Jobs
+        api_response = api_instance.read_jobs()
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaJobsApi->read_jobs: %s\n" % e)
+```
+
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/TurbiniaLogsApi.md b/turbinia/api/client/docs/TurbiniaLogsApi.md
new file mode 100644
index 000000000..da9369a52
--- /dev/null
+++ b/turbinia/api/client/docs/TurbiniaLogsApi.md
@@ -0,0 +1,88 @@
+# turbinia_api_client.TurbiniaLogsApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**get_logs**](TurbiniaLogsApi.md#get_logs) | **GET** /api/logs/{query} | Get Logs
+
+
+# **get_logs**
+> bool, date, datetime, dict, float, int, list, str, none_type get_logs(query)
+
+Get Logs
+
+Retrieve log data.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_logs_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_logs_api.TurbiniaLogsApi(api_client)
+    query = "query_example" # str | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Get Logs
+        api_response = api_instance.get_logs(query)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaLogsApi->get_logs: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **query** | **str**|  |
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/TurbiniaRequestResultsApi.md b/turbinia/api/client/docs/TurbiniaRequestResultsApi.md
new file mode 100644
index 000000000..8e678a6e4
--- /dev/null
+++ b/turbinia/api/client/docs/TurbiniaRequestResultsApi.md
@@ -0,0 +1,168 @@
+# turbinia_api_client.TurbiniaRequestResultsApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**get_request_output**](TurbiniaRequestResultsApi.md#get_request_output) | **GET** /api/result/request/{request_id} | Get Request Output
+[**get_task_output**](TurbiniaRequestResultsApi.md#get_task_output) | **GET** /api/result/task/{task_id} | Get Task Output
+
+
+# **get_request_output**
+> file_type get_request_output(request_id)
+
+Get Request Output
+
+Retrieve request output.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_request_results_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_request_results_api.TurbiniaRequestResultsApi(api_client)
+    request_id = "request_id_example" # str | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Get Request Output
+        api_response = api_instance.get_request_output(request_id)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaRequestResultsApi->get_request_output: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **request_id** | **str**|  |
+
+### Return type
+
+**file_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/octet-stream, application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **get_task_output**
+> file_type get_task_output(task_id)
+
+Get Task Output
+
+Retrieves a task's output files.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_request_results_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_request_results_api.TurbiniaRequestResultsApi(api_client)
+    task_id = "task_id_example" # str | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Get Task Output
+        api_response = api_instance.get_task_output(task_id)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaRequestResultsApi->get_task_output: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **task_id** | **str**|  |
+
+### Return type
+
+**file_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/octet-stream, application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/TurbiniaRequestsApi.md b/turbinia/api/client/docs/TurbiniaRequestsApi.md
new file mode 100644
index 000000000..c95b769c1
--- /dev/null
+++ b/turbinia/api/client/docs/TurbiniaRequestsApi.md
@@ -0,0 +1,265 @@
+# turbinia_api_client.TurbiniaRequestsApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**create_request**](TurbiniaRequestsApi.md#create_request) | **POST** /api/request/ | Create Request
+[**get_request_status**](TurbiniaRequestsApi.md#get_request_status) | **GET** /api/request/{request_id} | Get Request Status
+[**get_requests_summary**](TurbiniaRequestsApi.md#get_requests_summary) | **GET** /api/request/summary | Get Requests Summary
+
+
+# **create_request**
+> bool, date, datetime, dict, float, int, list, str, none_type create_request(request)
+
+Create Request
+
+Create a new Turbinia request.  Args:   request (turbinia.api.schema.request): JSON object from the HTTP POST data       matching the schema defined for a Turbinia Request. The schema is used       by pydantic for field validation.  Raises:   ValidationError: if the Request object contains invalid data.   HTTPException: If pre-conditions are not met.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_requests_api
+from turbinia_api_client.model.request import Request
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_requests_api.TurbiniaRequestsApi(api_client)
+    request = Request(
+        description="Turbinia request object",
+        evidence={},
+        request_options=BaseRequestOptions(
+            filter_patterns=[
+                "filter_patterns_example",
+            ],
+            group_id="group_id_example",
+            jobs_allowlist=[
+                "jobs_allowlist_example",
+            ],
+            jobs_denylist=[
+                "jobs_denylist_example",
+            ],
+            reason="reason_example",
+            recipe_data="recipe_data_example",
+            recipe_name="recipe_name_example",
+            request_id="request_id_example",
+            requester="requester_example",
+            sketch_id=1,
+            yara_rules="yara_rules_example",
+        ),
+    ) # Request | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Create Request
+        api_response = api_instance.create_request(request)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaRequestsApi->create_request: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **request** | [**Request**](Request.md)|  |
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **get_request_status**
+> bool, date, datetime, dict, float, int, list, str, none_type get_request_status(request_id)
+
+Get Request Status
+
+Retrieves status for a Turbinia Request.  Args:   request_id (str): A Turbinia request identifier.  Raises:   HTTPException: if another exception is caught.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_requests_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_requests_api.TurbiniaRequestsApi(api_client)
+    request_id = "request_id_example" # str | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Get Request Status
+        api_response = api_instance.get_request_status(request_id)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaRequestsApi->get_request_status: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **request_id** | **str**|  |
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **get_requests_summary**
+> bool, date, datetime, dict, float, int, list, str, none_type get_requests_summary()
+
+Get Requests Summary
+
+Retrieves a summary of all Turbinia requests.  The response is validated against the RequestSummary model.  Raises:   HTTPException: if another exception is caught.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_requests_api
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_requests_api.TurbiniaRequestsApi(api_client)
+
+    # example, this endpoint has no required or optional parameters
+    try:
+        # Get Requests Summary
+        api_response = api_instance.get_requests_summary()
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaRequestsApi->get_requests_summary: %s\n" % e)
+```
+
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/TurbiniaTasksApi.md b/turbinia/api/client/docs/TurbiniaTasksApi.md
new file mode 100644
index 000000000..90f7c96b9
--- /dev/null
+++ b/turbinia/api/client/docs/TurbiniaTasksApi.md
@@ -0,0 +1,88 @@
+# turbinia_api_client.TurbiniaTasksApi
+
+All URIs are relative to *http://localhost*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**get_task_status**](TurbiniaTasksApi.md#get_task_status) | **GET** /api/task/{task_id} | Get Task Status
+
+
+# **get_task_status**
+> bool, date, datetime, dict, float, int, list, str, none_type get_task_status(task_id)
+
+Get Task Status
+
+Retrieve task information.
+
+### Example
+
+* OAuth Authentication (oAuth2):
+
+```python
+import time
+import turbinia_api_client
+from turbinia_api_client.api import turbinia_tasks_api
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure OAuth2 access token for authorization: oAuth2
+configuration = turbinia_api_client.Configuration(
+    host = "http://localhost"
+)
+configuration.access_token = 'YOUR_ACCESS_TOKEN'
+
+# Enter a context with an instance of the API client
+with turbinia_api_client.ApiClient(configuration) as api_client:
+    # Create an instance of the API class
+    api_instance = turbinia_tasks_api.TurbiniaTasksApi(api_client)
+    task_id = "task_id_example" # str | 
+
+    # example passing only required values which don't have defaults set
+    try:
+        # Get Task Status
+        api_response = api_instance.get_task_status(task_id)
+        pprint(api_response)
+    except turbinia_api_client.ApiException as e:
+        print("Exception when calling TurbiniaTasksApi->get_task_status: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **task_id** | **str**|  |
+
+### Return type
+
+**bool, date, datetime, dict, float, int, list, str, none_type**
+
+### Authorization
+
+[oAuth2](../README.md#oAuth2)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | Successful Response |  -  |
+**422** | Validation Error |  -  |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/turbinia/api/client/docs/ValidationError.md b/turbinia/api/client/docs/ValidationError.md
new file mode 100644
index 000000000..6736deda7
--- /dev/null
+++ b/turbinia/api/client/docs/ValidationError.md
@@ -0,0 +1,14 @@
+# ValidationError
+
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**loc** | [**[ValidationErrorLocInner]**](ValidationErrorLocInner.md) |  | 
+**msg** | **str** |  | 
+**type** | **str** |  | 
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/turbinia/api/client/docs/ValidationErrorLocInner.md b/turbinia/api/client/docs/ValidationErrorLocInner.md
new file mode 100644
index 000000000..607989b43
--- /dev/null
+++ b/turbinia/api/client/docs/ValidationErrorLocInner.md
@@ -0,0 +1,11 @@
+# ValidationErrorLocInner
+
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/turbinia/api/client/git_push.sh b/turbinia/api/client/git_push.sh
new file mode 100644
index 000000000..f53a75d4f
--- /dev/null
+++ b/turbinia/api/client/git_push.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
+#
+# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
+
+git_user_id=$1
+git_repo_id=$2
+release_note=$3
+git_host=$4
+
+if [ "$git_host" = "" ]; then
+    git_host="github.com"
+    echo "[INFO] No command line input provided. Set \$git_host to $git_host"
+fi
+
+if [ "$git_user_id" = "" ]; then
+    git_user_id="GIT_USER_ID"
+    echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
+fi
+
+if [ "$git_repo_id" = "" ]; then
+    git_repo_id="GIT_REPO_ID"
+    echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
+fi
+
+if [ "$release_note" = "" ]; then
+    release_note="Minor update"
+    echo "[INFO] No command line input provided. Set \$release_note to $release_note"
+fi
+
+# Initialize the local directory as a Git repository
+git init
+
+# Adds the files in the local repository and stages them for commit.
+git add .
+
+# Commits the tracked changes and prepares them to be pushed to a remote repository.
+git commit -m "$release_note"
+
+# Sets the new remote
+git_remote=$(git remote)
+if [ "$git_remote" = "" ]; then # git remote not defined
+
+    if [ "$GIT_TOKEN" = "" ]; then
+        echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
+        git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
+    else
+        git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
+    fi
+
+fi
+
+git pull origin master
+
+# Pushes (Forces) the changes in the local repository up to the remote repository
+echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
+git push origin master 2>&1 | grep -v 'To https'
diff --git a/turbinia/api/client/requirements.txt b/turbinia/api/client/requirements.txt
new file mode 100644
index 000000000..96947f604
--- /dev/null
+++ b/turbinia/api/client/requirements.txt
@@ -0,0 +1,3 @@
+python_dateutil >= 2.5.3
+setuptools >= 21.0.0
+urllib3 >= 1.25.3
diff --git a/turbinia/api/client/setup.cfg b/turbinia/api/client/setup.cfg
new file mode 100644
index 000000000..11433ee87
--- /dev/null
+++ b/turbinia/api/client/setup.cfg
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length=99
diff --git a/turbinia/api/client/setup.py b/turbinia/api/client/setup.py
new file mode 100644
index 000000000..9742a02c6
--- /dev/null
+++ b/turbinia/api/client/setup.py
@@ -0,0 +1,43 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+from setuptools import setup, find_packages  # noqa: H301
+
+NAME = "turbinia-api-client"
+VERSION = "1.0.0"
+# To install the library, run the following
+#
+# python setup.py install
+#
+# prerequisite: setuptools
+# http://pypi.python.org/pypi/setuptools
+
+REQUIRES = [
+  "urllib3 >= 1.25.3",
+  "python-dateutil",
+]
+
+setup(
+    name=NAME,
+    version=VERSION,
+    description="Turbinia API Server",
+    author="OpenAPI Generator community",
+    author_email="team@openapitools.org",
+    url="",
+    keywords=["OpenAPI", "OpenAPI-Generator", "Turbinia API Server"],
+    python_requires=">=3.6",
+    install_requires=REQUIRES,
+    packages=find_packages(exclude=["test", "tests"]),
+    include_package_data=True,
+    license="Apache License 2.0",
+    long_description="""\
+    Turbinia API server  # noqa: E501
+    """
+)
diff --git a/turbinia/api/client/tox.ini b/turbinia/api/client/tox.ini
new file mode 100644
index 000000000..bc6ad4117
--- /dev/null
+++ b/turbinia/api/client/tox.ini
@@ -0,0 +1,9 @@
+[tox]
+envlist = py3
+
+[testenv]
+deps=-r{toxinidir}/requirements.txt
+     -r{toxinidir}/test-requirements.txt
+
+commands=
+   pytest --cov=turbinia_api_client
diff --git a/turbinia/api/client/turbinia_api_client/__init__.py b/turbinia/api/client/turbinia_api_client/__init__.py
new file mode 100644
index 000000000..5ad925519
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/__init__.py
@@ -0,0 +1,27 @@
+# flake8: noqa
+
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+__version__ = "1.0.0"
+
+# import ApiClient
+from turbinia_api_client.api_client import ApiClient
+
+# import Configuration
+from turbinia_api_client.configuration import Configuration
+
+# import exceptions
+from turbinia_api_client.exceptions import OpenApiException
+from turbinia_api_client.exceptions import ApiAttributeError
+from turbinia_api_client.exceptions import ApiTypeError
+from turbinia_api_client.exceptions import ApiValueError
+from turbinia_api_client.exceptions import ApiKeyError
+from turbinia_api_client.exceptions import ApiException
diff --git a/turbinia/api/client/turbinia_api_client/api/__init__.py b/turbinia/api/client/turbinia_api_client/api/__init__.py
new file mode 100644
index 000000000..b66da0d38
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/__init__.py
@@ -0,0 +1,3 @@
+# do not import all apis into this module because that uses a lot of memory and stack frames
+# if you need the ability to import all apis from one package, import them with
+# from turbinia_api_client.apis import TurbiniaConfigurationApi
diff --git a/turbinia/api/client/turbinia_api_client/api/open_api_specification_api.py b/turbinia/api/client/turbinia_api_client/api/open_api_specification_api.py
new file mode 100644
index 000000000..9421ab1b2
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/open_api_specification_api.py
@@ -0,0 +1,159 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.api_client import ApiClient, Endpoint as _Endpoint
+from turbinia_api_client.model_utils import (  # noqa: F401
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_and_convert_types
+)
+
+
+class OpenAPISpecificationApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+        self.read_openapi_yaml_docs_openapi_yaml_get_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/docs/openapi.yaml',
+                'operation_id': 'read_openapi_yaml_docs_openapi_yaml_get',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                ],
+                'required': [],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                },
+                'attribute_map': {
+                },
+                'location_map': {
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+
+    def read_openapi_yaml_docs_openapi_yaml_get(
+        self,
+        **kwargs
+    ):
+        """Read Openapi Yaml  # noqa: E501
+
+        Serve the OpenAPI specification in YAML format.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.read_openapi_yaml_docs_openapi_yaml_get(async_req=True)
+        >>> result = thread.get()
+
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        return self.read_openapi_yaml_docs_openapi_yaml_get_endpoint.call_with_http_info(**kwargs)
+
diff --git a/turbinia/api/client/turbinia_api_client/api/turbinia_configuration_api.py b/turbinia/api/client/turbinia_api_client/api/turbinia_configuration_api.py
new file mode 100644
index 000000000..c1af17c06
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/turbinia_configuration_api.py
@@ -0,0 +1,538 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.api_client import ApiClient, Endpoint as _Endpoint
+from turbinia_api_client.model_utils import (  # noqa: F401
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_and_convert_types
+)
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+
+
+class TurbiniaConfigurationApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+        self.get_evidence_attributes_by_name_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/config/evidence/{evidence_name}',
+                'operation_id': 'get_evidence_attributes_by_name',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                    'evidence_name',
+                ],
+                'required': [
+                    'evidence_name',
+                ],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                    'evidence_name':
+                        (bool, date, datetime, dict, float, int, list, str, none_type,),
+                },
+                'attribute_map': {
+                    'evidence_name': 'evidence_name',
+                },
+                'location_map': {
+                    'evidence_name': 'path',
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+        self.get_evidence_types_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/config/evidence',
+                'operation_id': 'get_evidence_types',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                ],
+                'required': [],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                },
+                'attribute_map': {
+                },
+                'location_map': {
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+        self.get_request_options_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/config/request_options',
+                'operation_id': 'get_request_options',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                ],
+                'required': [],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                },
+                'attribute_map': {
+                },
+                'location_map': {
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+        self.read_config_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/config/',
+                'operation_id': 'read_config',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                ],
+                'required': [],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                },
+                'attribute_map': {
+                },
+                'location_map': {
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+
+    def get_evidence_attributes_by_name(
+        self,
+        evidence_name,
+        **kwargs
+    ):
+        """Get Evidence Attributes By Name  # noqa: E501
+
+        Returns supported Evidence object types and required parameters.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_evidence_attributes_by_name(evidence_name, async_req=True)
+        >>> result = thread.get()
+
+        Args:
+            evidence_name (bool, date, datetime, dict, float, int, list, str, none_type):
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        kwargs['evidence_name'] = \
+            evidence_name
+        return self.get_evidence_attributes_by_name_endpoint.call_with_http_info(**kwargs)
+
+    def get_evidence_types(
+        self,
+        **kwargs
+    ):
+        """Get Evidence Types  # noqa: E501
+
+        Returns supported Evidence object types and required parameters.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_evidence_types(async_req=True)
+        >>> result = thread.get()
+
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        return self.get_evidence_types_endpoint.call_with_http_info(**kwargs)
+
+    def get_request_options(
+        self,
+        **kwargs
+    ):
+        """Get Request Options  # noqa: E501
+
+        Returns a list BaseRequestOptions attributes.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_request_options(async_req=True)
+        >>> result = thread.get()
+
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        return self.get_request_options_endpoint.call_with_http_info(**kwargs)
+
+    def read_config(
+        self,
+        **kwargs
+    ):
+        """Read Config  # noqa: E501
+
+        Retrieve turbinia config.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.read_config(async_req=True)
+        >>> result = thread.get()
+
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        return self.read_config_endpoint.call_with_http_info(**kwargs)
+
diff --git a/turbinia/api/client/turbinia_api_client/api/turbinia_jobs_api.py b/turbinia/api/client/turbinia_api_client/api/turbinia_jobs_api.py
new file mode 100644
index 000000000..79c0a4659
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/turbinia_jobs_api.py
@@ -0,0 +1,159 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.api_client import ApiClient, Endpoint as _Endpoint
+from turbinia_api_client.model_utils import (  # noqa: F401
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_and_convert_types
+)
+
+
+class TurbiniaJobsApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+        self.read_jobs_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/jobs/',
+                'operation_id': 'read_jobs',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                ],
+                'required': [],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                },
+                'attribute_map': {
+                },
+                'location_map': {
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+
+    def read_jobs(
+        self,
+        **kwargs
+    ):
+        """Read Jobs  # noqa: E501
+
+        Return enabled jobs from the current Turbinia config.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.read_jobs(async_req=True)
+        >>> result = thread.get()
+
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        return self.read_jobs_endpoint.call_with_http_info(**kwargs)
+
diff --git a/turbinia/api/client/turbinia_api_client/api/turbinia_logs_api.py b/turbinia/api/client/turbinia_api_client/api/turbinia_logs_api.py
new file mode 100644
index 000000000..f9c3229d6
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/turbinia_logs_api.py
@@ -0,0 +1,172 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.api_client import ApiClient, Endpoint as _Endpoint
+from turbinia_api_client.model_utils import (  # noqa: F401
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_and_convert_types
+)
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+
+
+class TurbiniaLogsApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+        self.get_logs_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/logs/{query}',
+                'operation_id': 'get_logs',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                    'query',
+                ],
+                'required': [
+                    'query',
+                ],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                    'query':
+                        (str,),
+                },
+                'attribute_map': {
+                    'query': 'query',
+                },
+                'location_map': {
+                    'query': 'path',
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+
+    def get_logs(
+        self,
+        query,
+        **kwargs
+    ):
+        """Get Logs  # noqa: E501
+
+        Retrieve log data.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_logs(query, async_req=True)
+        >>> result = thread.get()
+
+        Args:
+            query (str):
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        kwargs['query'] = \
+            query
+        return self.get_logs_endpoint.call_with_http_info(**kwargs)
+
diff --git a/turbinia/api/client/turbinia_api_client/api/turbinia_request_results_api.py b/turbinia/api/client/turbinia_api_client/api/turbinia_request_results_api.py
new file mode 100644
index 000000000..1270c6e72
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/turbinia_request_results_api.py
@@ -0,0 +1,308 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.api_client import ApiClient, Endpoint as _Endpoint
+from turbinia_api_client.model_utils import (  # noqa: F401
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_and_convert_types
+)
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+
+
+class TurbiniaRequestResultsApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+        self.get_request_output_endpoint = _Endpoint(
+            settings={
+                'response_type': (file_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/result/request/{request_id}',
+                'operation_id': 'get_request_output',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                    'request_id',
+                ],
+                'required': [
+                    'request_id',
+                ],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                    'request_id':
+                        (str,),
+                },
+                'attribute_map': {
+                    'request_id': 'request_id',
+                },
+                'location_map': {
+                    'request_id': 'path',
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/octet-stream',
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+        self.get_task_output_endpoint = _Endpoint(
+            settings={
+                'response_type': (file_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/result/task/{task_id}',
+                'operation_id': 'get_task_output',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                    'task_id',
+                ],
+                'required': [
+                    'task_id',
+                ],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                    'task_id':
+                        (str,),
+                },
+                'attribute_map': {
+                    'task_id': 'task_id',
+                },
+                'location_map': {
+                    'task_id': 'path',
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/octet-stream',
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+
+    def get_request_output(
+        self,
+        request_id,
+        **kwargs
+    ):
+        """Get Request Output  # noqa: E501
+
+        Retrieve request output.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_request_output(request_id, async_req=True)
+        >>> result = thread.get()
+
+        Args:
+            request_id (str):
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            file_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        kwargs['request_id'] = \
+            request_id
+        return self.get_request_output_endpoint.call_with_http_info(**kwargs)
+
+    def get_task_output(
+        self,
+        task_id,
+        **kwargs
+    ):
+        """Get Task Output  # noqa: E501
+
+        Retrieves a task's output files.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_task_output(task_id, async_req=True)
+        >>> result = thread.get()
+
+        Args:
+            task_id (str):
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            file_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        kwargs['task_id'] = \
+            task_id
+        return self.get_task_output_endpoint.call_with_http_info(**kwargs)
+
diff --git a/turbinia/api/client/turbinia_api_client/api/turbinia_requests_api.py b/turbinia/api/client/turbinia_api_client/api/turbinia_requests_api.py
new file mode 100644
index 000000000..448e44873
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/turbinia_requests_api.py
@@ -0,0 +1,430 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.api_client import ApiClient, Endpoint as _Endpoint
+from turbinia_api_client.model_utils import (  # noqa: F401
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_and_convert_types
+)
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from turbinia_api_client.model.request import Request
+
+
+class TurbiniaRequestsApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+        self.create_request_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/request/',
+                'operation_id': 'create_request',
+                'http_method': 'POST',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                    'request',
+                ],
+                'required': [
+                    'request',
+                ],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                    'request':
+                        (Request,),
+                },
+                'attribute_map': {
+                },
+                'location_map': {
+                    'request': 'body',
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [
+                    'application/json'
+                ]
+            },
+            api_client=api_client
+        )
+        self.get_request_status_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/request/{request_id}',
+                'operation_id': 'get_request_status',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                    'request_id',
+                ],
+                'required': [
+                    'request_id',
+                ],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                    'request_id':
+                        (str,),
+                },
+                'attribute_map': {
+                    'request_id': 'request_id',
+                },
+                'location_map': {
+                    'request_id': 'path',
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+        self.get_requests_summary_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/request/summary',
+                'operation_id': 'get_requests_summary',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                ],
+                'required': [],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                },
+                'attribute_map': {
+                },
+                'location_map': {
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+
+    def create_request(
+        self,
+        request,
+        **kwargs
+    ):
+        """Create Request  # noqa: E501
+
+        Create a new Turbinia request.  Args:   request (turbinia.api.schema.request): JSON object from the HTTP POST data       matching the schema defined for a Turbinia Request. The schema is used       by pydantic for field validation.  Raises:   ValidationError: if the Request object contains invalid data.   HTTPException: If pre-conditions are not met.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.create_request(request, async_req=True)
+        >>> result = thread.get()
+
+        Args:
+            request (Request):
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        kwargs['request'] = \
+            request
+        return self.create_request_endpoint.call_with_http_info(**kwargs)
+
+    def get_request_status(
+        self,
+        request_id,
+        **kwargs
+    ):
+        """Get Request Status  # noqa: E501
+
+        Retrieves status for a Turbinia Request.  Args:   request_id (str): A Turbinia request identifier.  Raises:   HTTPException: if another exception is caught.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_request_status(request_id, async_req=True)
+        >>> result = thread.get()
+
+        Args:
+            request_id (str):
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        kwargs['request_id'] = \
+            request_id
+        return self.get_request_status_endpoint.call_with_http_info(**kwargs)
+
+    def get_requests_summary(
+        self,
+        **kwargs
+    ):
+        """Get Requests Summary  # noqa: E501
+
+        Retrieves a summary of all Turbinia requests.  The response is validated against the RequestSummary model.  Raises:   HTTPException: if another exception is caught.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_requests_summary(async_req=True)
+        >>> result = thread.get()
+
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        return self.get_requests_summary_endpoint.call_with_http_info(**kwargs)
+
diff --git a/turbinia/api/client/turbinia_api_client/api/turbinia_tasks_api.py b/turbinia/api/client/turbinia_api_client/api/turbinia_tasks_api.py
new file mode 100644
index 000000000..823c95f7d
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api/turbinia_tasks_api.py
@@ -0,0 +1,172 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.api_client import ApiClient, Endpoint as _Endpoint
+from turbinia_api_client.model_utils import (  # noqa: F401
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_and_convert_types
+)
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+
+
+class TurbiniaTasksApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+        self.get_task_status_endpoint = _Endpoint(
+            settings={
+                'response_type': (bool, date, datetime, dict, float, int, list, str, none_type,),
+                'auth': [
+                    'oAuth2'
+                ],
+                'endpoint_path': '/api/task/{task_id}',
+                'operation_id': 'get_task_status',
+                'http_method': 'GET',
+                'servers': None,
+            },
+            params_map={
+                'all': [
+                    'task_id',
+                ],
+                'required': [
+                    'task_id',
+                ],
+                'nullable': [
+                ],
+                'enum': [
+                ],
+                'validation': [
+                ]
+            },
+            root_map={
+                'validations': {
+                },
+                'allowed_values': {
+                },
+                'openapi_types': {
+                    'task_id':
+                        (str,),
+                },
+                'attribute_map': {
+                    'task_id': 'task_id',
+                },
+                'location_map': {
+                    'task_id': 'path',
+                },
+                'collection_format_map': {
+                }
+            },
+            headers_map={
+                'accept': [
+                    'application/json'
+                ],
+                'content_type': [],
+            },
+            api_client=api_client
+        )
+
+    def get_task_status(
+        self,
+        task_id,
+        **kwargs
+    ):
+        """Get Task Status  # noqa: E501
+
+        Retrieve task information.  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+
+        >>> thread = api.get_task_status(task_id, async_req=True)
+        >>> result = thread.get()
+
+        Args:
+            task_id (str):
+
+        Keyword Args:
+            _return_http_data_only (bool): response data without head status
+                code and headers. Default is True.
+            _preload_content (bool): if False, the urllib3.HTTPResponse object
+                will be returned without reading/decoding response data.
+                Default is True.
+            _request_timeout (int/float/tuple): timeout setting for this request. If
+                one number provided, it will be total request timeout. It can also
+                be a pair (tuple) of (connection, read) timeouts.
+                Default is None.
+            _check_input_type (bool): specifies if type checking
+                should be done one the data sent to the server.
+                Default is True.
+            _check_return_type (bool): specifies if type checking
+                should be done one the data received from the server.
+                Default is True.
+            _spec_property_naming (bool): True if the variable names in the input data
+                are serialized names, as specified in the OpenAPI document.
+                False if the variable names in the input data
+                are pythonic names, e.g. snake case (default)
+            _content_type (str/None): force body content-type.
+                Default is None and content-type will be predicted by allowed
+                content-types and body.
+            _host_index (int/None): specifies the index of the server
+                that we want to use.
+                Default is read from the configuration.
+            _request_auths (list): set to override the auth_settings for an a single
+                request; this effectively ignores the authentication
+                in the spec for a single request.
+                Default is None
+            async_req (bool): execute request asynchronously
+
+        Returns:
+            bool, date, datetime, dict, float, int, list, str, none_type
+                If the method is called asynchronously, returns the request
+                thread.
+        """
+        kwargs['async_req'] = kwargs.get(
+            'async_req', False
+        )
+        kwargs['_return_http_data_only'] = kwargs.get(
+            '_return_http_data_only', True
+        )
+        kwargs['_preload_content'] = kwargs.get(
+            '_preload_content', True
+        )
+        kwargs['_request_timeout'] = kwargs.get(
+            '_request_timeout', None
+        )
+        kwargs['_check_input_type'] = kwargs.get(
+            '_check_input_type', True
+        )
+        kwargs['_check_return_type'] = kwargs.get(
+            '_check_return_type', True
+        )
+        kwargs['_spec_property_naming'] = kwargs.get(
+            '_spec_property_naming', False
+        )
+        kwargs['_content_type'] = kwargs.get(
+            '_content_type')
+        kwargs['_host_index'] = kwargs.get('_host_index')
+        kwargs['_request_auths'] = kwargs.get('_request_auths', None)
+        kwargs['task_id'] = \
+            task_id
+        return self.get_task_status_endpoint.call_with_http_info(**kwargs)
+
diff --git a/turbinia/api/client/turbinia_api_client/api_client.py b/turbinia/api/client/turbinia_api_client/api_client.py
new file mode 100644
index 000000000..e34775b22
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/api_client.py
@@ -0,0 +1,887 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import json
+import atexit
+import mimetypes
+from multiprocessing.pool import ThreadPool
+import io
+import os
+import re
+import typing
+from urllib.parse import quote
+from urllib3.fields import RequestField
+
+
+from turbinia_api_client import rest
+from turbinia_api_client.configuration import Configuration
+from turbinia_api_client.exceptions import ApiTypeError, ApiValueError, ApiException
+from turbinia_api_client.model_utils import (
+    ModelNormal,
+    ModelSimple,
+    ModelComposed,
+    check_allowed_values,
+    check_validations,
+    date,
+    datetime,
+    deserialize_file,
+    file_type,
+    model_to_dict,
+    none_type,
+    validate_and_convert_types
+)
+
+
+class ApiClient(object):
+    """Generic API client for OpenAPI client library builds.
+
+    OpenAPI generic API client. This client handles the client-
+    server communication, and is invariant across implementations. Specifics of
+    the methods and models for each application are generated from the OpenAPI
+    templates.
+
+    NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+    Do not edit the class manually.
+
+    :param configuration: .Configuration object for this client
+    :param header_name: a header to pass when making calls to the API.
+    :param header_value: a header value to pass when making calls to
+        the API.
+    :param cookie: a cookie to include in the header when making calls
+        to the API
+    :param pool_threads: The number of threads to use for async requests
+        to the API. More threads means more concurrent API requests.
+    """
+
+    _pool = None
+
+    def __init__(self, configuration=None, header_name=None, header_value=None,
+                 cookie=None, pool_threads=1):
+        if configuration is None:
+            configuration = Configuration.get_default_copy()
+        self.configuration = configuration
+        self.pool_threads = pool_threads
+
+        self.rest_client = rest.RESTClientObject(configuration)
+        self.default_headers = {}
+        if header_name is not None:
+            self.default_headers[header_name] = header_value
+        self.cookie = cookie
+        # Set default User-Agent.
+        self.user_agent = 'OpenAPI-Generator/1.0.0/python'
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.close()
+
+    def close(self):
+        if self._pool:
+            self._pool.close()
+            self._pool.join()
+            self._pool = None
+            if hasattr(atexit, 'unregister'):
+                atexit.unregister(self.close)
+
+    @property
+    def pool(self):
+        """Create thread pool on first request
+         avoids instantiating unused threadpool for blocking clients.
+        """
+        if self._pool is None:
+            atexit.register(self.close)
+            self._pool = ThreadPool(self.pool_threads)
+        return self._pool
+
+    @property
+    def user_agent(self):
+        """User agent for this API client"""
+        return self.default_headers['User-Agent']
+
+    @user_agent.setter
+    def user_agent(self, value):
+        self.default_headers['User-Agent'] = value
+
+    def set_default_header(self, header_name, header_value):
+        self.default_headers[header_name] = header_value
+
+    def __call_api(
+        self,
+        resource_path: str,
+        method: str,
+        path_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
+        query_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
+        header_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
+        body: typing.Optional[typing.Any] = None,
+        post_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
+        files: typing.Optional[typing.Dict[str, typing.List[io.IOBase]]] = None,
+        response_type: typing.Optional[typing.Tuple[typing.Any]] = None,
+        auth_settings: typing.Optional[typing.List[str]] = None,
+        _return_http_data_only: typing.Optional[bool] = None,
+        collection_formats: typing.Optional[typing.Dict[str, str]] = None,
+        _preload_content: bool = True,
+        _request_timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None,
+        _host: typing.Optional[str] = None,
+        _check_type: typing.Optional[bool] = None,
+        _content_type: typing.Optional[str] = None,
+        _request_auths: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] = None
+    ):
+
+        config = self.configuration
+
+        # header parameters
+        header_params = header_params or {}
+        header_params.update(self.default_headers)
+        if self.cookie:
+            header_params['Cookie'] = self.cookie
+        if header_params:
+            header_params = self.sanitize_for_serialization(header_params)
+            header_params = dict(self.parameters_to_tuples(header_params,
+                                                           collection_formats))
+
+        # path parameters
+        if path_params:
+            path_params = self.sanitize_for_serialization(path_params)
+            path_params = self.parameters_to_tuples(path_params,
+                                                    collection_formats)
+            for k, v in path_params:
+                # specified safe chars, encode everything
+                resource_path = resource_path.replace(
+                    '{%s}' % k,
+                    quote(str(v), safe=config.safe_chars_for_path_param)
+                )
+
+        # query parameters
+        if query_params:
+            query_params = self.sanitize_for_serialization(query_params)
+            query_params = self.parameters_to_tuples(query_params,
+                                                     collection_formats)
+
+        # post parameters
+        if post_params or files:
+            post_params = post_params if post_params else []
+            post_params = self.sanitize_for_serialization(post_params)
+            post_params = self.parameters_to_tuples(post_params,
+                                                    collection_formats)
+            post_params.extend(self.files_parameters(files))
+            if header_params['Content-Type'].startswith("multipart"):
+                post_params = self.parameters_to_multipart(post_params,
+                                                          (dict) )
+
+        # body
+        if body:
+            body = self.sanitize_for_serialization(body)
+
+        # auth setting
+        self.update_params_for_auth(header_params, query_params,
+                                    auth_settings, resource_path, method, body,
+                                    request_auths=_request_auths)
+
+        # request url
+        if _host is None:
+            url = self.configuration.host + resource_path
+        else:
+            # use server/host defined in path or operation instead
+            url = _host + resource_path
+
+        try:
+            # perform request and return response
+            response_data = self.request(
+                method, url, query_params=query_params, headers=header_params,
+                post_params=post_params, body=body,
+                _preload_content=_preload_content,
+                _request_timeout=_request_timeout)
+        except ApiException as e:
+            e.body = e.body.decode('utf-8')
+            raise e
+
+        self.last_response = response_data
+
+        return_data = response_data
+
+        if not _preload_content:
+            return (return_data)
+            return return_data
+
+        # deserialize response data
+        if response_type:
+            if response_type != (file_type,):
+                encoding = "utf-8"
+                content_type = response_data.getheader('content-type')
+                if content_type is not None:
+                    match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
+                    if match:
+                        encoding = match.group(1)
+                response_data.data = response_data.data.decode(encoding)
+
+            return_data = self.deserialize(
+                response_data,
+                response_type,
+                _check_type
+            )
+        else:
+            return_data = None
+
+        if _return_http_data_only:
+            return (return_data)
+        else:
+            return (return_data, response_data.status,
+                    response_data.getheaders())
+
+    def parameters_to_multipart(self, params, collection_types):
+        """Get parameters as list of tuples, formatting as json if value is collection_types
+
+        :param params: Parameters as list of two-tuples
+        :param dict collection_types: Parameter collection types
+        :return: Parameters as list of tuple or urllib3.fields.RequestField
+        """
+        new_params = []
+        if collection_types is None:
+            collection_types = (dict)
+        for k, v in params.items() if isinstance(params, dict) else params:  # noqa: E501
+            if isinstance(v, collection_types): # v is instance of collection_type, formatting as application/json
+                 v = json.dumps(v, ensure_ascii=False).encode("utf-8")
+                 field = RequestField(k, v)
+                 field.make_multipart(content_type="application/json; charset=utf-8")
+                 new_params.append(field)
+            else:
+                 new_params.append((k, v))
+        return new_params
+
+    @classmethod
+    def sanitize_for_serialization(cls, obj):
+        """Prepares data for transmission before it is sent with the rest client
+        If obj is None, return None.
+        If obj is str, int, long, float, bool, return directly.
+        If obj is datetime.datetime, datetime.date
+            convert to string in iso8601 format.
+        If obj is list, sanitize each element in the list.
+        If obj is dict, return the dict.
+        If obj is OpenAPI model, return the properties dict.
+        If obj is io.IOBase, return the bytes
+        :param obj: The data to serialize.
+        :return: The serialized form of data.
+        """
+        if isinstance(obj, (ModelNormal, ModelComposed)):
+            return {
+                key: cls.sanitize_for_serialization(val) for key, val in model_to_dict(obj, serialize=True).items()
+            }
+        elif isinstance(obj, io.IOBase):
+            return cls.get_file_data_and_close_file(obj)
+        elif isinstance(obj, (str, int, float, none_type, bool)):
+            return obj
+        elif isinstance(obj, (datetime, date)):
+            return obj.isoformat()
+        elif isinstance(obj, ModelSimple):
+            return cls.sanitize_for_serialization(obj.value)
+        elif isinstance(obj, (list, tuple)):
+            return [cls.sanitize_for_serialization(item) for item in obj]
+        if isinstance(obj, dict):
+            return {key: cls.sanitize_for_serialization(val) for key, val in obj.items()}
+        raise ApiValueError('Unable to prepare type {} for serialization'.format(obj.__class__.__name__))
+
+    def deserialize(self, response, response_type, _check_type):
+        """Deserializes response into an object.
+
+        :param response: RESTResponse object to be deserialized.
+        :param response_type: For the response, a tuple containing:
+            valid classes
+            a list containing valid classes (for list schemas)
+            a dict containing a tuple of valid classes as the value
+            Example values:
+            (str,)
+            (Pet,)
+            (float, none_type)
+            ([int, none_type],)
+            ({str: (bool, str, int, float, date, datetime, str, none_type)},)
+        :param _check_type: boolean, whether to check the types of the data
+            received from the server
+        :type _check_type: bool
+
+        :return: deserialized object.
+        """
+        # handle file downloading
+        # save response body into a tmp file and return the instance
+        if response_type == (file_type,):
+            content_disposition = response.getheader("Content-Disposition")
+            return deserialize_file(response.data, self.configuration,
+                                    content_disposition=content_disposition)
+
+        # fetch data from response object
+        try:
+            received_data = json.loads(response.data)
+        except ValueError:
+            received_data = response.data
+
+        # store our data under the key of 'received_data' so users have some
+        # context if they are deserializing a string and the data type is wrong
+        deserialized_data = validate_and_convert_types(
+            received_data,
+            response_type,
+            ['received_data'],
+            True,
+            _check_type,
+            configuration=self.configuration
+        )
+        return deserialized_data
+
+    def call_api(
+        self,
+        resource_path: str,
+        method: str,
+        path_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
+        query_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
+        header_params: typing.Optional[typing.Dict[str, typing.Any]] = None,
+        body: typing.Optional[typing.Any] = None,
+        post_params: typing.Optional[typing.List[typing.Tuple[str, typing.Any]]] = None,
+        files: typing.Optional[typing.Dict[str, typing.List[io.IOBase]]] = None,
+        response_type: typing.Optional[typing.Tuple[typing.Any]] = None,
+        auth_settings: typing.Optional[typing.List[str]] = None,
+        async_req: typing.Optional[bool] = None,
+        _return_http_data_only: typing.Optional[bool] = None,
+        collection_formats: typing.Optional[typing.Dict[str, str]] = None,
+        _preload_content: bool = True,
+        _request_timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None,
+        _host: typing.Optional[str] = None,
+        _check_type: typing.Optional[bool] = None,
+        _request_auths: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] = None
+    ):
+        """Makes the HTTP request (synchronous) and returns deserialized data.
+
+        To make an async_req request, set the async_req parameter.
+
+        :param resource_path: Path to method endpoint.
+        :param method: Method to call.
+        :param path_params: Path parameters in the url.
+        :param query_params: Query parameters in the url.
+        :param header_params: Header parameters to be
+            placed in the request header.
+        :param body: Request body.
+        :param post_params dict: Request post form parameters,
+            for `application/x-www-form-urlencoded`, `multipart/form-data`.
+        :param auth_settings list: Auth Settings names for the request.
+        :param response_type: For the response, a tuple containing:
+            valid classes
+            a list containing valid classes (for list schemas)
+            a dict containing a tuple of valid classes as the value
+            Example values:
+            (str,)
+            (Pet,)
+            (float, none_type)
+            ([int, none_type],)
+            ({str: (bool, str, int, float, date, datetime, str, none_type)},)
+        :param files: key -> field name, value -> a list of open file
+            objects for `multipart/form-data`.
+        :type files: dict
+        :param async_req bool: execute request asynchronously
+        :type async_req: bool, optional
+        :param _return_http_data_only: response data without head status code
+                                       and headers
+        :type _return_http_data_only: bool, optional
+        :param collection_formats: dict of collection formats for path, query,
+            header, and post parameters.
+        :type collection_formats: dict, optional
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :type _preload_content: bool, optional
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        :param _check_type: boolean describing if the data back from the server
+            should have its type checked.
+        :type _check_type: bool, optional
+        :param _request_auths: set to override the auth_settings for an a single
+                              request; this effectively ignores the authentication
+                              in the spec for a single request.
+        :type _request_auths: list, optional
+        :return:
+            If async_req parameter is True,
+            the request will be called asynchronously.
+            The method will return the request thread.
+            If parameter async_req is False or missing,
+            then the method will return the response directly.
+        """
+        if not async_req:
+            return self.__call_api(resource_path, method,
+                                   path_params, query_params, header_params,
+                                   body, post_params, files,
+                                   response_type, auth_settings,
+                                   _return_http_data_only, collection_formats,
+                                   _preload_content, _request_timeout, _host,
+                                   _check_type, _request_auths=_request_auths)
+
+        return self.pool.apply_async(self.__call_api, (resource_path,
+                                                       method, path_params,
+                                                       query_params,
+                                                       header_params, body,
+                                                       post_params, files,
+                                                       response_type,
+                                                       auth_settings,
+                                                       _return_http_data_only,
+                                                       collection_formats,
+                                                       _preload_content,
+                                                       _request_timeout,
+                                                       _host, _check_type, None, _request_auths))
+
+    def request(self, method, url, query_params=None, headers=None,
+                post_params=None, body=None, _preload_content=True,
+                _request_timeout=None):
+        """Makes the HTTP request using RESTClient."""
+        if method == "GET":
+            return self.rest_client.GET(url,
+                                        query_params=query_params,
+                                        _preload_content=_preload_content,
+                                        _request_timeout=_request_timeout,
+                                        headers=headers)
+        elif method == "HEAD":
+            return self.rest_client.HEAD(url,
+                                         query_params=query_params,
+                                         _preload_content=_preload_content,
+                                         _request_timeout=_request_timeout,
+                                         headers=headers)
+        elif method == "OPTIONS":
+            return self.rest_client.OPTIONS(url,
+                                            query_params=query_params,
+                                            headers=headers,
+                                            post_params=post_params,
+                                            _preload_content=_preload_content,
+                                            _request_timeout=_request_timeout,
+                                            body=body)
+        elif method == "POST":
+            return self.rest_client.POST(url,
+                                         query_params=query_params,
+                                         headers=headers,
+                                         post_params=post_params,
+                                         _preload_content=_preload_content,
+                                         _request_timeout=_request_timeout,
+                                         body=body)
+        elif method == "PUT":
+            return self.rest_client.PUT(url,
+                                        query_params=query_params,
+                                        headers=headers,
+                                        post_params=post_params,
+                                        _preload_content=_preload_content,
+                                        _request_timeout=_request_timeout,
+                                        body=body)
+        elif method == "PATCH":
+            return self.rest_client.PATCH(url,
+                                          query_params=query_params,
+                                          headers=headers,
+                                          post_params=post_params,
+                                          _preload_content=_preload_content,
+                                          _request_timeout=_request_timeout,
+                                          body=body)
+        elif method == "DELETE":
+            return self.rest_client.DELETE(url,
+                                           query_params=query_params,
+                                           headers=headers,
+                                           _preload_content=_preload_content,
+                                           _request_timeout=_request_timeout,
+                                           body=body)
+        else:
+            raise ApiValueError(
+                "http method must be `GET`, `HEAD`, `OPTIONS`,"
+                " `POST`, `PATCH`, `PUT` or `DELETE`."
+            )
+
+    def parameters_to_tuples(self, params, collection_formats):
+        """Get parameters as list of tuples, formatting collections.
+
+        :param params: Parameters as dict or list of two-tuples
+        :param dict collection_formats: Parameter collection formats
+        :return: Parameters as list of tuples, collections formatted
+        """
+        new_params = []
+        if collection_formats is None:
+            collection_formats = {}
+        for k, v in params.items() if isinstance(params, dict) else params:  # noqa: E501
+            if k in collection_formats:
+                collection_format = collection_formats[k]
+                if collection_format == 'multi':
+                    new_params.extend((k, value) for value in v)
+                else:
+                    if collection_format == 'ssv':
+                        delimiter = ' '
+                    elif collection_format == 'tsv':
+                        delimiter = '\t'
+                    elif collection_format == 'pipes':
+                        delimiter = '|'
+                    else:  # csv is the default
+                        delimiter = ','
+                    new_params.append(
+                        (k, delimiter.join(str(value) for value in v)))
+            else:
+                new_params.append((k, v))
+        return new_params
+
+    @staticmethod
+    def get_file_data_and_close_file(file_instance: io.IOBase) -> bytes:
+        file_data = file_instance.read()
+        file_instance.close()
+        return file_data
+
+    def files_parameters(self, files: typing.Optional[typing.Dict[str, typing.List[io.IOBase]]] = None):
+        """Builds form parameters.
+
+        :param files: None or a dict with key=param_name and
+            value is a list of open file objects
+        :return: List of tuples of form parameters with file data
+        """
+        if files is None:
+            return []
+
+        params = []
+        for param_name, file_instances in files.items():
+            if file_instances is None:
+                # if the file field is nullable, skip None values
+                continue
+            for file_instance in file_instances:
+                if file_instance is None:
+                    # if the file field is nullable, skip None values
+                    continue
+                if file_instance.closed is True:
+                    raise ApiValueError(
+                        "Cannot read a closed file. The passed in file_type "
+                        "for %s must be open." % param_name
+                    )
+                filename = os.path.basename(file_instance.name)
+                filedata = self.get_file_data_and_close_file(file_instance)
+                mimetype = (mimetypes.guess_type(filename)[0] or
+                            'application/octet-stream')
+                params.append(
+                    tuple([param_name, tuple([filename, filedata, mimetype])]))
+
+        return params
+
+    def select_header_accept(self, accepts):
+        """Returns `Accept` based on an array of accepts provided.
+
+        :param accepts: List of headers.
+        :return: Accept (e.g. application/json).
+        """
+        if not accepts:
+            return
+
+        accepts = [x.lower() for x in accepts]
+
+        if 'application/json' in accepts:
+            return 'application/json'
+        else:
+            return ', '.join(accepts)
+
+    def select_header_content_type(self, content_types, method=None, body=None):
+        """Returns `Content-Type` based on an array of content_types provided.
+
+        :param content_types: List of content-types.
+        :param method: http method (e.g. POST, PATCH).
+        :param body: http body to send.
+        :return: Content-Type (e.g. application/json).
+        """
+        if not content_types:
+            return None
+
+        content_types = [x.lower() for x in content_types]
+
+        if (method == 'PATCH' and
+                'application/json-patch+json' in content_types and
+                isinstance(body, list)):
+            return 'application/json-patch+json'
+
+        if 'application/json' in content_types or '*/*' in content_types:
+            return 'application/json'
+        else:
+            return content_types[0]
+
+    def update_params_for_auth(self, headers, queries, auth_settings,
+                               resource_path, method, body, request_auths=None):
+        """Updates header and query params based on authentication setting.
+
+        :param headers: Header parameters dict to be updated.
+        :param queries: Query parameters tuple list to be updated.
+        :param auth_settings: Authentication setting identifiers list.
+        :param resource_path: A string representation of the HTTP request resource path.
+        :param method: A string representation of the HTTP request method.
+        :param body: A object representing the body of the HTTP request.
+            The object type is the return value of _encoder.default().
+        :param request_auths: if set, the provided settings will
+            override the token in the configuration.
+        """
+        if not auth_settings:
+            return
+
+        if request_auths:
+            for auth_setting in request_auths:
+                self._apply_auth_params(headers, queries, resource_path, method, body, auth_setting)
+            return
+
+        for auth in auth_settings:
+            auth_setting = self.configuration.auth_settings().get(auth)
+            if auth_setting:
+                self._apply_auth_params(headers, queries, resource_path, method, body, auth_setting)
+
+    def _apply_auth_params(self, headers, queries, resource_path, method, body, auth_setting):
+        if auth_setting['in'] == 'cookie':
+            headers['Cookie'] = auth_setting['value']
+        elif auth_setting['in'] == 'header':
+            if auth_setting['type'] != 'http-signature':
+                headers[auth_setting['key']] = auth_setting['value']
+        elif auth_setting['in'] == 'query':
+            queries.append((auth_setting['key'], auth_setting['value']))
+        else:
+            raise ApiValueError(
+                'Authentication token must be in `query` or `header`'
+            )
+
+
+class Endpoint(object):
+    def __init__(self, settings=None, params_map=None, root_map=None,
+                 headers_map=None, api_client=None, callable=None):
+        """Creates an endpoint
+
+        Args:
+            settings (dict): see below key value pairs
+                'response_type' (tuple/None): response type
+                'auth' (list): a list of auth type keys
+                'endpoint_path' (str): the endpoint path
+                'operation_id' (str): endpoint string identifier
+                'http_method' (str): POST/PUT/PATCH/GET etc
+                'servers' (list): list of str servers that this endpoint is at
+            params_map (dict): see below key value pairs
+                'all' (list): list of str endpoint parameter names
+                'required' (list): list of required parameter names
+                'nullable' (list): list of nullable parameter names
+                'enum' (list): list of parameters with enum values
+                'validation' (list): list of parameters with validations
+            root_map
+                'validations' (dict): the dict mapping endpoint parameter tuple
+                    paths to their validation dictionaries
+                'allowed_values' (dict): the dict mapping endpoint parameter
+                    tuple paths to their allowed_values (enum) dictionaries
+                'openapi_types' (dict): param_name to openapi type
+                'attribute_map' (dict): param_name to camelCase name
+                'location_map' (dict): param_name to  'body', 'file', 'form',
+                    'header', 'path', 'query'
+                collection_format_map (dict): param_name to `csv` etc.
+            headers_map (dict): see below key value pairs
+                'accept' (list): list of Accept header strings
+                'content_type' (list): list of Content-Type header strings
+            api_client (ApiClient) api client instance
+            callable (function): the function which is invoked when the
+                Endpoint is called
+        """
+        self.settings = settings
+        self.params_map = params_map
+        self.params_map['all'].extend([
+            'async_req',
+            '_host_index',
+            '_preload_content',
+            '_request_timeout',
+            '_return_http_data_only',
+            '_check_input_type',
+            '_check_return_type',
+            '_content_type',
+            '_spec_property_naming',
+            '_request_auths'
+        ])
+        self.params_map['nullable'].extend(['_request_timeout'])
+        self.validations = root_map['validations']
+        self.allowed_values = root_map['allowed_values']
+        self.openapi_types = root_map['openapi_types']
+        extra_types = {
+            'async_req': (bool,),
+            '_host_index': (none_type, int),
+            '_preload_content': (bool,),
+            '_request_timeout': (none_type, float, (float,), [float], int, (int,), [int]),
+            '_return_http_data_only': (bool,),
+            '_check_input_type': (bool,),
+            '_check_return_type': (bool,),
+            '_spec_property_naming': (bool,),
+            '_content_type': (none_type, str),
+            '_request_auths': (none_type, list)
+        }
+        self.openapi_types.update(extra_types)
+        self.attribute_map = root_map['attribute_map']
+        self.location_map = root_map['location_map']
+        self.collection_format_map = root_map['collection_format_map']
+        self.headers_map = headers_map
+        self.api_client = api_client
+        self.callable = callable
+
+    def __validate_inputs(self, kwargs):
+        for param in self.params_map['enum']:
+            if param in kwargs:
+                check_allowed_values(
+                    self.allowed_values,
+                    (param,),
+                    kwargs[param]
+                )
+
+        for param in self.params_map['validation']:
+            if param in kwargs:
+                check_validations(
+                    self.validations,
+                    (param,),
+                    kwargs[param],
+                    configuration=self.api_client.configuration
+                )
+
+        if kwargs['_check_input_type'] is False:
+            return
+
+        for key, value in kwargs.items():
+            fixed_val = validate_and_convert_types(
+                value,
+                self.openapi_types[key],
+                [key],
+                kwargs['_spec_property_naming'],
+                kwargs['_check_input_type'],
+                configuration=self.api_client.configuration
+            )
+            kwargs[key] = fixed_val
+
+    def __gather_params(self, kwargs):
+        params = {
+            'body': None,
+            'collection_format': {},
+            'file': {},
+            'form': [],
+            'header': {},
+            'path': {},
+            'query': []
+        }
+
+        for param_name, param_value in kwargs.items():
+            param_location = self.location_map.get(param_name)
+            if param_location is None:
+                continue
+            if param_location:
+                if param_location == 'body':
+                    params['body'] = param_value
+                    continue
+                base_name = self.attribute_map[param_name]
+                if (param_location == 'form' and
+                        self.openapi_types[param_name] == (file_type,)):
+                    params['file'][base_name] = [param_value]
+                elif (param_location == 'form' and
+                        self.openapi_types[param_name] == ([file_type],)):
+                    # param_value is already a list
+                    params['file'][base_name] = param_value
+                elif param_location in {'form', 'query'}:
+                    param_value_full = (base_name, param_value)
+                    params[param_location].append(param_value_full)
+                if param_location not in {'form', 'query'}:
+                    params[param_location][base_name] = param_value
+                collection_format = self.collection_format_map.get(param_name)
+                if collection_format:
+                    params['collection_format'][base_name] = collection_format
+
+        return params
+
+    def __call__(self, *args, **kwargs):
+        """ This method is invoked when endpoints are called
+        Example:
+
+        api_instance = TurbiniaConfigurationApi()
+        api_instance.get_evidence_attributes_by_name  # this is an instance of the class Endpoint
+        api_instance.get_evidence_attributes_by_name()  # this invokes api_instance.get_evidence_attributes_by_name.__call__()
+        which then invokes the callable functions stored in that endpoint at
+        api_instance.get_evidence_attributes_by_name.callable or self.callable in this class
+
+        """
+        return self.callable(self, *args, **kwargs)
+
+    def call_with_http_info(self, **kwargs):
+
+        try:
+            index = self.api_client.configuration.server_operation_index.get(
+                self.settings['operation_id'], self.api_client.configuration.server_index
+            ) if kwargs['_host_index'] is None else kwargs['_host_index']
+            server_variables = self.api_client.configuration.server_operation_variables.get(
+                self.settings['operation_id'], self.api_client.configuration.server_variables
+            )
+            _host = self.api_client.configuration.get_host_from_settings(
+                index, variables=server_variables, servers=self.settings['servers']
+            )
+        except IndexError:
+            if self.settings['servers']:
+                raise ApiValueError(
+                    "Invalid host index. Must be 0 <= index < %s" %
+                    len(self.settings['servers'])
+                )
+            _host = None
+
+        for key, value in kwargs.items():
+            if key not in self.params_map['all']:
+                raise ApiTypeError(
+                    "Got an unexpected parameter '%s'"
+                    " to method `%s`" %
+                    (key, self.settings['operation_id'])
+                )
+            # only throw this nullable ApiValueError if _check_input_type
+            # is False, if _check_input_type==True we catch this case
+            # in self.__validate_inputs
+            if (key not in self.params_map['nullable'] and value is None
+                    and kwargs['_check_input_type'] is False):
+                raise ApiValueError(
+                    "Value may not be None for non-nullable parameter `%s`"
+                    " when calling `%s`" %
+                    (key, self.settings['operation_id'])
+                )
+
+        for key in self.params_map['required']:
+            if key not in kwargs.keys():
+                raise ApiValueError(
+                    "Missing the required parameter `%s` when calling "
+                    "`%s`" % (key, self.settings['operation_id'])
+                )
+
+        self.__validate_inputs(kwargs)
+
+        params = self.__gather_params(kwargs)
+
+        accept_headers_list = self.headers_map['accept']
+        if accept_headers_list:
+            params['header']['Accept'] = self.api_client.select_header_accept(
+                accept_headers_list)
+
+        if kwargs.get('_content_type'):
+            params['header']['Content-Type'] = kwargs['_content_type']
+        else:
+            content_type_headers_list = self.headers_map['content_type']
+            if content_type_headers_list:
+                if params['body'] != "":
+                    content_types_list = self.api_client.select_header_content_type(
+                        content_type_headers_list, self.settings['http_method'],
+                        params['body'])
+                    if content_types_list:
+                        params['header']['Content-Type'] = content_types_list
+
+        return self.api_client.call_api(
+            self.settings['endpoint_path'], self.settings['http_method'],
+            params['path'],
+            params['query'],
+            params['header'],
+            body=params['body'],
+            post_params=params['form'],
+            files=params['file'],
+            response_type=self.settings['response_type'],
+            auth_settings=self.settings['auth'],
+            async_req=kwargs['async_req'],
+            _check_type=kwargs['_check_return_type'],
+            _return_http_data_only=kwargs['_return_http_data_only'],
+            _preload_content=kwargs['_preload_content'],
+            _request_timeout=kwargs['_request_timeout'],
+            _host=_host,
+            _request_auths=kwargs['_request_auths'],
+            collection_formats=params['collection_format'])
diff --git a/turbinia/api/client/turbinia_api_client/apis/__init__.py b/turbinia/api/client/turbinia_api_client/apis/__init__.py
new file mode 100644
index 000000000..6b1267afe
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/apis/__init__.py
@@ -0,0 +1,22 @@
+
+# flake8: noqa
+
+# Import all APIs into this package.
+# If you have many APIs here with many many models used in each API this may
+# raise a `RecursionError`.
+# In order to avoid this, import only the API that you directly need like:
+#
+#   from turbinia_api_client.api.turbinia_configuration_api import TurbiniaConfigurationApi
+#
+# or import this package, but before doing it, use:
+#
+#   import sys
+#   sys.setrecursionlimit(n)
+
+# Import APIs into API package:
+from turbinia_api_client.api.turbinia_configuration_api import TurbiniaConfigurationApi
+from turbinia_api_client.api.turbinia_jobs_api import TurbiniaJobsApi
+from turbinia_api_client.api.turbinia_logs_api import TurbiniaLogsApi
+from turbinia_api_client.api.turbinia_request_results_api import TurbiniaRequestResultsApi
+from turbinia_api_client.api.turbinia_requests_api import TurbiniaRequestsApi
+from turbinia_api_client.api.turbinia_tasks_api import TurbiniaTasksApi
diff --git a/turbinia/api/client/turbinia_api_client/configuration.py b/turbinia/api/client/turbinia_api_client/configuration.py
new file mode 100644
index 000000000..7c4d3377e
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/configuration.py
@@ -0,0 +1,454 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import copy
+import logging
+import multiprocessing
+import sys
+import urllib3
+
+from http import client as http_client
+from turbinia_api_client.exceptions import ApiValueError
+
+
+JSON_SCHEMA_VALIDATION_KEYWORDS = {
+    'multipleOf', 'maximum', 'exclusiveMaximum',
+    'minimum', 'exclusiveMinimum', 'maxLength',
+    'minLength', 'pattern', 'maxItems', 'minItems'
+}
+
+class Configuration(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+
+    Ref: https://openapi-generator.tech
+    Do not edit the class manually.
+
+    :param host: Base url
+    :param api_key: Dict to store API key(s).
+      Each entry in the dict specifies an API key.
+      The dict key is the name of the security scheme in the OAS specification.
+      The dict value is the API key secret.
+    :param api_key_prefix: Dict to store API prefix (e.g. Bearer)
+      The dict key is the name of the security scheme in the OAS specification.
+      The dict value is an API key prefix when generating the auth data.
+    :param username: Username for HTTP basic authentication
+    :param password: Password for HTTP basic authentication
+    :param discard_unknown_keys: Boolean value indicating whether to discard
+      unknown properties. A server may send a response that includes additional
+      properties that are not known by the client in the following scenarios:
+      1. The OpenAPI document is incomplete, i.e. it does not match the server
+         implementation.
+      2. The client was generated using an older version of the OpenAPI document
+         and the server has been upgraded since then.
+      If a schema in the OpenAPI document defines the additionalProperties attribute,
+      then all undeclared properties received by the server are injected into the
+      additional properties map. In that case, there are undeclared properties, and
+      nothing to discard.
+    :param disabled_client_side_validations (string): Comma-separated list of
+      JSON schema validation keywords to disable JSON schema structural validation
+      rules. The following keywords may be specified: multipleOf, maximum,
+      exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern,
+      maxItems, minItems.
+      By default, the validation is performed for data generated locally by the client
+      and data received from the server, independent of any validation performed by
+      the server side. If the input data does not satisfy the JSON schema validation
+      rules specified in the OpenAPI document, an exception is raised.
+      If disabled_client_side_validations is set, structural validation is
+      disabled. This can be useful to troubleshoot data validation problem, such as
+      when the OpenAPI document validation rules do not match the actual API data
+      received by the server.
+    :param server_index: Index to servers configuration.
+    :param server_variables: Mapping with string values to replace variables in
+      templated server configuration. The validation of enums is performed for
+      variables with defined enum values before.
+    :param server_operation_index: Mapping from operation ID to an index to server
+      configuration.
+    :param server_operation_variables: Mapping from operation ID to a mapping with
+      string values to replace variables in templated server configuration.
+      The validation of enums is performed for variables with defined enum values before.
+    :param ssl_ca_cert: str - the path to a file of concatenated CA certificates
+      in PEM format
+
+    :Example:
+    """
+
+    _default = None
+
+    def __init__(self, host=None,
+                 api_key=None, api_key_prefix=None,
+                 access_token=None,
+                 username=None, password=None,
+                 discard_unknown_keys=False,
+                 disabled_client_side_validations="",
+                 server_index=None, server_variables=None,
+                 server_operation_index=None, server_operation_variables=None,
+                 ssl_ca_cert=None,
+                 ):
+        """Constructor
+        """
+        self._base_path = "http://localhost" if host is None else host
+        """Default Base url
+        """
+        self.server_index = 0 if server_index is None and host is None else server_index
+        self.server_operation_index = server_operation_index or {}
+        """Default server index
+        """
+        self.server_variables = server_variables or {}
+        self.server_operation_variables = server_operation_variables or {}
+        """Default server variables
+        """
+        self.temp_folder_path = None
+        """Temp file folder for downloading files
+        """
+        # Authentication Settings
+        self.access_token = access_token
+        self.api_key = {}
+        if api_key:
+            self.api_key = api_key
+        """dict to store API key(s)
+        """
+        self.api_key_prefix = {}
+        if api_key_prefix:
+            self.api_key_prefix = api_key_prefix
+        """dict to store API prefix (e.g. Bearer)
+        """
+        self.refresh_api_key_hook = None
+        """function hook to refresh API key if expired
+        """
+        self.username = username
+        """Username for HTTP basic authentication
+        """
+        self.password = password
+        """Password for HTTP basic authentication
+        """
+        self.discard_unknown_keys = discard_unknown_keys
+        self.disabled_client_side_validations = disabled_client_side_validations
+        self.logger = {}
+        """Logging Settings
+        """
+        self.logger["package_logger"] = logging.getLogger("turbinia_api_client")
+        self.logger["urllib3_logger"] = logging.getLogger("urllib3")
+        self.logger_format = '%(asctime)s %(levelname)s %(message)s'
+        """Log format
+        """
+        self.logger_stream_handler = None
+        """Log stream handler
+        """
+        self.logger_file_handler = None
+        """Log file handler
+        """
+        self.logger_file = None
+        """Debug file location
+        """
+        self.debug = False
+        """Debug switch
+        """
+
+        self.verify_ssl = True
+        """SSL/TLS verification
+           Set this to false to skip verifying SSL certificate when calling API
+           from https server.
+        """
+        self.ssl_ca_cert = ssl_ca_cert
+        """Set this to customize the certificate file to verify the peer.
+        """
+        self.cert_file = None
+        """client certificate file
+        """
+        self.key_file = None
+        """client key file
+        """
+        self.assert_hostname = None
+        """Set this to True/False to enable/disable SSL hostname verification.
+        """
+
+        self.connection_pool_maxsize = multiprocessing.cpu_count() * 5
+        """urllib3 connection pool's maximum number of connections saved
+           per pool. urllib3 uses 1 connection as default value, but this is
+           not the best value when you are making a lot of possibly parallel
+           requests to the same host, which is often the case here.
+           cpu_count * 5 is used as default value to increase performance.
+        """
+
+        self.proxy = None
+        """Proxy URL
+        """
+        self.no_proxy = None
+        """bypass proxy for host in the no_proxy list.
+        """
+        self.proxy_headers = None
+        """Proxy headers
+        """
+        self.safe_chars_for_path_param = ''
+        """Safe chars for path_param
+        """
+        self.retries = None
+        """Adding retries to override urllib3 default value 3
+        """
+        # Enable client side validation
+        self.client_side_validation = True
+
+        # Options to pass down to the underlying urllib3 socket
+        self.socket_options = None
+
+    def __deepcopy__(self, memo):
+        cls = self.__class__
+        result = cls.__new__(cls)
+        memo[id(self)] = result
+        for k, v in self.__dict__.items():
+            if k not in ('logger', 'logger_file_handler'):
+                setattr(result, k, copy.deepcopy(v, memo))
+        # shallow copy of loggers
+        result.logger = copy.copy(self.logger)
+        # use setters to configure loggers
+        result.logger_file = self.logger_file
+        result.debug = self.debug
+        return result
+
+    def __setattr__(self, name, value):
+        object.__setattr__(self, name, value)
+        if name == 'disabled_client_side_validations':
+            s = set(filter(None, value.split(',')))
+            for v in s:
+                if v not in JSON_SCHEMA_VALIDATION_KEYWORDS:
+                    raise ApiValueError(
+                        "Invalid keyword: '{0}''".format(v))
+            self._disabled_client_side_validations = s
+
+    @classmethod
+    def set_default(cls, default):
+        """Set default instance of configuration.
+
+        It stores default configuration, which can be
+        returned by get_default_copy method.
+
+        :param default: object of Configuration
+        """
+        cls._default = copy.deepcopy(default)
+
+    @classmethod
+    def get_default_copy(cls):
+        """Return new instance of configuration.
+
+        This method returns newly created, based on default constructor,
+        object of Configuration class or returns a copy of default
+        configuration passed by the set_default method.
+
+        :return: The configuration object.
+        """
+        if cls._default is not None:
+            return copy.deepcopy(cls._default)
+        return Configuration()
+
+    @property
+    def logger_file(self):
+        """The logger file.
+
+        If the logger_file is None, then add stream handler and remove file
+        handler. Otherwise, add file handler and remove stream handler.
+
+        :param value: The logger_file path.
+        :type: str
+        """
+        return self.__logger_file
+
+    @logger_file.setter
+    def logger_file(self, value):
+        """The logger file.
+
+        If the logger_file is None, then add stream handler and remove file
+        handler. Otherwise, add file handler and remove stream handler.
+
+        :param value: The logger_file path.
+        :type: str
+        """
+        self.__logger_file = value
+        if self.__logger_file:
+            # If set logging file,
+            # then add file handler and remove stream handler.
+            self.logger_file_handler = logging.FileHandler(self.__logger_file)
+            self.logger_file_handler.setFormatter(self.logger_formatter)
+            for _, logger in self.logger.items():
+                logger.addHandler(self.logger_file_handler)
+
+    @property
+    def debug(self):
+        """Debug status
+
+        :param value: The debug status, True or False.
+        :type: bool
+        """
+        return self.__debug
+
+    @debug.setter
+    def debug(self, value):
+        """Debug status
+
+        :param value: The debug status, True or False.
+        :type: bool
+        """
+        self.__debug = value
+        if self.__debug:
+            # if debug status is True, turn on debug logging
+            for _, logger in self.logger.items():
+                logger.setLevel(logging.DEBUG)
+            # turn on http_client debug
+            http_client.HTTPConnection.debuglevel = 1
+        else:
+            # if debug status is False, turn off debug logging,
+            # setting log level to default `logging.WARNING`
+            for _, logger in self.logger.items():
+                logger.setLevel(logging.WARNING)
+            # turn off http_client debug
+            http_client.HTTPConnection.debuglevel = 0
+
+    @property
+    def logger_format(self):
+        """The logger format.
+
+        The logger_formatter will be updated when sets logger_format.
+
+        :param value: The format string.
+        :type: str
+        """
+        return self.__logger_format
+
+    @logger_format.setter
+    def logger_format(self, value):
+        """The logger format.
+
+        The logger_formatter will be updated when sets logger_format.
+
+        :param value: The format string.
+        :type: str
+        """
+        self.__logger_format = value
+        self.logger_formatter = logging.Formatter(self.__logger_format)
+
+    def get_api_key_with_prefix(self, identifier, alias=None):
+        """Gets API key (with prefix if set).
+
+        :param identifier: The identifier of apiKey.
+        :param alias: The alternative identifier of apiKey.
+        :return: The token for api key authentication.
+        """
+        if self.refresh_api_key_hook is not None:
+            self.refresh_api_key_hook(self)
+        key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None)
+        if key:
+            prefix = self.api_key_prefix.get(identifier)
+            if prefix:
+                return "%s %s" % (prefix, key)
+            else:
+                return key
+
+    def get_basic_auth_token(self):
+        """Gets HTTP basic authentication header (string).
+
+        :return: The token for basic HTTP authentication.
+        """
+        username = ""
+        if self.username is not None:
+            username = self.username
+        password = ""
+        if self.password is not None:
+            password = self.password
+        return urllib3.util.make_headers(
+            basic_auth=username + ':' + password
+        ).get('authorization')
+
+    def auth_settings(self):
+        """Gets Auth Settings dict for api client.
+
+        :return: The Auth Settings information dict.
+        """
+        auth = {}
+        if self.access_token is not None:
+            auth['oAuth2'] = {
+                'type': 'oauth2',
+                'in': 'header',
+                'key': 'Authorization',
+                'value': 'Bearer ' + self.access_token
+            }
+        return auth
+
+    def to_debug_report(self):
+        """Gets the essential information for debugging.
+
+        :return: The report for debugging.
+        """
+        return "Python SDK Debug Report:\n"\
+               "OS: {env}\n"\
+               "Python Version: {pyversion}\n"\
+               "Version of the API: 1.0.0\n"\
+               "SDK Package Version: 1.0.0".\
+               format(env=sys.platform, pyversion=sys.version)
+
+    def get_host_settings(self):
+        """Gets an array of host settings
+
+        :return: An array of host settings
+        """
+        return [
+            {
+                'url': "",
+                'description': "No description provided",
+            }
+        ]
+
+    def get_host_from_settings(self, index, variables=None, servers=None):
+        """Gets host URL based on the index and variables
+        :param index: array index of the host settings
+        :param variables: hash of variable and the corresponding value
+        :param servers: an array of host settings or None
+        :return: URL based on host settings
+        """
+        if index is None:
+            return self._base_path
+
+        variables = {} if variables is None else variables
+        servers = self.get_host_settings() if servers is None else servers
+
+        try:
+            server = servers[index]
+        except IndexError:
+            raise ValueError(
+                "Invalid index {0} when selecting the host settings. "
+                "Must be less than {1}".format(index, len(servers)))
+
+        url = server['url']
+
+        # go through variables and replace placeholders
+        for variable_name, variable in server.get('variables', {}).items():
+            used_value = variables.get(
+                variable_name, variable['default_value'])
+
+            if 'enum_values' in variable \
+                    and used_value not in variable['enum_values']:
+                raise ValueError(
+                    "The variable `{0}` in the host URL has invalid value "
+                    "{1}. Must be {2}.".format(
+                        variable_name, variables[variable_name],
+                        variable['enum_values']))
+
+            url = url.replace("{" + variable_name + "}", used_value)
+
+        return url
+
+    @property
+    def host(self):
+        """Return generated host."""
+        return self.get_host_from_settings(self.server_index, variables=self.server_variables)
+
+    @host.setter
+    def host(self, value):
+        """Fix base path."""
+        self._base_path = value
+        self.server_index = None
diff --git a/turbinia/api/client/turbinia_api_client/exceptions.py b/turbinia/api/client/turbinia_api_client/exceptions.py
new file mode 100644
index 000000000..a6fbcd40c
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/exceptions.py
@@ -0,0 +1,159 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+
+class OpenApiException(Exception):
+    """The base exception class for all OpenAPIExceptions"""
+
+
+class ApiTypeError(OpenApiException, TypeError):
+    def __init__(self, msg, path_to_item=None, valid_classes=None,
+                 key_type=None):
+        """ Raises an exception for TypeErrors
+
+        Args:
+            msg (str): the exception message
+
+        Keyword Args:
+            path_to_item (list): a list of keys an indices to get to the
+                                 current_item
+                                 None if unset
+            valid_classes (tuple): the primitive classes that current item
+                                   should be an instance of
+                                   None if unset
+            key_type (bool): False if our value is a value in a dict
+                             True if it is a key in a dict
+                             False if our item is an item in a list
+                             None if unset
+        """
+        self.path_to_item = path_to_item
+        self.valid_classes = valid_classes
+        self.key_type = key_type
+        full_msg = msg
+        if path_to_item:
+            full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
+        super(ApiTypeError, self).__init__(full_msg)
+
+
+class ApiValueError(OpenApiException, ValueError):
+    def __init__(self, msg, path_to_item=None):
+        """
+        Args:
+            msg (str): the exception message
+
+        Keyword Args:
+            path_to_item (list) the path to the exception in the
+                received_data dict. None if unset
+        """
+
+        self.path_to_item = path_to_item
+        full_msg = msg
+        if path_to_item:
+            full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
+        super(ApiValueError, self).__init__(full_msg)
+
+
+class ApiAttributeError(OpenApiException, AttributeError):
+    def __init__(self, msg, path_to_item=None):
+        """
+        Raised when an attribute reference or assignment fails.
+
+        Args:
+            msg (str): the exception message
+
+        Keyword Args:
+            path_to_item (None/list) the path to the exception in the
+                received_data dict
+        """
+        self.path_to_item = path_to_item
+        full_msg = msg
+        if path_to_item:
+            full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
+        super(ApiAttributeError, self).__init__(full_msg)
+
+
+class ApiKeyError(OpenApiException, KeyError):
+    def __init__(self, msg, path_to_item=None):
+        """
+        Args:
+            msg (str): the exception message
+
+        Keyword Args:
+            path_to_item (None/list) the path to the exception in the
+                received_data dict
+        """
+        self.path_to_item = path_to_item
+        full_msg = msg
+        if path_to_item:
+            full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
+        super(ApiKeyError, self).__init__(full_msg)
+
+
+class ApiException(OpenApiException):
+
+    def __init__(self, status=None, reason=None, http_resp=None):
+        if http_resp:
+            self.status = http_resp.status
+            self.reason = http_resp.reason
+            self.body = http_resp.data
+            self.headers = http_resp.getheaders()
+        else:
+            self.status = status
+            self.reason = reason
+            self.body = None
+            self.headers = None
+
+    def __str__(self):
+        """Custom error messages for exception"""
+        error_message = "Status Code: {0}\n"\
+                        "Reason: {1}\n".format(self.status, self.reason)
+        if self.headers:
+            error_message += "HTTP response headers: {0}\n".format(
+                self.headers)
+
+        if self.body:
+            error_message += "HTTP response body: {0}\n".format(self.body)
+
+        return error_message
+
+
+class NotFoundException(ApiException):
+
+    def __init__(self, status=None, reason=None, http_resp=None):
+        super(NotFoundException, self).__init__(status, reason, http_resp)
+
+
+class UnauthorizedException(ApiException):
+
+    def __init__(self, status=None, reason=None, http_resp=None):
+        super(UnauthorizedException, self).__init__(status, reason, http_resp)
+
+
+class ForbiddenException(ApiException):
+
+    def __init__(self, status=None, reason=None, http_resp=None):
+        super(ForbiddenException, self).__init__(status, reason, http_resp)
+
+
+class ServiceException(ApiException):
+
+    def __init__(self, status=None, reason=None, http_resp=None):
+        super(ServiceException, self).__init__(status, reason, http_resp)
+
+
+def render_path(path_to_item):
+    """Returns a string representation of a path"""
+    result = ""
+    for pth in path_to_item:
+        if isinstance(pth, int):
+            result += "[{0}]".format(pth)
+        else:
+            result += "['{0}']".format(pth)
+    return result
diff --git a/turbinia/api/client/turbinia_api_client/model/__init__.py b/turbinia/api/client/turbinia_api_client/model/__init__.py
new file mode 100644
index 000000000..3278d67ce
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/__init__.py
@@ -0,0 +1,5 @@
+# we can not import model classes here because that would create a circular
+# reference which would not work in python2
+# do not import all models into this module because that uses a lot of memory and stack frames
+# if you need the ability to import all models from one package, import them with
+# from turbinia_api_client.models import ModelA, ModelB
diff --git a/turbinia/api/client/turbinia_api_client/model/base_request_options.py b/turbinia/api/client/turbinia_api_client/model/base_request_options.py
new file mode 100644
index 000000000..c25d00f41
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/base_request_options.py
@@ -0,0 +1,303 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.model_utils import (  # noqa: F401
+    ApiTypeError,
+    ModelComposed,
+    ModelNormal,
+    ModelSimple,
+    cached_property,
+    change_keys_js_to_python,
+    convert_js_args_to_python_args,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_get_composed_info,
+    OpenApiModel
+)
+from turbinia_api_client.exceptions import ApiAttributeError
+
+
+
+class BaseRequestOptions(ModelNormal):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+
+    Attributes:
+      allowed_values (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          with a capitalized key describing the allowed value and an allowed
+          value. These dicts store the allowed enum values.
+      attribute_map (dict): The key is attribute name
+          and the value is json key in definition.
+      discriminator_value_class_map (dict): A dict to go from the discriminator
+          variable value to the discriminator class name.
+      validations (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          that stores validations for max_length, min_length, max_items,
+          min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+          inclusive_minimum, and regex.
+      additional_properties_type (tuple): A tuple of classes accepted
+          as additional properties values.
+    """
+
+    allowed_values = {
+    }
+
+    validations = {
+    }
+
+    @cached_property
+    def additional_properties_type():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+        """
+        return (bool, date, datetime, dict, float, int, list, str, none_type,)  # noqa: E501
+
+    _nullable = False
+
+    @cached_property
+    def openapi_types():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+
+        Returns
+            openapi_types (dict): The key is attribute name
+                and the value is attribute type.
+        """
+        return {
+            'filter_patterns': ([str],),  # noqa: E501
+            'group_id': (str,),  # noqa: E501
+            'jobs_allowlist': ([str],),  # noqa: E501
+            'jobs_denylist': ([str],),  # noqa: E501
+            'reason': (str,),  # noqa: E501
+            'recipe_data': (str,),  # noqa: E501
+            'recipe_name': (str,),  # noqa: E501
+            'request_id': (str,),  # noqa: E501
+            'requester': (str,),  # noqa: E501
+            'sketch_id': (int,),  # noqa: E501
+            'yara_rules': (str,),  # noqa: E501
+        }
+
+    @cached_property
+    def discriminator():
+        return None
+
+
+    attribute_map = {
+        'filter_patterns': 'filter_patterns',  # noqa: E501
+        'group_id': 'group_id',  # noqa: E501
+        'jobs_allowlist': 'jobs_allowlist',  # noqa: E501
+        'jobs_denylist': 'jobs_denylist',  # noqa: E501
+        'reason': 'reason',  # noqa: E501
+        'recipe_data': 'recipe_data',  # noqa: E501
+        'recipe_name': 'recipe_name',  # noqa: E501
+        'request_id': 'request_id',  # noqa: E501
+        'requester': 'requester',  # noqa: E501
+        'sketch_id': 'sketch_id',  # noqa: E501
+        'yara_rules': 'yara_rules',  # noqa: E501
+    }
+
+    read_only_vars = {
+    }
+
+    _composed_schemas = {}
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _from_openapi_data(cls, *args, **kwargs):  # noqa: E501
+        """BaseRequestOptions - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            filter_patterns ([str]): [optional]  # noqa: E501
+            group_id (str): [optional]  # noqa: E501
+            jobs_allowlist ([str]): [optional]  # noqa: E501
+            jobs_denylist ([str]): [optional]  # noqa: E501
+            reason (str): [optional]  # noqa: E501
+            recipe_data (str): [optional]  # noqa: E501
+            recipe_name (str): [optional]  # noqa: E501
+            request_id (str): [optional]  # noqa: E501
+            requester (str): [optional]  # noqa: E501
+            sketch_id (int): [optional]  # noqa: E501
+            yara_rules (str): [optional]  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', True)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        self = super(OpenApiModel, cls).__new__(cls)
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+        return self
+
+    required_properties = set([
+        '_data_store',
+        '_check_type',
+        '_spec_property_naming',
+        '_path_to_item',
+        '_configuration',
+        '_visited_composed_classes',
+    ])
+
+    @convert_js_args_to_python_args
+    def __init__(self, *args, **kwargs):  # noqa: E501
+        """BaseRequestOptions - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            filter_patterns ([str]): [optional]  # noqa: E501
+            group_id (str): [optional]  # noqa: E501
+            jobs_allowlist ([str]): [optional]  # noqa: E501
+            jobs_denylist ([str]): [optional]  # noqa: E501
+            reason (str): [optional]  # noqa: E501
+            recipe_data (str): [optional]  # noqa: E501
+            recipe_name (str): [optional]  # noqa: E501
+            request_id (str): [optional]  # noqa: E501
+            requester (str): [optional]  # noqa: E501
+            sketch_id (int): [optional]  # noqa: E501
+            yara_rules (str): [optional]  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+            if var_name in self.read_only_vars:
+                raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+                                     f"class with read only attributes.")
diff --git a/turbinia/api/client/turbinia_api_client/model/http_validation_error.py b/turbinia/api/client/turbinia_api_client/model/http_validation_error.py
new file mode 100644
index 000000000..b946fdc22
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/http_validation_error.py
@@ -0,0 +1,269 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.model_utils import (  # noqa: F401
+    ApiTypeError,
+    ModelComposed,
+    ModelNormal,
+    ModelSimple,
+    cached_property,
+    change_keys_js_to_python,
+    convert_js_args_to_python_args,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_get_composed_info,
+    OpenApiModel
+)
+from turbinia_api_client.exceptions import ApiAttributeError
+
+
+def lazy_import():
+    from turbinia_api_client.model.validation_error import ValidationError
+    globals()['ValidationError'] = ValidationError
+
+
+class HTTPValidationError(ModelNormal):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+
+    Attributes:
+      allowed_values (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          with a capitalized key describing the allowed value and an allowed
+          value. These dicts store the allowed enum values.
+      attribute_map (dict): The key is attribute name
+          and the value is json key in definition.
+      discriminator_value_class_map (dict): A dict to go from the discriminator
+          variable value to the discriminator class name.
+      validations (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          that stores validations for max_length, min_length, max_items,
+          min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+          inclusive_minimum, and regex.
+      additional_properties_type (tuple): A tuple of classes accepted
+          as additional properties values.
+    """
+
+    allowed_values = {
+    }
+
+    validations = {
+    }
+
+    @cached_property
+    def additional_properties_type():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+        """
+        lazy_import()
+        return (bool, date, datetime, dict, float, int, list, str, none_type,)  # noqa: E501
+
+    _nullable = False
+
+    @cached_property
+    def openapi_types():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+
+        Returns
+            openapi_types (dict): The key is attribute name
+                and the value is attribute type.
+        """
+        lazy_import()
+        return {
+            'detail': ([ValidationError],),  # noqa: E501
+        }
+
+    @cached_property
+    def discriminator():
+        return None
+
+
+    attribute_map = {
+        'detail': 'detail',  # noqa: E501
+    }
+
+    read_only_vars = {
+    }
+
+    _composed_schemas = {}
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _from_openapi_data(cls, *args, **kwargs):  # noqa: E501
+        """HTTPValidationError - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            detail ([ValidationError]): [optional]  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', True)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        self = super(OpenApiModel, cls).__new__(cls)
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+        return self
+
+    required_properties = set([
+        '_data_store',
+        '_check_type',
+        '_spec_property_naming',
+        '_path_to_item',
+        '_configuration',
+        '_visited_composed_classes',
+    ])
+
+    @convert_js_args_to_python_args
+    def __init__(self, *args, **kwargs):  # noqa: E501
+        """HTTPValidationError - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            detail ([ValidationError]): [optional]  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+            if var_name in self.read_only_vars:
+                raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+                                     f"class with read only attributes.")
diff --git a/turbinia/api/client/turbinia_api_client/model/request.py b/turbinia/api/client/turbinia_api_client/model/request.py
new file mode 100644
index 000000000..b6820fa80
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/request.py
@@ -0,0 +1,285 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.model_utils import (  # noqa: F401
+    ApiTypeError,
+    ModelComposed,
+    ModelNormal,
+    ModelSimple,
+    cached_property,
+    change_keys_js_to_python,
+    convert_js_args_to_python_args,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_get_composed_info,
+    OpenApiModel
+)
+from turbinia_api_client.exceptions import ApiAttributeError
+
+
+def lazy_import():
+    from turbinia_api_client.model.base_request_options import BaseRequestOptions
+    globals()['BaseRequestOptions'] = BaseRequestOptions
+
+
+class Request(ModelNormal):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+
+    Attributes:
+      allowed_values (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          with a capitalized key describing the allowed value and an allowed
+          value. These dicts store the allowed enum values.
+      attribute_map (dict): The key is attribute name
+          and the value is json key in definition.
+      discriminator_value_class_map (dict): A dict to go from the discriminator
+          variable value to the discriminator class name.
+      validations (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          that stores validations for max_length, min_length, max_items,
+          min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+          inclusive_minimum, and regex.
+      additional_properties_type (tuple): A tuple of classes accepted
+          as additional properties values.
+    """
+
+    allowed_values = {
+    }
+
+    validations = {
+    }
+
+    @cached_property
+    def additional_properties_type():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+        """
+        lazy_import()
+        return (bool, date, datetime, dict, float, int, list, str, none_type,)  # noqa: E501
+
+    _nullable = False
+
+    @cached_property
+    def openapi_types():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+
+        Returns
+            openapi_types (dict): The key is attribute name
+                and the value is attribute type.
+        """
+        lazy_import()
+        return {
+            'evidence': ({str: (bool, date, datetime, dict, float, int, list, str, none_type)},),  # noqa: E501
+            'request_options': (BaseRequestOptions,),  # noqa: E501
+            'description': (str,),  # noqa: E501
+        }
+
+    @cached_property
+    def discriminator():
+        return None
+
+
+    attribute_map = {
+        'evidence': 'evidence',  # noqa: E501
+        'request_options': 'request_options',  # noqa: E501
+        'description': 'description',  # noqa: E501
+    }
+
+    read_only_vars = {
+    }
+
+    _composed_schemas = {}
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _from_openapi_data(cls, evidence, request_options, *args, **kwargs):  # noqa: E501
+        """Request - a model defined in OpenAPI
+
+        Args:
+            evidence ({str: (bool, date, datetime, dict, float, int, list, str, none_type)}):
+            request_options (BaseRequestOptions):
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            description (str): [optional] if omitted the server will use the default value of "Turbinia request object"  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', True)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        self = super(OpenApiModel, cls).__new__(cls)
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        self.evidence = evidence
+        self.request_options = request_options
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+        return self
+
+    required_properties = set([
+        '_data_store',
+        '_check_type',
+        '_spec_property_naming',
+        '_path_to_item',
+        '_configuration',
+        '_visited_composed_classes',
+    ])
+
+    @convert_js_args_to_python_args
+    def __init__(self, evidence, request_options, *args, **kwargs):  # noqa: E501
+        """Request - a model defined in OpenAPI
+
+        Args:
+            evidence ({str: (bool, date, datetime, dict, float, int, list, str, none_type)}):
+            request_options (BaseRequestOptions):
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            description (str): [optional] if omitted the server will use the default value of "Turbinia request object"  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        self.evidence = evidence
+        self.request_options = request_options
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+            if var_name in self.read_only_vars:
+                raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+                                     f"class with read only attributes.")
diff --git a/turbinia/api/client/turbinia_api_client/model/request_status.py b/turbinia/api/client/turbinia_api_client/model/request_status.py
new file mode 100644
index 000000000..378e8011b
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/request_status.py
@@ -0,0 +1,303 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.model_utils import (  # noqa: F401
+    ApiTypeError,
+    ModelComposed,
+    ModelNormal,
+    ModelSimple,
+    cached_property,
+    change_keys_js_to_python,
+    convert_js_args_to_python_args,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_get_composed_info,
+    OpenApiModel
+)
+from turbinia_api_client.exceptions import ApiAttributeError
+
+
+
+class RequestStatus(ModelNormal):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+
+    Attributes:
+      allowed_values (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          with a capitalized key describing the allowed value and an allowed
+          value. These dicts store the allowed enum values.
+      attribute_map (dict): The key is attribute name
+          and the value is json key in definition.
+      discriminator_value_class_map (dict): A dict to go from the discriminator
+          variable value to the discriminator class name.
+      validations (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          that stores validations for max_length, min_length, max_items,
+          min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+          inclusive_minimum, and regex.
+      additional_properties_type (tuple): A tuple of classes accepted
+          as additional properties values.
+    """
+
+    allowed_values = {
+    }
+
+    validations = {
+    }
+
+    @cached_property
+    def additional_properties_type():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+        """
+        return (bool, date, datetime, dict, float, int, list, str, none_type,)  # noqa: E501
+
+    _nullable = False
+
+    @cached_property
+    def openapi_types():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+
+        Returns
+            openapi_types (dict): The key is attribute name
+                and the value is attribute type.
+        """
+        return {
+            'failed_tasks': (int,),  # noqa: E501
+            'last_task_update_time': (str,),  # noqa: E501
+            'queued_tasks': (int,),  # noqa: E501
+            'reason': (str,),  # noqa: E501
+            'request_id': (str,),  # noqa: E501
+            'requester': (str,),  # noqa: E501
+            'running_tasks': (int,),  # noqa: E501
+            'status': (str,),  # noqa: E501
+            'successful_tasks': (int,),  # noqa: E501
+            'task_count': (int,),  # noqa: E501
+            'tasks': ([{str: (bool, date, datetime, dict, float, int, list, str, none_type)}],),  # noqa: E501
+        }
+
+    @cached_property
+    def discriminator():
+        return None
+
+
+    attribute_map = {
+        'failed_tasks': 'failed_tasks',  # noqa: E501
+        'last_task_update_time': 'last_task_update_time',  # noqa: E501
+        'queued_tasks': 'queued_tasks',  # noqa: E501
+        'reason': 'reason',  # noqa: E501
+        'request_id': 'request_id',  # noqa: E501
+        'requester': 'requester',  # noqa: E501
+        'running_tasks': 'running_tasks',  # noqa: E501
+        'status': 'status',  # noqa: E501
+        'successful_tasks': 'successful_tasks',  # noqa: E501
+        'task_count': 'task_count',  # noqa: E501
+        'tasks': 'tasks',  # noqa: E501
+    }
+
+    read_only_vars = {
+    }
+
+    _composed_schemas = {}
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _from_openapi_data(cls, *args, **kwargs):  # noqa: E501
+        """RequestStatus - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            failed_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            last_task_update_time (str): [optional]  # noqa: E501
+            queued_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            reason (str): [optional]  # noqa: E501
+            request_id (str): [optional]  # noqa: E501
+            requester (str): [optional]  # noqa: E501
+            running_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            status (str): [optional]  # noqa: E501
+            successful_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            task_count (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            tasks ([{str: (bool, date, datetime, dict, float, int, list, str, none_type)}]): [optional] if omitted the server will use the default value of []  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', True)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        self = super(OpenApiModel, cls).__new__(cls)
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+        return self
+
+    required_properties = set([
+        '_data_store',
+        '_check_type',
+        '_spec_property_naming',
+        '_path_to_item',
+        '_configuration',
+        '_visited_composed_classes',
+    ])
+
+    @convert_js_args_to_python_args
+    def __init__(self, *args, **kwargs):  # noqa: E501
+        """RequestStatus - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            failed_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            last_task_update_time (str): [optional]  # noqa: E501
+            queued_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            reason (str): [optional]  # noqa: E501
+            request_id (str): [optional]  # noqa: E501
+            requester (str): [optional]  # noqa: E501
+            running_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            status (str): [optional]  # noqa: E501
+            successful_tasks (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            task_count (int): [optional] if omitted the server will use the default value of 0  # noqa: E501
+            tasks ([{str: (bool, date, datetime, dict, float, int, list, str, none_type)}]): [optional] if omitted the server will use the default value of []  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+            if var_name in self.read_only_vars:
+                raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+                                     f"class with read only attributes.")
diff --git a/turbinia/api/client/turbinia_api_client/model/requests_summary.py b/turbinia/api/client/turbinia_api_client/model/requests_summary.py
new file mode 100644
index 000000000..ccc683272
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/requests_summary.py
@@ -0,0 +1,269 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.model_utils import (  # noqa: F401
+    ApiTypeError,
+    ModelComposed,
+    ModelNormal,
+    ModelSimple,
+    cached_property,
+    change_keys_js_to_python,
+    convert_js_args_to_python_args,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_get_composed_info,
+    OpenApiModel
+)
+from turbinia_api_client.exceptions import ApiAttributeError
+
+
+def lazy_import():
+    from turbinia_api_client.model.request_status import RequestStatus
+    globals()['RequestStatus'] = RequestStatus
+
+
+class RequestsSummary(ModelNormal):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+
+    Attributes:
+      allowed_values (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          with a capitalized key describing the allowed value and an allowed
+          value. These dicts store the allowed enum values.
+      attribute_map (dict): The key is attribute name
+          and the value is json key in definition.
+      discriminator_value_class_map (dict): A dict to go from the discriminator
+          variable value to the discriminator class name.
+      validations (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          that stores validations for max_length, min_length, max_items,
+          min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+          inclusive_minimum, and regex.
+      additional_properties_type (tuple): A tuple of classes accepted
+          as additional properties values.
+    """
+
+    allowed_values = {
+    }
+
+    validations = {
+    }
+
+    @cached_property
+    def additional_properties_type():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+        """
+        lazy_import()
+        return (bool, date, datetime, dict, float, int, list, str, none_type,)  # noqa: E501
+
+    _nullable = False
+
+    @cached_property
+    def openapi_types():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+
+        Returns
+            openapi_types (dict): The key is attribute name
+                and the value is attribute type.
+        """
+        lazy_import()
+        return {
+            'requests_status': ([RequestStatus],),  # noqa: E501
+        }
+
+    @cached_property
+    def discriminator():
+        return None
+
+
+    attribute_map = {
+        'requests_status': 'requests_status',  # noqa: E501
+    }
+
+    read_only_vars = {
+    }
+
+    _composed_schemas = {}
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _from_openapi_data(cls, *args, **kwargs):  # noqa: E501
+        """RequestsSummary - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            requests_status ([RequestStatus]): [optional] if omitted the server will use the default value of []  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', True)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        self = super(OpenApiModel, cls).__new__(cls)
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+        return self
+
+    required_properties = set([
+        '_data_store',
+        '_check_type',
+        '_spec_property_naming',
+        '_path_to_item',
+        '_configuration',
+        '_visited_composed_classes',
+    ])
+
+    @convert_js_args_to_python_args
+    def __init__(self, *args, **kwargs):  # noqa: E501
+        """RequestsSummary - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+            requests_status ([RequestStatus]): [optional] if omitted the server will use the default value of []  # noqa: E501
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+            if var_name in self.read_only_vars:
+                raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+                                     f"class with read only attributes.")
diff --git a/turbinia/api/client/turbinia_api_client/model/validation_error.py b/turbinia/api/client/turbinia_api_client/model/validation_error.py
new file mode 100644
index 000000000..f17d9c14e
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/validation_error.py
@@ -0,0 +1,287 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.model_utils import (  # noqa: F401
+    ApiTypeError,
+    ModelComposed,
+    ModelNormal,
+    ModelSimple,
+    cached_property,
+    change_keys_js_to_python,
+    convert_js_args_to_python_args,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_get_composed_info,
+    OpenApiModel
+)
+from turbinia_api_client.exceptions import ApiAttributeError
+
+
+def lazy_import():
+    from turbinia_api_client.model.validation_error_loc_inner import ValidationErrorLocInner
+    globals()['ValidationErrorLocInner'] = ValidationErrorLocInner
+
+
+class ValidationError(ModelNormal):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+
+    Attributes:
+      allowed_values (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          with a capitalized key describing the allowed value and an allowed
+          value. These dicts store the allowed enum values.
+      attribute_map (dict): The key is attribute name
+          and the value is json key in definition.
+      discriminator_value_class_map (dict): A dict to go from the discriminator
+          variable value to the discriminator class name.
+      validations (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          that stores validations for max_length, min_length, max_items,
+          min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+          inclusive_minimum, and regex.
+      additional_properties_type (tuple): A tuple of classes accepted
+          as additional properties values.
+    """
+
+    allowed_values = {
+    }
+
+    validations = {
+    }
+
+    @cached_property
+    def additional_properties_type():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+        """
+        lazy_import()
+        return (bool, date, datetime, dict, float, int, list, str, none_type,)  # noqa: E501
+
+    _nullable = False
+
+    @cached_property
+    def openapi_types():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+
+        Returns
+            openapi_types (dict): The key is attribute name
+                and the value is attribute type.
+        """
+        lazy_import()
+        return {
+            'loc': ([ValidationErrorLocInner],),  # noqa: E501
+            'msg': (str,),  # noqa: E501
+            'type': (str,),  # noqa: E501
+        }
+
+    @cached_property
+    def discriminator():
+        return None
+
+
+    attribute_map = {
+        'loc': 'loc',  # noqa: E501
+        'msg': 'msg',  # noqa: E501
+        'type': 'type',  # noqa: E501
+    }
+
+    read_only_vars = {
+    }
+
+    _composed_schemas = {}
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _from_openapi_data(cls, loc, msg, type, *args, **kwargs):  # noqa: E501
+        """ValidationError - a model defined in OpenAPI
+
+        Args:
+            loc ([ValidationErrorLocInner]):
+            msg (str):
+            type (str):
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', True)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        self = super(OpenApiModel, cls).__new__(cls)
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        self.loc = loc
+        self.msg = msg
+        self.type = type
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+        return self
+
+    required_properties = set([
+        '_data_store',
+        '_check_type',
+        '_spec_property_naming',
+        '_path_to_item',
+        '_configuration',
+        '_visited_composed_classes',
+    ])
+
+    @convert_js_args_to_python_args
+    def __init__(self, loc, msg, type, *args, **kwargs):  # noqa: E501
+        """ValidationError - a model defined in OpenAPI
+
+        Args:
+            loc ([ValidationErrorLocInner]):
+            msg (str):
+            type (str):
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        self.loc = loc
+        self.msg = msg
+        self.type = type
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+            if var_name in self.read_only_vars:
+                raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+                                     f"class with read only attributes.")
diff --git a/turbinia/api/client/turbinia_api_client/model/validation_error_loc_inner.py b/turbinia/api/client/turbinia_api_client/model/validation_error_loc_inner.py
new file mode 100644
index 000000000..59bf01c4c
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model/validation_error_loc_inner.py
@@ -0,0 +1,259 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import re  # noqa: F401
+import sys  # noqa: F401
+
+from turbinia_api_client.model_utils import (  # noqa: F401
+    ApiTypeError,
+    ModelComposed,
+    ModelNormal,
+    ModelSimple,
+    cached_property,
+    change_keys_js_to_python,
+    convert_js_args_to_python_args,
+    date,
+    datetime,
+    file_type,
+    none_type,
+    validate_get_composed_info,
+    OpenApiModel
+)
+from turbinia_api_client.exceptions import ApiAttributeError
+
+
+
+class ValidationErrorLocInner(ModelNormal):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+
+    Attributes:
+      allowed_values (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          with a capitalized key describing the allowed value and an allowed
+          value. These dicts store the allowed enum values.
+      attribute_map (dict): The key is attribute name
+          and the value is json key in definition.
+      discriminator_value_class_map (dict): A dict to go from the discriminator
+          variable value to the discriminator class name.
+      validations (dict): The key is the tuple path to the attribute
+          and the for var_name this is (var_name,). The value is a dict
+          that stores validations for max_length, min_length, max_items,
+          min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+          inclusive_minimum, and regex.
+      additional_properties_type (tuple): A tuple of classes accepted
+          as additional properties values.
+    """
+
+    allowed_values = {
+    }
+
+    validations = {
+    }
+
+    @cached_property
+    def additional_properties_type():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+        """
+        return (bool, date, datetime, dict, float, int, list, str, none_type,)  # noqa: E501
+
+    _nullable = False
+
+    @cached_property
+    def openapi_types():
+        """
+        This must be a method because a model may have properties that are
+        of type self, this must run after the class is loaded
+
+        Returns
+            openapi_types (dict): The key is attribute name
+                and the value is attribute type.
+        """
+        return {
+        }
+
+    @cached_property
+    def discriminator():
+        return None
+
+
+    attribute_map = {
+    }
+
+    read_only_vars = {
+    }
+
+    _composed_schemas = {}
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _from_openapi_data(cls, *args, **kwargs):  # noqa: E501
+        """ValidationErrorLocInner - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', True)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        self = super(OpenApiModel, cls).__new__(cls)
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+        return self
+
+    required_properties = set([
+        '_data_store',
+        '_check_type',
+        '_spec_property_naming',
+        '_path_to_item',
+        '_configuration',
+        '_visited_composed_classes',
+    ])
+
+    @convert_js_args_to_python_args
+    def __init__(self, *args, **kwargs):  # noqa: E501
+        """ValidationErrorLocInner - a model defined in OpenAPI
+
+        Keyword Args:
+            _check_type (bool): if True, values for parameters in openapi_types
+                                will be type checked and a TypeError will be
+                                raised if the wrong type is input.
+                                Defaults to True
+            _path_to_item (tuple/list): This is a list of keys or values to
+                                drill down to the model in received_data
+                                when deserializing a response
+            _spec_property_naming (bool): True if the variable names in the input data
+                                are serialized names, as specified in the OpenAPI document.
+                                False if the variable names in the input data
+                                are pythonic names, e.g. snake case (default)
+            _configuration (Configuration): the instance to use when
+                                deserializing a file_type parameter.
+                                If passed, type conversion is attempted
+                                If omitted no type conversion is done.
+            _visited_composed_classes (tuple): This stores a tuple of
+                                classes that we have traveled through so that
+                                if we see that class again we will not use its
+                                discriminator again.
+                                When traveling through a discriminator, the
+                                composed schema that is
+                                is traveled through is added to this set.
+                                For example if Animal has a discriminator
+                                petType and we pass in "Dog", and the class Dog
+                                allOf includes Animal, we move through Animal
+                                once using the discriminator, and pick Dog.
+                                Then in Dog, we will make an instance of the
+                                Animal class but this time we won't travel
+                                through its discriminator because we passed in
+                                _visited_composed_classes = (Animal,)
+        """
+
+        _check_type = kwargs.pop('_check_type', True)
+        _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+        _path_to_item = kwargs.pop('_path_to_item', ())
+        _configuration = kwargs.pop('_configuration', None)
+        _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+        if args:
+            for arg in args:
+                if isinstance(arg, dict):
+                    kwargs.update(arg)
+                else:
+                    raise ApiTypeError(
+                        "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+                            args,
+                            self.__class__.__name__,
+                        ),
+                        path_to_item=_path_to_item,
+                        valid_classes=(self.__class__,),
+                    )
+
+        self._data_store = {}
+        self._check_type = _check_type
+        self._spec_property_naming = _spec_property_naming
+        self._path_to_item = _path_to_item
+        self._configuration = _configuration
+        self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+        for var_name, var_value in kwargs.items():
+            if var_name not in self.attribute_map and \
+                        self._configuration is not None and \
+                        self._configuration.discard_unknown_keys and \
+                        self.additional_properties_type is None:
+                # discard variable.
+                continue
+            setattr(self, var_name, var_value)
+            if var_name in self.read_only_vars:
+                raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+                                     f"class with read only attributes.")
diff --git a/turbinia/api/client/turbinia_api_client/model_utils.py b/turbinia/api/client/turbinia_api_client/model_utils.py
new file mode 100644
index 000000000..d644bf21b
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/model_utils.py
@@ -0,0 +1,2046 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+from datetime import date, datetime  # noqa: F401
+from copy import deepcopy
+import inspect
+import io
+import os
+import pprint
+import re
+import tempfile
+import uuid
+
+from dateutil.parser import parse
+
+from turbinia_api_client.exceptions import (
+    ApiKeyError,
+    ApiAttributeError,
+    ApiTypeError,
+    ApiValueError,
+)
+
+none_type = type(None)
+file_type = io.IOBase
+
+
+def convert_js_args_to_python_args(fn):
+    from functools import wraps
+    @wraps(fn)
+    def wrapped_init(_self, *args, **kwargs):
+        """
+        An attribute named `self` received from the api will conflicts with the reserved `self`
+        parameter of a class method. During generation, `self` attributes are mapped
+        to `_self` in models. Here, we name `_self` instead of `self` to avoid conflicts.
+        """
+        spec_property_naming = kwargs.get('_spec_property_naming', False)
+        if spec_property_naming:
+            kwargs = change_keys_js_to_python(kwargs, _self if isinstance(_self, type) else _self.__class__)
+        return fn(_self, *args, **kwargs)
+    return wrapped_init
+
+
+class cached_property(object):
+    # this caches the result of the function call for fn with no inputs
+    # use this as a decorator on function methods that you want converted
+    # into cached properties
+    result_key = '_results'
+
+    def __init__(self, fn):
+        self._fn = fn
+
+    def __get__(self, instance, cls=None):
+        if self.result_key in vars(self):
+            return vars(self)[self.result_key]
+        else:
+            result = self._fn()
+            setattr(self, self.result_key, result)
+            return result
+
+
+PRIMITIVE_TYPES = (list, float, int, bool, datetime, date, str, file_type)
+
+def allows_single_value_input(cls):
+    """
+    This function returns True if the input composed schema model or any
+    descendant model allows a value only input
+    This is true for cases where oneOf contains items like:
+    oneOf:
+      - float
+      - NumberWithValidation
+      - StringEnum
+      - ArrayModel
+      - null
+    TODO: lru_cache this
+    """
+    if (
+        issubclass(cls, ModelSimple) or
+        cls in PRIMITIVE_TYPES
+    ):
+        return True
+    elif issubclass(cls, ModelComposed):
+        if not cls._composed_schemas['oneOf']:
+            return False
+        return any(allows_single_value_input(c) for c in cls._composed_schemas['oneOf'])
+    return False
+
+def composed_model_input_classes(cls):
+    """
+    This function returns a list of the possible models that can be accepted as
+    inputs.
+    TODO: lru_cache this
+    """
+    if issubclass(cls, ModelSimple) or cls in PRIMITIVE_TYPES:
+        return [cls]
+    elif issubclass(cls, ModelNormal):
+        if cls.discriminator is None:
+            return [cls]
+        else:
+            return get_discriminated_classes(cls)
+    elif issubclass(cls, ModelComposed):
+        if not cls._composed_schemas['oneOf']:
+            return []
+        if cls.discriminator is None:
+            input_classes = []
+            for c in cls._composed_schemas['oneOf']:
+                input_classes.extend(composed_model_input_classes(c))
+            return input_classes
+        else:
+            return get_discriminated_classes(cls)
+    return []
+
+
+class OpenApiModel(object):
+    """The base class for all OpenAPIModels"""
+
+    def set_attribute(self, name, value):
+        # this is only used to set properties on self
+
+        path_to_item = []
+        if self._path_to_item:
+            path_to_item.extend(self._path_to_item)
+        path_to_item.append(name)
+
+        if name in self.openapi_types:
+            required_types_mixed = self.openapi_types[name]
+        elif self.additional_properties_type is None:
+            raise ApiAttributeError(
+                "{0} has no attribute '{1}'".format(
+                    type(self).__name__, name),
+                path_to_item
+            )
+        elif self.additional_properties_type is not None:
+            required_types_mixed = self.additional_properties_type
+
+        if get_simple_class(name) != str:
+            error_msg = type_error_message(
+                var_name=name,
+                var_value=name,
+                valid_classes=(str,),
+                key_type=True
+            )
+            raise ApiTypeError(
+                error_msg,
+                path_to_item=path_to_item,
+                valid_classes=(str,),
+                key_type=True
+            )
+
+        if self._check_type:
+            value = validate_and_convert_types(
+                value, required_types_mixed, path_to_item, self._spec_property_naming,
+                self._check_type, configuration=self._configuration)
+        if (name,) in self.allowed_values:
+            check_allowed_values(
+                self.allowed_values,
+                (name,),
+                value
+            )
+        if (name,) in self.validations:
+            check_validations(
+                self.validations,
+                (name,),
+                value,
+                self._configuration
+            )
+        self.__dict__['_data_store'][name] = value
+
+    def __repr__(self):
+        """For `print` and `pprint`"""
+        return self.to_str()
+
+    def __ne__(self, other):
+        """Returns true if both objects are not equal"""
+        return not self == other
+
+    def __setattr__(self, attr, value):
+        """set the value of an attribute using dot notation: `instance.attr = val`"""
+        self[attr] = value
+
+    def __getattr__(self, attr):
+        """get the value of an attribute using dot notation: `instance.attr`"""
+        return self.__getitem__(attr)
+
+    def __copy__(self):
+        cls = self.__class__
+        if self.get("_spec_property_naming", False):
+            return cls._new_from_openapi_data(**self.__dict__)
+        else:
+            return cls.__new__(cls, **self.__dict__)
+
+    def __deepcopy__(self, memo):
+        cls = self.__class__
+
+        if self.get("_spec_property_naming", False):
+            new_inst = cls._new_from_openapi_data()
+        else:
+            new_inst = cls.__new__(cls)
+
+        for k, v in self.__dict__.items():
+            setattr(new_inst, k, deepcopy(v, memo))
+        return new_inst
+
+
+    def __new__(cls, *args, **kwargs):
+        # this function uses the discriminator to
+        # pick a new schema/class to instantiate because a discriminator
+        # propertyName value was passed in
+
+        if len(args) == 1:
+            arg = args[0]
+            if arg is None and is_type_nullable(cls):
+                # The input data is the 'null' value and the type is nullable.
+                return None
+
+            if issubclass(cls, ModelComposed) and allows_single_value_input(cls):
+                model_kwargs = {}
+                oneof_instance = get_oneof_instance(cls, model_kwargs, kwargs, model_arg=arg)
+                return oneof_instance
+
+
+        visited_composed_classes = kwargs.get('_visited_composed_classes', ())
+        if (
+            cls.discriminator is None or
+            cls in visited_composed_classes
+        ):
+            # Use case 1: this openapi schema (cls) does not have a discriminator
+            # Use case 2: we have already visited this class before and are sure that we
+            # want to instantiate it this time. We have visited this class deserializing
+            # a payload with a discriminator. During that process we traveled through
+            # this class but did not make an instance of it. Now we are making an
+            # instance of a composed class which contains cls in it, so this time make an instance of cls.
+            #
+            # Here's an example of use case 2: If Animal has a discriminator
+            # petType and we pass in "Dog", and the class Dog
+            # allOf includes Animal, we move through Animal
+            # once using the discriminator, and pick Dog.
+            # Then in the composed schema dog Dog, we will make an instance of the
+            # Animal class (because Dal has allOf: Animal) but this time we won't travel
+            # through Animal's discriminator because we passed in
+            # _visited_composed_classes = (Animal,)
+
+            return super(OpenApiModel, cls).__new__(cls)
+
+        # Get the name and value of the discriminator property.
+        # The discriminator name is obtained from the discriminator meta-data
+        # and the discriminator value is obtained from the input data.
+        discr_propertyname_py = list(cls.discriminator.keys())[0]
+        discr_propertyname_js = cls.attribute_map[discr_propertyname_py]
+        if discr_propertyname_js in kwargs:
+            discr_value = kwargs[discr_propertyname_js]
+        elif discr_propertyname_py in kwargs:
+            discr_value = kwargs[discr_propertyname_py]
+        else:
+            # The input data does not contain the discriminator property.
+            path_to_item = kwargs.get('_path_to_item', ())
+            raise ApiValueError(
+                "Cannot deserialize input data due to missing discriminator. "
+                "The discriminator property '%s' is missing at path: %s" %
+                (discr_propertyname_js, path_to_item)
+            )
+
+        # Implementation note: the last argument to get_discriminator_class
+        # is a list of visited classes. get_discriminator_class may recursively
+        # call itself and update the list of visited classes, and the initial
+        # value must be an empty list. Hence not using 'visited_composed_classes'
+        new_cls = get_discriminator_class(
+                    cls, discr_propertyname_py, discr_value, [])
+        if new_cls is None:
+            path_to_item = kwargs.get('_path_to_item', ())
+            disc_prop_value = kwargs.get(
+                discr_propertyname_js, kwargs.get(discr_propertyname_py))
+            raise ApiValueError(
+                "Cannot deserialize input data due to invalid discriminator "
+                "value. The OpenAPI document has no mapping for discriminator "
+                "property '%s'='%s' at path: %s" %
+                (discr_propertyname_js, disc_prop_value, path_to_item)
+            )
+
+        if new_cls in visited_composed_classes:
+            # if we are making an instance of a composed schema Descendent
+            # which allOf includes Ancestor, then Ancestor contains
+            # a discriminator that includes Descendent.
+            # So if we make an instance of Descendent, we have to make an
+            # instance of Ancestor to hold the allOf properties.
+            # This code detects that use case and makes the instance of Ancestor
+            # For example:
+            # When making an instance of Dog, _visited_composed_classes = (Dog,)
+            # then we make an instance of Animal to include in dog._composed_instances
+            # so when we are here, cls is Animal
+            # cls.discriminator != None
+            # cls not in _visited_composed_classes
+            # new_cls = Dog
+            # but we know we know that we already have Dog
+            # because it is in visited_composed_classes
+            # so make Animal here
+            return super(OpenApiModel, cls).__new__(cls)
+
+        # Build a list containing all oneOf and anyOf descendants.
+        oneof_anyof_classes = None
+        if cls._composed_schemas is not None:
+            oneof_anyof_classes = (
+                cls._composed_schemas.get('oneOf', ()) +
+                cls._composed_schemas.get('anyOf', ()))
+        oneof_anyof_child = new_cls in oneof_anyof_classes
+        kwargs['_visited_composed_classes'] = visited_composed_classes + (cls,)
+
+        if cls._composed_schemas.get('allOf') and oneof_anyof_child:
+            # Validate that we can make self because when we make the
+            # new_cls it will not include the allOf validations in self
+            self_inst = super(OpenApiModel, cls).__new__(cls)
+            self_inst.__init__(*args, **kwargs)
+
+        if kwargs.get("_spec_property_naming", False):
+            # when true, implies new is from deserialization
+            new_inst = new_cls._new_from_openapi_data(*args, **kwargs)
+        else:
+            new_inst = new_cls.__new__(new_cls, *args, **kwargs)
+            new_inst.__init__(*args, **kwargs)
+
+        return new_inst
+
+
+    @classmethod
+    @convert_js_args_to_python_args
+    def _new_from_openapi_data(cls, *args, **kwargs):
+        # this function uses the discriminator to
+        # pick a new schema/class to instantiate because a discriminator
+        # propertyName value was passed in
+
+        if len(args) == 1:
+            arg = args[0]
+            if arg is None and is_type_nullable(cls):
+                # The input data is the 'null' value and the type is nullable.
+                return None
+
+            if issubclass(cls, ModelComposed) and allows_single_value_input(cls):
+                model_kwargs = {}
+                oneof_instance = get_oneof_instance(cls, model_kwargs, kwargs, model_arg=arg)
+                return oneof_instance
+
+
+        visited_composed_classes = kwargs.get('_visited_composed_classes', ())
+        if (
+            cls.discriminator is None or
+            cls in visited_composed_classes
+        ):
+            # Use case 1: this openapi schema (cls) does not have a discriminator
+            # Use case 2: we have already visited this class before and are sure that we
+            # want to instantiate it this time. We have visited this class deserializing
+            # a payload with a discriminator. During that process we traveled through
+            # this class but did not make an instance of it. Now we are making an
+            # instance of a composed class which contains cls in it, so this time make an instance of cls.
+            #
+            # Here's an example of use case 2: If Animal has a discriminator
+            # petType and we pass in "Dog", and the class Dog
+            # allOf includes Animal, we move through Animal
+            # once using the discriminator, and pick Dog.
+            # Then in the composed schema dog Dog, we will make an instance of the
+            # Animal class (because Dal has allOf: Animal) but this time we won't travel
+            # through Animal's discriminator because we passed in
+            # _visited_composed_classes = (Animal,)
+
+            return cls._from_openapi_data(*args, **kwargs)
+
+        # Get the name and value of the discriminator property.
+        # The discriminator name is obtained from the discriminator meta-data
+        # and the discriminator value is obtained from the input data.
+        discr_propertyname_py = list(cls.discriminator.keys())[0]
+        discr_propertyname_js = cls.attribute_map[discr_propertyname_py]
+        if discr_propertyname_js in kwargs:
+            discr_value = kwargs[discr_propertyname_js]
+        elif discr_propertyname_py in kwargs:
+            discr_value = kwargs[discr_propertyname_py]
+        else:
+            # The input data does not contain the discriminator property.
+            path_to_item = kwargs.get('_path_to_item', ())
+            raise ApiValueError(
+                "Cannot deserialize input data due to missing discriminator. "
+                "The discriminator property '%s' is missing at path: %s" %
+                (discr_propertyname_js, path_to_item)
+            )
+
+        # Implementation note: the last argument to get_discriminator_class
+        # is a list of visited classes. get_discriminator_class may recursively
+        # call itself and update the list of visited classes, and the initial
+        # value must be an empty list. Hence not using 'visited_composed_classes'
+        new_cls = get_discriminator_class(
+                    cls, discr_propertyname_py, discr_value, [])
+        if new_cls is None:
+            path_to_item = kwargs.get('_path_to_item', ())
+            disc_prop_value = kwargs.get(
+                discr_propertyname_js, kwargs.get(discr_propertyname_py))
+            raise ApiValueError(
+                "Cannot deserialize input data due to invalid discriminator "
+                "value. The OpenAPI document has no mapping for discriminator "
+                "property '%s'='%s' at path: %s" %
+                (discr_propertyname_js, disc_prop_value, path_to_item)
+            )
+
+        if new_cls in visited_composed_classes:
+            # if we are making an instance of a composed schema Descendent
+            # which allOf includes Ancestor, then Ancestor contains
+            # a discriminator that includes Descendent.
+            # So if we make an instance of Descendent, we have to make an
+            # instance of Ancestor to hold the allOf properties.
+            # This code detects that use case and makes the instance of Ancestor
+            # For example:
+            # When making an instance of Dog, _visited_composed_classes = (Dog,)
+            # then we make an instance of Animal to include in dog._composed_instances
+            # so when we are here, cls is Animal
+            # cls.discriminator != None
+            # cls not in _visited_composed_classes
+            # new_cls = Dog
+            # but we know we know that we already have Dog
+            # because it is in visited_composed_classes
+            # so make Animal here
+            return cls._from_openapi_data(*args, **kwargs)
+
+        # Build a list containing all oneOf and anyOf descendants.
+        oneof_anyof_classes = None
+        if cls._composed_schemas is not None:
+            oneof_anyof_classes = (
+                cls._composed_schemas.get('oneOf', ()) +
+                cls._composed_schemas.get('anyOf', ()))
+        oneof_anyof_child = new_cls in oneof_anyof_classes
+        kwargs['_visited_composed_classes'] = visited_composed_classes + (cls,)
+
+        if cls._composed_schemas.get('allOf') and oneof_anyof_child:
+            # Validate that we can make self because when we make the
+            # new_cls it will not include the allOf validations in self
+            self_inst = cls._from_openapi_data(*args, **kwargs)
+
+
+        new_inst = new_cls._new_from_openapi_data(*args, **kwargs)
+        return new_inst
+
+
+class ModelSimple(OpenApiModel):
+    """the parent class of models whose type != object in their
+    swagger/openapi"""
+
+    def __setitem__(self, name, value):
+        """set the value of an attribute using square-bracket notation: `instance[attr] = val`"""
+        if name in self.required_properties:
+            self.__dict__[name] = value
+            return
+
+        self.set_attribute(name, value)
+
+    def get(self, name, default=None):
+        """returns the value of an attribute or some default value if the attribute was not set"""
+        if name in self.required_properties:
+            return self.__dict__[name]
+
+        return self.__dict__['_data_store'].get(name, default)
+
+    def __getitem__(self, name):
+        """get the value of an attribute using square-bracket notation: `instance[attr]`"""
+        if name in self:
+            return self.get(name)
+
+        raise ApiAttributeError(
+            "{0} has no attribute '{1}'".format(
+                type(self).__name__, name),
+            [e for e in [self._path_to_item, name] if e]
+        )
+
+    def __contains__(self, name):
+        """used by `in` operator to check if an attribute value was set in an instance: `'attr' in instance`"""
+        if name in self.required_properties:
+            return name in self.__dict__
+
+        return name in self.__dict__['_data_store']
+
+    def to_str(self):
+        """Returns the string representation of the model"""
+        return str(self.value)
+
+    def __eq__(self, other):
+        """Returns true if both objects are equal"""
+        if not isinstance(other, self.__class__):
+            return False
+
+        this_val = self._data_store['value']
+        that_val = other._data_store['value']
+        types = set()
+        types.add(this_val.__class__)
+        types.add(that_val.__class__)
+        vals_equal = this_val == that_val
+        return vals_equal
+
+
+class ModelNormal(OpenApiModel):
+    """the parent class of models whose type == object in their
+    swagger/openapi"""
+
+    def __setitem__(self, name, value):
+        """set the value of an attribute using square-bracket notation: `instance[attr] = val`"""
+        if name in self.required_properties:
+            self.__dict__[name] = value
+            return
+
+        self.set_attribute(name, value)
+
+    def get(self, name, default=None):
+        """returns the value of an attribute or some default value if the attribute was not set"""
+        if name in self.required_properties:
+            return self.__dict__[name]
+
+        return self.__dict__['_data_store'].get(name, default)
+
+    def __getitem__(self, name):
+        """get the value of an attribute using square-bracket notation: `instance[attr]`"""
+        if name in self:
+            return self.get(name)
+
+        raise ApiAttributeError(
+            "{0} has no attribute '{1}'".format(
+                type(self).__name__, name),
+            [e for e in [self._path_to_item, name] if e]
+        )
+
+    def __contains__(self, name):
+        """used by `in` operator to check if an attribute value was set in an instance: `'attr' in instance`"""
+        if name in self.required_properties:
+            return name in self.__dict__
+
+        return name in self.__dict__['_data_store']
+
+    def to_dict(self):
+        """Returns the model properties as a dict"""
+        return model_to_dict(self, serialize=False)
+
+    def to_str(self):
+        """Returns the string representation of the model"""
+        return pprint.pformat(self.to_dict())
+
+    def __eq__(self, other):
+        """Returns true if both objects are equal"""
+        if not isinstance(other, self.__class__):
+            return False
+
+        if not set(self._data_store.keys()) == set(other._data_store.keys()):
+            return False
+        for _var_name, this_val in self._data_store.items():
+            that_val = other._data_store[_var_name]
+            types = set()
+            types.add(this_val.__class__)
+            types.add(that_val.__class__)
+            vals_equal = this_val == that_val
+            if not vals_equal:
+                return False
+        return True
+
+
+class ModelComposed(OpenApiModel):
+    """the parent class of models whose type == object in their
+    swagger/openapi and have oneOf/allOf/anyOf
+
+    When one sets a property we use var_name_to_model_instances to store the value in
+    the correct class instances + run any type checking + validation code.
+    When one gets a property we use var_name_to_model_instances to get the value
+    from the correct class instances.
+    This allows multiple composed schemas to contain the same property with additive
+    constraints on the value.
+
+    _composed_schemas (dict) stores the anyOf/allOf/oneOf classes
+    key (str): allOf/oneOf/anyOf
+    value (list): the classes in the XOf definition.
+        Note: none_type can be included when the openapi document version >= 3.1.0
+    _composed_instances (list): stores a list of instances of the composed schemas
+    defined in _composed_schemas. When properties are accessed in the self instance,
+    they are returned from the self._data_store or the data stores in the instances
+    in self._composed_schemas
+    _var_name_to_model_instances (dict): maps between a variable name on self and
+    the composed instances (self included) which contain that data
+    key (str): property name
+    value (list): list of class instances, self or instances in _composed_instances
+    which contain the value that the key is referring to.
+    """
+
+    def __setitem__(self, name, value):
+        """set the value of an attribute using square-bracket notation: `instance[attr] = val`"""
+        if name in self.required_properties:
+            self.__dict__[name] = value
+            return
+
+        """
+        Use cases:
+        1. additional_properties_type is None (additionalProperties == False in spec)
+            Check for property presence in self.openapi_types
+            if not present then throw an error
+            if present set in self, set attribute
+            always set on composed schemas
+        2.  additional_properties_type exists
+            set attribute on self
+            always set on composed schemas
+        """
+        if self.additional_properties_type is None:
+            """
+            For an attribute to exist on a composed schema it must:
+            - fulfill schema_requirements in the self composed schema not considering oneOf/anyOf/allOf schemas AND
+            - fulfill schema_requirements in each oneOf/anyOf/allOf schemas
+
+            schema_requirements:
+            For an attribute to exist on a schema it must:
+            - be present in properties at the schema OR
+            - have additionalProperties unset (defaults additionalProperties = any type) OR
+            - have additionalProperties set
+            """
+            if name not in self.openapi_types:
+                raise ApiAttributeError(
+                    "{0} has no attribute '{1}'".format(
+                        type(self).__name__, name),
+                    [e for e in [self._path_to_item, name] if e]
+                )
+        # attribute must be set on self and composed instances
+        self.set_attribute(name, value)
+        for model_instance in self._composed_instances:
+            setattr(model_instance, name, value)
+        if name not in self._var_name_to_model_instances:
+            # we assigned an additional property
+            self.__dict__['_var_name_to_model_instances'][name] = self._composed_instances + [self]
+        return None
+
+    __unset_attribute_value__ = object()
+
+    def get(self, name, default=None):
+        """returns the value of an attribute or some default value if the attribute was not set"""
+        if name in self.required_properties:
+            return self.__dict__[name]
+
+        # get the attribute from the correct instance
+        model_instances = self._var_name_to_model_instances.get(name)
+        values = []
+        # A composed model stores self and child (oneof/anyOf/allOf) models under
+        # self._var_name_to_model_instances.
+        # Any property must exist in self and all model instances
+        # The value stored in all model instances must be the same
+        if model_instances:
+            for model_instance in model_instances:
+                if name in model_instance._data_store:
+                    v = model_instance._data_store[name]
+                    if v not in values:
+                        values.append(v)
+        len_values = len(values)
+        if len_values == 0:
+            return default
+        elif len_values == 1:
+            return values[0]
+        elif len_values > 1:
+            raise ApiValueError(
+                "Values stored for property {0} in {1} differ when looking "
+                "at self and self's composed instances. All values must be "
+                "the same".format(name, type(self).__name__),
+                [e for e in [self._path_to_item, name] if e]
+            )
+
+    def __getitem__(self, name):
+        """get the value of an attribute using square-bracket notation: `instance[attr]`"""
+        value = self.get(name, self.__unset_attribute_value__)
+        if value is self.__unset_attribute_value__:
+            raise ApiAttributeError(
+                "{0} has no attribute '{1}'".format(
+                    type(self).__name__, name),
+                    [e for e in [self._path_to_item, name] if e]
+            )
+        return value
+
+    def __contains__(self, name):
+        """used by `in` operator to check if an attribute value was set in an instance: `'attr' in instance`"""
+
+        if name in self.required_properties:
+            return name in self.__dict__
+
+        model_instances = self._var_name_to_model_instances.get(
+            name, self._additional_properties_model_instances)
+
+        if model_instances:
+            for model_instance in model_instances:
+                if name in model_instance._data_store:
+                    return True
+
+        return False
+
+    def to_dict(self):
+        """Returns the model properties as a dict"""
+        return model_to_dict(self, serialize=False)
+
+    def to_str(self):
+        """Returns the string representation of the model"""
+        return pprint.pformat(self.to_dict())
+
+    def __eq__(self, other):
+        """Returns true if both objects are equal"""
+        if not isinstance(other, self.__class__):
+            return False
+
+        if not set(self._data_store.keys()) == set(other._data_store.keys()):
+            return False
+        for _var_name, this_val in self._data_store.items():
+            that_val = other._data_store[_var_name]
+            types = set()
+            types.add(this_val.__class__)
+            types.add(that_val.__class__)
+            vals_equal = this_val == that_val
+            if not vals_equal:
+                return False
+        return True
+
+
+COERCION_INDEX_BY_TYPE = {
+    ModelComposed: 0,
+    ModelNormal: 1,
+    ModelSimple: 2,
+    none_type: 3,    # The type of 'None'.
+    list: 4,
+    dict: 5,
+    float: 6,
+    int: 7,
+    bool: 8,
+    datetime: 9,
+    date: 10,
+    str: 11,
+    file_type: 12,   # 'file_type' is an alias for the built-in 'file' or 'io.IOBase' type.
+}
+
+# these are used to limit what type conversions we try to do
+# when we have a valid type already and we want to try converting
+# to another type
+UPCONVERSION_TYPE_PAIRS = (
+    (str, datetime),
+    (str, date),
+    (int, float),             # A float may be serialized as an integer, e.g. '3' is a valid serialized float.
+    (list, ModelComposed),
+    (dict, ModelComposed),
+    (str, ModelComposed),
+    (int, ModelComposed),
+    (float, ModelComposed),
+    (list, ModelComposed),
+    (list, ModelNormal),
+    (dict, ModelNormal),
+    (str, ModelSimple),
+    (int, ModelSimple),
+    (float, ModelSimple),
+    (list, ModelSimple),
+)
+
+COERCIBLE_TYPE_PAIRS = {
+    False: (  # client instantiation of a model with client data
+        # (dict, ModelComposed),
+        # (list, ModelComposed),
+        # (dict, ModelNormal),
+        # (list, ModelNormal),
+        # (str, ModelSimple),
+        # (int, ModelSimple),
+        # (float, ModelSimple),
+        # (list, ModelSimple),
+        # (str, int),
+        # (str, float),
+        # (str, datetime),
+        # (str, date),
+        # (int, str),
+        # (float, str),
+    ),
+    True: (  # server -> client data
+        (dict, ModelComposed),
+        (list, ModelComposed),
+        (dict, ModelNormal),
+        (list, ModelNormal),
+        (str, ModelSimple),
+        (int, ModelSimple),
+        (float, ModelSimple),
+        (list, ModelSimple),
+        # (str, int),
+        # (str, float),
+        (str, datetime),
+        (str, date),
+        # (int, str),
+        # (float, str),
+        (str, file_type)
+    ),
+}
+
+
+def get_simple_class(input_value):
+    """Returns an input_value's simple class that we will use for type checking
+    Python2:
+    float and int will return int, where int is the python3 int backport
+    str and unicode will return str, where str is the python3 str backport
+    Note: float and int ARE both instances of int backport
+    Note: str_py2 and unicode_py2 are NOT both instances of str backport
+
+    Args:
+        input_value (class/class_instance): the item for which we will return
+                                            the simple class
+    """
+    if isinstance(input_value, type):
+        # input_value is a class
+        return input_value
+    elif isinstance(input_value, tuple):
+        return tuple
+    elif isinstance(input_value, list):
+        return list
+    elif isinstance(input_value, dict):
+        return dict
+    elif isinstance(input_value, none_type):
+        return none_type
+    elif isinstance(input_value, file_type):
+        return file_type
+    elif isinstance(input_value, bool):
+        # this must be higher than the int check because
+        # isinstance(True, int) == True
+        return bool
+    elif isinstance(input_value, int):
+        return int
+    elif isinstance(input_value, datetime):
+        # this must be higher than the date check because
+        # isinstance(datetime_instance, date) == True
+        return datetime
+    elif isinstance(input_value, date):
+        return date
+    elif isinstance(input_value, str):
+        return str
+    return type(input_value)
+
+
+def check_allowed_values(allowed_values, input_variable_path, input_values):
+    """Raises an exception if the input_values are not allowed
+
+    Args:
+        allowed_values (dict): the allowed_values dict
+        input_variable_path (tuple): the path to the input variable
+        input_values (list/str/int/float/date/datetime): the values that we
+            are checking to see if they are in allowed_values
+    """
+    these_allowed_values = list(allowed_values[input_variable_path].values())
+    if (isinstance(input_values, list)
+            and not set(input_values).issubset(
+                set(these_allowed_values))):
+        invalid_values = ", ".join(
+            map(str, set(input_values) - set(these_allowed_values))),
+        raise ApiValueError(
+            "Invalid values for `%s` [%s], must be a subset of [%s]" %
+            (
+                input_variable_path[0],
+                invalid_values,
+                ", ".join(map(str, these_allowed_values))
+            )
+        )
+    elif (isinstance(input_values, dict)
+            and not set(
+                input_values.keys()).issubset(set(these_allowed_values))):
+        invalid_values = ", ".join(
+            map(str, set(input_values.keys()) - set(these_allowed_values)))
+        raise ApiValueError(
+            "Invalid keys in `%s` [%s], must be a subset of [%s]" %
+            (
+                input_variable_path[0],
+                invalid_values,
+                ", ".join(map(str, these_allowed_values))
+            )
+        )
+    elif (not isinstance(input_values, (list, dict))
+            and input_values not in these_allowed_values):
+        raise ApiValueError(
+            "Invalid value for `%s` (%s), must be one of %s" %
+            (
+                input_variable_path[0],
+                input_values,
+                these_allowed_values
+            )
+        )
+
+
+def is_json_validation_enabled(schema_keyword, configuration=None):
+    """Returns true if JSON schema validation is enabled for the specified
+    validation keyword. This can be used to skip JSON schema structural validation
+    as requested in the configuration.
+
+    Args:
+        schema_keyword (string): the name of a JSON schema validation keyword.
+        configuration (Configuration): the configuration class.
+    """
+
+    return (configuration is None or
+        not hasattr(configuration, '_disabled_client_side_validations') or
+        schema_keyword not in configuration._disabled_client_side_validations)
+
+
+def check_validations(
+        validations, input_variable_path, input_values,
+        configuration=None):
+    """Raises an exception if the input_values are invalid
+
+    Args:
+        validations (dict): the validation dictionary.
+        input_variable_path (tuple): the path to the input variable.
+        input_values (list/str/int/float/date/datetime): the values that we
+            are checking.
+        configuration (Configuration): the configuration class.
+    """
+
+    if input_values is None:
+        return
+
+    current_validations = validations[input_variable_path]
+    if (is_json_validation_enabled('multipleOf', configuration) and
+            'multiple_of' in current_validations and
+            isinstance(input_values, (int, float)) and
+            not (float(input_values) / current_validations['multiple_of']).is_integer()):
+        # Note 'multipleOf' will be as good as the floating point arithmetic.
+        raise ApiValueError(
+            "Invalid value for `%s`, value must be a multiple of "
+            "`%s`" % (
+                input_variable_path[0],
+                current_validations['multiple_of']
+            )
+        )
+
+    if (is_json_validation_enabled('maxLength', configuration) and
+            'max_length' in current_validations and
+            len(input_values) > current_validations['max_length']):
+        raise ApiValueError(
+            "Invalid value for `%s`, length must be less than or equal to "
+            "`%s`" % (
+                input_variable_path[0],
+                current_validations['max_length']
+            )
+        )
+
+    if (is_json_validation_enabled('minLength', configuration) and
+            'min_length' in current_validations and
+            len(input_values) < current_validations['min_length']):
+        raise ApiValueError(
+            "Invalid value for `%s`, length must be greater than or equal to "
+            "`%s`" % (
+                input_variable_path[0],
+                current_validations['min_length']
+            )
+        )
+
+    if (is_json_validation_enabled('maxItems', configuration) and
+            'max_items' in current_validations and
+            len(input_values) > current_validations['max_items']):
+        raise ApiValueError(
+            "Invalid value for `%s`, number of items must be less than or "
+            "equal to `%s`" % (
+                input_variable_path[0],
+                current_validations['max_items']
+            )
+        )
+
+    if (is_json_validation_enabled('minItems', configuration) and
+            'min_items' in current_validations and
+            len(input_values) < current_validations['min_items']):
+        raise ValueError(
+            "Invalid value for `%s`, number of items must be greater than or "
+            "equal to `%s`" % (
+                input_variable_path[0],
+                current_validations['min_items']
+            )
+        )
+
+    items = ('exclusive_maximum', 'inclusive_maximum', 'exclusive_minimum',
+             'inclusive_minimum')
+    if (any(item in current_validations for item in items)):
+        if isinstance(input_values, list):
+            max_val = max(input_values)
+            min_val = min(input_values)
+        elif isinstance(input_values, dict):
+            max_val = max(input_values.values())
+            min_val = min(input_values.values())
+        else:
+            max_val = input_values
+            min_val = input_values
+
+    if (is_json_validation_enabled('exclusiveMaximum', configuration) and
+            'exclusive_maximum' in current_validations and
+            max_val >= current_validations['exclusive_maximum']):
+        raise ApiValueError(
+            "Invalid value for `%s`, must be a value less than `%s`" % (
+                input_variable_path[0],
+                current_validations['exclusive_maximum']
+            )
+        )
+
+    if (is_json_validation_enabled('maximum', configuration) and
+            'inclusive_maximum' in current_validations and
+            max_val > current_validations['inclusive_maximum']):
+        raise ApiValueError(
+            "Invalid value for `%s`, must be a value less than or equal to "
+            "`%s`" % (
+                input_variable_path[0],
+                current_validations['inclusive_maximum']
+            )
+        )
+
+    if (is_json_validation_enabled('exclusiveMinimum', configuration) and
+            'exclusive_minimum' in current_validations and
+            min_val <= current_validations['exclusive_minimum']):
+        raise ApiValueError(
+            "Invalid value for `%s`, must be a value greater than `%s`" %
+            (
+                input_variable_path[0],
+                current_validations['exclusive_maximum']
+            )
+        )
+
+    if (is_json_validation_enabled('minimum', configuration) and
+            'inclusive_minimum' in current_validations and
+            min_val < current_validations['inclusive_minimum']):
+        raise ApiValueError(
+            "Invalid value for `%s`, must be a value greater than or equal "
+            "to `%s`" % (
+                input_variable_path[0],
+                current_validations['inclusive_minimum']
+            )
+        )
+    flags = current_validations.get('regex', {}).get('flags', 0)
+    if (is_json_validation_enabled('pattern', configuration) and
+            'regex' in current_validations and
+            not re.search(current_validations['regex']['pattern'],
+                          input_values, flags=flags)):
+        err_msg = r"Invalid value for `%s`, must match regular expression `%s`" % (
+                    input_variable_path[0],
+                    current_validations['regex']['pattern']
+                )
+        if flags != 0:
+            # Don't print the regex flags if the flags are not
+            # specified in the OAS document.
+            err_msg = r"%s with flags=`%s`" % (err_msg, flags)
+        raise ApiValueError(err_msg)
+
+
+def order_response_types(required_types):
+    """Returns the required types sorted in coercion order
+
+    Args:
+        required_types (list/tuple): collection of classes or instance of
+            list or dict with class information inside it.
+
+    Returns:
+        (list): coercion order sorted collection of classes or instance
+            of list or dict with class information inside it.
+    """
+
+    def index_getter(class_or_instance):
+        if isinstance(class_or_instance, list):
+            return COERCION_INDEX_BY_TYPE[list]
+        elif isinstance(class_or_instance, dict):
+            return COERCION_INDEX_BY_TYPE[dict]
+        elif (inspect.isclass(class_or_instance)
+                and issubclass(class_or_instance, ModelComposed)):
+            return COERCION_INDEX_BY_TYPE[ModelComposed]
+        elif (inspect.isclass(class_or_instance)
+                and issubclass(class_or_instance, ModelNormal)):
+            return COERCION_INDEX_BY_TYPE[ModelNormal]
+        elif (inspect.isclass(class_or_instance)
+                and issubclass(class_or_instance, ModelSimple)):
+            return COERCION_INDEX_BY_TYPE[ModelSimple]
+        elif class_or_instance in COERCION_INDEX_BY_TYPE:
+            return COERCION_INDEX_BY_TYPE[class_or_instance]
+        raise ApiValueError("Unsupported type: %s" % class_or_instance)
+
+    sorted_types = sorted(
+        required_types,
+        key=lambda class_or_instance: index_getter(class_or_instance)
+    )
+    return sorted_types
+
+
+def remove_uncoercible(required_types_classes, current_item, spec_property_naming,
+                       must_convert=True):
+    """Only keeps the type conversions that are possible
+
+    Args:
+        required_types_classes (tuple): tuple of classes that are required
+                          these should be ordered by COERCION_INDEX_BY_TYPE
+        spec_property_naming (bool): True if the variable names in the input
+            data are serialized names as specified in the OpenAPI document.
+            False if the variables names in the input data are python
+            variable names in PEP-8 snake case.
+        current_item (any): the current item (input data) to be converted
+
+    Keyword Args:
+        must_convert (bool): if True the item to convert is of the wrong
+                          type and we want a big list of coercibles
+                          if False, we want a limited list of coercibles
+
+    Returns:
+        (list): the remaining coercible required types, classes only
+    """
+    current_type_simple = get_simple_class(current_item)
+
+    results_classes = []
+    for required_type_class in required_types_classes:
+        # convert our models to OpenApiModel
+        required_type_class_simplified = required_type_class
+        if isinstance(required_type_class_simplified, type):
+            if issubclass(required_type_class_simplified, ModelComposed):
+                required_type_class_simplified = ModelComposed
+            elif issubclass(required_type_class_simplified, ModelNormal):
+                required_type_class_simplified = ModelNormal
+            elif issubclass(required_type_class_simplified, ModelSimple):
+                required_type_class_simplified = ModelSimple
+
+        if required_type_class_simplified == current_type_simple:
+            # don't consider converting to one's own class
+            continue
+
+        class_pair = (current_type_simple, required_type_class_simplified)
+        if must_convert and class_pair in COERCIBLE_TYPE_PAIRS[spec_property_naming]:
+            results_classes.append(required_type_class)
+        elif class_pair in UPCONVERSION_TYPE_PAIRS:
+            results_classes.append(required_type_class)
+    return results_classes
+
+def get_discriminated_classes(cls):
+    """
+    Returns all the classes that a discriminator converts to
+    TODO: lru_cache this
+    """
+    possible_classes = []
+    key = list(cls.discriminator.keys())[0]
+    if is_type_nullable(cls):
+        possible_classes.append(cls)
+    for discr_cls in cls.discriminator[key].values():
+        if hasattr(discr_cls, 'discriminator') and discr_cls.discriminator is not None:
+            possible_classes.extend(get_discriminated_classes(discr_cls))
+        else:
+            possible_classes.append(discr_cls)
+    return possible_classes
+
+
+def get_possible_classes(cls, from_server_context):
+    # TODO: lru_cache this
+    possible_classes = [cls]
+    if from_server_context:
+        return possible_classes
+    if hasattr(cls, 'discriminator') and cls.discriminator is not None:
+        possible_classes = []
+        possible_classes.extend(get_discriminated_classes(cls))
+    elif issubclass(cls, ModelComposed):
+        possible_classes.extend(composed_model_input_classes(cls))
+    return possible_classes
+
+
+def get_required_type_classes(required_types_mixed, spec_property_naming):
+    """Converts the tuple required_types into a tuple and a dict described
+    below
+
+    Args:
+        required_types_mixed (tuple/list): will contain either classes or
+            instance of list or dict
+        spec_property_naming (bool): if True these values came from the
+            server, and we use the data types in our endpoints.
+            If False, we are client side and we need to include
+            oneOf and discriminator classes inside the data types in our endpoints
+
+    Returns:
+        (valid_classes, dict_valid_class_to_child_types_mixed):
+            valid_classes (tuple): the valid classes that the current item
+                                   should be
+            dict_valid_class_to_child_types_mixed (dict):
+                valid_class (class): this is the key
+                child_types_mixed (list/dict/tuple): describes the valid child
+                    types
+    """
+    valid_classes = []
+    child_req_types_by_current_type = {}
+    for required_type in required_types_mixed:
+        if isinstance(required_type, list):
+            valid_classes.append(list)
+            child_req_types_by_current_type[list] = required_type
+        elif isinstance(required_type, tuple):
+            valid_classes.append(tuple)
+            child_req_types_by_current_type[tuple] = required_type
+        elif isinstance(required_type, dict):
+            valid_classes.append(dict)
+            child_req_types_by_current_type[dict] = required_type[str]
+        else:
+            valid_classes.extend(get_possible_classes(required_type, spec_property_naming))
+    return tuple(valid_classes), child_req_types_by_current_type
+
+
+def change_keys_js_to_python(input_dict, model_class):
+    """
+    Converts from javascript_key keys in the input_dict to python_keys in
+    the output dict using the mapping in model_class.
+    If the input_dict contains a key which does not declared in the model_class,
+    the key is added to the output dict as is. The assumption is the model_class
+    may have undeclared properties (additionalProperties attribute in the OAS
+    document).
+    """
+
+    if getattr(model_class, 'attribute_map', None) is None:
+        return input_dict
+    output_dict = {}
+    reversed_attr_map = {value: key for key, value in
+                         model_class.attribute_map.items()}
+    for javascript_key, value in input_dict.items():
+        python_key = reversed_attr_map.get(javascript_key)
+        if python_key is None:
+            # if the key is unknown, it is in error or it is an
+            # additionalProperties variable
+            python_key = javascript_key
+        output_dict[python_key] = value
+    return output_dict
+
+
+def get_type_error(var_value, path_to_item, valid_classes, key_type=False):
+    error_msg = type_error_message(
+        var_name=path_to_item[-1],
+        var_value=var_value,
+        valid_classes=valid_classes,
+        key_type=key_type
+    )
+    return ApiTypeError(
+        error_msg,
+        path_to_item=path_to_item,
+        valid_classes=valid_classes,
+        key_type=key_type
+    )
+
+
+def deserialize_primitive(data, klass, path_to_item):
+    """Deserializes string to primitive type.
+
+    :param data: str/int/float
+    :param klass: str/class the class to convert to
+
+    :return: int, float, str, bool, date, datetime
+    """
+    additional_message = ""
+    try:
+        if klass in {datetime, date}:
+            additional_message = (
+                "If you need your parameter to have a fallback "
+                "string value, please set its type as `type: {}` in your "
+                "spec. That allows the value to be any type. "
+            )
+            if klass == datetime:
+                if len(data) < 8:
+                    raise ValueError("This is not a datetime")
+                # The string should be in iso8601 datetime format.
+                parsed_datetime = parse(data)
+                date_only = (
+                    parsed_datetime.hour == 0 and
+                    parsed_datetime.minute == 0 and
+                    parsed_datetime.second == 0 and
+                    parsed_datetime.tzinfo is None and
+                    8 <= len(data) <= 10
+                )
+                if date_only:
+                    raise ValueError("This is a date, not a datetime")
+                return parsed_datetime
+            elif klass == date:
+                if len(data) < 8:
+                    raise ValueError("This is not a date")
+                return parse(data).date()
+        else:
+            converted_value = klass(data)
+            if isinstance(data, str) and klass == float:
+                if str(converted_value) != data:
+                    # '7' -> 7.0 -> '7.0' != '7'
+                    raise ValueError('This is not a float')
+            return converted_value
+    except (OverflowError, ValueError) as ex:
+        # parse can raise OverflowError
+        raise ApiValueError(
+            "{0}Failed to parse {1} as {2}".format(
+                additional_message, repr(data), klass.__name__
+            ),
+            path_to_item=path_to_item
+        ) from ex
+
+
+def get_discriminator_class(model_class,
+                            discr_name,
+                            discr_value, cls_visited):
+    """Returns the child class specified by the discriminator.
+
+    Args:
+        model_class (OpenApiModel): the model class.
+        discr_name (string): the name of the discriminator property.
+        discr_value (any): the discriminator value.
+        cls_visited (list): list of model classes that have been visited.
+            Used to determine the discriminator class without
+            visiting circular references indefinitely.
+
+    Returns:
+        used_model_class (class/None): the chosen child class that will be used
+            to deserialize the data, for example dog.Dog.
+            If a class is not found, None is returned.
+    """
+
+    if model_class in cls_visited:
+        # The class has already been visited and no suitable class was found.
+        return None
+    cls_visited.append(model_class)
+    used_model_class = None
+    if discr_name in model_class.discriminator:
+        class_name_to_discr_class = model_class.discriminator[discr_name]
+        used_model_class = class_name_to_discr_class.get(discr_value)
+    if used_model_class is None:
+        # We didn't find a discriminated class in class_name_to_discr_class.
+        # So look in the ancestor or descendant discriminators
+        # The discriminator mapping may exist in a descendant (anyOf, oneOf)
+        # or ancestor (allOf).
+        # Ancestor example: in the GrandparentAnimal -> ParentPet -> ChildCat
+        #   hierarchy, the discriminator mappings may be defined at any level
+        #   in the hierarchy.
+        # Descendant example:  mammal -> whale/zebra/Pig -> BasquePig/DanishPig
+        #   if we try to make BasquePig from mammal, we need to travel through
+        #   the oneOf descendant discriminators to find BasquePig
+        descendant_classes =  model_class._composed_schemas.get('oneOf', ()) + \
+            model_class._composed_schemas.get('anyOf', ())
+        ancestor_classes = model_class._composed_schemas.get('allOf', ())
+        possible_classes = descendant_classes + ancestor_classes
+        for cls in possible_classes:
+            # Check if the schema has inherited discriminators.
+            if hasattr(cls, 'discriminator') and cls.discriminator is not None:
+                used_model_class = get_discriminator_class(
+                                    cls, discr_name, discr_value, cls_visited)
+                if used_model_class is not None:
+                    return used_model_class
+    return used_model_class
+
+
+def deserialize_model(model_data, model_class, path_to_item, check_type,
+                      configuration, spec_property_naming):
+    """Deserializes model_data to model instance.
+
+    Args:
+        model_data (int/str/float/bool/none_type/list/dict): data to instantiate the model
+        model_class (OpenApiModel): the model class
+        path_to_item (list): path to the model in the received data
+        check_type (bool): whether to check the data tupe for the values in
+            the model
+        configuration (Configuration): the instance to use to convert files
+        spec_property_naming (bool): True if the variable names in the input
+            data are serialized names as specified in the OpenAPI document.
+            False if the variables names in the input data are python
+            variable names in PEP-8 snake case.
+
+    Returns:
+        model instance
+
+    Raise:
+        ApiTypeError
+        ApiValueError
+        ApiKeyError
+    """
+
+    kw_args = dict(_check_type=check_type,
+                   _path_to_item=path_to_item,
+                   _configuration=configuration,
+                   _spec_property_naming=spec_property_naming)
+
+    if issubclass(model_class, ModelSimple):
+        return model_class._new_from_openapi_data(model_data, **kw_args)
+    elif isinstance(model_data, list):
+        return model_class._new_from_openapi_data(*model_data, **kw_args)
+    if isinstance(model_data, dict):
+        kw_args.update(model_data)
+        return model_class._new_from_openapi_data(**kw_args)
+    elif isinstance(model_data, PRIMITIVE_TYPES):
+        return model_class._new_from_openapi_data(model_data, **kw_args)
+
+
+def deserialize_file(response_data, configuration, content_disposition=None):
+    """Deserializes body to file
+
+    Saves response body into a file in a temporary folder,
+    using the filename from the `Content-Disposition` header if provided.
+
+    Args:
+        param response_data (str):  the file data to write
+        configuration (Configuration): the instance to use to convert files
+
+    Keyword Args:
+        content_disposition (str):  the value of the Content-Disposition
+            header
+
+    Returns:
+        (file_type): the deserialized file which is open
+            The user is responsible for closing and reading the file
+    """
+    fd, path = tempfile.mkstemp(dir=configuration.temp_folder_path)
+    os.close(fd)
+    os.remove(path)
+
+    if content_disposition:
+        filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?',
+                             content_disposition,
+                             flags=re.I)
+        if filename is not None:
+            filename = filename.group(1)
+        else:
+            filename = "default_" + str(uuid.uuid4())
+
+        path = os.path.join(os.path.dirname(path), filename)
+
+    with open(path, "wb") as f:
+        if isinstance(response_data, str):
+            # change str to bytes so we can write it
+            response_data = response_data.encode('utf-8')
+        f.write(response_data)
+
+    f = open(path, "rb")
+    return f
+
+
+def attempt_convert_item(input_value, valid_classes, path_to_item,
+                         configuration, spec_property_naming, key_type=False,
+                         must_convert=False, check_type=True):
+    """
+    Args:
+        input_value (any): the data to convert
+        valid_classes (any): the classes that are valid
+        path_to_item (list): the path to the item to convert
+        configuration (Configuration): the instance to use to convert files
+        spec_property_naming (bool): True if the variable names in the input
+            data are serialized names as specified in the OpenAPI document.
+            False if the variables names in the input data are python
+            variable names in PEP-8 snake case.
+        key_type (bool): if True we need to convert a key type (not supported)
+        must_convert (bool): if True we must convert
+        check_type (bool): if True we check the type or the returned data in
+            ModelComposed/ModelNormal/ModelSimple instances
+
+    Returns:
+        instance (any) the fixed item
+
+    Raises:
+        ApiTypeError
+        ApiValueError
+        ApiKeyError
+    """
+    valid_classes_ordered = order_response_types(valid_classes)
+    valid_classes_coercible = remove_uncoercible(
+        valid_classes_ordered, input_value, spec_property_naming)
+    if not valid_classes_coercible or key_type:
+        # we do not handle keytype errors, json will take care
+        # of this for us
+        if configuration is None or not configuration.discard_unknown_keys:
+            raise get_type_error(input_value, path_to_item, valid_classes,
+                                 key_type=key_type)
+    for valid_class in valid_classes_coercible:
+        try:
+            if issubclass(valid_class, OpenApiModel):
+                return deserialize_model(input_value, valid_class,
+                                         path_to_item, check_type,
+                                         configuration, spec_property_naming)
+            elif valid_class == file_type:
+                return deserialize_file(input_value, configuration)
+            return deserialize_primitive(input_value, valid_class,
+                                         path_to_item)
+        except (ApiTypeError, ApiValueError, ApiKeyError) as conversion_exc:
+            if must_convert:
+                raise conversion_exc
+            # if we have conversion errors when must_convert == False
+            # we ignore the exception and move on to the next class
+            continue
+    # we were unable to convert, must_convert == False
+    return input_value
+
+
+def is_type_nullable(input_type):
+    """
+    Returns true if None is an allowed value for the specified input_type.
+
+    A type is nullable if at least one of the following conditions is true:
+    1. The OAS 'nullable' attribute has been specified,
+    1. The type is the 'null' type,
+    1. The type is a anyOf/oneOf composed schema, and a child schema is
+       the 'null' type.
+    Args:
+        input_type (type): the class of the input_value that we are
+            checking
+    Returns:
+        bool
+    """
+    if input_type is none_type:
+        return True
+    if issubclass(input_type, OpenApiModel) and input_type._nullable:
+        return True
+    if issubclass(input_type, ModelComposed):
+        # If oneOf/anyOf, check if the 'null' type is one of the allowed types.
+        for t in input_type._composed_schemas.get('oneOf', ()):
+            if is_type_nullable(t): return True
+        for t in input_type._composed_schemas.get('anyOf', ()):
+            if is_type_nullable(t): return True
+    return False
+
+
+def is_valid_type(input_class_simple, valid_classes):
+    """
+    Args:
+        input_class_simple (class): the class of the input_value that we are
+            checking
+        valid_classes (tuple): the valid classes that the current item
+            should be
+    Returns:
+        bool
+    """
+    if issubclass(input_class_simple, OpenApiModel) and \
+        valid_classes == (bool, date, datetime, dict, float, int, list, str, none_type,):
+        return True
+    valid_type = input_class_simple in valid_classes
+    if not valid_type and (
+            issubclass(input_class_simple, OpenApiModel) or
+            input_class_simple is none_type):
+        for valid_class in valid_classes:
+            if input_class_simple is none_type and is_type_nullable(valid_class):
+                # Schema is oneOf/anyOf and the 'null' type is one of the allowed types.
+                return True
+            if not (issubclass(valid_class, OpenApiModel) and valid_class.discriminator):
+                continue
+            discr_propertyname_py = list(valid_class.discriminator.keys())[0]
+            discriminator_classes = (
+                valid_class.discriminator[discr_propertyname_py].values()
+            )
+            valid_type = is_valid_type(input_class_simple, discriminator_classes)
+            if valid_type:
+                return True
+    return valid_type
+
+
+def validate_and_convert_types(input_value, required_types_mixed, path_to_item,
+                               spec_property_naming, _check_type, configuration=None):
+    """Raises a TypeError is there is a problem, otherwise returns value
+
+    Args:
+        input_value (any): the data to validate/convert
+        required_types_mixed (list/dict/tuple): A list of
+            valid classes, or a list tuples of valid classes, or a dict where
+            the value is a tuple of value classes
+        path_to_item: (list) the path to the data being validated
+            this stores a list of keys or indices to get to the data being
+            validated
+        spec_property_naming (bool): True if the variable names in the input
+            data are serialized names as specified in the OpenAPI document.
+            False if the variables names in the input data are python
+            variable names in PEP-8 snake case.
+        _check_type: (boolean) if true, type will be checked and conversion
+            will be attempted.
+        configuration: (Configuration): the configuration class to use
+            when converting file_type items.
+            If passed, conversion will be attempted when possible
+            If not passed, no conversions will be attempted and
+            exceptions will be raised
+
+    Returns:
+        the correctly typed value
+
+    Raises:
+        ApiTypeError
+    """
+    results = get_required_type_classes(required_types_mixed, spec_property_naming)
+    valid_classes, child_req_types_by_current_type = results
+
+    input_class_simple = get_simple_class(input_value)
+    valid_type = is_valid_type(input_class_simple, valid_classes)
+    if not valid_type:
+        if (configuration 
+                or (input_class_simple == dict 
+                    and not dict in valid_classes)):
+            # if input_value is not valid_type try to convert it
+            converted_instance = attempt_convert_item(
+                input_value,
+                valid_classes,
+                path_to_item,
+                configuration,
+                spec_property_naming,
+                key_type=False,
+                must_convert=True,
+                check_type=_check_type
+            )
+            return converted_instance
+        else:
+            raise get_type_error(input_value, path_to_item, valid_classes,
+                                 key_type=False)
+
+    # input_value's type is in valid_classes
+    if len(valid_classes) > 1 and configuration:
+        # there are valid classes which are not the current class
+        valid_classes_coercible = remove_uncoercible(
+            valid_classes, input_value, spec_property_naming, must_convert=False)
+        if valid_classes_coercible:
+            converted_instance = attempt_convert_item(
+                input_value,
+                valid_classes_coercible,
+                path_to_item,
+                configuration,
+                spec_property_naming,
+                key_type=False,
+                must_convert=False,
+                check_type=_check_type
+            )
+            return converted_instance
+
+    if child_req_types_by_current_type == {}:
+        # all types are of the required types and there are no more inner
+        # variables left to look at
+        return input_value
+    inner_required_types = child_req_types_by_current_type.get(
+        type(input_value)
+    )
+    if inner_required_types is None:
+        # for this type, there are not more inner variables left to look at
+        return input_value
+    if isinstance(input_value, list):
+        if input_value == []:
+            # allow an empty list
+            return input_value
+        for index, inner_value in enumerate(input_value):
+            inner_path = list(path_to_item)
+            inner_path.append(index)
+            input_value[index] = validate_and_convert_types(
+                inner_value,
+                inner_required_types,
+                inner_path,
+                spec_property_naming,
+                _check_type,
+                configuration=configuration
+            )
+    elif isinstance(input_value, dict):
+        if input_value == {}:
+            # allow an empty dict
+            return input_value
+        for inner_key, inner_val in input_value.items():
+            inner_path = list(path_to_item)
+            inner_path.append(inner_key)
+            if get_simple_class(inner_key) != str:
+                raise get_type_error(inner_key, inner_path, valid_classes,
+                                     key_type=True)
+            input_value[inner_key] = validate_and_convert_types(
+                inner_val,
+                inner_required_types,
+                inner_path,
+                spec_property_naming,
+                _check_type,
+                configuration=configuration
+            )
+    return input_value
+
+
+def model_to_dict(model_instance, serialize=True):
+    """Returns the model properties as a dict
+
+    Args:
+        model_instance (one of your model instances): the model instance that
+            will be converted to a dict.
+
+    Keyword Args:
+        serialize (bool): if True, the keys in the dict will be values from
+            attribute_map
+    """
+    result = {}
+    extract_item = lambda item: (item[0], model_to_dict(item[1], serialize=serialize)) if hasattr(item[1], '_data_store') else item
+
+    model_instances = [model_instance]
+    if model_instance._composed_schemas:
+        model_instances.extend(model_instance._composed_instances)
+    seen_json_attribute_names = set()
+    used_fallback_python_attribute_names = set()
+    py_to_json_map = {}
+    for model_instance in model_instances:
+        for attr, value in model_instance._data_store.items():
+            if serialize:
+                # we use get here because additional property key names do not
+                # exist in attribute_map
+                try:
+                    attr = model_instance.attribute_map[attr]
+                    py_to_json_map.update(model_instance.attribute_map)
+                    seen_json_attribute_names.add(attr)
+                except KeyError:
+                    used_fallback_python_attribute_names.add(attr)
+            if isinstance(value, list):
+               if not value:
+                   # empty list or None
+                   result[attr] = value
+               else:
+                   res = []
+                   for v in value:
+                       if isinstance(v, PRIMITIVE_TYPES) or v is None:
+                           res.append(v)
+                       elif isinstance(v, ModelSimple):
+                           res.append(v.value)
+                       elif isinstance(v, dict):
+                           res.append(dict(map(
+                               extract_item,
+                               v.items()
+                           )))
+                       else:
+                           res.append(model_to_dict(v, serialize=serialize))
+                   result[attr] = res
+            elif isinstance(value, dict):
+                result[attr] = dict(map(
+                    extract_item,
+                    value.items()
+                ))
+            elif isinstance(value, ModelSimple):
+                result[attr] = value.value
+            elif hasattr(value, '_data_store'):
+                result[attr] = model_to_dict(value, serialize=serialize)
+            else:
+                result[attr] = value
+    if serialize:
+        for python_key in used_fallback_python_attribute_names:
+            json_key = py_to_json_map.get(python_key)
+            if json_key is None:
+                continue
+            if python_key == json_key:
+                continue
+            json_key_assigned_no_need_for_python_key = json_key in seen_json_attribute_names
+            if json_key_assigned_no_need_for_python_key:
+                del result[python_key]
+
+    return result
+
+
+def type_error_message(var_value=None, var_name=None, valid_classes=None,
+                       key_type=None):
+    """
+    Keyword Args:
+        var_value (any): the variable which has the type_error
+        var_name (str): the name of the variable which has the typ error
+        valid_classes (tuple): the accepted classes for current_item's
+                                  value
+        key_type (bool): False if our value is a value in a dict
+                         True if it is a key in a dict
+                         False if our item is an item in a list
+    """
+    key_or_value = 'value'
+    if key_type:
+        key_or_value = 'key'
+    valid_classes_phrase = get_valid_classes_phrase(valid_classes)
+    msg = (
+        "Invalid type for variable '{0}'. Required {1} type {2} and "
+        "passed type was {3}".format(
+            var_name,
+            key_or_value,
+            valid_classes_phrase,
+            type(var_value).__name__,
+        )
+    )
+    return msg
+
+
+def get_valid_classes_phrase(input_classes):
+    """Returns a string phrase describing what types are allowed
+    """
+    all_classes = list(input_classes)
+    all_classes = sorted(all_classes, key=lambda cls: cls.__name__)
+    all_class_names = [cls.__name__ for cls in all_classes]
+    if len(all_class_names) == 1:
+        return 'is {0}'.format(all_class_names[0])
+    return "is one of [{0}]".format(", ".join(all_class_names))
+
+
+def get_allof_instances(self, model_args, constant_args):
+    """
+    Args:
+        self: the class we are handling
+        model_args (dict): var_name to var_value
+            used to make instances
+        constant_args (dict):
+            metadata arguments:
+            _check_type
+            _path_to_item
+            _spec_property_naming
+            _configuration
+            _visited_composed_classes
+
+    Returns
+        composed_instances (list)
+    """
+    composed_instances = []
+    for allof_class in self._composed_schemas['allOf']:
+
+        try:
+            if constant_args.get('_spec_property_naming'):
+                allof_instance = allof_class._from_openapi_data(**model_args, **constant_args)
+            else:
+                allof_instance = allof_class(**model_args, **constant_args)
+            composed_instances.append(allof_instance)
+        except Exception as ex:
+            raise ApiValueError(
+                "Invalid inputs given to generate an instance of '%s'. The "
+                "input data was invalid for the allOf schema '%s' in the composed "
+                "schema '%s'. Error=%s" % (
+                    allof_class.__name__,
+                    allof_class.__name__,
+                    self.__class__.__name__,
+                    str(ex)
+                )
+            ) from ex
+    return composed_instances
+
+
+def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
+    """
+    Find the oneOf schema that matches the input data (e.g. payload).
+    If exactly one schema matches the input data, an instance of that schema
+    is returned.
+    If zero or more than one schema match the input data, an exception is raised.
+    In OAS 3.x, the payload MUST, by validation, match exactly one of the
+    schemas described by oneOf.
+
+    Args:
+        cls: the class we are handling
+        model_kwargs (dict): var_name to var_value
+            The input data, e.g. the payload that must match a oneOf schema
+            in the OpenAPI document.
+        constant_kwargs (dict): var_name to var_value
+            args that every model requires, including configuration, server
+            and path to item.
+
+    Kwargs:
+        model_arg: (int, float, bool, str, date, datetime, ModelSimple, None):
+            the value to assign to a primitive class or ModelSimple class
+            Notes:
+            - this is only passed in when oneOf includes types which are not object
+            - None is used to suppress handling of model_arg, nullable models are handled in __new__
+
+    Returns
+        oneof_instance (instance)
+    """
+    if len(cls._composed_schemas['oneOf']) == 0:
+        return None
+
+    oneof_instances = []
+    # Iterate over each oneOf schema and determine if the input data
+    # matches the oneOf schemas.
+    for oneof_class in cls._composed_schemas['oneOf']:
+        # The composed oneOf schema allows the 'null' type and the input data
+        # is the null value. This is a OAS >= 3.1 feature.
+        if oneof_class is none_type:
+            # skip none_types because we are deserializing dict data.
+            # none_type deserialization is handled in the __new__ method
+            continue
+
+        single_value_input = allows_single_value_input(oneof_class)
+
+        try:
+            if not single_value_input:
+                if constant_kwargs.get('_spec_property_naming'):
+                    oneof_instance = oneof_class._from_openapi_data(**model_kwargs, **constant_kwargs)
+                else:
+                    oneof_instance = oneof_class(**model_kwargs, **constant_kwargs)
+            else:
+                if issubclass(oneof_class, ModelSimple):
+                    if constant_kwargs.get('_spec_property_naming'):
+                        oneof_instance = oneof_class._from_openapi_data(model_arg, **constant_kwargs)
+                    else:
+                        oneof_instance = oneof_class(model_arg, **constant_kwargs)
+                elif oneof_class in PRIMITIVE_TYPES:
+                    oneof_instance = validate_and_convert_types(
+                        model_arg,
+                        (oneof_class,),
+                        constant_kwargs['_path_to_item'],
+                        constant_kwargs['_spec_property_naming'],
+                        constant_kwargs['_check_type'],
+                        configuration=constant_kwargs['_configuration']
+                    )
+            oneof_instances.append(oneof_instance)
+        except Exception:
+            pass
+    if len(oneof_instances) == 0:
+        raise ApiValueError(
+            "Invalid inputs given to generate an instance of %s. None "
+            "of the oneOf schemas matched the input data." %
+            cls.__name__
+        )
+    elif len(oneof_instances) > 1:
+        raise ApiValueError(
+            "Invalid inputs given to generate an instance of %s. Multiple "
+            "oneOf schemas matched the inputs, but a max of one is allowed." %
+            cls.__name__
+        )
+    return oneof_instances[0]
+
+
+def get_anyof_instances(self, model_args, constant_args):
+    """
+    Args:
+        self: the class we are handling
+        model_args (dict): var_name to var_value
+            The input data, e.g. the payload that must match at least one
+            anyOf child schema in the OpenAPI document.
+        constant_args (dict): var_name to var_value
+            args that every model requires, including configuration, server
+            and path to item.
+
+    Returns
+        anyof_instances (list)
+    """
+    anyof_instances = []
+    if len(self._composed_schemas['anyOf']) == 0:
+        return anyof_instances
+
+    for anyof_class in self._composed_schemas['anyOf']:
+        # The composed oneOf schema allows the 'null' type and the input data
+        # is the null value. This is a OAS >= 3.1 feature.
+        if anyof_class is none_type:
+            # skip none_types because we are deserializing dict data.
+            # none_type deserialization is handled in the __new__ method
+            continue
+
+        try:
+            if constant_args.get('_spec_property_naming'):
+                anyof_instance = anyof_class._from_openapi_data(**model_args, **constant_args)
+            else:
+                anyof_instance = anyof_class(**model_args, **constant_args)
+            anyof_instances.append(anyof_instance)
+        except Exception:
+            pass
+    if len(anyof_instances) == 0:
+        raise ApiValueError(
+            "Invalid inputs given to generate an instance of %s. None of the "
+            "anyOf schemas matched the inputs." %
+            self.__class__.__name__
+        )
+    return anyof_instances
+
+
+def get_discarded_args(self, composed_instances, model_args):
+    """
+    Gathers the args that were discarded by configuration.discard_unknown_keys
+    """
+    model_arg_keys = model_args.keys()
+    discarded_args = set()
+    # arguments passed to self were already converted to python names
+    # before __init__ was called
+    for instance in composed_instances:
+        if instance.__class__ in self._composed_schemas['allOf']:
+            try:
+                keys = instance.to_dict().keys()
+                discarded_keys = model_args - keys
+                discarded_args.update(discarded_keys)
+            except Exception:
+                # allOf integer schema will throw exception
+                pass
+        else:
+            try:
+                all_keys = set(model_to_dict(instance, serialize=False).keys())
+                js_keys = model_to_dict(instance, serialize=True).keys()
+                all_keys.update(js_keys)
+                discarded_keys = model_arg_keys - all_keys
+                discarded_args.update(discarded_keys)
+            except Exception:
+                # allOf integer schema will throw exception
+                pass
+    return discarded_args
+
+
+def validate_get_composed_info(constant_args, model_args, self):
+    """
+    For composed schemas, generate schema instances for
+    all schemas in the oneOf/anyOf/allOf definition. If additional
+    properties are allowed, also assign those properties on
+    all matched schemas that contain additionalProperties.
+    Openapi schemas are python classes.
+
+    Exceptions are raised if:
+    - 0 or > 1 oneOf schema matches the model_args input data
+    - no anyOf schema matches the model_args input data
+    - any of the allOf schemas do not match the model_args input data
+
+    Args:
+        constant_args (dict): these are the args that every model requires
+        model_args (dict): these are the required and optional spec args that
+            were passed in to make this model
+        self (class): the class that we are instantiating
+            This class contains self._composed_schemas
+
+    Returns:
+        composed_info (list): length three
+            composed_instances (list): the composed instances which are not
+                self
+            var_name_to_model_instances (dict): a dict going from var_name
+                to the model_instance which holds that var_name
+                the model_instance may be self or an instance of one of the
+                classes in self.composed_instances()
+            additional_properties_model_instances (list): a list of the
+                model instances which have the property
+                additional_properties_type. This list can include self
+    """
+    # create composed_instances
+    composed_instances = []
+    allof_instances = get_allof_instances(self, model_args, constant_args)
+    composed_instances.extend(allof_instances)
+    oneof_instance = get_oneof_instance(self.__class__, model_args, constant_args)
+    if oneof_instance is not None:
+        composed_instances.append(oneof_instance)
+    anyof_instances = get_anyof_instances(self, model_args, constant_args)
+    composed_instances.extend(anyof_instances)
+    """
+    set additional_properties_model_instances
+    additional properties must be evaluated at the schema level
+    so self's additional properties are most important
+    If self is a composed schema with:
+    - no properties defined in self
+    - additionalProperties: False
+    Then for object payloads every property is an additional property
+    and they are not allowed, so only empty dict is allowed
+
+    Properties must be set on all matching schemas
+    so when a property is assigned toa composed instance, it must be set on all
+    composed instances regardless of additionalProperties presence
+    keeping it to prevent breaking changes in v5.0.1
+    TODO remove cls._additional_properties_model_instances in 6.0.0
+    """
+    additional_properties_model_instances = []
+    if self.additional_properties_type is not None:
+        additional_properties_model_instances = [self]
+
+    """
+    no need to set properties on self in here, they will be set in __init__
+    By here all composed schema oneOf/anyOf/allOf instances have their properties set using
+    model_args
+    """
+    discarded_args = get_discarded_args(self, composed_instances, model_args)
+
+    # map variable names to composed_instances
+    var_name_to_model_instances = {}
+    for prop_name in model_args:
+        if prop_name not in discarded_args:
+            var_name_to_model_instances[prop_name] = [self] + composed_instances
+
+    return [
+      composed_instances,
+      var_name_to_model_instances,
+      additional_properties_model_instances,
+      discarded_args
+    ]
diff --git a/turbinia/api/client/turbinia_api_client/models/__init__.py b/turbinia/api/client/turbinia_api_client/models/__init__.py
new file mode 100644
index 000000000..7325db01a
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/models/__init__.py
@@ -0,0 +1,16 @@
+# flake8: noqa
+
+# import all models into this package
+# if you have many models here with many references from one model to another this may
+# raise a RecursionError
+# to avoid this, import only the models that you directly need like:
+# from from turbinia_api_client.model.pet import Pet
+# or import this package, but before doing it, use:
+# import sys
+# sys.setrecursionlimit(n)
+
+from turbinia_api_client.model.base_request_options import BaseRequestOptions
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+from turbinia_api_client.model.request import Request
+from turbinia_api_client.model.validation_error import ValidationError
+from turbinia_api_client.model.validation_error_loc_inner import ValidationErrorLocInner
diff --git a/turbinia/api/client/turbinia_api_client/rest.py b/turbinia/api/client/turbinia_api_client/rest.py
new file mode 100644
index 000000000..704a340e3
--- /dev/null
+++ b/turbinia/api/client/turbinia_api_client/rest.py
@@ -0,0 +1,346 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import io
+import json
+import logging
+import re
+import ssl
+from urllib.parse import urlencode
+from urllib.parse import urlparse
+from urllib.request import proxy_bypass_environment
+import urllib3
+import ipaddress
+
+from turbinia_api_client.exceptions import ApiException, UnauthorizedException, ForbiddenException, NotFoundException, ServiceException, ApiValueError
+
+
+logger = logging.getLogger(__name__)
+
+
+class RESTResponse(io.IOBase):
+
+    def __init__(self, resp):
+        self.urllib3_response = resp
+        self.status = resp.status
+        self.reason = resp.reason
+        self.data = resp.data
+
+    def getheaders(self):
+        """Returns a dictionary of the response headers."""
+        return self.urllib3_response.getheaders()
+
+    def getheader(self, name, default=None):
+        """Returns a given response header."""
+        return self.urllib3_response.getheader(name, default)
+
+
+class RESTClientObject(object):
+
+    def __init__(self, configuration, pools_size=4, maxsize=None):
+        # urllib3.PoolManager will pass all kw parameters to connectionpool
+        # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75  # noqa: E501
+        # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680  # noqa: E501
+        # maxsize is the number of requests to host that are allowed in parallel  # noqa: E501
+        # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html  # noqa: E501
+
+        # cert_reqs
+        if configuration.verify_ssl:
+            cert_reqs = ssl.CERT_REQUIRED
+        else:
+            cert_reqs = ssl.CERT_NONE
+
+        addition_pool_args = {}
+        if configuration.assert_hostname is not None:
+            addition_pool_args['assert_hostname'] = configuration.assert_hostname  # noqa: E501
+
+        if configuration.retries is not None:
+            addition_pool_args['retries'] = configuration.retries
+
+        if configuration.socket_options is not None:
+            addition_pool_args['socket_options'] = configuration.socket_options
+
+        if maxsize is None:
+            if configuration.connection_pool_maxsize is not None:
+                maxsize = configuration.connection_pool_maxsize
+            else:
+                maxsize = 4
+
+        # https pool manager
+        if configuration.proxy and not should_bypass_proxies(configuration.host, no_proxy=configuration.no_proxy or ''):
+            self.pool_manager = urllib3.ProxyManager(
+                num_pools=pools_size,
+                maxsize=maxsize,
+                cert_reqs=cert_reqs,
+                ca_certs=configuration.ssl_ca_cert,
+                cert_file=configuration.cert_file,
+                key_file=configuration.key_file,
+                proxy_url=configuration.proxy,
+                proxy_headers=configuration.proxy_headers,
+                **addition_pool_args
+            )
+        else:
+            self.pool_manager = urllib3.PoolManager(
+                num_pools=pools_size,
+                maxsize=maxsize,
+                cert_reqs=cert_reqs,
+                ca_certs=configuration.ssl_ca_cert,
+                cert_file=configuration.cert_file,
+                key_file=configuration.key_file,
+                **addition_pool_args
+            )
+
+    def request(self, method, url, query_params=None, headers=None,
+                body=None, post_params=None, _preload_content=True,
+                _request_timeout=None):
+        """Perform requests.
+
+        :param method: http request method
+        :param url: http request url
+        :param query_params: query parameters in the url
+        :param headers: http request headers
+        :param body: request json body, for `application/json`
+        :param post_params: request post parameters,
+                            `application/x-www-form-urlencoded`
+                            and `multipart/form-data`
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        """
+        method = method.upper()
+        assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
+                          'PATCH', 'OPTIONS']
+
+        if post_params and body:
+            raise ApiValueError(
+                "body parameter cannot be used with post_params parameter."
+            )
+
+        post_params = post_params or {}
+        headers = headers or {}
+
+        timeout = None
+        if _request_timeout:
+            if isinstance(_request_timeout, (int, float)):  # noqa: E501,F821
+                timeout = urllib3.Timeout(total=_request_timeout)
+            elif (isinstance(_request_timeout, tuple) and
+                  len(_request_timeout) == 2):
+                timeout = urllib3.Timeout(
+                    connect=_request_timeout[0], read=_request_timeout[1])
+
+        try:
+            # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
+            if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
+                # Only set a default Content-Type for POST, PUT, PATCH and OPTIONS requests
+                if (method != 'DELETE') and ('Content-Type' not in headers):
+                    headers['Content-Type'] = 'application/json'
+                if query_params:
+                    url += '?' + urlencode(query_params)
+                if ('Content-Type' not in headers) or (re.search('json', headers['Content-Type'], re.IGNORECASE)):
+                    request_body = None
+                    if body is not None:
+                        request_body = json.dumps(body)
+                    r = self.pool_manager.request(
+                        method, url,
+                        body=request_body,
+                        preload_content=_preload_content,
+                        timeout=timeout,
+                        headers=headers)
+                elif headers['Content-Type'] == 'application/x-www-form-urlencoded':  # noqa: E501
+                    r = self.pool_manager.request(
+                        method, url,
+                        fields=post_params,
+                        encode_multipart=False,
+                        preload_content=_preload_content,
+                        timeout=timeout,
+                        headers=headers)
+                elif headers['Content-Type'] == 'multipart/form-data':
+                    # must del headers['Content-Type'], or the correct
+                    # Content-Type which generated by urllib3 will be
+                    # overwritten.
+                    del headers['Content-Type']
+                    r = self.pool_manager.request(
+                        method, url,
+                        fields=post_params,
+                        encode_multipart=True,
+                        preload_content=_preload_content,
+                        timeout=timeout,
+                        headers=headers)
+                # Pass a `string` parameter directly in the body to support
+                # other content types than Json when `body` argument is
+                # provided in serialized form
+                elif isinstance(body, str) or isinstance(body, bytes):
+                    request_body = body
+                    r = self.pool_manager.request(
+                        method, url,
+                        body=request_body,
+                        preload_content=_preload_content,
+                        timeout=timeout,
+                        headers=headers)
+                else:
+                    # Cannot generate the request from given parameters
+                    msg = """Cannot prepare a request message for provided
+                             arguments. Please check that your arguments match
+                             declared content type."""
+                    raise ApiException(status=0, reason=msg)
+            # For `GET`, `HEAD`
+            else:
+                r = self.pool_manager.request(method, url,
+                                              fields=query_params,
+                                              preload_content=_preload_content,
+                                              timeout=timeout,
+                                              headers=headers)
+        except urllib3.exceptions.SSLError as e:
+            msg = "{0}\n{1}".format(type(e).__name__, str(e))
+            raise ApiException(status=0, reason=msg)
+
+        if _preload_content:
+            r = RESTResponse(r)
+
+            # log response body
+            logger.debug("response body: %s", r.data)
+
+        if not 200 <= r.status <= 299:
+            if r.status == 401:
+                raise UnauthorizedException(http_resp=r)
+
+            if r.status == 403:
+                raise ForbiddenException(http_resp=r)
+
+            if r.status == 404:
+                raise NotFoundException(http_resp=r)
+
+            if 500 <= r.status <= 599:
+                raise ServiceException(http_resp=r)
+
+            raise ApiException(http_resp=r)
+
+        return r
+
+    def GET(self, url, headers=None, query_params=None, _preload_content=True,
+            _request_timeout=None):
+        return self.request("GET", url,
+                            headers=headers,
+                            _preload_content=_preload_content,
+                            _request_timeout=_request_timeout,
+                            query_params=query_params)
+
+    def HEAD(self, url, headers=None, query_params=None, _preload_content=True,
+             _request_timeout=None):
+        return self.request("HEAD", url,
+                            headers=headers,
+                            _preload_content=_preload_content,
+                            _request_timeout=_request_timeout,
+                            query_params=query_params)
+
+    def OPTIONS(self, url, headers=None, query_params=None, post_params=None,
+                body=None, _preload_content=True, _request_timeout=None):
+        return self.request("OPTIONS", url,
+                            headers=headers,
+                            query_params=query_params,
+                            post_params=post_params,
+                            _preload_content=_preload_content,
+                            _request_timeout=_request_timeout,
+                            body=body)
+
+    def DELETE(self, url, headers=None, query_params=None, body=None,
+               _preload_content=True, _request_timeout=None):
+        return self.request("DELETE", url,
+                            headers=headers,
+                            query_params=query_params,
+                            _preload_content=_preload_content,
+                            _request_timeout=_request_timeout,
+                            body=body)
+
+    def POST(self, url, headers=None, query_params=None, post_params=None,
+             body=None, _preload_content=True, _request_timeout=None):
+        return self.request("POST", url,
+                            headers=headers,
+                            query_params=query_params,
+                            post_params=post_params,
+                            _preload_content=_preload_content,
+                            _request_timeout=_request_timeout,
+                            body=body)
+
+    def PUT(self, url, headers=None, query_params=None, post_params=None,
+            body=None, _preload_content=True, _request_timeout=None):
+        return self.request("PUT", url,
+                            headers=headers,
+                            query_params=query_params,
+                            post_params=post_params,
+                            _preload_content=_preload_content,
+                            _request_timeout=_request_timeout,
+                            body=body)
+
+    def PATCH(self, url, headers=None, query_params=None, post_params=None,
+              body=None, _preload_content=True, _request_timeout=None):
+        return self.request("PATCH", url,
+                            headers=headers,
+                            query_params=query_params,
+                            post_params=post_params,
+                            _preload_content=_preload_content,
+                            _request_timeout=_request_timeout,
+                            body=body)
+
+# end of class RESTClientObject
+def is_ipv4(target):
+    """ Test if IPv4 address or not
+    """
+    try:
+       chk = ipaddress.IPv4Address(target)
+       return True
+    except ipaddress.AddressValueError:
+       return False
+
+def in_ipv4net(target, net):
+    """ Test if target belongs to given IPv4 network
+    """
+    try:
+        nw = ipaddress.IPv4Network(net)
+        ip = ipaddress.IPv4Address(target)
+        if ip in nw:
+            return True
+        return False
+    except ipaddress.AddressValueError:
+        return False
+    except ipaddress.NetmaskValueError:
+        return False
+
+def should_bypass_proxies(url, no_proxy=None):
+    """ Yet another requests.should_bypass_proxies
+    Test if proxies should not be used for a particular url.
+    """
+
+    parsed = urlparse(url)
+
+    # special cases
+    if parsed.hostname in [None, '']:
+        return True
+
+    # special cases
+    if no_proxy in [None , '']:
+        return False
+    if no_proxy == '*':
+        return True
+
+    no_proxy = no_proxy.lower().replace(' ','');
+    entries = (
+        host for host in no_proxy.split(',') if host
+    )
+
+    if is_ipv4(parsed.hostname):
+        for item in entries:
+           if in_ipv4net(parsed.hostname, item):
+               return True
+    return proxy_bypass_environment(parsed.hostname, {'no': no_proxy} )
diff --git a/turbinia/api/models/request_status.py b/turbinia/api/models/request_status.py
index f2f8216f6..a9ba83570 100644
--- a/turbinia/api/models/request_status.py
+++ b/turbinia/api/models/request_status.py
@@ -41,7 +41,7 @@ class RequestStatus(BaseModel):
 
   def get_request_data(
       self, request_id: str, tasks: Optional[List[Dict]] = None,
-      summary: bool = False):
+      summary: bool = False) -> bool:
     """Gets task information for a specific Turbinia request.
 
     Args:
@@ -97,7 +97,7 @@ def get_request_data(
             turbinia_config.DATETIME_FORMAT)
 
     if self.last_task_update_time:
-      if isinstance(self.last_task_update_time, datetime.datetime):
+      if isinstance(self.last_task_update_time, float):
         self.last_task_update_time = datetime.datetime.fromtimestamp(
             self.last_task_update_time).strftime(
                 turbinia_config.DATETIME_FORMAT)
@@ -118,7 +118,7 @@ class RequestsSummary(BaseModel):
   """Represents a summary view of multiple Turbinia requests."""
   requests_status: List[RequestStatus] = []
 
-  def get_requests_summary(self):
+  def get_requests_summary(self) -> bool:
     """Generates a status summary for each Turbinia request."""
     _state_manager = state_manager.get_state_manager()
     instance_id = turbinia_config.INSTANCE_ID
diff --git a/turbinia/api/openapi.yaml b/turbinia/api/openapi.yaml
index a233e4415..83d355399 100644
--- a/turbinia/api/openapi.yaml
+++ b/turbinia/api/openapi.yaml
@@ -11,13 +11,16 @@ components:
           scopes: {}
   schemas:
     BaseRequestOptions:
-      description: "Base Request Options class to be extended by other option types. "
+      description: 'Base Request Options class to be extended by other option types. '
       properties:
         filter_patterns:
           items:
             type: string
           title: Filter Patterns
           type: array
+        group_id:
+          title: Group Id
+          type: string
         jobs_allowlist:
           items:
             type: string
@@ -28,12 +31,21 @@ components:
             type: string
           title: Jobs Denylist
           type: array
+        reason:
+          title: Reason
+          type: string
         recipe_data:
           title: Recipe Data
           type: string
         recipe_name:
           title: Recipe Name
           type: string
+        request_id:
+          title: Request Id
+          type: string
+        requester:
+          title: Requester
+          type: string
         sketch_id:
           title: Sketch Id
           type: integer
@@ -46,13 +58,13 @@ components:
       properties:
         detail:
           items:
-            $ref: "#/components/schemas/ValidationError"
+            $ref: '#/components/schemas/ValidationError'
           title: Detail
           type: array
       title: HTTPValidationError
       type: object
     Request:
-      description: "Base request object. "
+      description: 'Base request object. '
       properties:
         description:
           default: Turbinia request object
@@ -61,89 +73,20 @@ components:
         evidence:
           title: Evidence
           type: object
-        group_id:
-          title: Group Id
-          type: string
-        reason:
-          title: Reason
-          type: string
-        request_id:
-          title: Request Id
-          type: string
         request_options:
-          $ref: "#/components/schemas/BaseRequestOptions"
-        requester:
-          title: Requester
-          type: string
+          $ref: '#/components/schemas/BaseRequestOptions'
       required:
-        - evidence
-        - request_options
+      - evidence
+      - request_options
       title: Request
       type: object
-    RequestStatus:
-      description: Represents a Turbinia request status object.
-      properties:
-        failed_tasks:
-          default: 0
-          title: Failed Tasks
-          type: integer
-        last_task_update_time:
-          title: Last Task Update Time
-          type: string
-        queued_tasks:
-          default: 0
-          title: Queued Tasks
-          type: integer
-        reason:
-          title: Reason
-          type: string
-        request_id:
-          title: Request Id
-          type: string
-        requester:
-          title: Requester
-          type: string
-        running_tasks:
-          default: 0
-          title: Running Tasks
-          type: integer
-        status:
-          title: Status
-          type: string
-        successful_tasks:
-          default: 0
-          title: Successful Tasks
-          type: integer
-        task_count:
-          default: 0
-          title: Task Count
-          type: integer
-        tasks:
-          default: []
-          items:
-            type: object
-          title: Tasks
-          type: array
-      title: RequestStatus
-      type: object
-    RequestsSummary:
-      description: Represents a summary view of multiple Turbinia requests.
-      properties:
-        requests_status:
-          default: []
-          items:
-            $ref: "#/components/schemas/RequestStatus"
-          title: Requests Status
-          type: array
-      title: RequestsSummary
-      type: object
     ValidationError:
       properties:
         loc:
           items:
             anyOf:
-              - type: string
-              - type: integer
+            - type: string
+            - type: integer
           title: Location
           type: array
         msg:
@@ -153,9 +96,9 @@ components:
           title: Error Type
           type: string
       required:
-        - loc
-        - msg
-        - type
+      - loc
+      - msg
+      - type
       title: ValidationError
       type: object
 info:
@@ -172,57 +115,107 @@ paths:
       description: Retrieve turbinia config.
       operationId: read_config
       responses:
-        "200":
+        '200':
           content:
             application/json:
               schema: {}
           description: Successful Response
       summary: Read Config
       tags:
-        - Turbinia Configuration
+      - Turbinia Configuration
+  /api/config/evidence:
+    get:
+      description: Returns supported Evidence object types and required parameters.
+      operationId: get_evidence_types
+      responses:
+        '200':
+          content:
+            application/json:
+              schema: {}
+          description: Successful Response
+      summary: Get Evidence Types
+      tags:
+      - Turbinia Configuration
+  /api/config/evidence/{evidence_name}:
+    get:
+      description: Returns supported Evidence object types and required parameters.
+      operationId: get_evidence_attributes_by_name
+      parameters:
+      - in: path
+        name: evidence_name
+        required: true
+        schema:
+          title: Evidence Name
+      responses:
+        '200':
+          content:
+            application/json:
+              schema: {}
+          description: Successful Response
+        '422':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+          description: Validation Error
+      summary: Get Evidence Attributes By Name
+      tags:
+      - Turbinia Configuration
+  /api/config/request_options:
+    get:
+      description: Returns a list BaseRequestOptions attributes.
+      operationId: get_request_options
+      responses:
+        '200':
+          content:
+            application/json:
+              schema: {}
+          description: Successful Response
+      summary: Get Request Options
+      tags:
+      - Turbinia Configuration
   /api/jobs/:
     get:
       description: Return enabled jobs from the current Turbinia config.
       operationId: read_jobs
       responses:
-        "200":
+        '200':
           content:
             application/json:
               schema: {}
           description: Successful Response
       summary: Read Jobs
       tags:
-        - Turbinia Jobs
+      - Turbinia Jobs
   /api/logs/{query}:
     get:
       description: Retrieve log data.
       operationId: get_logs
       parameters:
-        - in: path
-          name: query
-          required: true
-          schema:
-            title: Query
-            type: string
+      - in: path
+        name: query
+        required: true
+        schema:
+          title: Query
+          type: string
       responses:
-        "200":
+        '200':
           content:
             application/json:
               schema: {}
           description: Successful Response
-        "422":
+        '422':
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/HTTPValidationError"
+                $ref: '#/components/schemas/HTTPValidationError'
           description: Validation Error
       summary: Get Logs
       tags:
-        - Logs
+      - Turbinia Logs
   /api/request/:
     post:
-      description:
-        "Create a new Turbinia request.\n\nArgs:\n  request (turbinia.api.schema.request):\
+      description: "Create a new Turbinia request.\n\nArgs:\n  request (turbinia.api.schema.request):\
         \ JSON object from the HTTP POST data\n      matching the schema defined for\
         \ a Turbinia Request. The schema is used\n      by pydantic for field validation.\n\
         \nRaises:\n  ValidationError: if the Request object contains invalid data.\n\
@@ -232,164 +225,147 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/components/schemas/Request"
+              $ref: '#/components/schemas/Request'
         required: true
       responses:
-        "200":
+        '200':
           content:
             application/json:
               schema: {}
           description: Successful Response
-        "422":
+        '422':
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/HTTPValidationError"
+                $ref: '#/components/schemas/HTTPValidationError'
           description: Validation Error
       summary: Create Request
       tags:
-        - Turbinia Requests
+      - Turbinia Requests
   /api/request/summary:
     get:
-      description:
-        "Retrieves a summary of all Turbinia requests.\n\nThe response\
+      description: "Retrieves a summary of all Turbinia requests.\n\nThe response\
         \ is validated against the RequestSummary model.\n\nRaises:\n  HTTPException:\
         \ if another exception is caught."
       operationId: get_requests_summary
       responses:
-        "200":
+        '200':
           content:
             application/json:
-              schema:
-                $ref: "#/components/schemas/RequestsSummary"
+              schema: {}
           description: Successful Response
       summary: Get Requests Summary
       tags:
-        - Turbinia Requests
+      - Turbinia Requests
   /api/request/{request_id}:
     get:
-      description:
-        "Retrieves status for a Turbinia Request.\n\nArgs:\n  request_id\
+      description: "Retrieves status for a Turbinia Request.\n\nArgs:\n  request_id\
         \ (str): A Turbinia request identifier.\n\nRaises:\n  HTTPException: if another\
         \ exception is caught."
       operationId: get_request_status
       parameters:
-        - in: path
-          name: request_id
-          required: true
-          schema:
-            title: Request Id
-            type: string
+      - in: path
+        name: request_id
+        required: true
+        schema:
+          title: Request Id
+          type: string
       responses:
-        "200":
+        '200':
           content:
             application/json:
-              schema:
-                $ref: "#/components/schemas/RequestStatus"
+              schema: {}
           description: Successful Response
-        "422":
+        '422':
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/HTTPValidationError"
+                $ref: '#/components/schemas/HTTPValidationError'
           description: Validation Error
       summary: Get Request Status
       tags:
-        - Turbinia Requests
+      - Turbinia Requests
   /api/result/request/{request_id}:
     get:
       description: Retrieve request output.
       operationId: get_request_output
       parameters:
-        - in: path
-          name: request_id
-          required: true
-          schema:
-            title: Request Id
-            type: string
+      - in: path
+        name: request_id
+        required: true
+        schema:
+          title: Request Id
+          type: string
       responses:
-        "200":
+        '200':
           content:
-            application/x-zip-compressed:
+            application/octet-stream:
               schema:
-                type: string
                 format: binary
+                type: string
           description: Successful Response
-        "422":
+        '422':
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/HTTPValidationError"
+                $ref: '#/components/schemas/HTTPValidationError'
           description: Validation Error
       summary: Get Request Output
       tags:
-        - Turbinia Request Results
+      - Turbinia Request Results
   /api/result/task/{task_id}:
     get:
       description: Retrieves a task's output files.
       operationId: get_task_output
       parameters:
-        - in: path
-          name: task_id
-          required: true
-          schema:
-            title: Task Id
-            type: string
+      - in: path
+        name: task_id
+        required: true
+        schema:
+          title: Task Id
+          type: string
       responses:
-        "200":
+        '200':
           content:
-            application/x-zip-compressed:
+            application/octet-stream:
               schema:
-                type: string
                 format: binary
+                type: string
           description: Successful Response
-        "422":
+        '422':
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/HTTPValidationError"
+                $ref: '#/components/schemas/HTTPValidationError'
           description: Validation Error
       summary: Get Task Output
       tags:
-        - Turbinia Request Results
+      - Turbinia Request Results
   /api/task/{task_id}:
     get:
       description: Retrieve task information.
       operationId: get_task_status
       parameters:
-        - in: path
-          name: task_id
-          required: true
-          schema:
-            title: Task Id
-            type: string
+      - in: path
+        name: task_id
+        required: true
+        schema:
+          title: Task Id
+          type: string
       responses:
-        "200":
+        '200':
           content:
             application/json:
               schema: {}
           description: Successful Response
-        "422":
+        '422':
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/HTTPValidationError"
+                $ref: '#/components/schemas/HTTPValidationError'
           description: Validation Error
       summary: Get Task Status
       tags:
-        - Turbinia Tasks
-  /docs/openapi.yaml:
-    get:
-      description: Serve the OpenAPI specification in YAML format.
-      operationId: read_openapi_yaml
-      responses:
-        "200":
-          content:
-            application/json:
-              schema: {}
-          description: Successful Response
-      summary: Read Openapi Yaml
-      tags:
-        - OpenAPI Specification
+      - Turbinia Tasks
 security:
   - oAuth2: []
diff --git a/turbinia/api/routes/config.py b/turbinia/api/routes/config.py
index 4c84271d0..daa199520 100644
--- a/turbinia/api/routes/config.py
+++ b/turbinia/api/routes/config.py
@@ -19,16 +19,19 @@
 
 from fastapi import HTTPException, APIRouter
 from fastapi.responses import JSONResponse
+from fastapi.requests import Request
 
 from turbinia import config as turbinia_config
+from turbinia import evidence
+from turbinia.api.schemas import request_options
 
-log = logging.getLogger('turbinia:api_server:config')
+log = logging.getLogger('turbinia')
 
 router = APIRouter(prefix='/config', tags=['Turbinia Configuration'])
 
 
-@router.get("/")
-async def read_config():
+@router.get('/')
+async def read_config(request: Request):
   """Retrieve turbinia config."""
   try:
     current_config = turbinia_config.toDict()
@@ -38,3 +41,32 @@ async def read_config():
     log.error('Error reading configuration: {0!s}'.format(exception))
     raise HTTPException(
         status_code=500, detail='error reading configuration') from exception
+
+
+@router.get('/evidence')
+async def get_evidence_types(request: Request):
+  """Returns supported Evidence object types and required parameters."""
+  attribute_mapping = evidence.map_evidence_attributes()
+  return JSONResponse(content=attribute_mapping, status_code=200)
+
+
+@router.get('/evidence/{evidence_name}')
+async def get_evidence_attributes_by_name(request: Request, evidence_name):
+  """Returns supported Evidence object types and required parameters."""
+  attribute_mapping = evidence.map_evidence_attributes()
+  attribute_mapping = {evidence_name: attribute_mapping.get(evidence_name)}
+  if not attribute_mapping:
+    raise HTTPException(
+        status_code=404,
+        detail='Evidence type ({0:s}) not found.'.format(evidence_name))
+  return JSONResponse(content=attribute_mapping, status_code=200)
+
+
+@router.get('/request_options')
+async def get_request_options(request: Request):
+  """Returns a list BaseRequestOptions attributes."""
+  attributes = request_options.BaseRequestOptions.__annotations__
+  attributes_dict = {}
+  for attribute_name, attribute_type in attributes.items():
+    attributes_dict[attribute_name] = {'type': str(attribute_type)}
+  return JSONResponse(content=attributes_dict, status_code=200)
diff --git a/turbinia/api/routes/jobs.py b/turbinia/api/routes/jobs.py
index 720f8f46d..b3476f79a 100644
--- a/turbinia/api/routes/jobs.py
+++ b/turbinia/api/routes/jobs.py
@@ -19,23 +19,24 @@
 
 from fastapi import HTTPException, APIRouter
 from fastapi.responses import JSONResponse
+from fastapi.requests import Request
 
 from turbinia import config as turbinia_config
 from turbinia.jobs import manager as jobs_manager
 
-log = logging.getLogger('turbinia:api_server:jobs')
+log = logging.getLogger('turbinia')
 
 router = APIRouter(prefix='/jobs', tags=['Turbinia Jobs'])
 
 
-@router.get("/")
-async def read_jobs():
+@router.get('/')
+async def read_jobs(request: Request):
   """Return enabled jobs from the current Turbinia config."""
   try:
-    _jobs_manager = jobs_manager.JobsManager()
-    registered_jobs = set(_jobs_manager.GetJobNames())
-    disabled_jobs = set(turbinia_config.CONFIG.DISABLED_JOBS)
-    enabled_jobs = registered_jobs.difference(disabled_jobs)
+    _jobs_manager: jobs_manager.JobsManager = jobs_manager.JobsManager()
+    registered_jobs: set = set(_jobs_manager.GetJobNames())
+    disabled_jobs: set = set(turbinia_config.CONFIG.DISABLED_JOBS)
+    enabled_jobs: set = registered_jobs.difference(disabled_jobs)
 
     if not registered_jobs:
       raise HTTPException(status_code=404, detail='No registered jobs found.')
diff --git a/turbinia/api/routes/logs.py b/turbinia/api/routes/logs.py
index d58920469..4a1a8f68b 100644
--- a/turbinia/api/routes/logs.py
+++ b/turbinia/api/routes/logs.py
@@ -18,14 +18,15 @@
 
 from fastapi import APIRouter
 from fastapi.responses import JSONResponse
+from fastapi.requests import Request
 
-log = logging.getLogger('turbinia:api_server:logs')
+log = logging.getLogger('turbinia')
 
-router = APIRouter(prefix='/logs', tags=['Logs'])
+router = APIRouter(prefix='/logs', tags=['Turbinia Logs'])
 
 
-@router.get("/{query}")
-async def get_logs(query: str):
+@router.get('/{query}')
+async def get_logs(request: Request, query: str):
   """Retrieve log data."""
   return JSONResponse(
       content={'detail': 'Not implemented yet.'}, status_code=200)
diff --git a/turbinia/api/routes/request.py b/turbinia/api/routes/request.py
index 10b7a0516..978085386 100644
--- a/turbinia/api/routes/request.py
+++ b/turbinia/api/routes/request.py
@@ -16,23 +16,25 @@
 
 import logging
 import uuid
+import json
 
 from fastapi import HTTPException, APIRouter
 from fastapi.responses import JSONResponse
-
+from fastapi.requests import Request
 from pydantic import ValidationError
 from turbinia import TurbiniaException, client as turbinia_client
 from turbinia import evidence
 from turbinia.lib import recipe_helpers
-from turbinia.api.schemas import request
+from turbinia.api.schemas import request as turbinia_request
 from turbinia.api.models import request_status
 
-log = logging.getLogger('turbinia:api_server:models:request')
+log = logging.getLogger('turbinia')
+
 router = APIRouter(prefix="/request", tags=["Turbinia Requests"])
 
 
-@router.get("/summary", response_model=request_status.RequestsSummary)
-async def get_requests_summary():
+@router.get('/summary')
+async def get_requests_summary(request: Request):
   """Retrieves a summary of all Turbinia requests.
 
   The response is validated against the RequestSummary model.
@@ -45,8 +47,12 @@ async def get_requests_summary():
     if not requests_summary.get_requests_summary():
       return JSONResponse(
           content={'detail': 'Request summary is empty'}, status_code=200)
-    return requests_summary
-  except (ValidationError, ValueError, TypeError) as exception:
+    response_json = requests_summary.json(sort_keys=True)
+    return JSONResponse(
+        status_code=200, content=json.loads(response_json),
+        media_type='application/json')
+  except (json.JSONDecodeError, TypeError, ValueError,
+          ValidationError) as exception:
     log.error(
         'Error retrieving requests summary: {0!s}'.format(exception),
         exc_info=True)
@@ -55,8 +61,8 @@ async def get_requests_summary():
         detail='Error retrieving requests summary') from exception
 
 
-@router.get("/{request_id}", response_model=request_status.RequestStatus)
-async def get_request_status(request_id: str):
+@router.get('/{request_id}')
+async def get_request_status(request: Request, request_id: str):
   """Retrieves status for a Turbinia Request.
 
   Args:
@@ -66,22 +72,25 @@ async def get_request_status(request_id: str):
     HTTPException: if another exception is caught.
   """
   request_out = request_status.RequestStatus(request_id=request_id)
-  response_ok = request_out.get_request_data(request_id)
   try:
-    if not response_ok:
+    if not request_out.get_request_data(request_id):
       raise HTTPException(
           status_code=404,
           detail='Request ID not found or the request had no associated tasks.')
-    return request_out
-  except (ValidationError, ValueError, TypeError) as exception:
+    response_json = request_out.json(sort_keys=True)
+    return JSONResponse(
+        status_code=200, content=json.loads(response_json),
+        media_type='application/json')
+  except (json.JSONDecodeError, TypeError, ValueError,
+          ValidationError) as exception:
     log.error('Error retrieving request information: {0!s}'.format(exception))
     raise HTTPException(
         status_code=500,
         detail='Error retrieving request information') from exception
 
 
-@router.post("/")
-async def create_request(req: request.Request):
+@router.post('/')
+async def create_request(request: Request, req: turbinia_request.Request):
   """Create a new Turbinia request.
 
   Args:
@@ -95,14 +104,12 @@ async def create_request(req: request.Request):
   """
   client = turbinia_client.get_turbinia_client()
   evidence_list = []
-  request_id = req.request_id
-  group_id = req.group_id
-  requester = req.requester
-  reason = req.reason
   recipe = None
-  recipe_name = req.request_options.recipe_name
-  recipe_data = req.request_options.recipe_data
   options = req.request_options
+  recipe_name = options.recipe_name
+  recipe_data = options.recipe_data
+  request_id = options.request_id
+  group_id = options.group_id
 
   if not request_id:
     request_id = uuid.uuid4().hex
@@ -126,7 +133,7 @@ async def create_request(req: request.Request):
       # Use a client-provided recipe name or path for an existing recipe.
       recipe = client.create_recipe(
           group_id=group_id, recipe_name=recipe_name,
-          sketch_id=req.request_options.sketch_id)
+          sketch_id=options.sketch_id)
     elif (options.jobs_allowlist or options.jobs_denylist or
           options.filter_patterns or options.yara_rules):
       recipe = client.create_recipe(
@@ -149,9 +156,9 @@ async def create_request(req: request.Request):
       "request_options": {
         "sketch_id": 1234,
         "recipe_name": "triage-linux"
+        "reason": "test",
+        "requester": "tester"
       },
-      "reason": "test",
-      "requester": "tester"
     }
     ----
     {
@@ -164,9 +171,9 @@ async def create_request(req: request.Request):
       "request_options": {
       "sketch_id": 1234,
       "recipe_data": "Z2xvYmFsczoKICBqb2JzX2FsbG93bGlzdDoKICAgIC0gQ3JvbkV4dHJhY3Rpb25Kb2IKICAgIC0gQ3JvbkFuYWx5c2lzSm9iCiAgICAtIFBsYXNvSm9iCiAgICAtIFBzb3J0Sm9iCiAgICAtIEZpbGVTeXN0ZW1UaW1lbGluZUpvYgoKcGxhc29fYmFzZToKICB0YXNrOiAnUGxhc29UYXNrJwoKICBhcnRpZmFjdF9maWx0ZXJzOiBbCiAgICAnQWxsVXNlcnNTaGVsbEhpc3RvcnknLAogICAgJ0FwYWNoZUFjY2Vzc0xvZ3MnLAogICAgJ0Jyb3dzZXJDYWNoZScsCiAgICAnQnJvd3Nlckhpc3RvcnknLAogICAgJ0Nocm9tZVN0b3JhZ2UnLAogICAgJ0xpbnV4QXVkaXRMb2dzJywKICAgICdMaW51eEF1dGhMb2dzJywKICAgICdMaW51eENyb25Mb2dzJywKICAgICdMaW51eEtlcm5lbExvZ0ZpbGVzJywKICAgICdMaW51eExhc3Rsb2dGaWxlJywKICAgICdMaW51eE1lc3NhZ2VzTG9nRmlsZXMnLAogICAgJ0xpbnV4U2NoZWR1bGVGaWxlcycsCiAgICAnTGludXhTeXNMb2dGaWxlcycsCiAgICAnTGludXhVdG1wRmlsZXMnLAogICAgJ0xpbnV4V3RtcCcsCiAgXQ=="
-      },
       "reason": "test",
       "requester": "tester"
+      },
     }
     """
     evidence_object = evidence.evidence_decode(req.evidence, strict=True)
@@ -179,8 +186,8 @@ async def create_request(req: request.Request):
     # If at this point the recipe is None, the TurbiniaClient will create
     # a generic recipe based on recipe_helpers.DEFAULT_RECIPE.
     request_out = client.create_request(
-        evidence_=evidence_list, request_id=request_id, reason=reason,
-        recipe=recipe, group_id=group_id, requester=requester)
+        evidence_=evidence_list, request_id=request_id, reason=options.reason,
+        recipe=recipe, group_id=group_id, requester=options.requester)
     # Send the Turbinia request to the appropriate queue.
     client.send_request(request_out)
   except TurbiniaException as exception:
diff --git a/turbinia/api/routes/result.py b/turbinia/api/routes/result.py
index a94f4bf87..d6ac001e8 100644
--- a/turbinia/api/routes/result.py
+++ b/turbinia/api/routes/result.py
@@ -17,21 +17,35 @@
 import logging
 
 from fastapi import HTTPException, APIRouter
-from fastapi.responses import Response
+from fastapi.responses import StreamingResponse
+from fastapi.requests import Request
+
 from turbinia import config as turbinia_config
 from turbinia import state_manager
 from turbinia.api import utils as api_utils
 
-log = logging.getLogger('turbinia:api_server:result')
+log = logging.getLogger('turbinia')
 
 router = APIRouter(prefix='/result', tags=['Turbinia Request Results'])
 
-_ATTACHMENT_RESPONSE = {'200': {'content': {'application/x-zip-compressed'}}}
+_ATTACHMENT_RESPONSE = {
+    '200': {
+        'content': {
+            'application/octet-stream': {
+                'schema': {
+                    'type': 'string',
+                    'format': 'binary'
+                }
+            }
+        }
+    }
+}
 
 
 @router.get(
-    "/task/{task_id}", response_class=Response, responses=_ATTACHMENT_RESPONSE)
-async def get_task_output(task_id: str):
+    '/task/{task_id}', response_class=StreamingResponse,
+    responses=_ATTACHMENT_RESPONSE)
+async def get_task_output(request: Request, task_id: str):
   """Retrieves a task's output files."""
   # Get the request_id for the task. This is needed to find the right path.
   data = None
@@ -41,31 +55,37 @@ async def get_task_output(task_id: str):
   if not tasks:
     raise HTTPException(
         status_code=404, detail='Task {0:s} not found.'.format(task_id))
+
   request_id = tasks[0].get('request_id')
-  if request_id:
-    data = api_utils.create_zip(request_id, task_id)
+  output_path = api_utils.get_task_output_path(request_id, task_id)
+
+  if request_id and output_path:
+    data: bytes = api_utils.create_tarball(output_path)
 
   if not data:
     raise HTTPException(
         status_code=500, detail='Unable to retrieve task output files.')
-  return Response(
-      data, media_type='application/x-zip-compressed', headers={
-          "Content-Disposition": 'attachment;filename={}.zip'.format(task_id)
+  return StreamingResponse(
+      data, headers={
+          "Content-Disposition": 'attachment;filename={}.tgz'.format(task_id)
       })
 
 
 @router.get(
-    "/request/{request_id}", response_class=Response,
+    '/request/{request_id}', response_class=StreamingResponse,
     responses=_ATTACHMENT_RESPONSE)
-async def get_request_output(request_id: str):
+async def get_request_output(request: Request, request_id: str):
   """Retrieve request output."""
-  data = api_utils.create_zip(request_id, task_id=None)
+  data = None
+  request_output_path = api_utils.get_request_output_path(request_id)
+  if request_output_path:
+    data: bytes = api_utils.create_tarball(request_output_path)
 
   if not data:
     raise HTTPException(
         status_code=500, detail='Unable to retrieve task output files.')
-  return Response(
-      data, media_type='application/x-zip-compressed', headers={
+  return StreamingResponse(
+      data, headers={
           "Content-Disposition":
-              'attachment;filename={}.zip'.format(request_id)
+              'attachment;filename={}.tgz'.format(request_id)
       })
diff --git a/turbinia/api/routes/router.py b/turbinia/api/routes/router.py
index d613c4ee5..6c63a73d2 100644
--- a/turbinia/api/routes/router.py
+++ b/turbinia/api/routes/router.py
@@ -24,12 +24,12 @@
 from turbinia.api.routes import task
 
 # Prefix API endpoints with /api/
-router = APIRouter(prefix='/api')
+api_router = APIRouter(prefix='/api')
 
 # Register all API endpoints.
-router.include_router(config.router)
-router.include_router(jobs.router)
-router.include_router(logs.router)
-router.include_router(request.router)
-router.include_router(result.router)
-router.include_router(task.router)
+api_router.include_router(config.router)
+api_router.include_router(jobs.router)
+api_router.include_router(logs.router)
+api_router.include_router(request.router)
+api_router.include_router(result.router)
+api_router.include_router(task.router)
diff --git a/turbinia/api/routes/task.py b/turbinia/api/routes/task.py
index a2a979469..246e478e0 100644
--- a/turbinia/api/routes/task.py
+++ b/turbinia/api/routes/task.py
@@ -15,10 +15,16 @@
 """Turbinia API - Task router"""
 
 import logging
+import json
+
+from collections import OrderedDict
 
 from fastapi import HTTPException, APIRouter
-from pydantic import ValidationError
+from fastapi.responses import JSONResponse
+from fastapi.requests import Request
+from fastapi.encoders import jsonable_encoder
 
+from pydantic import ValidationError
 from turbinia import state_manager
 from turbinia import config as turbinia_config
 
@@ -27,8 +33,8 @@
 router = APIRouter(prefix='/task', tags=['Turbinia Tasks'])
 
 
-@router.get("/{task_id}")
-async def get_task_status(task_id: str):
+@router.get('/{task_id}')
+async def get_task_status(request: Request, task_id: str):
   """Retrieve task information."""
   try:
     _state_manager = state_manager.get_state_manager()
@@ -36,9 +42,14 @@ async def get_task_status(task_id: str):
         instance=turbinia_config.INSTANCE_ID, task_id=task_id)
     if tasks:
       task = tasks[0]
-      return task
+      task_json = jsonable_encoder(task)
+      task_json_sorted = OrderedDict(sorted(task_json.items()))
+      return JSONResponse(
+          status_code=200, content=task_json_sorted,
+          media_type='application/json')
     raise HTTPException(status_code=404, detail='Task ID not found.')
-  except ValidationError as exception:
+  except (json.JSONDecodeError, TypeError, ValueError,
+          ValidationError) as exception:
     log.error('Error retrieving task information: {0!s}'.format(exception))
     raise HTTPException(
         status_code=500,
diff --git a/turbinia/api/routes/ui.py b/turbinia/api/routes/ui.py
new file mode 100644
index 000000000..f95038a60
--- /dev/null
+++ b/turbinia/api/routes/ui.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia Web UI routes."""
+
+import os
+import pathlib
+from fastapi import APIRouter, HTTPException
+from fastapi.requests import Request
+from fastapi.responses import RedirectResponse, FileResponse
+from turbinia import config
+
+ui_router = APIRouter(tags=['Turbinia Web UI'])
+_config = config.LoadConfig()
+
+
+@ui_router.get('/', include_in_schema=False)
+async def root():
+  """Default route."""
+  return RedirectResponse('/web')
+
+
+@ui_router.get('/web', name='web', include_in_schema=False)
+async def web(request: Request):
+  """Serves the Web UI main page."""
+  static_content_path = pathlib.Path(
+      _config.WEBUI_PATH).joinpath('dist/index.html')
+  if os.path.exists(static_content_path):
+    response = FileResponse(
+        path=static_content_path, headers={'Cache-Control': 'no-cache'})
+    return response
+
+  raise HTTPException(status_code=404, detail='Not found')
+
+
+@ui_router.get('/css/{catchall:path}', name='css', include_in_schema=False)
+async def serve_css(request: Request):
+  """Serves CSS content."""
+  static_content_path = pathlib.Path(_config.WEBUI_PATH).joinpath('dist/css')
+  path = request.path_params['catchall']
+  file = static_content_path.joinpath(path)
+  if os.path.exists(file):
+    return FileResponse(file)
+
+  raise HTTPException(status_code=404, detail='Not found')
+
+
+@ui_router.get('/js/{catchall:path}', name='js', include_in_schema=False)
+async def serve_js(request: Request):
+  """Serves JavaScript content."""
+  static_content_path = pathlib.Path(_config.WEBUI_PATH).joinpath('dist/js')
+  path = request.path_params['catchall']
+  file = static_content_path.joinpath(path)
+  if os.path.exists(file):
+    return FileResponse(file)
+
+  raise HTTPException(status_code=404, detail='Not found')
diff --git a/turbinia/api/schemas/request.py b/turbinia/api/schemas/request.py
index 851fbbc28..5eaf5bed0 100644
--- a/turbinia/api/schemas/request.py
+++ b/turbinia/api/schemas/request.py
@@ -24,7 +24,3 @@ class Request(BaseModel):
   description: Optional[str] = 'Turbinia request object'
   evidence: Dict[Any, Any]
   request_options: request_options.BaseRequestOptions
-  group_id: Optional[str] = None
-  reason: Optional[str] = None
-  request_id: Optional[str] = None
-  requester: Optional[str] = None
diff --git a/turbinia/api/schemas/request_options.py b/turbinia/api/schemas/request_options.py
index b8ac67410..061d53abe 100644
--- a/turbinia/api/schemas/request_options.py
+++ b/turbinia/api/schemas/request_options.py
@@ -27,3 +27,7 @@ class BaseRequestOptions(BaseModel):
   filter_patterns: Optional[List[str]] = None
   yara_rules: Optional[str] = None
   sketch_id: Optional[int] = None
+  group_id: Optional[str] = None
+  reason: Optional[str] = None
+  request_id: Optional[str] = None
+  requester: Optional[str] = None
diff --git a/turbinia/api/utils.py b/turbinia/api/utils.py
index 58d913763..70e68f467 100644
--- a/turbinia/api/utils.py
+++ b/turbinia/api/utils.py
@@ -15,35 +15,30 @@
 """Turbinia API Server helper methods."""
 
 import logging
-import mmap
-import shutil
-import tempfile
 import os
-
+import tarfile
+import io
 from fastapi import HTTPException
 from turbinia import config as turbinia_config
 
-log = logging.getLogger('turbinia:api_server:utils')
-
-
-def create_zip(request_id: str, task_id: str):
-  """Compress a Turbinia request or task's output directories into a zip file.
-
-  Args:
-    request_id (str): Turbinia request identifier.
-    task_id (str): Turbinia task identifier.
+log = logging.getLogger('turbinia')
 
-  Returns:
-    A zip compressed bytes stream
 
-  Raises:
-    HTTPException if the request/task output paths could not be found
-        on the file system.
-  """
+def get_request_output_path(request_id: str) -> str:
+  """Returns the output path for a request_id."""
   log_path = turbinia_config.toDict().get('OUTPUT_DIR')
-
   request_output_path = os.path.join(log_path, request_id)
+  if not os.path.exists(request_output_path):
+    message = 'Output path {0:s} for request {1:s} could not be found.'.format(
+        request_output_path, request_id)
+    log.error(message)
+    raise HTTPException(status_code=404, detail=message)
+  return request_output_path
+
 
+def get_task_output_path(request_id: str, task_id: str) -> str:
+  """Returns the output path for a task_id."""
+  request_output_path = get_request_output_path(request_id)
   if task_id:
     try:
       request_dirs = os.listdir(request_output_path)
@@ -57,30 +52,97 @@ def create_zip(request_id: str, task_id: str):
       if task_id in request_dir:
         request_output_path = os.path.join(request_output_path, request_dir)
         break
+    return request_output_path
 
-  if not os.path.exists(request_output_path):
-    message = 'Output path {0:s} for request {1:s} could not be found.'.format(
-        request_output_path, request_id)
-    log.error(message)
-    raise HTTPException(status_code=404, detail=message)
 
-  # Create a temporary directory to store the zip file
-  # containing the request result files.
-  # Directory and its contents deleted upon context exit.
-  temp_directory = tempfile.TemporaryDirectory()
-  with temp_directory as directory_name:
-    if task_id:
-      zip_path = os.path.join(directory_name, task_id)
-    else:
-      zip_path = os.path.join(directory_name, request_id)
-
-    # Create the zip file
-    zip_filename = shutil.make_archive(zip_path, 'zip', request_output_path)
-    data = None
-
-    # Read the zip using a memory-mapped file and return it
-    with open(zip_filename, 'rb') as zip_obj:
-      mm = mmap.mmap(zip_obj.fileno(), 0, access=mmap.ACCESS_READ)
-      data = mm.read()
+class ByteStream:
+  """A writeable in-memory stream used to create tgz files."""
+
+  def __init__(self):
+    """Initialize the object."""
+    self.buffer = io.BytesIO()
+    self.block_size = 4194304  # 4 MiB
+    self.offset = 0
+
+  def __enter__(self):
+    return self
+
+  def __exit__(self, exc_type, exc_value, traceback):
+    del exc_type, exc_value, traceback  # Unused.
+    self.close()
+
+  def write(self, data):
+    """Writes data to the buffer and adjusts offset."""
+    self.buffer.write(data)
+    self.offset += len(data)
+
+  def tell(self):
+    """Returns the current byte offset."""
+    return self.offset
+
+  def close(self):
+    """Closes the buffer."""
+    self.buffer.close()
+
+  def pop(self):
+    """Returns the bytes from the buffer.
 
+    Seeks buffer to position 0 and truncates the buffer for re-use.
+    """
+    data = self.buffer.getvalue()
+    self.buffer.seek(0)
+    self.buffer.truncate()
     return data
+
+
+async def create_tarball(output_path: str) -> bytes:
+  """Creates an in-memory TGZ file from output_path contents.
+
+  Partially inspired by the StreamingTarGenerator class from Google's GRR.
+
+  Reference:
+      https://github.com/google/grr/blob/master/grr/core/grr_response_core/lib/utils.py
+
+  Args:
+    output_path (str): The output path of a request or task to archive.
+
+  Yields:
+    bytes: tgz file chunk.
+  """
+  file_paths = []
+  for root, _, filenames in os.walk(output_path):
+    for filename in filenames:
+      file_path = os.path.join(root, filename)
+      log.info(f'Adding {file_path} to tarball.')
+      file_paths.append(file_path)
+
+  with ByteStream() as stream:
+    with tarfile.TarFile.open(fileobj=stream, mode='w:gz',
+                              compresslevel=1) as tar:
+      for file_path in file_paths:
+        tar_info = tar.gettarinfo(name=file_path)
+        tar.addfile(tar_info)
+        # Yield the header for the tarinfo file.
+        yield stream.pop()
+
+        with open(file_path, 'rb') as in_fp:
+          # Read the input file in chunks of stream.block_size bytes.
+          while True:
+            data = in_fp.read(stream.block_size)
+            if len(data) > 0:
+              # Write the data to the buffer.
+              tar.fileobj.write(data)
+              # Yield a compressed file chunk so the client can receive it.
+              yield stream.pop()
+            # Write padding if necessary.
+            if len(data) < stream.block_size:
+              blocks, remainder = divmod(tar_info.size, tarfile.BLOCKSIZE)
+              if remainder > 0:
+                tar.fileobj.write(tarfile.NUL * (tarfile.BLOCKSIZE - remainder))
+                yield stream.pop()
+                blocks += 1
+              tar.offset += blocks * tarfile.BLOCKSIZE
+              break
+
+    # Yield end-of-archive marker.
+    yield stream.pop()
diff --git a/turbinia/client.py b/turbinia/client.py
index 2828df270..0cfac3215 100644
--- a/turbinia/client.py
+++ b/turbinia/client.py
@@ -37,7 +37,7 @@
 from turbinia.lib import recipe_helpers
 from turbinia.lib import text_formatter as fmt
 from turbinia.message import TurbiniaRequest
-from turbinia.workers import Priority
+from turbinia.workers import Priority, TurbiniaTask
 
 MAX_RETRIES = 10
 RETRY_SLEEP = 60
@@ -1077,17 +1077,47 @@ def __init__(self, *args, **kwargs):
     super(TurbiniaCeleryClient, self).__init__(*args, **kwargs)
     self.redis = RedisStateManager()
 
+  # pylint: disable=arguments-differ
   def close_tasks(
-      self, instance, project, region, request_id=None, task_id=None, user=None,
-      requester=None):
+      self, instance, request_id=None, group_id=None, task_id=None,
+      user=None) -> bool:
     """Close Turbinia Tasks based on Request ID.
 
-    Currently needs to be implemented for Redis/Celery:
-    https://github.com/google/turbinia/issues/999
+    This method uses Celery app.control.terminate to terminate and revoke a
+    task_id or a list of task_ids. If a task is revoked, the workers will
+    ignore the task and not execute it after all.
+
+    Args:
+      instance (string): The Turbinia instance name (by default the same as the
+          INSTANCE_ID in the config).
+      request_id (string): The request identifier we want tasks for.
+      task_id (string): The task identifier we want.
+      group_id (str): A group identifier we want tasks for.
+      user (string): The user of the request we want tasks for.
+
+    Returns: True if a terminate command was successfully broadcast to the
+        Celery workers.
     """
-    raise TurbiniaException(
-        '--close_tasks is not yet implemented for Redis: '
-        'https://github.com/google/turbinia/issues/999')
+    tasks = self.redis.get_task_data(
+        instance, task_id=task_id, request_id=request_id, group_id=group_id,
+        user=user)
+    task_ids = [task.get('id') for task in tasks if task.get('id')]
+    result = False
+
+    if task_ids:
+      for _task_id in task_ids:
+        self.task_manager.celery.app.control.terminate(_task_id)
+        log.info(f'Closed task {_task_id}.')
+        task_dict = self.redis.get_task_data(
+            instance=config.INSTANCE_ID, task_id=_task_id)[0]
+        task = TurbiniaTask().deserialize(task_dict)
+        task['success'] = False
+        task['status'] = 'Task forcefully closed.'
+        self.redis.update_task(task)
+      result = True
+    else:
+      log.info('No tasks found with the given filter(s). Not closing any tasks')
+    return bool(result)
 
   def send_request(self, request):
     """Sends a TurbiniaRequest message.
diff --git a/turbinia/config/__init__.py b/turbinia/config/__init__.py
index 9ab929512..3b220b99a 100644
--- a/turbinia/config/__init__.py
+++ b/turbinia/config/__init__.py
@@ -67,7 +67,8 @@
     'API_SERVER_ADDRESS',
     'API_SERVER_PORT',
     'API_ALLOWED_ORIGINS',
-    'API_AUTHENTICATION_ENABLED'
+    'API_AUTHENTICATION_ENABLED',
+    'WEBUI_PATH',
 ]
 
 # Optional config vars.  Some may be mandatory depending on the configuration
@@ -113,7 +114,14 @@
     'DFDEWEY_OS_PORT',
     'DFDEWEY_OS_URL',
     # General config
-    'TURBINIA_COMMAND'
+    'TURBINIA_COMMAND',
+    # API config
+    'OIDC_SCOPE',
+    'OIDC_KEYS',
+    'OIDC_ISSUER',
+    'OIDC_VALID_CLIENT_IDS',
+    'AUTHORIZED_EMAILS',
+    'WEBUI_CLIENT_SECRETS_FILE'
 ]
 
 # Environment variable to look for path data in
diff --git a/turbinia/config/turbinia_config_tmpl.py b/turbinia/config/turbinia_config_tmpl.py
index ae98e83b0..de017e780 100644
--- a/turbinia/config/turbinia_config_tmpl.py
+++ b/turbinia/config/turbinia_config_tmpl.py
@@ -113,12 +113,15 @@
 API_SERVER_PORT = 8000
 
 # Allowed CORS origins
-API_ALLOWED_ORIGINS = ['http://localhost:8000', 'http://localhost']
+API_ALLOWED_ORIGINS = ['http://localhost:8000']
 
 # Enable/Disable API authentication. This will determine whether the API server will
 # check for OAuth 2.0 bearer tokens in the 'Authorization' header.
 API_AUTHENTICATION_ENABLED = False
 
+# Path to Turbinia Web UI static files
+WEBUI_PATH = '/tmp/web'
+
 ################################################################################
 #                         External Dependency Configurations
 #
diff --git a/turbinia/evidence.py b/turbinia/evidence.py
index d2c11de30..58ba12cf6 100644
--- a/turbinia/evidence.py
+++ b/turbinia/evidence.py
@@ -63,7 +63,7 @@ def evidence_class_names(all_classes=False):
     ignored_classes = (
         'BinaryExtraction', 'BulkExtractorOutput', 'Evidence', 'EvidenceState',
         'EvidenceCollection', 'ExportedFileArtifact', 'FilteredTextFile',
-        'FinalReport', 'IntEnum', 'PlasoCsvFile', 'PlasoFile', 'PhotorecOutput',
+        'FinalReport', 'IntEnum', 'PlasoCsvFile', 'PhotorecOutput',
         'ReportText', 'TextFile', 'VolatilityReport', 'TurbiniaException')
     class_names = filter(
         lambda class_tuple: class_tuple[0] not in ignored_classes, class_names)
@@ -123,8 +123,8 @@ def evidence_decode(evidence_dict, strict=False):
     raise TurbiniaException(
         'Evidence_dict is not a dictionary, type is {0:s}'.format(
             str(type(evidence_dict))))
-
   type_ = evidence_dict.pop('type', None)
+  name_ = evidence_dict.pop('_name', None)
   if not type_:
     raise TurbiniaException(
         'No Type attribute for evidence object [{0:s}]'.format(
@@ -132,7 +132,7 @@ def evidence_decode(evidence_dict, strict=False):
   evidence = None
   try:
     evidence_class = getattr(sys.modules[__name__], type_)
-    evidence = evidence_class.from_dict(evidence_dict)
+    evidence = evidence_class(name=name_, type=type_, **evidence_dict)
     evidence_object = evidence_class(source_path='dummy_object')
     if strict and evidence_object:
       for attribute_key in evidence_dict.keys():
@@ -256,40 +256,40 @@ class Evidence:
   # docstrings for more info.
   POSSIBLE_STATES = []
 
-  def __init__(
-      self, name=None, description=None, size=None, source=None,
-      source_path=None, tags=None, request_id=None, copyable=False):
+  def __init__(self, *args, **kwargs):
     """Initialization for Evidence."""
-    self.copyable = copyable
-    self.config = {}
-    self.context_dependent = False
-    self.cloud_only = False
-    self.description = description
-    self.size = size
-    self.mount_path = None
-    self.credentials = []
-    self.source = source
-    self.source_path = source_path
-    self.tags = tags if tags else {}
-    self.request_id = request_id
-    self.has_child_evidence = False
-    self.parent_evidence = None
-    self.save_metadata = False
-    self.resource_tracked = False
-    self.resource_id = None
-
-    self.local_path = source_path
-
+    self.cloud_only = kwargs.get('cloud_only', False)
+    self.config = kwargs.get('config', {})
+    self.context_dependent = kwargs.get('context_dependent', False)
+    self.copyable = kwargs.get('copyable', False)
+    self.credentials = kwargs.get('credentials', [])
+    self.description = kwargs.get('description', None)
+    self.has_child_evidence = kwargs.get('has_child_evidence', False)
+    self.mount_path = kwargs.get('mount_path', None)
+    self._name = kwargs.get('name')
+    self.parent_evidence = kwargs.get('parent_evidence', None)
     # List of jobs that have processed this evidence
-    self.processed_by = []
+    self.processed_by = kwargs.get('processed_by', [])
+    self.request_id = kwargs.get('request_id', None)
+    self.resource_id = kwargs.get('resource_id', None)
+    self.resource_tracked = kwargs.get('resource_tracked', False)
+    self.save_metadata = kwargs.get('save_metadata', False)
+    self.saved_path = kwargs.get('saved_path', None)
+    self.saved_path_type = kwargs.get('saved_path_type', None)
+    self.size = kwargs.get('size', None)
+    self.source = kwargs.get('source', None)
+    self.source_path = kwargs.get('source_path', None)
+    self.tags = kwargs.get('tags', {})
     self.type = self.__class__.__name__
-    self._name = name
-    self.saved_path = None
-    self.saved_path_type = None
 
-    self.state = {}
-    for state in EvidenceState:
-      self.state[state] = False
+    self.local_path = self.source_path
+
+    if 'state' in kwargs:
+      self.state = kwargs.get('state')
+    else:
+      self.state = {}
+      for state in EvidenceState:
+        self.state[state] = False
 
     if self.copyable and not self.local_path:
       raise TurbiniaException(
@@ -646,9 +646,10 @@ class ChromiumProfile(Evidence):
 
   def __init__(self, browser_type=None, output_format=None, *args, **kwargs):
     """Initialization for chromium profile evidence object."""
-    super(ChromiumProfile, self).__init__(copyable=True, *args, **kwargs)
+    super(ChromiumProfile, self).__init__(*args, **kwargs)
     self.browser_type = browser_type
     self.output_format = output_format
+    self.copyable = True
 
 
 class RawDisk(Evidence):
@@ -936,9 +937,10 @@ class PlasoFile(Evidence):
 
   def __init__(self, plaso_version=None, *args, **kwargs):
     """Initialization for Plaso File evidence."""
-    self.plaso_version = plaso_version
-    super(PlasoFile, self).__init__(copyable=True, *args, **kwargs)
+    super(PlasoFile, self).__init__(*args, **kwargs)
     self.save_metadata = True
+    self.copyable = True
+    self.plaso_version = plaso_version
 
 
 class PlasoCsvFile(Evidence):
@@ -946,9 +948,10 @@ class PlasoCsvFile(Evidence):
 
   def __init__(self, plaso_version=None, *args, **kwargs):
     """Initialization for Plaso File evidence."""
-    self.plaso_version = plaso_version
-    super(PlasoCsvFile, self).__init__(copyable=True, *args, **kwargs)
+    super(PlasoCsvFile, self).__init__(*args, **kwargs)
     self.save_metadata = False
+    self.copyable = True
+    self.plaso_version = plaso_version
 
 
 # TODO(aarontp): Find a way to integrate this into TurbiniaTaskResult instead.
@@ -956,8 +959,9 @@ class ReportText(Evidence):
   """Text data for general reporting."""
 
   def __init__(self, text_data=None, *args, **kwargs):
-    super(ReportText, self).__init__(copyable=True, *args, **kwargs)
+    super(ReportText, self).__init__(*args, **kwargs)
     self.text_data = text_data
+    self.copyable = True
 
 
 class FinalReport(ReportText):
@@ -972,7 +976,8 @@ class TextFile(Evidence):
   """Text data."""
 
   def __init__(self, *args, **kwargs):
-    super(TextFile, self).__init__(copyable=True, *args, **kwargs)
+    super(TextFile, self).__init__(*args, **kwargs)
+    self.copyable = True
 
 
 class FilteredTextFile(TextFile):
@@ -984,8 +989,9 @@ class BodyFile(Evidence):
   """Bodyfile data."""
 
   def __init__(self, *args, **kwargs):
+    super(BodyFile, self).__init__(*args, **kwargs)
+    self.copyable = True
     self.number_of_entries = None
-    super(BodyFile, self).__init__(copyable=True, *args, **kwargs)
 
 
 class ExportedFileArtifact(Evidence):
@@ -995,8 +1001,9 @@ class ExportedFileArtifact(Evidence):
 
   def __init__(self, artifact_name=None, *args, **kwargs):
     """Initializes an exported file artifact."""
-    super(ExportedFileArtifact, self).__init__(copyable=True, *args, **kwargs)
+    super(ExportedFileArtifact, self).__init__(*args, **kwargs)
     self.artifact_name = artifact_name
+    self.copyable = True
 
 
 class VolatilityReport(TextFile):
@@ -1123,7 +1130,7 @@ class ContainerdContainer(Evidence):
     _image_path (str): Path where disk image is mounted.
     _container_fs_path (str): Path where containerd filesystem is mounted.
   """
-
+  REQUIRED_ATTRIBUTES = ['namespace', 'container_id']
   POSSIBLE_STATES = [EvidenceState.CONTAINER_MOUNTED]
 
   def __init__(self, namespace=None, container_id=None, *args, **kwargs):
diff --git a/turbinia/processors/mount_local.py b/turbinia/processors/mount_local.py
index 5b4549766..d9995e379 100644
--- a/turbinia/processors/mount_local.py
+++ b/turbinia/processors/mount_local.py
@@ -506,8 +506,9 @@ def PostprocessDeleteLosetup(device_path, lv_uuid=None):
         if reg_search:
           # TODO(wyassine): Add lsof check for file handles on device path
           # https://github.com/google/turbinia/issues/1148
-          log.debug('losetup retry check {0!s}/{1!s} for device {2!s}').format(
-              _, RETRY_MAX, device_path)
+          log.debug(
+              'losetup retry check {0!s}/{1!s} for device {2!s}'.format(
+                  _, RETRY_MAX, device_path))
           time.sleep(1)
         else:
           break
diff --git a/turbinia/task_manager.py b/turbinia/task_manager.py
index 53838604e..cdc510849 100644
--- a/turbinia/task_manager.py
+++ b/turbinia/task_manager.py
@@ -723,8 +723,17 @@ def get_evidence(self):
           evidence_.config['globals']['group_name'] = request.group_name
           evidence_.config['globals']['reason'] = request.reason
           evidence_.config['globals']['all_args'] = request.all_args
-          evidence_.config['globals']['group_id'] = request.recipe['globals'][
-              'group_id']
+
+          # A recipe could contain a group_id key so that tasks can be grouped
+          # together, but this is optional. If the recipe doesn't specify a
+          # group_id, then we grab it from the request object itself.
+          try:
+            recipe_group_id = request.recipe['globals']['group_id']
+            if recipe_group_id:
+              evidence_.config['globals']['group_id'] = recipe_group_id
+          except KeyError:
+            evidence_.config['globals']['group_id'] = request.group_id
+
           evidence_list.append(evidence_)
       turbinia_server_request_total.inc()
 
diff --git a/turbinia/turbiniactl.py b/turbinia/turbiniactl.py
index 9c82beb2e..92db9954c 100644
--- a/turbinia/turbiniactl.py
+++ b/turbinia/turbiniactl.py
@@ -485,12 +485,12 @@ def process_args(args):
     config.OUTPUT_DIR = args.output_dir
 
   config.TURBINIA_COMMAND = args.command
-  server_flags_set = args.command == 'server'
-  worker_flags_set = args.command in ('psqworker', 'celeryworker')
+  flags_set = args.command in (
+      'api_server', 'server', 'psqworker', 'celeryworker')
   # Run logger setup again if we're running as a server or worker (or have a log
   # file explicitly set on the command line) to set a file-handler now that we
   # have the logfile path from the config.
-  if server_flags_set or worker_flags_set or args.log_file:
+  if flags_set or args.log_file:
     if args.log_file:
       logger.setup(log_file_path=user_specified_log)
     else:
diff --git a/turbinia/workers/__init__.py b/turbinia/workers/__init__.py
index aa6c281b0..38b80573a 100644
--- a/turbinia/workers/__init__.py
+++ b/turbinia/workers/__init__.py
@@ -250,7 +250,8 @@ def close(self, task, success, status=None):
       except Exception as exception:
         message = 'Evidence post-processing for {0!s} failed: {1!s}'.format(
             self.input_evidence.name, exception)
-        self.log(message, level=logging.ERROR)
+        self.log(
+            message, level=logging.ERROR, traceback_=traceback.format_exc())
         with filelock.FileLock(config.RESOURCE_FILE_LOCK):
           resource_manager.PostProcessResourceState(
               self.input_evidence.resource_id, self.task_id)
@@ -304,7 +305,7 @@ def log(self, message, level=logging.INFO, traceback_=None):
       log.critical(message)
 
     if traceback_:
-      self.result.set_error(message, traceback_)
+      self.set_error(message, traceback_)
 
   def update_task_status(self, task, status=None):
     """Updates the task status and pushes it directly to datastore.
diff --git a/web/package-lock.json b/web/package-lock.json
index dcb94b3e5..d052a3293 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -8,33 +8,33 @@
       "name": "turbinia-web",
       "version": "0.1.0",
       "dependencies": {
-        "axios": "^1.1.2",
-        "core-js": "^3.25.5",
-        "vue": "^2.7.12",
-        "vuetify": "^2.6.11"
+        "axios": "^0.27.2",
+        "core-js": "^3.8.3",
+        "vue": "^2.6.14",
+        "vuetify": "^2.6.10"
       },
       "devDependencies": {
-        "@babel/core": "^7.19.3",
-        "@babel/eslint-parser": "^7.19.1",
-        "@vue/cli-plugin-babel": "~5.0.8",
-        "@vue/cli-plugin-eslint": "~5.0.8",
-        "@vue/cli-service": "~5.0.8",
-        "eslint": "^8.25.0",
-        "eslint-config-prettier": "^8.5.0",
-        "eslint-plugin-prettier": "^4.2.1",
-        "eslint-plugin-vue": "^9.6.0",
-        "prettier": "^2.7.1",
-        "sass": "~1.55.0",
-        "sass-loader": "^13.1.0",
-        "vue-cli-plugin-vuetify": "~2.5.8",
-        "vue-template-compiler": "^2.7.12",
-        "vuetify-loader": "^1.9.2"
+        "@babel/core": "^7.12.16",
+        "@babel/eslint-parser": "^7.12.16",
+        "@vue/cli-plugin-babel": "~5.0.0",
+        "@vue/cli-plugin-eslint": "~5.0.0",
+        "@vue/cli-service": "~5.0.0",
+        "eslint": "^7.32.0",
+        "eslint-config-prettier": "^8.3.0",
+        "eslint-plugin-prettier": "^4.0.0",
+        "eslint-plugin-vue": "^8.0.3",
+        "prettier": "^2.4.1",
+        "sass": "~1.32.0",
+        "sass-loader": "^10.0.0",
+        "vue-cli-plugin-vuetify": "~2.4.8",
+        "vue-template-compiler": "^2.6.14",
+        "vuetify-loader": "^1.7.0"
       }
     },
     "node_modules/@achrinza/node-ipc": {
-      "version": "9.2.5",
-      "resolved": "https://registry.npmjs.org/@achrinza/node-ipc/-/node-ipc-9.2.5.tgz",
-      "integrity": "sha512-kBX7Ay911iXZ3VZ1pYltj3Rfu7Ow9H7sK4H4RSfWIfWR2JKNB40K808wppoRIEzE2j2hXLU+r6TJgCAliCGhyQ==",
+      "version": "9.2.6",
+      "resolved": "https://registry.npmjs.org/@achrinza/node-ipc/-/node-ipc-9.2.6.tgz",
+      "integrity": "sha512-ULSIYPy4ZPM301dfCxRz0l2GJjOwIo/PqmWonIu1bLml7UmnVQmH+juJcoyXp6E8gIRRNAjGYftJnNQlfy4vPg==",
       "dev": true,
       "dependencies": {
         "@node-ipc/js-queue": "2.0.3",
@@ -42,7 +42,7 @@
         "js-message": "1.0.7"
       },
       "engines": {
-        "node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18"
+        "node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19"
       }
     },
     "node_modules/@ampproject/remapping": {
@@ -71,30 +71,30 @@
       }
     },
     "node_modules/@babel/compat-data": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz",
-      "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz",
+      "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/core": {
-      "version": "7.19.3",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz",
-      "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz",
+      "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==",
       "dev": true,
       "dependencies": {
         "@ampproject/remapping": "^2.1.0",
         "@babel/code-frame": "^7.18.6",
-        "@babel/generator": "^7.19.3",
+        "@babel/generator": "^7.19.6",
         "@babel/helper-compilation-targets": "^7.19.3",
-        "@babel/helper-module-transforms": "^7.19.0",
-        "@babel/helpers": "^7.19.0",
-        "@babel/parser": "^7.19.3",
+        "@babel/helper-module-transforms": "^7.19.6",
+        "@babel/helpers": "^7.19.4",
+        "@babel/parser": "^7.19.6",
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.3",
-        "@babel/types": "^7.19.3",
+        "@babel/traverse": "^7.19.6",
+        "@babel/types": "^7.19.4",
         "convert-source-map": "^1.7.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
@@ -128,12 +128,12 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.19.5",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz",
-      "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
+      "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.19.4",
+        "@babel/types": "^7.20.0",
         "@jridgewell/gen-mapping": "^0.3.2",
         "jsesc": "^2.5.1"
       },
@@ -181,12 +181,12 @@
       }
     },
     "node_modules/@babel/helper-compilation-targets": {
-      "version": "7.19.3",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz",
-      "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz",
+      "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==",
       "dev": true,
       "dependencies": {
-        "@babel/compat-data": "^7.19.3",
+        "@babel/compat-data": "^7.20.0",
         "@babel/helper-validator-option": "^7.18.6",
         "browserslist": "^4.21.3",
         "semver": "^6.3.0"
@@ -323,19 +323,19 @@
       }
     },
     "node_modules/@babel/helper-module-transforms": {
-      "version": "7.19.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz",
-      "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz",
+      "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==",
       "dev": true,
       "dependencies": {
         "@babel/helper-environment-visitor": "^7.18.9",
         "@babel/helper-module-imports": "^7.18.6",
-        "@babel/helper-simple-access": "^7.18.6",
+        "@babel/helper-simple-access": "^7.19.4",
         "@babel/helper-split-export-declaration": "^7.18.6",
-        "@babel/helper-validator-identifier": "^7.18.6",
+        "@babel/helper-validator-identifier": "^7.19.1",
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.0",
-        "@babel/types": "^7.19.0"
+        "@babel/traverse": "^7.19.6",
+        "@babel/types": "^7.19.4"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -409,12 +409,12 @@
       }
     },
     "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
-      "version": "7.18.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz",
-      "integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz",
+      "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.18.9"
+        "@babel/types": "^7.20.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -475,14 +475,14 @@
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz",
-      "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
+      "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
       "dev": true,
       "dependencies": {
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.4",
-        "@babel/types": "^7.19.4"
+        "@babel/traverse": "^7.20.1",
+        "@babel/types": "^7.20.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -503,9 +503,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz",
-      "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
+      "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -546,9 +546,9 @@
       }
     },
     "node_modules/@babel/plugin-proposal-async-generator-functions": {
-      "version": "7.19.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz",
-      "integrity": "sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz",
+      "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==",
       "dev": true,
       "dependencies": {
         "@babel/helper-environment-visitor": "^7.18.9",
@@ -597,9 +597,9 @@
       }
     },
     "node_modules/@babel/plugin-proposal-decorators": {
-      "version": "7.19.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.19.3.tgz",
-      "integrity": "sha512-MbgXtNXqo7RTKYIXVchVJGPvaVufQH3pxvQyfbGvNw1DObIhph+PesYXJTcd8J4DdWibvf6Z2eanOyItX8WnJg==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.0.tgz",
+      "integrity": "sha512-vnuRRS20ygSxclEYikHzVrP9nZDFXaSzvJxGLQNAiBX041TmhS4hOUHWNIpq/q4muENuEP9XPJFXTNFejhemkg==",
       "dev": true,
       "dependencies": {
         "@babel/helper-create-class-features-plugin": "^7.19.0",
@@ -892,12 +892,12 @@
       }
     },
     "node_modules/@babel/plugin-syntax-import-assertions": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz",
-      "integrity": "sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz",
+      "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.18.6"
+        "@babel/helper-plugin-utils": "^7.19.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1083,9 +1083,9 @@
       }
     },
     "node_modules/@babel/plugin-transform-block-scoping": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.19.4.tgz",
-      "integrity": "sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.0.tgz",
+      "integrity": "sha512-sXOohbpHZSk7GjxK9b3dKB7CfqUD5DwOH+DggKzOQ7TXYP+RCSbRykfjQmn/zq+rBjycVRtLf9pYhAaEJA786w==",
       "dev": true,
       "dependencies": {
         "@babel/helper-plugin-utils": "^7.19.0"
@@ -1136,9 +1136,9 @@
       }
     },
     "node_modules/@babel/plugin-transform-destructuring": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.19.4.tgz",
-      "integrity": "sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.0.tgz",
+      "integrity": "sha512-1dIhvZfkDVx/zn2S1aFwlruspTt4189j7fEkH0Y0VyuDM6bQt7bD6kLcz3l4IlLG+e5OReaBz9ROAbttRtUHqA==",
       "dev": true,
       "dependencies": {
         "@babel/helper-plugin-utils": "^7.19.0"
@@ -1260,14 +1260,13 @@
       }
     },
     "node_modules/@babel/plugin-transform-modules-amd": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz",
-      "integrity": "sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz",
+      "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-module-transforms": "^7.18.6",
-        "@babel/helper-plugin-utils": "^7.18.6",
-        "babel-plugin-dynamic-import-node": "^2.3.3"
+        "@babel/helper-module-transforms": "^7.19.6",
+        "@babel/helper-plugin-utils": "^7.19.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1277,15 +1276,14 @@
       }
     },
     "node_modules/@babel/plugin-transform-modules-commonjs": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz",
-      "integrity": "sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz",
+      "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-module-transforms": "^7.18.6",
-        "@babel/helper-plugin-utils": "^7.18.6",
-        "@babel/helper-simple-access": "^7.18.6",
-        "babel-plugin-dynamic-import-node": "^2.3.3"
+        "@babel/helper-module-transforms": "^7.19.6",
+        "@babel/helper-plugin-utils": "^7.19.0",
+        "@babel/helper-simple-access": "^7.19.4"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1295,16 +1293,15 @@
       }
     },
     "node_modules/@babel/plugin-transform-modules-systemjs": {
-      "version": "7.19.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.0.tgz",
-      "integrity": "sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz",
+      "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==",
       "dev": true,
       "dependencies": {
         "@babel/helper-hoist-variables": "^7.18.6",
-        "@babel/helper-module-transforms": "^7.19.0",
+        "@babel/helper-module-transforms": "^7.19.6",
         "@babel/helper-plugin-utils": "^7.19.0",
-        "@babel/helper-validator-identifier": "^7.18.6",
-        "babel-plugin-dynamic-import-node": "^2.3.3"
+        "@babel/helper-validator-identifier": "^7.19.1"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1377,12 +1374,12 @@
       }
     },
     "node_modules/@babel/plugin-transform-parameters": {
-      "version": "7.18.8",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz",
-      "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.1.tgz",
+      "integrity": "sha512-nDvKLrAvl+kf6BOy1UJ3MGwzzfTMgppxwiD2Jb4LO3xjYyZq30oQzDNJbCQpMdG9+j2IXHoiMrw5Cm/L6ZoxXQ==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.18.6"
+        "@babel/helper-plugin-utils": "^7.19.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1438,9 +1435,9 @@
       }
     },
     "node_modules/@babel/plugin-transform-runtime": {
-      "version": "7.19.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.1.tgz",
-      "integrity": "sha512-2nJjTUFIzBMP/f/miLxEK9vxwW/KUXsdvN4sR//TmuDhe6yU2h57WmIOE12Gng3MDP/xpjUV/ToZRdcf8Yj4fA==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz",
+      "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==",
       "dev": true,
       "dependencies": {
         "@babel/helper-module-imports": "^7.18.6",
@@ -1670,12 +1667,12 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz",
-      "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz",
+      "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==",
       "dev": true,
       "dependencies": {
-        "regenerator-runtime": "^0.13.4"
+        "regenerator-runtime": "^0.13.10"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1696,19 +1693,19 @@
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz",
-      "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
+      "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
       "dev": true,
       "dependencies": {
         "@babel/code-frame": "^7.18.6",
-        "@babel/generator": "^7.19.4",
+        "@babel/generator": "^7.20.1",
         "@babel/helper-environment-visitor": "^7.18.9",
         "@babel/helper-function-name": "^7.19.0",
         "@babel/helper-hoist-variables": "^7.18.6",
         "@babel/helper-split-export-declaration": "^7.18.6",
-        "@babel/parser": "^7.19.4",
-        "@babel/types": "^7.19.4",
+        "@babel/parser": "^7.20.1",
+        "@babel/types": "^7.20.0",
         "debug": "^4.1.0",
         "globals": "^11.1.0"
       },
@@ -1717,9 +1714,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz",
-      "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz",
+      "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==",
       "dev": true,
       "dependencies": {
         "@babel/helper-string-parser": "^7.19.4",
@@ -1731,26 +1728,23 @@
       }
     },
     "node_modules/@eslint/eslintrc": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
-      "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+      "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
       "dev": true,
       "dependencies": {
         "ajv": "^6.12.4",
-        "debug": "^4.3.2",
-        "espree": "^9.4.0",
-        "globals": "^13.15.0",
-        "ignore": "^5.2.0",
+        "debug": "^4.1.1",
+        "espree": "^7.3.0",
+        "globals": "^13.9.0",
+        "ignore": "^4.0.6",
         "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "minimatch": "^3.1.2",
+        "js-yaml": "^3.13.1",
+        "minimatch": "^3.0.4",
         "strip-json-comments": "^3.1.1"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
+        "node": "^10.12.0 || >=12.0.0"
       }
     },
     "node_modules/@eslint/eslintrc/node_modules/globals": {
@@ -1796,12 +1790,12 @@
       }
     },
     "node_modules/@humanwhocodes/config-array": {
-      "version": "0.10.7",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
-      "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+      "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
       "dev": true,
       "dependencies": {
-        "@humanwhocodes/object-schema": "^1.2.1",
+        "@humanwhocodes/object-schema": "^1.2.0",
         "debug": "^4.1.1",
         "minimatch": "^3.0.4"
       },
@@ -1809,19 +1803,6 @@
         "node": ">=10.10.0"
       }
     },
-    "node_modules/@humanwhocodes/module-importer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-      "dev": true,
-      "engines": {
-        "node": ">=12.22"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/nzakas"
-      }
-    },
     "node_modules/@humanwhocodes/object-schema": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
@@ -1890,9 +1871,9 @@
       "dev": true
     },
     "node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.16",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz",
-      "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==",
+      "version": "0.3.17",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
+      "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
       "dev": true,
       "dependencies": {
         "@jridgewell/resolve-uri": "3.1.0",
@@ -2127,9 +2108,9 @@
       }
     },
     "node_modules/@types/eslint": {
-      "version": "8.4.6",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
-      "integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==",
+      "version": "8.4.9",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz",
+      "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==",
       "dev": true,
       "dependencies": {
         "@types/estree": "*",
@@ -2209,9 +2190,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "18.8.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.5.tgz",
-      "integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q==",
+      "version": "18.11.9",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
+      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
       "dev": true
     },
     "node_modules/@types/normalize-package-data": {
@@ -2785,9 +2766,9 @@
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "2.7.12",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.12.tgz",
-      "integrity": "sha512-7FOITA2+4ND7lMAfegljHBpNSG3X9mVzgQwcS3g928QZM1EADedUw2JLKcgOm1ZEJEkvyDHh6lwa08vrLmoCOA==",
+      "version": "2.7.13",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.13.tgz",
+      "integrity": "sha512-zzu2rLRZlgIU+OT3Atbr7Y6PG+LW4wVQpPfNRrGDH3dM9PsrcVfa+1pKb8bW467bGM3aDOvAnsYLWVpYIv3GRg==",
       "dependencies": {
         "@babel/parser": "^7.18.4",
         "postcss": "^8.4.14",
@@ -3068,9 +3049,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.8.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
-      "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
+      "version": "8.8.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
+      "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
       "dev": true,
       "bin": {
         "acorn": "bin/acorn"
@@ -3179,6 +3160,15 @@
         "ajv": "^6.9.1"
       }
     },
+    "node_modules/ansi-colors": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+      "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/ansi-escapes": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
@@ -3261,10 +3251,13 @@
       ]
     },
     "node_modules/argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "dependencies": {
+        "sprintf-js": "~1.0.2"
+      }
     },
     "node_modules/array-flatten": {
       "version": "2.1.2",
@@ -3281,6 +3274,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/astral-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/async": {
       "version": "2.6.4",
       "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
@@ -3305,9 +3307,9 @@
       }
     },
     "node_modules/autoprefixer": {
-      "version": "10.4.12",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.12.tgz",
-      "integrity": "sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==",
+      "version": "10.4.13",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
+      "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==",
       "dev": true,
       "funding": [
         {
@@ -3321,7 +3323,7 @@
       ],
       "dependencies": {
         "browserslist": "^4.21.4",
-        "caniuse-lite": "^1.0.30001407",
+        "caniuse-lite": "^1.0.30001426",
         "fraction.js": "^4.2.0",
         "normalize-range": "^0.1.2",
         "picocolors": "^1.0.0",
@@ -3338,19 +3340,18 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.2.tgz",
-      "integrity": "sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==",
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
       "dependencies": {
-        "follow-redirects": "^1.15.0",
-        "form-data": "^4.0.0",
-        "proxy-from-env": "^1.1.0"
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
       }
     },
     "node_modules/babel-loader": {
-      "version": "8.2.5",
-      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz",
-      "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz",
+      "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==",
       "dev": true,
       "dependencies": {
         "find-cache-dir": "^3.3.1",
@@ -3716,9 +3717,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001418",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz",
-      "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==",
+      "version": "1.0.30001429",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz",
+      "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==",
       "dev": true,
       "funding": [
         {
@@ -4199,9 +4200,9 @@
       }
     },
     "node_modules/core-js": {
-      "version": "3.25.5",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.5.tgz",
-      "integrity": "sha512-nbm6eZSjm+ZuBQxCUPQKQCoUEfFOXjUZ8dTTyikyKaWrTYmAVbykQfwsKE5dBK88u3QCkCrzsx/PPlKfhsvgpw==",
+      "version": "3.26.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
+      "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==",
       "hasInstallScript": true,
       "funding": {
         "type": "opencollective",
@@ -4209,9 +4210,9 @@
       }
     },
     "node_modules/core-js-compat": {
-      "version": "3.25.5",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.5.tgz",
-      "integrity": "sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA==",
+      "version": "3.26.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.0.tgz",
+      "integrity": "sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==",
       "dev": true,
       "dependencies": {
         "browserslist": "^4.21.4"
@@ -4455,12 +4456,12 @@
       }
     },
     "node_modules/cssnano": {
-      "version": "5.1.13",
-      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.13.tgz",
-      "integrity": "sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==",
+      "version": "5.1.14",
+      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz",
+      "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==",
       "dev": true,
       "dependencies": {
-        "cssnano-preset-default": "^5.2.12",
+        "cssnano-preset-default": "^5.2.13",
         "lilconfig": "^2.0.3",
         "yaml": "^1.10.2"
       },
@@ -4476,25 +4477,25 @@
       }
     },
     "node_modules/cssnano-preset-default": {
-      "version": "5.2.12",
-      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.12.tgz",
-      "integrity": "sha512-OyCBTZi+PXgylz9HAA5kHyoYhfGcYdwFmyaJzWnzxuGRtnMw/kR6ilW9XzlzlRAtB6PLT/r+prYgkef7hngFew==",
+      "version": "5.2.13",
+      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz",
+      "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==",
       "dev": true,
       "dependencies": {
-        "css-declaration-sorter": "^6.3.0",
+        "css-declaration-sorter": "^6.3.1",
         "cssnano-utils": "^3.1.0",
         "postcss-calc": "^8.2.3",
         "postcss-colormin": "^5.3.0",
-        "postcss-convert-values": "^5.1.2",
+        "postcss-convert-values": "^5.1.3",
         "postcss-discard-comments": "^5.1.2",
         "postcss-discard-duplicates": "^5.1.0",
         "postcss-discard-empty": "^5.1.1",
         "postcss-discard-overridden": "^5.1.0",
-        "postcss-merge-longhand": "^5.1.6",
-        "postcss-merge-rules": "^5.1.2",
+        "postcss-merge-longhand": "^5.1.7",
+        "postcss-merge-rules": "^5.1.3",
         "postcss-minify-font-values": "^5.1.0",
         "postcss-minify-gradients": "^5.1.1",
-        "postcss-minify-params": "^5.1.3",
+        "postcss-minify-params": "^5.1.4",
         "postcss-minify-selectors": "^5.2.1",
         "postcss-normalize-charset": "^5.1.0",
         "postcss-normalize-display-values": "^5.1.0",
@@ -4502,11 +4503,11 @@
         "postcss-normalize-repeat-style": "^5.1.1",
         "postcss-normalize-string": "^5.1.0",
         "postcss-normalize-timing-functions": "^5.1.0",
-        "postcss-normalize-unicode": "^5.1.0",
+        "postcss-normalize-unicode": "^5.1.1",
         "postcss-normalize-url": "^5.1.0",
         "postcss-normalize-whitespace": "^5.1.1",
         "postcss-ordered-values": "^5.1.3",
-        "postcss-reduce-initial": "^5.1.0",
+        "postcss-reduce-initial": "^5.1.1",
         "postcss-reduce-transforms": "^5.1.0",
         "postcss-svgo": "^5.1.0",
         "postcss-unique-selectors": "^5.1.1"
@@ -4888,9 +4889,9 @@
       "dev": true
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.279",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.279.tgz",
-      "integrity": "sha512-xs7vEuSZ84+JsHSTFqqG0TE3i8EAivHomRQZhhcRvsmnjsh5C2KdhwNKf4ZRYtzq75wojpFyqb62m32Oam57wA==",
+      "version": "1.4.284",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
+      "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
       "dev": true
     },
     "node_modules/emoji-regex": {
@@ -4939,6 +4940,18 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/enquirer": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+      "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+      "dev": true,
+      "dependencies": {
+        "ansi-colors": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
     "node_modules/entities": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
@@ -4997,55 +5010,57 @@
       }
     },
     "node_modules/eslint": {
-      "version": "8.25.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
-      "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
+      "version": "7.32.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+      "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
       "dev": true,
       "dependencies": {
-        "@eslint/eslintrc": "^1.3.3",
-        "@humanwhocodes/config-array": "^0.10.5",
-        "@humanwhocodes/module-importer": "^1.0.1",
+        "@babel/code-frame": "7.12.11",
+        "@eslint/eslintrc": "^0.4.3",
+        "@humanwhocodes/config-array": "^0.5.0",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
-        "debug": "^4.3.2",
+        "debug": "^4.0.1",
         "doctrine": "^3.0.0",
+        "enquirer": "^2.3.5",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.1.1",
-        "eslint-utils": "^3.0.0",
-        "eslint-visitor-keys": "^3.3.0",
-        "espree": "^9.4.0",
+        "eslint-scope": "^5.1.1",
+        "eslint-utils": "^2.1.0",
+        "eslint-visitor-keys": "^2.0.0",
+        "espree": "^7.3.1",
         "esquery": "^1.4.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
         "file-entry-cache": "^6.0.1",
-        "find-up": "^5.0.0",
-        "glob-parent": "^6.0.1",
-        "globals": "^13.15.0",
-        "globby": "^11.1.0",
-        "grapheme-splitter": "^1.0.4",
-        "ignore": "^5.2.0",
+        "functional-red-black-tree": "^1.0.1",
+        "glob-parent": "^5.1.2",
+        "globals": "^13.6.0",
+        "ignore": "^4.0.6",
         "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
-        "js-sdsl": "^4.1.4",
-        "js-yaml": "^4.1.0",
+        "js-yaml": "^3.13.1",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.4.1",
         "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.2",
+        "minimatch": "^3.0.4",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.1",
-        "regexpp": "^3.2.0",
-        "strip-ansi": "^6.0.1",
+        "progress": "^2.0.0",
+        "regexpp": "^3.1.0",
+        "semver": "^7.2.1",
+        "strip-ansi": "^6.0.0",
         "strip-json-comments": "^3.1.0",
-        "text-table": "^0.2.0"
+        "table": "^6.0.9",
+        "text-table": "^0.2.0",
+        "v8-compile-cache": "^2.0.3"
       },
       "bin": {
         "eslint": "bin/eslint.js"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^10.12.0 || >=12.0.0"
       },
       "funding": {
         "url": "https://opencollective.com/eslint"
@@ -5085,9 +5100,9 @@
       }
     },
     "node_modules/eslint-plugin-vue": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.6.0.tgz",
-      "integrity": "sha512-zzySkJgVbFCylnG2+9MDF7N+2Rjze2y0bF8GyUNpFOnT8mCMfqqtLDJkHBuYu9N/psW1A6DVbQhPkP92E+qakA==",
+      "version": "8.7.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz",
+      "integrity": "sha512-28sbtm4l4cOzoO1LtzQPxfxhQABararUb1JtqusQqObJpWX2e/gmVyeYVfepizPFne0Q5cILkYGiBoV36L12Wg==",
       "dev": true,
       "dependencies": {
         "eslint-utils": "^3.0.0",
@@ -5095,16 +5110,33 @@
         "nth-check": "^2.0.1",
         "postcss-selector-parser": "^6.0.9",
         "semver": "^7.3.5",
-        "vue-eslint-parser": "^9.0.1",
-        "xml-name-validator": "^4.0.0"
+        "vue-eslint-parser": "^8.0.1"
       },
       "engines": {
-        "node": "^14.17.0 || >=16.0.0"
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
       "peerDependencies": {
         "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/eslint-plugin-vue/node_modules/eslint-utils": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+      "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+      "dev": true,
+      "dependencies": {
+        "eslint-visitor-keys": "^2.0.0"
+      },
+      "engines": {
+        "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      },
+      "peerDependencies": {
+        "eslint": ">=5"
+      }
+    },
     "node_modules/eslint-plugin-vue/node_modules/semver": {
       "version": "7.3.8",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -5134,21 +5166,27 @@
       }
     },
     "node_modules/eslint-utils": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
-      "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+      "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
       "dev": true,
       "dependencies": {
-        "eslint-visitor-keys": "^2.0.0"
+        "eslint-visitor-keys": "^1.1.0"
       },
       "engines": {
-        "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+        "node": ">=6"
       },
       "funding": {
         "url": "https://github.com/sponsors/mysticatea"
-      },
-      "peerDependencies": {
-        "eslint": ">=5"
+      }
+    },
+    "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+      "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
       }
     },
     "node_modules/eslint-visitor-keys": {
@@ -5275,6 +5313,15 @@
         "url": "https://github.com/chalk/supports-color?sponsor=1"
       }
     },
+    "node_modules/eslint/node_modules/@babel/code-frame": {
+      "version": "7.12.11",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+      "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/highlight": "^7.10.4"
+      }
+    },
     "node_modules/eslint/node_modules/ansi-styles": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -5336,35 +5383,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/eslint/node_modules/eslint-scope": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
-      "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+    "node_modules/eslint/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
       "dev": true,
       "dependencies": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^5.2.0"
+        "is-glob": "^4.0.1"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    },
-    "node_modules/eslint/node_modules/eslint-visitor-keys": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
-      "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
-      "dev": true,
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    },
-    "node_modules/eslint/node_modules/estraverse": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4.0"
+        "node": ">= 6"
       }
     },
     "node_modules/eslint/node_modules/globals": {
@@ -5391,6 +5419,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/eslint/node_modules/semver": {
+      "version": "7.3.8",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/eslint/node_modules/supports-color": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -5416,29 +5459,51 @@
       }
     },
     "node_modules/espree": {
-      "version": "9.4.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
-      "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+      "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
       "dev": true,
       "dependencies": {
-        "acorn": "^8.8.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^3.3.0"
+        "acorn": "^7.4.0",
+        "acorn-jsx": "^5.3.1",
+        "eslint-visitor-keys": "^1.3.0"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/espree/node_modules/acorn": {
+      "version": "7.4.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+      "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+      "dev": true,
+      "bin": {
+        "acorn": "bin/acorn"
       },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
+      "engines": {
+        "node": ">=0.4.0"
       }
     },
     "node_modules/espree/node_modules/eslint-visitor-keys": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
-      "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+      "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
       "dev": true,
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": ">=4"
+      }
+    },
+    "node_modules/esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true,
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
       }
     },
     "node_modules/esquery": {
@@ -5894,19 +5959,16 @@
       }
     },
     "node_modules/find-up": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
       "dev": true,
       "dependencies": {
-        "locate-path": "^6.0.0",
+        "locate-path": "^5.0.0",
         "path-exists": "^4.0.0"
       },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=8"
       }
     },
     "node_modules/flat-cache": {
@@ -6038,6 +6100,12 @@
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
+    "node_modules/functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+      "dev": true
+    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -6149,18 +6217,21 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/globby/node_modules/ignore": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+      "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
     "node_modules/graceful-fs": {
       "version": "4.2.10",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
       "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
       "dev": true
     },
-    "node_modules/grapheme-splitter": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
-      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
-      "dev": true
-    },
     "node_modules/gzip-size": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -6500,20 +6571,14 @@
       ]
     },
     "node_modules/ignore": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
-      "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+      "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
       "dev": true,
       "engines": {
         "node": ">= 4"
       }
     },
-    "node_modules/immutable": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
-      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
-      "dev": true
-    },
     "node_modules/import-fresh": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -6604,9 +6669,9 @@
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.10.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
-      "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==",
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+      "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
       "dev": true,
       "dependencies": {
         "has": "^1.0.3"
@@ -6810,9 +6875,9 @@
       }
     },
     "node_modules/joi": {
-      "version": "17.6.3",
-      "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.3.tgz",
-      "integrity": "sha512-YlQsIaS9MHYekzf1Qe11LjTkNzx9qhYluK3172z38RxYoAUf82XMX1p1DG1H4Wtk2ED/vPdSn9OggqtDu+aTow==",
+      "version": "17.7.0",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz",
+      "integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==",
       "dev": true,
       "dependencies": {
         "@hapi/hoek": "^9.0.0",
@@ -6831,12 +6896,6 @@
         "node": ">=0.6.0"
       }
     },
-    "node_modules/js-sdsl": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
-      "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==",
-      "dev": true
-    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6844,12 +6903,13 @@
       "dev": true
     },
     "node_modules/js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "version": "3.14.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+      "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
       "dev": true,
       "dependencies": {
-        "argparse": "^2.0.1"
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
       },
       "bin": {
         "js-yaml": "bin/js-yaml.js"
@@ -7016,18 +7076,15 @@
       }
     },
     "node_modules/locate-path": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
       "dev": true,
       "dependencies": {
-        "p-locate": "^5.0.0"
+        "p-locate": "^4.1.0"
       },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=8"
       }
     },
     "node_modules/lodash": {
@@ -7072,6 +7129,12 @@
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
       "dev": true
     },
+    "node_modules/lodash.truncate": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+      "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+      "dev": true
+    },
     "node_modules/lodash.uniq": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -7332,9 +7395,9 @@
       }
     },
     "node_modules/memfs": {
-      "version": "3.4.7",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz",
-      "integrity": "sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==",
+      "version": "3.4.9",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.9.tgz",
+      "integrity": "sha512-3rm8kbrzpUGRyPKSGuk387NZOwQ90O4rI9tsWQkzNW7BLSnKGp23RsEsKK8N8QVCrtJoAMqy3spxHC4os4G6PQ==",
       "dev": true,
       "dependencies": {
         "fs-monkey": "^1.0.3"
@@ -8064,33 +8127,30 @@
       }
     },
     "node_modules/p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
       "dev": true,
       "dependencies": {
-        "yocto-queue": "^0.1.0"
+        "p-try": "^2.0.0"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=6"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/p-locate": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
       "dev": true,
       "dependencies": {
-        "p-limit": "^3.0.2"
+        "p-limit": "^2.2.0"
       },
       "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
+        "node": ">=8"
       }
     },
     "node_modules/p-retry": {
@@ -8272,58 +8332,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/pkg-dir/node_modules/find-up": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-      "dev": true,
-      "dependencies": {
-        "locate-path": "^5.0.0",
-        "path-exists": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/pkg-dir/node_modules/locate-path": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
-      "dev": true,
-      "dependencies": {
-        "p-locate": "^4.1.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/pkg-dir/node_modules/p-limit": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-      "dev": true,
-      "dependencies": {
-        "p-try": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/pkg-dir/node_modules/p-locate": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
-      "dev": true,
-      "dependencies": {
-        "p-limit": "^2.2.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/portfinder": {
       "version": "1.0.32",
       "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
@@ -8402,12 +8410,12 @@
       }
     },
     "node_modules/postcss-convert-values": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz",
-      "integrity": "sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==",
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz",
+      "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.20.3",
+        "browserslist": "^4.21.4",
         "postcss-value-parser": "^4.2.0"
       },
       "engines": {
@@ -8503,13 +8511,13 @@
       }
     },
     "node_modules/postcss-merge-longhand": {
-      "version": "5.1.6",
-      "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.6.tgz",
-      "integrity": "sha512-6C/UGF/3T5OE2CEbOuX7iNO63dnvqhGZeUnKkDeifebY0XqkkvrctYSZurpNE902LDf2yKwwPFgotnfSoPhQiw==",
+      "version": "5.1.7",
+      "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz",
+      "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==",
       "dev": true,
       "dependencies": {
         "postcss-value-parser": "^4.2.0",
-        "stylehacks": "^5.1.0"
+        "stylehacks": "^5.1.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14.0"
@@ -8519,12 +8527,12 @@
       }
     },
     "node_modules/postcss-merge-rules": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz",
-      "integrity": "sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==",
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz",
+      "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "caniuse-api": "^3.0.0",
         "cssnano-utils": "^3.1.0",
         "postcss-selector-parser": "^6.0.5"
@@ -8569,12 +8577,12 @@
       }
     },
     "node_modules/postcss-minify-params": {
-      "version": "5.1.3",
-      "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.3.tgz",
-      "integrity": "sha512-bkzpWcjykkqIujNL+EVEPOlLYi/eZ050oImVtHU7b4lFS82jPnsCb44gvC6pxaNt38Els3jWYDHTjHKf0koTgg==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz",
+      "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "cssnano-utils": "^3.1.0",
         "postcss-value-parser": "^4.2.0"
       },
@@ -8747,12 +8755,12 @@
       }
     },
     "node_modules/postcss-normalize-unicode": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz",
-      "integrity": "sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz",
+      "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "postcss-value-parser": "^4.2.0"
       },
       "engines": {
@@ -8810,12 +8818,12 @@
       }
     },
     "node_modules/postcss-reduce-initial": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz",
-      "integrity": "sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz",
+      "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "caniuse-api": "^3.0.0"
       },
       "engines": {
@@ -8942,6 +8950,15 @@
       "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
       "dev": true
     },
+    "node_modules/progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/progress-webpack-plugin": {
       "version": "1.0.16",
       "resolved": "https://registry.npmjs.org/progress-webpack-plugin/-/progress-webpack-plugin-1.0.16.tgz",
@@ -8981,11 +8998,6 @@
         "node": ">= 0.10"
       }
     },
-    "node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
-    },
     "node_modules/pseudomap": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -9120,58 +9132,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/read-pkg-up/node_modules/find-up": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-      "dev": true,
-      "dependencies": {
-        "locate-path": "^5.0.0",
-        "path-exists": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/read-pkg-up/node_modules/locate-path": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
-      "dev": true,
-      "dependencies": {
-        "p-locate": "^4.1.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/read-pkg-up/node_modules/p-limit": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-      "dev": true,
-      "dependencies": {
-        "p-try": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/read-pkg-up/node_modules/p-locate": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
-      "dev": true,
-      "dependencies": {
-        "p-limit": "^2.2.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/read-pkg-up/node_modules/type-fest": {
       "version": "0.8.1",
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
@@ -9238,9 +9198,9 @@
       }
     },
     "node_modules/regenerator-runtime": {
-      "version": "0.13.9",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
-      "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
+      "version": "0.13.10",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz",
+      "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==",
       "dev": true
     },
     "node_modules/regenerator-transform": {
@@ -9477,33 +9437,34 @@
       "dev": true
     },
     "node_modules/sass": {
-      "version": "1.55.0",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
-      "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
+      "version": "1.32.13",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.32.13.tgz",
+      "integrity": "sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==",
       "dev": true,
       "dependencies": {
-        "chokidar": ">=3.0.0 <4.0.0",
-        "immutable": "^4.0.0",
-        "source-map-js": ">=0.6.2 <2.0.0"
+        "chokidar": ">=3.0.0 <4.0.0"
       },
       "bin": {
         "sass": "sass.js"
       },
       "engines": {
-        "node": ">=12.0.0"
+        "node": ">=8.9.0"
       }
     },
     "node_modules/sass-loader": {
-      "version": "13.1.0",
-      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.1.0.tgz",
-      "integrity": "sha512-tZS1RJQ2n2+QNyf3CCAo1H562WjL/5AM6Gi8YcPVVoNxQX8d19mx8E+8fRrMWsyc93ZL6Q8vZDSM0FHVTJaVnQ==",
+      "version": "10.3.1",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.3.1.tgz",
+      "integrity": "sha512-y2aBdtYkbqorVavkC3fcJIUDGIegzDWPn3/LAFhsf3G+MzPKTJx37sROf5pXtUeggSVbNbmfj8TgRaSLMelXRA==",
       "dev": true,
       "dependencies": {
         "klona": "^2.0.4",
-        "neo-async": "^2.6.2"
+        "loader-utils": "^2.0.0",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.0.0",
+        "semver": "^7.3.2"
       },
       "engines": {
-        "node": ">= 14.15.0"
+        "node": ">= 10.13.0"
       },
       "funding": {
         "type": "opencollective",
@@ -9513,8 +9474,7 @@
         "fibers": ">= 3.1.0",
         "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
         "sass": "^1.3.0",
-        "sass-embedded": "*",
-        "webpack": "^5.0.0"
+        "webpack": "^4.36.0 || ^5.0.0"
       },
       "peerDependenciesMeta": {
         "fibers": {
@@ -9525,12 +9485,56 @@
         },
         "sass": {
           "optional": true
-        },
-        "sass-embedded": {
-          "optional": true
         }
       }
     },
+    "node_modules/sass-loader/node_modules/loader-utils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
+      "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
+      "dev": true,
+      "dependencies": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=8.9.0"
+      }
+    },
+    "node_modules/sass-loader/node_modules/schema-utils": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+      "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.8",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/sass-loader/node_modules/semver": {
+      "version": "7.3.8",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+      "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/schema-utils": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -9763,10 +9767,13 @@
       }
     },
     "node_modules/shell-quote": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
-      "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
-      "dev": true
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
+      "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/shelljs": {
       "version": "0.8.5",
@@ -9828,6 +9835,56 @@
         "node": ">=8"
       }
     },
+    "node_modules/slice-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+      "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/slice-ansi/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/slice-ansi/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/slice-ansi/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
     "node_modules/sockjs": {
       "version": "0.3.24",
       "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@@ -9927,6 +9984,12 @@
         "wbuf": "^1.7.3"
       }
     },
+    "node_modules/sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+      "dev": true
+    },
     "node_modules/ssri": {
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@@ -10036,12 +10099,12 @@
       }
     },
     "node_modules/stylehacks": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz",
-      "integrity": "sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
+      "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==",
       "dev": true,
       "dependencies": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "postcss-selector-parser": "^6.0.4"
       },
       "engines": {
@@ -10111,6 +10174,44 @@
         "node": ">= 10"
       }
     },
+    "node_modules/table": {
+      "version": "6.8.1",
+      "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+      "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+      "dev": true,
+      "dependencies": {
+        "ajv": "^8.0.1",
+        "lodash.truncate": "^4.4.2",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/table/node_modules/ajv": {
+      "version": "8.11.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
+      "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/table/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true
+    },
     "node_modules/tapable": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -10330,9 +10431,9 @@
       "dev": true
     },
     "node_modules/tslib": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
-      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
+      "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
       "dev": true
     },
     "node_modules/type-check": {
@@ -10492,6 +10593,12 @@
         "uuid": "dist/bin/uuid"
       }
     },
+    "node_modules/v8-compile-cache": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+      "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+      "dev": true
+    },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -10512,27 +10619,24 @@
       }
     },
     "node_modules/vue": {
-      "version": "2.7.12",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.12.tgz",
-      "integrity": "sha512-yRS44vPsCj6b5IZQHdEYqIwnay8stCnL8RsaVsm5aGtOhka00aoG+3ybaBAELDsXtNlzECe8myb2ukdzn19IOg==",
+      "version": "2.7.13",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.13.tgz",
+      "integrity": "sha512-QnM6ULTNnPmn71eUO+4hdjfBIA3H0GLsBnchnI/kS678tjI45GOUZhXd0oP/gX9isikXz1PAzSnkPspp9EUNfQ==",
       "dependencies": {
-        "@vue/compiler-sfc": "2.7.12",
+        "@vue/compiler-sfc": "2.7.13",
         "csstype": "^3.1.0"
       }
     },
     "node_modules/vue-cli-plugin-vuetify": {
-      "version": "2.5.8",
-      "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.5.8.tgz",
-      "integrity": "sha512-uqi0/URJETJBbWlQHD1l0pnY7JN8Ytu+AL1fw50HFlGByPa8/xx+mq19GkFXA9FcwFT01IqEc/TkxMPugchomg==",
+      "version": "2.4.8",
+      "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.8.tgz",
+      "integrity": "sha512-1e8tVbJNQPLpdJgx8tlsCrFVSKrohLe5axWwolOuMr9k++X1pg95jiqBxYZdhh7tIl9bNh4wzVPPGQzTIpoS+Q==",
       "dev": true,
       "dependencies": {
         "null-loader": "^4.0.1",
         "semver": "^7.1.2",
         "shelljs": "^0.8.3"
       },
-      "peerDependencies": {
-        "webpack": "^4.0.0 || ^5.0.0"
-      },
       "peerDependenciesMeta": {
         "sass-loader": {
           "optional": true
@@ -10558,21 +10662,21 @@
       }
     },
     "node_modules/vue-eslint-parser": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz",
-      "integrity": "sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz",
+      "integrity": "sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==",
       "dev": true,
       "dependencies": {
-        "debug": "^4.3.4",
-        "eslint-scope": "^7.1.1",
-        "eslint-visitor-keys": "^3.3.0",
-        "espree": "^9.3.1",
+        "debug": "^4.3.2",
+        "eslint-scope": "^7.0.0",
+        "eslint-visitor-keys": "^3.1.0",
+        "espree": "^9.0.0",
         "esquery": "^1.4.0",
         "lodash": "^4.17.21",
-        "semver": "^7.3.6"
+        "semver": "^7.3.5"
       },
       "engines": {
-        "node": "^14.17.0 || >=16.0.0"
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/mysticatea"
@@ -10603,6 +10707,23 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/vue-eslint-parser/node_modules/espree": {
+      "version": "9.4.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
+      "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
+      "dev": true,
+      "dependencies": {
+        "acorn": "^8.8.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
     "node_modules/vue-eslint-parser/node_modules/estraverse": {
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
@@ -10634,9 +10755,9 @@
       "dev": true
     },
     "node_modules/vue-loader": {
-      "version": "17.0.0",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz",
-      "integrity": "sha512-OWSXjrzIvbF2LtOUmxT3HYgwwubbfFelN8PAP9R9dwpIkj48TVioHhWWSx7W7fk+iF5cgg3CBJRxwTdtLU4Ecg==",
+      "version": "17.0.1",
+      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.1.tgz",
+      "integrity": "sha512-/OOyugJnImKCkAKrAvdsWMuwoCqGxWT5USLsjohzWbMgOwpA5wQmzQiLMzZd7DjhIfunzAGIApTOgIylz/kwcg==",
       "dev": true,
       "dependencies": {
         "chalk": "^4.1.0",
@@ -10645,6 +10766,14 @@
       },
       "peerDependencies": {
         "webpack": "^4.1.0 || ^5.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/compiler-sfc": {
+          "optional": true
+        },
+        "vue": {
+          "optional": true
+        }
       }
     },
     "node_modules/vue-loader/node_modules/ansi-styles": {
@@ -10748,9 +10877,9 @@
       "dev": true
     },
     "node_modules/vue-template-compiler": {
-      "version": "2.7.12",
-      "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.12.tgz",
-      "integrity": "sha512-6rhJAuo2vRzJMs8X/pd9yqtsJmnPEnv4E0cb9KCu0sfGhoDt8roCCa/6qbrvpc1b38zYgdmY/xrk4qfNWZIjwA==",
+      "version": "2.7.13",
+      "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.13.tgz",
+      "integrity": "sha512-jYM6TClwDS9YqP48gYrtAtaOhRKkbYmbzE+Q51gX5YDr777n7tNI/IZk4QV4l/PjQPNh/FVa/E92sh/RqKMrog==",
       "dev": true,
       "dependencies": {
         "de-indent": "^1.0.2",
@@ -10764,9 +10893,9 @@
       "dev": true
     },
     "node_modules/vuetify": {
-      "version": "2.6.11",
-      "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.11.tgz",
-      "integrity": "sha512-SX6BzS068t/RMj0d/sxxS/LsKdk9FpkL7CWShLyOMmJa/hiG63L0bvcopEaWYdRzzuQt7Nx174sk+KB6SvmYBw==",
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.12.tgz",
+      "integrity": "sha512-qe3hcMpWmT1O15tp+p65lOS7UKZ/hQYQktCsw9iXx2u3RwVbX6GR82gY2iROrKsiAzYDvMgrYxWQwY/pUfkekw==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/johnleider"
@@ -10906,9 +11035,9 @@
       }
     },
     "node_modules/webpack-bundle-analyzer": {
-      "version": "4.6.1",
-      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.6.1.tgz",
-      "integrity": "sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==",
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz",
+      "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==",
       "dev": true,
       "dependencies": {
         "acorn": "^8.0.4",
@@ -11205,9 +11334,9 @@
       }
     },
     "node_modules/webpack-dev-server/node_modules/ws": {
-      "version": "8.9.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz",
-      "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==",
+      "version": "8.10.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
+      "integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
       "dev": true,
       "engines": {
         "node": ">=10.0.0"
@@ -11248,9 +11377,9 @@
       }
     },
     "node_modules/webpack-virtual-modules": {
-      "version": "0.4.5",
-      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.5.tgz",
-      "integrity": "sha512-8bWq0Iluiv9lVf9YaqWQ9+liNgXSHICm+rg544yRgGYaR8yXZTVBaHZkINZSB2yZSWo4b0F6MIxqJezVfOEAlg==",
+      "version": "0.4.6",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz",
+      "integrity": "sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==",
       "dev": true
     },
     "node_modules/webpack/node_modules/@types/estree": {
@@ -11423,15 +11552,6 @@
         }
       }
     },
-    "node_modules/xml-name-validator": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
-      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      }
-    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -11483,18 +11603,6 @@
         "node": ">=10"
       }
     },
-    "node_modules/yocto-queue": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
-      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/yorkie": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/yorkie/-/yorkie-2.0.0.tgz",
@@ -11610,9 +11718,9 @@
   },
   "dependencies": {
     "@achrinza/node-ipc": {
-      "version": "9.2.5",
-      "resolved": "https://registry.npmjs.org/@achrinza/node-ipc/-/node-ipc-9.2.5.tgz",
-      "integrity": "sha512-kBX7Ay911iXZ3VZ1pYltj3Rfu7Ow9H7sK4H4RSfWIfWR2JKNB40K808wppoRIEzE2j2hXLU+r6TJgCAliCGhyQ==",
+      "version": "9.2.6",
+      "resolved": "https://registry.npmjs.org/@achrinza/node-ipc/-/node-ipc-9.2.6.tgz",
+      "integrity": "sha512-ULSIYPy4ZPM301dfCxRz0l2GJjOwIo/PqmWonIu1bLml7UmnVQmH+juJcoyXp6E8gIRRNAjGYftJnNQlfy4vPg==",
       "dev": true,
       "requires": {
         "@node-ipc/js-queue": "2.0.3",
@@ -11640,27 +11748,27 @@
       }
     },
     "@babel/compat-data": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz",
-      "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz",
+      "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==",
       "dev": true
     },
     "@babel/core": {
-      "version": "7.19.3",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz",
-      "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz",
+      "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==",
       "dev": true,
       "requires": {
         "@ampproject/remapping": "^2.1.0",
         "@babel/code-frame": "^7.18.6",
-        "@babel/generator": "^7.19.3",
+        "@babel/generator": "^7.19.6",
         "@babel/helper-compilation-targets": "^7.19.3",
-        "@babel/helper-module-transforms": "^7.19.0",
-        "@babel/helpers": "^7.19.0",
-        "@babel/parser": "^7.19.3",
+        "@babel/helper-module-transforms": "^7.19.6",
+        "@babel/helpers": "^7.19.4",
+        "@babel/parser": "^7.19.6",
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.3",
-        "@babel/types": "^7.19.3",
+        "@babel/traverse": "^7.19.6",
+        "@babel/types": "^7.19.4",
         "convert-source-map": "^1.7.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
@@ -11680,12 +11788,12 @@
       }
     },
     "@babel/generator": {
-      "version": "7.19.5",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz",
-      "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz",
+      "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.19.4",
+        "@babel/types": "^7.20.0",
         "@jridgewell/gen-mapping": "^0.3.2",
         "jsesc": "^2.5.1"
       },
@@ -11723,12 +11831,12 @@
       }
     },
     "@babel/helper-compilation-targets": {
-      "version": "7.19.3",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz",
-      "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz",
+      "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==",
       "dev": true,
       "requires": {
-        "@babel/compat-data": "^7.19.3",
+        "@babel/compat-data": "^7.20.0",
         "@babel/helper-validator-option": "^7.18.6",
         "browserslist": "^4.21.3",
         "semver": "^6.3.0"
@@ -11826,19 +11934,19 @@
       }
     },
     "@babel/helper-module-transforms": {
-      "version": "7.19.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz",
-      "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz",
+      "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==",
       "dev": true,
       "requires": {
         "@babel/helper-environment-visitor": "^7.18.9",
         "@babel/helper-module-imports": "^7.18.6",
-        "@babel/helper-simple-access": "^7.18.6",
+        "@babel/helper-simple-access": "^7.19.4",
         "@babel/helper-split-export-declaration": "^7.18.6",
-        "@babel/helper-validator-identifier": "^7.18.6",
+        "@babel/helper-validator-identifier": "^7.19.1",
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.0",
-        "@babel/types": "^7.19.0"
+        "@babel/traverse": "^7.19.6",
+        "@babel/types": "^7.19.4"
       }
     },
     "@babel/helper-optimise-call-expression": {
@@ -11891,12 +11999,12 @@
       }
     },
     "@babel/helper-skip-transparent-expression-wrappers": {
-      "version": "7.18.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz",
-      "integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz",
+      "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.18.9"
+        "@babel/types": "^7.20.0"
       }
     },
     "@babel/helper-split-export-declaration": {
@@ -11939,14 +12047,14 @@
       }
     },
     "@babel/helpers": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz",
-      "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
+      "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
       "dev": true,
       "requires": {
         "@babel/template": "^7.18.10",
-        "@babel/traverse": "^7.19.4",
-        "@babel/types": "^7.19.4"
+        "@babel/traverse": "^7.20.1",
+        "@babel/types": "^7.20.0"
       }
     },
     "@babel/highlight": {
@@ -11961,9 +12069,9 @@
       }
     },
     "@babel/parser": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz",
-      "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA=="
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz",
+      "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw=="
     },
     "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
       "version": "7.18.6",
@@ -11986,9 +12094,9 @@
       }
     },
     "@babel/plugin-proposal-async-generator-functions": {
-      "version": "7.19.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz",
-      "integrity": "sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz",
+      "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==",
       "dev": true,
       "requires": {
         "@babel/helper-environment-visitor": "^7.18.9",
@@ -12019,9 +12127,9 @@
       }
     },
     "@babel/plugin-proposal-decorators": {
-      "version": "7.19.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.19.3.tgz",
-      "integrity": "sha512-MbgXtNXqo7RTKYIXVchVJGPvaVufQH3pxvQyfbGvNw1DObIhph+PesYXJTcd8J4DdWibvf6Z2eanOyItX8WnJg==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.0.tgz",
+      "integrity": "sha512-vnuRRS20ygSxclEYikHzVrP9nZDFXaSzvJxGLQNAiBX041TmhS4hOUHWNIpq/q4muENuEP9XPJFXTNFejhemkg==",
       "dev": true,
       "requires": {
         "@babel/helper-create-class-features-plugin": "^7.19.0",
@@ -12212,12 +12320,12 @@
       }
     },
     "@babel/plugin-syntax-import-assertions": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz",
-      "integrity": "sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz",
+      "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==",
       "dev": true,
       "requires": {
-        "@babel/helper-plugin-utils": "^7.18.6"
+        "@babel/helper-plugin-utils": "^7.19.0"
       }
     },
     "@babel/plugin-syntax-json-strings": {
@@ -12340,9 +12448,9 @@
       }
     },
     "@babel/plugin-transform-block-scoping": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.19.4.tgz",
-      "integrity": "sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.0.tgz",
+      "integrity": "sha512-sXOohbpHZSk7GjxK9b3dKB7CfqUD5DwOH+DggKzOQ7TXYP+RCSbRykfjQmn/zq+rBjycVRtLf9pYhAaEJA786w==",
       "dev": true,
       "requires": {
         "@babel/helper-plugin-utils": "^7.19.0"
@@ -12375,9 +12483,9 @@
       }
     },
     "@babel/plugin-transform-destructuring": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.19.4.tgz",
-      "integrity": "sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.0.tgz",
+      "integrity": "sha512-1dIhvZfkDVx/zn2S1aFwlruspTt4189j7fEkH0Y0VyuDM6bQt7bD6kLcz3l4IlLG+e5OReaBz9ROAbttRtUHqA==",
       "dev": true,
       "requires": {
         "@babel/helper-plugin-utils": "^7.19.0"
@@ -12451,39 +12559,36 @@
       }
     },
     "@babel/plugin-transform-modules-amd": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz",
-      "integrity": "sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz",
+      "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==",
       "dev": true,
       "requires": {
-        "@babel/helper-module-transforms": "^7.18.6",
-        "@babel/helper-plugin-utils": "^7.18.6",
-        "babel-plugin-dynamic-import-node": "^2.3.3"
+        "@babel/helper-module-transforms": "^7.19.6",
+        "@babel/helper-plugin-utils": "^7.19.0"
       }
     },
     "@babel/plugin-transform-modules-commonjs": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz",
-      "integrity": "sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz",
+      "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==",
       "dev": true,
       "requires": {
-        "@babel/helper-module-transforms": "^7.18.6",
-        "@babel/helper-plugin-utils": "^7.18.6",
-        "@babel/helper-simple-access": "^7.18.6",
-        "babel-plugin-dynamic-import-node": "^2.3.3"
+        "@babel/helper-module-transforms": "^7.19.6",
+        "@babel/helper-plugin-utils": "^7.19.0",
+        "@babel/helper-simple-access": "^7.19.4"
       }
     },
     "@babel/plugin-transform-modules-systemjs": {
-      "version": "7.19.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.0.tgz",
-      "integrity": "sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz",
+      "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==",
       "dev": true,
       "requires": {
         "@babel/helper-hoist-variables": "^7.18.6",
-        "@babel/helper-module-transforms": "^7.19.0",
+        "@babel/helper-module-transforms": "^7.19.6",
         "@babel/helper-plugin-utils": "^7.19.0",
-        "@babel/helper-validator-identifier": "^7.18.6",
-        "babel-plugin-dynamic-import-node": "^2.3.3"
+        "@babel/helper-validator-identifier": "^7.19.1"
       }
     },
     "@babel/plugin-transform-modules-umd": {
@@ -12526,12 +12631,12 @@
       }
     },
     "@babel/plugin-transform-parameters": {
-      "version": "7.18.8",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz",
-      "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.1.tgz",
+      "integrity": "sha512-nDvKLrAvl+kf6BOy1UJ3MGwzzfTMgppxwiD2Jb4LO3xjYyZq30oQzDNJbCQpMdG9+j2IXHoiMrw5Cm/L6ZoxXQ==",
       "dev": true,
       "requires": {
-        "@babel/helper-plugin-utils": "^7.18.6"
+        "@babel/helper-plugin-utils": "^7.19.0"
       }
     },
     "@babel/plugin-transform-property-literals": {
@@ -12563,9 +12668,9 @@
       }
     },
     "@babel/plugin-transform-runtime": {
-      "version": "7.19.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.1.tgz",
-      "integrity": "sha512-2nJjTUFIzBMP/f/miLxEK9vxwW/KUXsdvN4sR//TmuDhe6yU2h57WmIOE12Gng3MDP/xpjUV/ToZRdcf8Yj4fA==",
+      "version": "7.19.6",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz",
+      "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==",
       "dev": true,
       "requires": {
         "@babel/helper-module-imports": "^7.18.6",
@@ -12738,12 +12843,12 @@
       }
     },
     "@babel/runtime": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz",
-      "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz",
+      "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==",
       "dev": true,
       "requires": {
-        "regenerator-runtime": "^0.13.4"
+        "regenerator-runtime": "^0.13.10"
       }
     },
     "@babel/template": {
@@ -12758,27 +12863,27 @@
       }
     },
     "@babel/traverse": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz",
-      "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==",
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
+      "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.18.6",
-        "@babel/generator": "^7.19.4",
+        "@babel/generator": "^7.20.1",
         "@babel/helper-environment-visitor": "^7.18.9",
         "@babel/helper-function-name": "^7.19.0",
         "@babel/helper-hoist-variables": "^7.18.6",
         "@babel/helper-split-export-declaration": "^7.18.6",
-        "@babel/parser": "^7.19.4",
-        "@babel/types": "^7.19.4",
+        "@babel/parser": "^7.20.1",
+        "@babel/types": "^7.20.0",
         "debug": "^4.1.0",
         "globals": "^11.1.0"
       }
     },
     "@babel/types": {
-      "version": "7.19.4",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz",
-      "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==",
+      "version": "7.20.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz",
+      "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==",
       "dev": true,
       "requires": {
         "@babel/helper-string-parser": "^7.19.4",
@@ -12787,19 +12892,19 @@
       }
     },
     "@eslint/eslintrc": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
-      "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+      "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
       "dev": true,
       "requires": {
         "ajv": "^6.12.4",
-        "debug": "^4.3.2",
-        "espree": "^9.4.0",
-        "globals": "^13.15.0",
-        "ignore": "^5.2.0",
+        "debug": "^4.1.1",
+        "espree": "^7.3.0",
+        "globals": "^13.9.0",
+        "ignore": "^4.0.6",
         "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "minimatch": "^3.1.2",
+        "js-yaml": "^3.13.1",
+        "minimatch": "^3.0.4",
         "strip-json-comments": "^3.1.1"
       },
       "dependencies": {
@@ -12836,22 +12941,16 @@
       }
     },
     "@humanwhocodes/config-array": {
-      "version": "0.10.7",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
-      "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+      "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
       "dev": true,
       "requires": {
-        "@humanwhocodes/object-schema": "^1.2.1",
+        "@humanwhocodes/object-schema": "^1.2.0",
         "debug": "^4.1.1",
         "minimatch": "^3.0.4"
       }
     },
-    "@humanwhocodes/module-importer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-      "dev": true
-    },
     "@humanwhocodes/object-schema": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
@@ -12910,9 +13009,9 @@
       "dev": true
     },
     "@jridgewell/trace-mapping": {
-      "version": "0.3.16",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz",
-      "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==",
+      "version": "0.3.17",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
+      "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
       "dev": true,
       "requires": {
         "@jridgewell/resolve-uri": "3.1.0",
@@ -13110,9 +13209,9 @@
       }
     },
     "@types/eslint": {
-      "version": "8.4.6",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
-      "integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==",
+      "version": "8.4.9",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz",
+      "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==",
       "dev": true,
       "requires": {
         "@types/estree": "*",
@@ -13192,9 +13291,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "18.8.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.5.tgz",
-      "integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q==",
+      "version": "18.11.9",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
+      "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
       "dev": true
     },
     "@types/normalize-package-data": {
@@ -13642,9 +13741,9 @@
       }
     },
     "@vue/compiler-sfc": {
-      "version": "2.7.12",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.12.tgz",
-      "integrity": "sha512-7FOITA2+4ND7lMAfegljHBpNSG3X9mVzgQwcS3g928QZM1EADedUw2JLKcgOm1ZEJEkvyDHh6lwa08vrLmoCOA==",
+      "version": "2.7.13",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.13.tgz",
+      "integrity": "sha512-zzu2rLRZlgIU+OT3Atbr7Y6PG+LW4wVQpPfNRrGDH3dM9PsrcVfa+1pKb8bW467bGM3aDOvAnsYLWVpYIv3GRg==",
       "requires": {
         "@babel/parser": "^7.18.4",
         "postcss": "^8.4.14",
@@ -13904,9 +14003,9 @@
       }
     },
     "acorn": {
-      "version": "8.8.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
-      "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
+      "version": "8.8.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
+      "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
       "dev": true
     },
     "acorn-import-assertions": {
@@ -13983,6 +14082,12 @@
       "dev": true,
       "requires": {}
     },
+    "ansi-colors": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+      "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+      "dev": true
+    },
     "ansi-escapes": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
@@ -14033,10 +14138,13 @@
       "dev": true
     },
     "argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
     },
     "array-flatten": {
       "version": "2.1.2",
@@ -14050,6 +14158,12 @@
       "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
       "dev": true
     },
+    "astral-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+      "dev": true
+    },
     "async": {
       "version": "2.6.4",
       "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
@@ -14071,13 +14185,13 @@
       "dev": true
     },
     "autoprefixer": {
-      "version": "10.4.12",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.12.tgz",
-      "integrity": "sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==",
+      "version": "10.4.13",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz",
+      "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==",
       "dev": true,
       "requires": {
         "browserslist": "^4.21.4",
-        "caniuse-lite": "^1.0.30001407",
+        "caniuse-lite": "^1.0.30001426",
         "fraction.js": "^4.2.0",
         "normalize-range": "^0.1.2",
         "picocolors": "^1.0.0",
@@ -14085,19 +14199,18 @@
       }
     },
     "axios": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.2.tgz",
-      "integrity": "sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==",
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
       "requires": {
-        "follow-redirects": "^1.15.0",
-        "form-data": "^4.0.0",
-        "proxy-from-env": "^1.1.0"
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
       }
     },
     "babel-loader": {
-      "version": "8.2.5",
-      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz",
-      "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz",
+      "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==",
       "dev": true,
       "requires": {
         "find-cache-dir": "^3.3.1",
@@ -14370,9 +14483,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30001418",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz",
-      "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==",
+      "version": "1.0.30001429",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz",
+      "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==",
       "dev": true
     },
     "case-sensitive-paths-webpack-plugin": {
@@ -14735,14 +14848,14 @@
       }
     },
     "core-js": {
-      "version": "3.25.5",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.5.tgz",
-      "integrity": "sha512-nbm6eZSjm+ZuBQxCUPQKQCoUEfFOXjUZ8dTTyikyKaWrTYmAVbykQfwsKE5dBK88u3QCkCrzsx/PPlKfhsvgpw=="
+      "version": "3.26.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
+      "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw=="
     },
     "core-js-compat": {
-      "version": "3.25.5",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.5.tgz",
-      "integrity": "sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA==",
+      "version": "3.26.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.0.tgz",
+      "integrity": "sha512-piOX9Go+Z4f9ZiBFLnZ5VrOpBl0h7IGCkiFUN11QTe6LjAvOT3ifL/5TdoizMh99hcGy5SoLyWbapIY/PIb/3A==",
       "dev": true,
       "requires": {
         "browserslist": "^4.21.4"
@@ -14903,36 +15016,36 @@
       "dev": true
     },
     "cssnano": {
-      "version": "5.1.13",
-      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.13.tgz",
-      "integrity": "sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==",
+      "version": "5.1.14",
+      "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz",
+      "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==",
       "dev": true,
       "requires": {
-        "cssnano-preset-default": "^5.2.12",
+        "cssnano-preset-default": "^5.2.13",
         "lilconfig": "^2.0.3",
         "yaml": "^1.10.2"
       }
     },
     "cssnano-preset-default": {
-      "version": "5.2.12",
-      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.12.tgz",
-      "integrity": "sha512-OyCBTZi+PXgylz9HAA5kHyoYhfGcYdwFmyaJzWnzxuGRtnMw/kR6ilW9XzlzlRAtB6PLT/r+prYgkef7hngFew==",
+      "version": "5.2.13",
+      "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz",
+      "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==",
       "dev": true,
       "requires": {
-        "css-declaration-sorter": "^6.3.0",
+        "css-declaration-sorter": "^6.3.1",
         "cssnano-utils": "^3.1.0",
         "postcss-calc": "^8.2.3",
         "postcss-colormin": "^5.3.0",
-        "postcss-convert-values": "^5.1.2",
+        "postcss-convert-values": "^5.1.3",
         "postcss-discard-comments": "^5.1.2",
         "postcss-discard-duplicates": "^5.1.0",
         "postcss-discard-empty": "^5.1.1",
         "postcss-discard-overridden": "^5.1.0",
-        "postcss-merge-longhand": "^5.1.6",
-        "postcss-merge-rules": "^5.1.2",
+        "postcss-merge-longhand": "^5.1.7",
+        "postcss-merge-rules": "^5.1.3",
         "postcss-minify-font-values": "^5.1.0",
         "postcss-minify-gradients": "^5.1.1",
-        "postcss-minify-params": "^5.1.3",
+        "postcss-minify-params": "^5.1.4",
         "postcss-minify-selectors": "^5.2.1",
         "postcss-normalize-charset": "^5.1.0",
         "postcss-normalize-display-values": "^5.1.0",
@@ -14940,11 +15053,11 @@
         "postcss-normalize-repeat-style": "^5.1.1",
         "postcss-normalize-string": "^5.1.0",
         "postcss-normalize-timing-functions": "^5.1.0",
-        "postcss-normalize-unicode": "^5.1.0",
+        "postcss-normalize-unicode": "^5.1.1",
         "postcss-normalize-url": "^5.1.0",
         "postcss-normalize-whitespace": "^5.1.1",
         "postcss-ordered-values": "^5.1.3",
-        "postcss-reduce-initial": "^5.1.0",
+        "postcss-reduce-initial": "^5.1.1",
         "postcss-reduce-transforms": "^5.1.0",
         "postcss-svgo": "^5.1.0",
         "postcss-unique-selectors": "^5.1.1"
@@ -15224,9 +15337,9 @@
       "dev": true
     },
     "electron-to-chromium": {
-      "version": "1.4.279",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.279.tgz",
-      "integrity": "sha512-xs7vEuSZ84+JsHSTFqqG0TE3i8EAivHomRQZhhcRvsmnjsh5C2KdhwNKf4ZRYtzq75wojpFyqb62m32Oam57wA==",
+      "version": "1.4.284",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
+      "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
       "dev": true
     },
     "emoji-regex": {
@@ -15266,6 +15379,15 @@
         "tapable": "^2.2.0"
       }
     },
+    "enquirer": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+      "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "^4.1.1"
+      }
+    },
     "entities": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
@@ -15315,51 +15437,62 @@
       "dev": true
     },
     "eslint": {
-      "version": "8.25.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
-      "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
+      "version": "7.32.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+      "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
       "dev": true,
       "requires": {
-        "@eslint/eslintrc": "^1.3.3",
-        "@humanwhocodes/config-array": "^0.10.5",
-        "@humanwhocodes/module-importer": "^1.0.1",
+        "@babel/code-frame": "7.12.11",
+        "@eslint/eslintrc": "^0.4.3",
+        "@humanwhocodes/config-array": "^0.5.0",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
-        "debug": "^4.3.2",
+        "debug": "^4.0.1",
         "doctrine": "^3.0.0",
+        "enquirer": "^2.3.5",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.1.1",
-        "eslint-utils": "^3.0.0",
-        "eslint-visitor-keys": "^3.3.0",
-        "espree": "^9.4.0",
+        "eslint-scope": "^5.1.1",
+        "eslint-utils": "^2.1.0",
+        "eslint-visitor-keys": "^2.0.0",
+        "espree": "^7.3.1",
         "esquery": "^1.4.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
         "file-entry-cache": "^6.0.1",
-        "find-up": "^5.0.0",
-        "glob-parent": "^6.0.1",
-        "globals": "^13.15.0",
-        "globby": "^11.1.0",
-        "grapheme-splitter": "^1.0.4",
-        "ignore": "^5.2.0",
+        "functional-red-black-tree": "^1.0.1",
+        "glob-parent": "^5.1.2",
+        "globals": "^13.6.0",
+        "ignore": "^4.0.6",
         "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
-        "js-sdsl": "^4.1.4",
-        "js-yaml": "^4.1.0",
+        "js-yaml": "^3.13.1",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.4.1",
         "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.2",
+        "minimatch": "^3.0.4",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.1",
-        "regexpp": "^3.2.0",
-        "strip-ansi": "^6.0.1",
+        "progress": "^2.0.0",
+        "regexpp": "^3.1.0",
+        "semver": "^7.2.1",
+        "strip-ansi": "^6.0.0",
         "strip-json-comments": "^3.1.0",
-        "text-table": "^0.2.0"
+        "table": "^6.0.9",
+        "text-table": "^0.2.0",
+        "v8-compile-cache": "^2.0.3"
       },
       "dependencies": {
+        "@babel/code-frame": {
+          "version": "7.12.11",
+          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+          "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
+          "dev": true,
+          "requires": {
+            "@babel/highlight": "^7.10.4"
+          }
+        },
         "ansi-styles": {
           "version": "4.3.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -15400,43 +15533,39 @@
           "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
           "dev": true
         },
-        "eslint-scope": {
-          "version": "7.1.1",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
-          "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+        "glob-parent": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+          "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
           "dev": true,
           "requires": {
-            "esrecurse": "^4.3.0",
-            "estraverse": "^5.2.0"
+            "is-glob": "^4.0.1"
           }
         },
-        "eslint-visitor-keys": {
-          "version": "3.3.0",
-          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
-          "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
-          "dev": true
-        },
-        "estraverse": {
-          "version": "5.3.0",
-          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
-          "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-          "dev": true
-        },
         "globals": {
           "version": "13.17.0",
           "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
           "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
           "dev": true,
           "requires": {
-            "type-fest": "^0.20.2"
+            "type-fest": "^0.20.2"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "semver": {
+          "version": "7.3.8",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+          "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
           }
         },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true
-        },
         "supports-color": {
           "version": "7.2.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -15471,9 +15600,9 @@
       }
     },
     "eslint-plugin-vue": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.6.0.tgz",
-      "integrity": "sha512-zzySkJgVbFCylnG2+9MDF7N+2Rjze2y0bF8GyUNpFOnT8mCMfqqtLDJkHBuYu9N/psW1A6DVbQhPkP92E+qakA==",
+      "version": "8.7.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz",
+      "integrity": "sha512-28sbtm4l4cOzoO1LtzQPxfxhQABararUb1JtqusQqObJpWX2e/gmVyeYVfepizPFne0Q5cILkYGiBoV36L12Wg==",
       "dev": true,
       "requires": {
         "eslint-utils": "^3.0.0",
@@ -15481,10 +15610,18 @@
         "nth-check": "^2.0.1",
         "postcss-selector-parser": "^6.0.9",
         "semver": "^7.3.5",
-        "vue-eslint-parser": "^9.0.1",
-        "xml-name-validator": "^4.0.0"
+        "vue-eslint-parser": "^8.0.1"
       },
       "dependencies": {
+        "eslint-utils": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+          "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+          "dev": true,
+          "requires": {
+            "eslint-visitor-keys": "^2.0.0"
+          }
+        },
         "semver": {
           "version": "7.3.8",
           "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -15507,12 +15644,20 @@
       }
     },
     "eslint-utils": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
-      "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+      "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
       "dev": true,
       "requires": {
-        "eslint-visitor-keys": "^2.0.0"
+        "eslint-visitor-keys": "^1.1.0"
+      },
+      "dependencies": {
+        "eslint-visitor-keys": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+          "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+          "dev": true
+        }
       }
     },
     "eslint-visitor-keys": {
@@ -15602,24 +15747,36 @@
       }
     },
     "espree": {
-      "version": "9.4.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
-      "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+      "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
       "dev": true,
       "requires": {
-        "acorn": "^8.8.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^3.3.0"
+        "acorn": "^7.4.0",
+        "acorn-jsx": "^5.3.1",
+        "eslint-visitor-keys": "^1.3.0"
       },
       "dependencies": {
+        "acorn": {
+          "version": "7.4.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+          "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+          "dev": true
+        },
         "eslint-visitor-keys": {
-          "version": "3.3.0",
-          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
-          "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+          "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
           "dev": true
         }
       }
     },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true
+    },
     "esquery": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
@@ -15989,12 +16146,12 @@
       }
     },
     "find-up": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
-      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
       "dev": true,
       "requires": {
-        "locate-path": "^6.0.0",
+        "locate-path": "^5.0.0",
         "path-exists": "^4.0.0"
       }
     },
@@ -16084,6 +16241,12 @@
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+      "dev": true
+    },
     "gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -16163,6 +16326,14 @@
         "ignore": "^5.2.0",
         "merge2": "^1.4.1",
         "slash": "^3.0.0"
+      },
+      "dependencies": {
+        "ignore": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
+          "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
+          "dev": true
+        }
       }
     },
     "graceful-fs": {
@@ -16171,12 +16342,6 @@
       "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
       "dev": true
     },
-    "grapheme-splitter": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
-      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
-      "dev": true
-    },
     "gzip-size": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -16420,15 +16585,9 @@
       "dev": true
     },
     "ignore": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
-      "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
-      "dev": true
-    },
-    "immutable": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
-      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+      "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
       "dev": true
     },
     "import-fresh": {
@@ -16500,9 +16659,9 @@
       }
     },
     "is-core-module": {
-      "version": "2.10.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
-      "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==",
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+      "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
       "dev": true,
       "requires": {
         "has": "^1.0.3"
@@ -16645,9 +16804,9 @@
       }
     },
     "joi": {
-      "version": "17.6.3",
-      "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.3.tgz",
-      "integrity": "sha512-YlQsIaS9MHYekzf1Qe11LjTkNzx9qhYluK3172z38RxYoAUf82XMX1p1DG1H4Wtk2ED/vPdSn9OggqtDu+aTow==",
+      "version": "17.7.0",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz",
+      "integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==",
       "dev": true,
       "requires": {
         "@hapi/hoek": "^9.0.0",
@@ -16663,12 +16822,6 @@
       "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
       "dev": true
     },
-    "js-sdsl": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz",
-      "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==",
-      "dev": true
-    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -16676,12 +16829,13 @@
       "dev": true
     },
     "js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "version": "3.14.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+      "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
       "dev": true,
       "requires": {
-        "argparse": "^2.0.1"
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
       }
     },
     "jsesc": {
@@ -16812,12 +16966,12 @@
       }
     },
     "locate-path": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
-      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
       "dev": true,
       "requires": {
-        "p-locate": "^5.0.0"
+        "p-locate": "^4.1.0"
       }
     },
     "lodash": {
@@ -16862,6 +17016,12 @@
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
       "dev": true
     },
+    "lodash.truncate": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+      "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+      "dev": true
+    },
     "lodash.uniq": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -17057,9 +17217,9 @@
       "dev": true
     },
     "memfs": {
-      "version": "3.4.7",
-      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz",
-      "integrity": "sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==",
+      "version": "3.4.9",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.9.tgz",
+      "integrity": "sha512-3rm8kbrzpUGRyPKSGuk387NZOwQ90O4rI9tsWQkzNW7BLSnKGp23RsEsKK8N8QVCrtJoAMqy3spxHC4os4G6PQ==",
       "dev": true,
       "requires": {
         "fs-monkey": "^1.0.3"
@@ -17594,21 +17754,21 @@
       "dev": true
     },
     "p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
       "dev": true,
       "requires": {
-        "yocto-queue": "^0.1.0"
+        "p-try": "^2.0.0"
       }
     },
     "p-locate": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
-      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
       "dev": true,
       "requires": {
-        "p-limit": "^3.0.2"
+        "p-limit": "^2.2.0"
       }
     },
     "p-retry": {
@@ -17751,45 +17911,6 @@
       "dev": true,
       "requires": {
         "find-up": "^4.0.0"
-      },
-      "dependencies": {
-        "find-up": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-          "dev": true,
-          "requires": {
-            "locate-path": "^5.0.0",
-            "path-exists": "^4.0.0"
-          }
-        },
-        "locate-path": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
-          "dev": true,
-          "requires": {
-            "p-locate": "^4.1.0"
-          }
-        },
-        "p-limit": {
-          "version": "2.3.0",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-          "dev": true,
-          "requires": {
-            "p-try": "^2.0.0"
-          }
-        },
-        "p-locate": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
-          "dev": true,
-          "requires": {
-            "p-limit": "^2.2.0"
-          }
-        }
       }
     },
     "portfinder": {
@@ -17847,12 +17968,12 @@
       }
     },
     "postcss-convert-values": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz",
-      "integrity": "sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==",
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz",
+      "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.20.3",
+        "browserslist": "^4.21.4",
         "postcss-value-parser": "^4.2.0"
       }
     },
@@ -17907,22 +18028,22 @@
       }
     },
     "postcss-merge-longhand": {
-      "version": "5.1.6",
-      "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.6.tgz",
-      "integrity": "sha512-6C/UGF/3T5OE2CEbOuX7iNO63dnvqhGZeUnKkDeifebY0XqkkvrctYSZurpNE902LDf2yKwwPFgotnfSoPhQiw==",
+      "version": "5.1.7",
+      "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz",
+      "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==",
       "dev": true,
       "requires": {
         "postcss-value-parser": "^4.2.0",
-        "stylehacks": "^5.1.0"
+        "stylehacks": "^5.1.1"
       }
     },
     "postcss-merge-rules": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz",
-      "integrity": "sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==",
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz",
+      "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "caniuse-api": "^3.0.0",
         "cssnano-utils": "^3.1.0",
         "postcss-selector-parser": "^6.0.5"
@@ -17949,12 +18070,12 @@
       }
     },
     "postcss-minify-params": {
-      "version": "5.1.3",
-      "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.3.tgz",
-      "integrity": "sha512-bkzpWcjykkqIujNL+EVEPOlLYi/eZ050oImVtHU7b4lFS82jPnsCb44gvC6pxaNt38Els3jWYDHTjHKf0koTgg==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz",
+      "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "cssnano-utils": "^3.1.0",
         "postcss-value-parser": "^4.2.0"
       }
@@ -18057,12 +18178,12 @@
       }
     },
     "postcss-normalize-unicode": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz",
-      "integrity": "sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz",
+      "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "postcss-value-parser": "^4.2.0"
       }
     },
@@ -18096,12 +18217,12 @@
       }
     },
     "postcss-reduce-initial": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz",
-      "integrity": "sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz",
+      "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "caniuse-api": "^3.0.0"
       }
     },
@@ -18186,6 +18307,12 @@
       "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
       "dev": true
     },
+    "progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true
+    },
     "progress-webpack-plugin": {
       "version": "1.0.16",
       "resolved": "https://registry.npmjs.org/progress-webpack-plugin/-/progress-webpack-plugin-1.0.16.tgz",
@@ -18215,11 +18342,6 @@
         }
       }
     },
-    "proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
-    },
     "pseudomap": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -18315,43 +18437,6 @@
         "type-fest": "^0.8.1"
       },
       "dependencies": {
-        "find-up": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-          "dev": true,
-          "requires": {
-            "locate-path": "^5.0.0",
-            "path-exists": "^4.0.0"
-          }
-        },
-        "locate-path": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
-          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
-          "dev": true,
-          "requires": {
-            "p-locate": "^4.1.0"
-          }
-        },
-        "p-limit": {
-          "version": "2.3.0",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-          "dev": true,
-          "requires": {
-            "p-try": "^2.0.0"
-          }
-        },
-        "p-locate": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
-          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
-          "dev": true,
-          "requires": {
-            "p-limit": "^2.2.0"
-          }
-        },
         "type-fest": {
           "version": "0.8.1",
           "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
@@ -18405,9 +18490,9 @@
       }
     },
     "regenerator-runtime": {
-      "version": "0.13.9",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
-      "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
+      "version": "0.13.10",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz",
+      "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==",
       "dev": true
     },
     "regenerator-transform": {
@@ -18569,24 +18654,58 @@
       "dev": true
     },
     "sass": {
-      "version": "1.55.0",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
-      "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
+      "version": "1.32.13",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.32.13.tgz",
+      "integrity": "sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==",
       "dev": true,
       "requires": {
-        "chokidar": ">=3.0.0 <4.0.0",
-        "immutable": "^4.0.0",
-        "source-map-js": ">=0.6.2 <2.0.0"
+        "chokidar": ">=3.0.0 <4.0.0"
       }
     },
     "sass-loader": {
-      "version": "13.1.0",
-      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.1.0.tgz",
-      "integrity": "sha512-tZS1RJQ2n2+QNyf3CCAo1H562WjL/5AM6Gi8YcPVVoNxQX8d19mx8E+8fRrMWsyc93ZL6Q8vZDSM0FHVTJaVnQ==",
+      "version": "10.3.1",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.3.1.tgz",
+      "integrity": "sha512-y2aBdtYkbqorVavkC3fcJIUDGIegzDWPn3/LAFhsf3G+MzPKTJx37sROf5pXtUeggSVbNbmfj8TgRaSLMelXRA==",
       "dev": true,
       "requires": {
         "klona": "^2.0.4",
-        "neo-async": "^2.6.2"
+        "loader-utils": "^2.0.0",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.0.0",
+        "semver": "^7.3.2"
+      },
+      "dependencies": {
+        "loader-utils": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
+          "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
+          "dev": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
+        "schema-utils": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+          "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+          "dev": true,
+          "requires": {
+            "@types/json-schema": "^7.0.8",
+            "ajv": "^6.12.5",
+            "ajv-keywords": "^3.5.2"
+          }
+        },
+        "semver": {
+          "version": "7.3.8",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+          "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        }
       }
     },
     "schema-utils": {
@@ -18787,9 +18906,9 @@
       "dev": true
     },
     "shell-quote": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
-      "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
+      "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
       "dev": true
     },
     "shelljs": {
@@ -18837,6 +18956,43 @@
       "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
       "dev": true
     },
+    "slice-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+      "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        }
+      }
+    },
     "sockjs": {
       "version": "0.3.24",
       "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@@ -18927,6 +19083,12 @@
         "wbuf": "^1.7.3"
       }
     },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+      "dev": true
+    },
     "ssri": {
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@@ -19008,12 +19170,12 @@
       "dev": true
     },
     "stylehacks": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz",
-      "integrity": "sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
+      "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.16.6",
+        "browserslist": "^4.21.4",
         "postcss-selector-parser": "^6.0.4"
       }
     },
@@ -19061,6 +19223,39 @@
         }
       }
     },
+    "table": {
+      "version": "6.8.1",
+      "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+      "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+      "dev": true,
+      "requires": {
+        "ajv": "^8.0.1",
+        "lodash.truncate": "^4.4.2",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "8.11.0",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
+          "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "json-schema-traverse": "^1.0.0",
+            "require-from-string": "^2.0.2",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+          "dev": true
+        }
+      }
+    },
     "tapable": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -19214,9 +19409,9 @@
       "dev": true
     },
     "tslib": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
-      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
+      "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
       "dev": true
     },
     "type-check": {
@@ -19327,6 +19522,12 @@
       "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
       "dev": true
     },
+    "v8-compile-cache": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+      "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+      "dev": true
+    },
     "validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -19344,18 +19545,18 @@
       "dev": true
     },
     "vue": {
-      "version": "2.7.12",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.12.tgz",
-      "integrity": "sha512-yRS44vPsCj6b5IZQHdEYqIwnay8stCnL8RsaVsm5aGtOhka00aoG+3ybaBAELDsXtNlzECe8myb2ukdzn19IOg==",
+      "version": "2.7.13",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.13.tgz",
+      "integrity": "sha512-QnM6ULTNnPmn71eUO+4hdjfBIA3H0GLsBnchnI/kS678tjI45GOUZhXd0oP/gX9isikXz1PAzSnkPspp9EUNfQ==",
       "requires": {
-        "@vue/compiler-sfc": "2.7.12",
+        "@vue/compiler-sfc": "2.7.13",
         "csstype": "^3.1.0"
       }
     },
     "vue-cli-plugin-vuetify": {
-      "version": "2.5.8",
-      "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.5.8.tgz",
-      "integrity": "sha512-uqi0/URJETJBbWlQHD1l0pnY7JN8Ytu+AL1fw50HFlGByPa8/xx+mq19GkFXA9FcwFT01IqEc/TkxMPugchomg==",
+      "version": "2.4.8",
+      "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.8.tgz",
+      "integrity": "sha512-1e8tVbJNQPLpdJgx8tlsCrFVSKrohLe5axWwolOuMr9k++X1pg95jiqBxYZdhh7tIl9bNh4wzVPPGQzTIpoS+Q==",
       "dev": true,
       "requires": {
         "null-loader": "^4.0.1",
@@ -19375,18 +19576,18 @@
       }
     },
     "vue-eslint-parser": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.1.0.tgz",
-      "integrity": "sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz",
+      "integrity": "sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==",
       "dev": true,
       "requires": {
-        "debug": "^4.3.4",
-        "eslint-scope": "^7.1.1",
-        "eslint-visitor-keys": "^3.3.0",
-        "espree": "^9.3.1",
+        "debug": "^4.3.2",
+        "eslint-scope": "^7.0.0",
+        "eslint-visitor-keys": "^3.1.0",
+        "espree": "^9.0.0",
         "esquery": "^1.4.0",
         "lodash": "^4.17.21",
-        "semver": "^7.3.6"
+        "semver": "^7.3.5"
       },
       "dependencies": {
         "eslint-scope": {
@@ -19405,6 +19606,17 @@
           "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
           "dev": true
         },
+        "espree": {
+          "version": "9.4.0",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
+          "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
+          "dev": true,
+          "requires": {
+            "acorn": "^8.8.0",
+            "acorn-jsx": "^5.3.2",
+            "eslint-visitor-keys": "^3.3.0"
+          }
+        },
         "estraverse": {
           "version": "5.3.0",
           "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
@@ -19429,9 +19641,9 @@
       "dev": true
     },
     "vue-loader": {
-      "version": "17.0.0",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.0.tgz",
-      "integrity": "sha512-OWSXjrzIvbF2LtOUmxT3HYgwwubbfFelN8PAP9R9dwpIkj48TVioHhWWSx7W7fk+iF5cgg3CBJRxwTdtLU4Ecg==",
+      "version": "17.0.1",
+      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.0.1.tgz",
+      "integrity": "sha512-/OOyugJnImKCkAKrAvdsWMuwoCqGxWT5USLsjohzWbMgOwpA5wQmzQiLMzZd7DjhIfunzAGIApTOgIylz/kwcg==",
       "dev": true,
       "requires": {
         "chalk": "^4.1.0",
@@ -19520,9 +19732,9 @@
       }
     },
     "vue-template-compiler": {
-      "version": "2.7.12",
-      "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.12.tgz",
-      "integrity": "sha512-6rhJAuo2vRzJMs8X/pd9yqtsJmnPEnv4E0cb9KCu0sfGhoDt8roCCa/6qbrvpc1b38zYgdmY/xrk4qfNWZIjwA==",
+      "version": "2.7.13",
+      "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.13.tgz",
+      "integrity": "sha512-jYM6TClwDS9YqP48gYrtAtaOhRKkbYmbzE+Q51gX5YDr777n7tNI/IZk4QV4l/PjQPNh/FVa/E92sh/RqKMrog==",
       "dev": true,
       "requires": {
         "de-indent": "^1.0.2",
@@ -19536,9 +19748,9 @@
       "dev": true
     },
     "vuetify": {
-      "version": "2.6.11",
-      "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.11.tgz",
-      "integrity": "sha512-SX6BzS068t/RMj0d/sxxS/LsKdk9FpkL7CWShLyOMmJa/hiG63L0bvcopEaWYdRzzuQt7Nx174sk+KB6SvmYBw==",
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.12.tgz",
+      "integrity": "sha512-qe3hcMpWmT1O15tp+p65lOS7UKZ/hQYQktCsw9iXx2u3RwVbX6GR82gY2iROrKsiAzYDvMgrYxWQwY/pUfkekw==",
       "requires": {}
     },
     "vuetify-loader": {
@@ -19653,9 +19865,9 @@
       }
     },
     "webpack-bundle-analyzer": {
-      "version": "4.6.1",
-      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.6.1.tgz",
-      "integrity": "sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==",
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz",
+      "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==",
       "dev": true,
       "requires": {
         "acorn": "^8.0.4",
@@ -19867,9 +20079,9 @@
           }
         },
         "ws": {
-          "version": "8.9.0",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz",
-          "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==",
+          "version": "8.10.0",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
+          "integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
           "dev": true,
           "requires": {}
         }
@@ -19892,9 +20104,9 @@
       "dev": true
     },
     "webpack-virtual-modules": {
-      "version": "0.4.5",
-      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.5.tgz",
-      "integrity": "sha512-8bWq0Iluiv9lVf9YaqWQ9+liNgXSHICm+rg544yRgGYaR8yXZTVBaHZkINZSB2yZSWo4b0F6MIxqJezVfOEAlg==",
+      "version": "0.4.6",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz",
+      "integrity": "sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==",
       "dev": true
     },
     "websocket-driver": {
@@ -20001,12 +20213,6 @@
       "dev": true,
       "requires": {}
     },
-    "xml-name-validator": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
-      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
-      "dev": true
-    },
     "y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -20046,12 +20252,6 @@
       "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
       "dev": true
     },
-    "yocto-queue": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
-      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
-      "dev": true
-    },
     "yorkie": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/yorkie/-/yorkie-2.0.0.tgz",
diff --git a/web/package.json b/web/package.json
index efae3705d..7d9d04b23 100644
--- a/web/package.json
+++ b/web/package.json
@@ -8,26 +8,26 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "axios": "^1.1.2",
-    "core-js": "^3.25.5",
-    "vue": "^2.7.12",
-    "vuetify": "^2.6.11"
+    "axios": "^0.27.2",
+    "core-js": "^3.8.3",
+    "vue": "^2.6.14",
+    "vuetify": "^2.6.10"
   },
   "devDependencies": {
-    "@babel/core": "^7.19.3",
-    "@babel/eslint-parser": "^7.19.1",
-    "@vue/cli-plugin-babel": "~5.0.8",
-    "@vue/cli-plugin-eslint": "~5.0.8",
-    "@vue/cli-service": "~5.0.8",
-    "eslint": "^8.25.0",
-    "eslint-config-prettier": "^8.5.0",
-    "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.6.0",
-    "prettier": "^2.7.1",
-    "sass": "~1.55.0",
-    "sass-loader": "^13.1.0",
-    "vue-cli-plugin-vuetify": "~2.5.8",
-    "vue-template-compiler": "^2.7.12",
-    "vuetify-loader": "^1.9.2"
+    "@babel/core": "^7.12.16",
+    "@babel/eslint-parser": "^7.12.16",
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@vue/cli-service": "~5.0.0",
+    "eslint": "^7.32.0",
+    "eslint-config-prettier": "^8.3.0",
+    "eslint-plugin-prettier": "^4.0.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "prettier": "^2.4.1",
+    "sass": "~1.32.0",
+    "sass-loader": "^10.0.0",
+    "vue-cli-plugin-vuetify": "~2.4.8",
+    "vue-template-compiler": "^2.6.14",
+    "vuetify-loader": "^1.7.0"
   }
-}
+}
\ No newline at end of file


Test patch:
diff --git a/turbinia/api/api_server_test.py b/turbinia/api/api_server_test.py
index 73232d467..e8eeb6428 100644
--- a/turbinia/api/api_server_test.py
+++ b/turbinia/api/api_server_test.py
@@ -14,16 +14,22 @@
 # limitations under the License.
 """Turbinia API server unit tests."""
 
+import importlib
+
+from collections import OrderedDict
+
 import unittest
 import json
-import fakeredis
 import os
+import fakeredis
 import mock
-import importlib
 
 from fastapi.testclient import TestClient
 
 from turbinia.api.api_server import app
+from turbinia.api.routes.router import api_router
+from turbinia.api.routes.ui import ui_router
+
 from turbinia import config as turbinia_config
 from turbinia import state_manager
 from turbinia.jobs import manager as jobs_manager
@@ -54,17 +60,17 @@ class testTurbiniaAPIServer(unittest.TestCase):
   }
 
   _REQUEST_TEST_DATA = {
-      'request_id': '41483253079448e59685d88f37ab91f7',
+      'failed_tasks': 0,
+      'last_task_update_time': '2022-04-01T19:17:14.791074Z',
+      'queued_tasks': 0,
       'reason': None,
-      'tasks': [],
+      'request_id': '41483253079448e59685d88f37ab91f7',
       'requester': 'root',
-      'last_task_update_time': '2022-04-01T19:17:14.791074Z',
+      'running_tasks': 0,
       'status': 'successful',
-      'task_count': 1,
       'successful_tasks': 1,
-      'running_tasks': 0,
-      'failed_tasks': 0,
-      'queued_tasks': 0
+      'task_count': 1,
+      'tasks': []
   }
 
   def _get_state_manager(self):
@@ -79,11 +85,17 @@ def setUp(self):
     self.client = TestClient(app)
     self.state_manager = self._get_state_manager()
 
-  def testReadRoot(self):
-    """Test root route."""
-    response = self.client.get("/")
-    self.assertEqual(response.status_code, 404)
-    self.assertEqual(response.json(), {"detail": "Not Found"})
+  def testWebRoutes(self):
+    """Test Web UI routes."""
+    ui_routes = ui_router.routes
+    for route in ui_routes:
+      self.assertIn(route, self.client.app.routes)
+
+  def testAPIroutes(self):
+    """Test API server routes."""
+    api_routes = api_router.routes
+    for route in api_routes:
+      self.assertIn(route, self.client.app.routes)
 
   def testGetConfig(self):
     """Test getting current Turbinia server config."""
@@ -120,11 +132,11 @@ def testGetTaskStatus(self, testTaskData):
     """Test getting task status."""
     redis_client = fakeredis.FakeStrictRedis()
     input_task = TurbiniaTask().deserialize(self._TASK_TEST_DATA)
-    expected_result = input_task.serialize()
+    expected_result_dict = OrderedDict(sorted(input_task.serialize().items()))
+    expected_result_str = json.dumps(expected_result_dict)
 
     redis_client.set(
-        'TurbiniaTask:41483253079448e59685d88f37ab91f7',
-        json.dumps(expected_result))
+        'TurbiniaTask:41483253079448e59685d88f37ab91f7', expected_result_str)
 
     testTaskData.return_value = [
         json.loads(
@@ -134,7 +146,7 @@ def testGetTaskStatus(self, testTaskData):
     result = self.client.get(
         '/api/task/{}'.format(self._TASK_TEST_DATA.get('id')))
     result = json.loads(result.content)
-    self.assertEqual(expected_result, result)
+    self.assertEqual(expected_result_dict, result)
 
   @mock.patch('turbinia.state_manager.RedisStateManager.get_task_data')
   def testRequestStatus(self, testTaskData):
@@ -144,6 +156,7 @@ def testRequestStatus(self, testTaskData):
     input_task_serialized = input_task.serialize()
     expected_result = self._REQUEST_TEST_DATA.copy()
     expected_result['tasks'] = [input_task_serialized]
+    expected_result_dict = OrderedDict(sorted(expected_result.items()))
 
     redis_client.set(
         'TurbiniaTask:41483253079448e59685d88f37ab91f7',
@@ -156,7 +169,7 @@ def testRequestStatus(self, testTaskData):
     result = self.client.get(
         '/api/request/{}'.format(self._REQUEST_TEST_DATA.get('request_id')))
     result = json.loads(result.content)
-    self.assertEqual(expected_result, result)
+    self.assertEqual(expected_result_dict, result)
 
   @mock.patch('turbinia.state_manager.RedisStateManager.get_task_data')
   def testRequestSummary(self, testTaskData):
diff --git a/turbinia/api/cli/turbinia_client/turbiniacli_test.py b/turbinia/api/cli/turbinia_client/turbiniacli_test.py
new file mode 100644
index 000000000..8409e8a82
--- /dev/null
+++ b/turbinia/api/cli/turbinia_client/turbiniacli_test.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# Copyright 2022 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Turbinia API client command-line tool."""
+
+import unittest
+import mock
+
+from turbinia_client.turbiniacli import TurbiniaCli
+from fastapi.testclient import TestClient
+
+from turbinia.api.api_server import app
+
+
+class TestTurbiniaCli(unittest.TestCase):
+  """Turbinia-client cli tool tests."""
+
+  def setUp(self) -> None:
+    """Sets up the client for the tests."""
+    self.api_client = TestClient(app)
+    self.client = TurbiniaCli(config_instance='default', config_path='../')
+
+  def testInitialization(self) -> None:
+    """Tests if the client was initialized properly."""
+    self.assertIsInstance(self.client, TurbiniaCli)
+
+  def testReadConfiguration(self) -> None:
+    """Tests the client reads the configuration file properly."""
+    self.assertEqual(self.client.config_dict, {})
+    self.client.read_api_configuration()
+    self.assertIn('API_SERVER_ADDRESS', self.client.config_dict)
+    self.assertIn('API_SERVER_PORT', self.client.config_dict)
+    self.assertIn('API_AUTHENTICATION_ENABLED', self.client.config_dict)
+    self.assertIn('CLIENT_SECRETS_FILENAME', self.client.config_dict)
+    self.assertIn('CREDENTIALS_FILENAME', self.client.config_dict)
+
+  @mock.patch(
+      'turbinia_api_client.api.turbinia_configuration_api.TurbiniaConfigurationApi.get_evidence_types'
+  )
+  def testGetEvidenceArguments(self, mock_response) -> None:
+    """Tests the get_evidence_arguments method."""
+    test_response = self.api_client.get('/api/config/evidence')
+    mock_response.return_value = test_response
+    api_response = self.client.get_evidence_arguments()
+    self.assertEqual(test_response, api_response)
+
+  @mock.patch(
+      'turbinia_api_client.api.turbinia_configuration_api.TurbiniaConfigurationApi.get_request_options'
+  )
+  def testGetRequestOptions(self, mock_response) -> None:
+    """Tests the get_request_options method."""
+    test_response = self.api_client.get('/api/config/request_options')
+    mock_response.return_value = test_response
+    api_response = self.client.get_request_options()
+    self.assertEqual(test_response, api_response)
diff --git a/turbinia/api/client/test-requirements.txt b/turbinia/api/client/test-requirements.txt
new file mode 100644
index 000000000..bb4f22bb7
--- /dev/null
+++ b/turbinia/api/client/test-requirements.txt
@@ -0,0 +1,1 @@
+pytest-cov>=2.8.1
diff --git a/turbinia/api/client/test/__init__.py b/turbinia/api/client/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/turbinia/api/client/test/test_base_request_options.py b/turbinia/api/client/test/test_base_request_options.py
new file mode 100644
index 000000000..bd87db442
--- /dev/null
+++ b/turbinia/api/client/test/test_base_request_options.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import sys
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.model.base_request_options import BaseRequestOptions
+
+
+class TestBaseRequestOptions(unittest.TestCase):
+    """BaseRequestOptions unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testBaseRequestOptions(self):
+        """Test BaseRequestOptions"""
+        # FIXME: construct object with mandatory attributes with example values
+        # model = BaseRequestOptions()  # noqa: E501
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_http_validation_error.py b/turbinia/api/client/test/test_http_validation_error.py
new file mode 100644
index 000000000..0f8bda5cc
--- /dev/null
+++ b/turbinia/api/client/test/test_http_validation_error.py
@@ -0,0 +1,37 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import sys
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.model.validation_error import ValidationError
+globals()['ValidationError'] = ValidationError
+from turbinia_api_client.model.http_validation_error import HTTPValidationError
+
+
+class TestHTTPValidationError(unittest.TestCase):
+    """HTTPValidationError unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testHTTPValidationError(self):
+        """Test HTTPValidationError"""
+        # FIXME: construct object with mandatory attributes with example values
+        # model = HTTPValidationError()  # noqa: E501
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_logs_api.py b/turbinia/api/client/test/test_logs_api.py
new file mode 100644
index 000000000..64664504b
--- /dev/null
+++ b/turbinia/api/client/test/test_logs_api.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.logs_api import LogsApi  # noqa: E501
+
+
+class TestLogsApi(unittest.TestCase):
+    """LogsApi unit test stubs"""
+
+    def setUp(self):
+        self.api = LogsApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_get_logs(self):
+        """Test case for get_logs
+
+        Get Logs  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_open_api_specification_api.py b/turbinia/api/client/test/test_open_api_specification_api.py
new file mode 100644
index 000000000..d75074c81
--- /dev/null
+++ b/turbinia/api/client/test/test_open_api_specification_api.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.open_api_specification_api import OpenAPISpecificationApi  # noqa: E501
+
+
+class TestOpenAPISpecificationApi(unittest.TestCase):
+    """OpenAPISpecificationApi unit test stubs"""
+
+    def setUp(self):
+        self.api = OpenAPISpecificationApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_read_openapi_yaml_docs_openapi_yaml_get(self):
+        """Test case for read_openapi_yaml_docs_openapi_yaml_get
+
+        Read Openapi Yaml  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_request.py b/turbinia/api/client/test/test_request.py
new file mode 100644
index 000000000..e587786ad
--- /dev/null
+++ b/turbinia/api/client/test/test_request.py
@@ -0,0 +1,37 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import sys
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.model.base_request_options import BaseRequestOptions
+globals()['BaseRequestOptions'] = BaseRequestOptions
+from turbinia_api_client.model.request import Request
+
+
+class TestRequest(unittest.TestCase):
+    """Request unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testRequest(self):
+        """Test Request"""
+        # FIXME: construct object with mandatory attributes with example values
+        # model = Request()  # noqa: E501
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_request_status.py b/turbinia/api/client/test/test_request_status.py
new file mode 100644
index 000000000..136825dab
--- /dev/null
+++ b/turbinia/api/client/test/test_request_status.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import sys
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.model.request_status import RequestStatus
+
+
+class TestRequestStatus(unittest.TestCase):
+    """RequestStatus unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testRequestStatus(self):
+        """Test RequestStatus"""
+        # FIXME: construct object with mandatory attributes with example values
+        # model = RequestStatus()  # noqa: E501
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_requests_summary.py b/turbinia/api/client/test/test_requests_summary.py
new file mode 100644
index 000000000..044334ecc
--- /dev/null
+++ b/turbinia/api/client/test/test_requests_summary.py
@@ -0,0 +1,37 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import sys
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.model.request_status import RequestStatus
+globals()['RequestStatus'] = RequestStatus
+from turbinia_api_client.model.requests_summary import RequestsSummary
+
+
+class TestRequestsSummary(unittest.TestCase):
+    """RequestsSummary unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testRequestsSummary(self):
+        """Test RequestsSummary"""
+        # FIXME: construct object with mandatory attributes with example values
+        # model = RequestsSummary()  # noqa: E501
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_turbinia_configuration_api.py b/turbinia/api/client/test/test_turbinia_configuration_api.py
new file mode 100644
index 000000000..860672b67
--- /dev/null
+++ b/turbinia/api/client/test/test_turbinia_configuration_api.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.turbinia_configuration_api import TurbiniaConfigurationApi  # noqa: E501
+
+
+class TestTurbiniaConfigurationApi(unittest.TestCase):
+    """TurbiniaConfigurationApi unit test stubs"""
+
+    def setUp(self):
+        self.api = TurbiniaConfigurationApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_read_config(self):
+        """Test case for read_config
+
+        Read Config  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_turbinia_jobs_api.py b/turbinia/api/client/test/test_turbinia_jobs_api.py
new file mode 100644
index 000000000..19a578d40
--- /dev/null
+++ b/turbinia/api/client/test/test_turbinia_jobs_api.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.turbinia_jobs_api import TurbiniaJobsApi  # noqa: E501
+
+
+class TestTurbiniaJobsApi(unittest.TestCase):
+    """TurbiniaJobsApi unit test stubs"""
+
+    def setUp(self):
+        self.api = TurbiniaJobsApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_read_jobs(self):
+        """Test case for read_jobs
+
+        Read Jobs  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_turbinia_logs_api.py b/turbinia/api/client/test/test_turbinia_logs_api.py
new file mode 100644
index 000000000..032db8c6b
--- /dev/null
+++ b/turbinia/api/client/test/test_turbinia_logs_api.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.turbinia_logs_api import TurbiniaLogsApi  # noqa: E501
+
+
+class TestTurbiniaLogsApi(unittest.TestCase):
+    """TurbiniaLogsApi unit test stubs"""
+
+    def setUp(self):
+        self.api = TurbiniaLogsApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_get_logs(self):
+        """Test case for get_logs
+
+        Get Logs  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_turbinia_request_results_api.py b/turbinia/api/client/test/test_turbinia_request_results_api.py
new file mode 100644
index 000000000..ec7ba0a34
--- /dev/null
+++ b/turbinia/api/client/test/test_turbinia_request_results_api.py
@@ -0,0 +1,42 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.turbinia_request_results_api import TurbiniaRequestResultsApi  # noqa: E501
+
+
+class TestTurbiniaRequestResultsApi(unittest.TestCase):
+    """TurbiniaRequestResultsApi unit test stubs"""
+
+    def setUp(self):
+        self.api = TurbiniaRequestResultsApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_get_request_output(self):
+        """Test case for get_request_output
+
+        Get Request Output  # noqa: E501
+        """
+        pass
+
+    def test_get_task_output(self):
+        """Test case for get_task_output
+
+        Get Task Output  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_turbinia_requests_api.py b/turbinia/api/client/test/test_turbinia_requests_api.py
new file mode 100644
index 000000000..b98b39c96
--- /dev/null
+++ b/turbinia/api/client/test/test_turbinia_requests_api.py
@@ -0,0 +1,49 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.turbinia_requests_api import TurbiniaRequestsApi  # noqa: E501
+
+
+class TestTurbiniaRequestsApi(unittest.TestCase):
+    """TurbiniaRequestsApi unit test stubs"""
+
+    def setUp(self):
+        self.api = TurbiniaRequestsApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_create_request(self):
+        """Test case for create_request
+
+        Create Request  # noqa: E501
+        """
+        pass
+
+    def test_get_request_status(self):
+        """Test case for get_request_status
+
+        Get Request Status  # noqa: E501
+        """
+        pass
+
+    def test_get_requests_summary(self):
+        """Test case for get_requests_summary
+
+        Get Requests Summary  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_turbinia_tasks_api.py b/turbinia/api/client/test/test_turbinia_tasks_api.py
new file mode 100644
index 000000000..8872fe131
--- /dev/null
+++ b/turbinia/api/client/test/test_turbinia_tasks_api.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.api.turbinia_tasks_api import TurbiniaTasksApi  # noqa: E501
+
+
+class TestTurbiniaTasksApi(unittest.TestCase):
+    """TurbiniaTasksApi unit test stubs"""
+
+    def setUp(self):
+        self.api = TurbiniaTasksApi()  # noqa: E501
+
+    def tearDown(self):
+        pass
+
+    def test_get_task_status(self):
+        """Test case for get_task_status
+
+        Get Task Status  # noqa: E501
+        """
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_validation_error.py b/turbinia/api/client/test/test_validation_error.py
new file mode 100644
index 000000000..e9d4da5b5
--- /dev/null
+++ b/turbinia/api/client/test/test_validation_error.py
@@ -0,0 +1,37 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import sys
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.model.validation_error_loc_inner import ValidationErrorLocInner
+globals()['ValidationErrorLocInner'] = ValidationErrorLocInner
+from turbinia_api_client.model.validation_error import ValidationError
+
+
+class TestValidationError(unittest.TestCase):
+    """ValidationError unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testValidationError(self):
+        """Test ValidationError"""
+        # FIXME: construct object with mandatory attributes with example values
+        # model = ValidationError()  # noqa: E501
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/api/client/test/test_validation_error_loc_inner.py b/turbinia/api/client/test/test_validation_error_loc_inner.py
new file mode 100644
index 000000000..8552b564b
--- /dev/null
+++ b/turbinia/api/client/test/test_validation_error_loc_inner.py
@@ -0,0 +1,35 @@
+"""
+    Turbinia API Server
+
+    Turbinia API server  # noqa: E501
+
+    The version of the OpenAPI document: 1.0.0
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import sys
+import unittest
+
+import turbinia_api_client
+from turbinia_api_client.model.validation_error_loc_inner import ValidationErrorLocInner
+
+
+class TestValidationErrorLocInner(unittest.TestCase):
+    """ValidationErrorLocInner unit test stubs"""
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def testValidationErrorLocInner(self):
+        """Test ValidationErrorLocInner"""
+        # FIXME: construct object with mandatory attributes with example values
+        # model = ValidationErrorLocInner()  # noqa: E501
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/turbinia/evidence_test.py b/turbinia/evidence_test.py
index 5f2e98dc6..24cc3ad17 100644
--- a/turbinia/evidence_test.py
+++ b/turbinia/evidence_test.py
@@ -49,7 +49,6 @@ def testEvidenceCollectionDeserialization(self):
     collection.add_evidence(rawdisk)
     collection_json = collection.to_json()
     self.assertTrue(isinstance(collection_json, str))
-
     collection_new = evidence.evidence_decode(json.loads(collection_json))
     rawdisk_new = collection_new.collection[0]
     # Make sure that both the collection, and the things in the collection
diff --git a/turbinia/workers/file_system_timeline_test.py b/turbinia/workers/file_system_timeline_test.py
index 06b56b9e5..b8a3c9593 100644
--- a/turbinia/workers/file_system_timeline_test.py
+++ b/turbinia/workers/file_system_timeline_test.py
@@ -16,7 +16,6 @@
 
 from __future__ import unicode_literals
 
-import glob
 import unittest
 import os
 import mock
diff --git a/turbinia/yapf_test.py b/turbinia/yapf_test.py
index 42651bb96..07ff5570b 100644
--- a/turbinia/yapf_test.py
+++ b/turbinia/yapf_test.py
@@ -12,9 +12,13 @@ def testCodeStyle(self):
     """Check YAPF style enforcement runs cleanly."""
     turbinia_path = os.path.abspath(os.path.dirname(__file__))
     config_path = os.path.join(turbinia_path, '..', '.style.yapf')
+    api_client_path = '*api/client/*'
+    cli_tool_setup = '*api/cli/setup.py'
     try:
-      subprocess.check_output(
-          ['yapf', '--style', config_path, '--diff', '-r', turbinia_path])
+      subprocess.check_output([
+          'yapf', '--exclude', api_client_path, '--exclude', cli_tool_setup,
+          '--style', config_path, '--diff', '-r', turbinia_path
+      ])
     except subprocess.CalledProcessError as exception:
       if hasattr(exception, 'output'):
         raise Exception(