Repository: cedarville-university/tdxlib (language: python)

Issue:
Fix dictionary configuration (#1)
* Add failing test supporting config loaded from SOAR environment

---------

Co-authored-by: David Riddle (@ddriddle)
Co-authored-by: Michelle Pitcel (@mpitcel)
Co-authored-by: Zach Carrington (@zdc217)
Co-authored-by: Tyler Turner (@tzturner)


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 0333e5c..d583ee3 100644
--- a/README.md
+++ b/README.md
@@ -113,17 +113,17 @@ TDXLib is a suite of Python libraries originally designed to take input from Goo
     The generated file will look something like this:
 
         [TDX API Settings]
-        'orgname': 'myuniversity',
-        'fullhost': 'its.myuniversity.edu',
+        'org_name': 'myuniversity',
+        'full_host': 'its.myuniversity.edu',
         'sandbox': True,
         'username': 'myuser@university.edu',
         'password': 'Prompt',
-        'ticketAppId': '123',
-        'assetAppId': '456',
+        'ticket_app_id': '123',
+        'asset_app_id': '456',
         'caching': True,
         'log_level': 'ERROR'
      
-    * The `orgname` field is whatever subdomain your organization uses to access TeamDynamix. For example, `https://myuniversity.teamdynamix.com`.
+    * The `org_name` field is whatever subdomain your organization uses to access TeamDynamix. For example, `https://myuniversity.teamdynamix.com`.
 
     * The `sandbox` field specifies whether TDXLib should interact with a sandbox version of the TDX Environment, which is written over by production monthly. It is recommended to use the sandbox environment when first getting familiar with the API environment.
 
@@ -133,22 +133,62 @@ TDXLib is a suite of Python libraries originally designed to take input from Goo
     
        <pre>Enter the TDX Password for user myuser@myuniversity.edu (this password will not be stored):   </pre>
 
-    * The `ticketAppId` and `assetAppId` fields are the numbers that appear after `Apps` in your TeamDynamix URL, and are specific to your organization. You can find these ID's in the URLs in the following locations when using a TDX Tickets or Assets app through TDNext in your browser: 
+    * The `ticket_app_id` and `asset_app_id` fields are the numbers that appear after `Apps` in your TeamDynamix URL, and are specific to your organization. You can find these ID's in the URLs in the following locations when using a TDX Tickets or Assets app through TDNext in your browser: 
     
       * Tickets: `https://myuniversity.teamdynamix.com/TDNext/Apps/{ticketAppId}/Tickets/...`  
       * Assets: `https://myuniversity.teamdynamix.com/TDNext/Apps/{assetAppId}/Assets/...`
 
     * The `caching` field specifies whether or not TDXLib should cache TeamDynamix objects such as valid ticket types, statuses and priorities. Setting this option to `True` reduces the volume of API calls and allows TDXLib to perform some batch operations much faster.
+    
+    * The `log_level` field specifies the python logging level that TDXLib will log at.
 
-    * You can optionally specify an alternative configuration file that TDXLib should search for in your working directory. By default, it will look for `tdxlib.ini`.
+    * The `timezone` field specifies the timezone you'd like TDXLib to operate in. TDXLib will translate date/time objects from TeamDyanmix in UTC to this timezone.
+    
+  * You can optionally specify an alternative configuration file that TDXLib should search for in your working directory. By default, it will look for `tdxlib.ini`.
 
           >>> tdx = tdx_ticket_integration.TDXTicketIntegration("specialconfig.ini")
 
-    * Depending on your `tdxlib.ini` settings, you may be prompted for a password to authenticate into the TeamDynamix API. Once everything is working, go ahead and test out a method:
+  * Depending on your `tdxlib.ini` settings, you may be prompted for a password to authenticate into the TeamDynamix API. Once everything is working, go ahead and test out a method:
 
           >>> accounts = tdx.get_all_accounts()
 
-9. Congratulations! You now have the power of the TeamDynamix API at your fingertips. For more detailed tutorials on how to use TDXLib to manipulate Tickets and Asset, as well as for information on the methods and classes included with TDXLib, check out our documentation on [ReadtheDocs.io](http://tdxlib.readthedocs.io).
+8. TDXLib can also be configured directly by a python dictionary. 
+
+    Settings passed in as a dictionary are overwritten by any values in a filename (and if no file name is passed, the default configuration file, `tdxlib.ini` in the working directory)
+   
+    Use the following format for the dictionary configuration:
+    <pre>
+    config = {
+         "org_name": "exampleorg",
+         "sandbox": true,
+         "auth_type": "password",
+         "username": "someone@example.edu",
+         "password": "a password",
+         "ticket_app_id": 300,
+         "asset_app_id": "",
+         "caching": true,
+         "timezone": "-0500",
+         "log_level": "ERROR"
+     }
+    </pre>
+    The only required settings in this are `org_name` and either `asset_app_id` or `ticket_app_id` if you're creating `TDXAssetIntegration` or `TDXTicketIntegration` objects. If you set `auth_type` to `password` a username is also required.
+    Unset settings will be defaulted per the values in `tdxlib.tdx_constants.default_config`.
+9. TDXLib can also be configured via Environment variables. Any values passed in via dictionary, from a file, or in the default configuration file (`tdxlib.ini` in the working directory) override any values passed in as environment variables. 
+
+    Here's a sample of environment variables you can use (they are the same as the config values for dictionary config, but uppercase and prefixed with `TDXLIB_`): 
+    <pre>
+        export TDXLIB_ORG_NAME=exampleorg
+        export TDXLIB_SANDBOX=true
+        export TDXLIB_AUTH_TYPE=password
+        export TDXLIB_USERNAME=someon@example.edu
+        export TDXLIB_PASSWORD=mypassword
+        export TDXLIB_TICKET_APP_ID=300
+        export TDXLIB_ASSET_APP_ID=400
+        export TDXLIB_CACHING=true
+        export TDXLIB_TIMEZONE=-0500
+        export TDXLIB_LOG_LEVEL=ERROR
+   </pre>
+10. Congratulations! You now have the power of the TeamDynamix API at your fingertips. For more detailed tutorials on how to use TDXLib to manipulate Tickets and Asset, as well as for information on the methods and classes included with TDXLib, check out our documentation on [ReadtheDocs.io](http://tdxlib.readthedocs.io).
     
 
 ##  TDXLib Implementation status and Future Plans
diff --git a/setup.py b/setup.py
index fd35725..8413f03 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@
 setup(
     name='TDXLib',
     description='a python library for interacting with the TeamDynamix Web API',
-    version='0.4.3',
+    version='0.5.0',
     author='Nat Biggs, Stephen Gaines, Josiah Lansford',
     author_email='tdxlib@cedarville.edu',
     packages=['tdxlib'],
diff --git a/tdxlib/__init__.py b/tdxlib/__init__.py
index 8282e5b..547cca9 100644
--- a/tdxlib/__init__.py
+++ b/tdxlib/__init__.py
@@ -1,6 +1,15 @@
-__all__ = ["tdx_ticket_integration", "tdx_asset_integration", "tdx_api_exceptions", "tdx_utils"]
+__all__ = [
+    "tdx_ticket_integration",
+    "tdx_asset_integration",
+    "tdx_api_exceptions",
+    "tdx_utils",
+    "tdx_constants",
+    "tdx_config"
+]
 
 import tdxlib.tdx_api_exceptions
+import tdxlib.tdx_constants
+import tdxlib.tdx_config
 import tdxlib.tdx_integration
 import tdxlib.tdx_asset_integration
 import tdxlib.tdx_ticket_integration
@@ -8,4 +17,4 @@
 import tdxlib.tdx_utils
 
 
-__version__ = "0.4.3"
+__version__ = "0.5.0"
diff --git a/tdxlib/tdx_asset_integration.py b/tdxlib/tdx_asset_integration.py
index e952fa1..bee3c5e 100644
--- a/tdxlib/tdx_asset_integration.py
+++ b/tdxlib/tdx_asset_integration.py
@@ -9,8 +9,8 @@
 class TDXAssetIntegration(tdxlib.tdx_integration.TDXIntegration):
     def __init__(self, filename: str = None):
         tdxlib.tdx_integration.TDXIntegration.__init__(self, filename)
-        if self.asset_app_id is None:
-            raise ValueError("Asset App Id is required. Check your INI file for 'assetappid = 000'")
+        if self.config.asset_app_id is None:
+            raise RuntimeError("Asset App Id is required. Check your configuration.")
         
         self.clean_cache()
 
@@ -19,7 +19,7 @@ def clean_cache(self) -> None:
         Internal method to refresh the cache in a tdxlib object.
         """
         super().clean_cache()
-        if not self.caching:
+        if not self.config.caching:
             return
         self.cache['product_model'] = {}
         self.cache['product_type'] = {}
@@ -28,13 +28,13 @@ def clean_cache(self) -> None:
         self.cache['asset_status'] = {}
         self.cache['custom_attributes'] = self.get_all_asset_custom_attributes()
         self.cache['custom_attributes'].append(self.get_all_custom_attributes(
-            tdxlib.tdx_integration.TDXIntegration.component_ids['configuration_item'], app_id=self.asset_app_id))
+            tdxlib.tdx_integration.TDXIntegration.component_ids['configuration_item'], app_id=self.config.asset_app_id))
 
     def _make_asset_call(self, url: str, action: str, post_body: Union[dict, list] = None) -> Union[list, dict]:
         """
         Internal method to make a http call using the assets endpoints and the provided HTTP verb.
         """
-        url_string = '/' + str(self.asset_app_id) + '/assets'
+        url_string = '/' + str(self.config.asset_app_id) + '/assets'
         if len(url) > 0:
             url_string += '/' + url
         if action == 'get':
@@ -57,7 +57,7 @@ def make_call(self, url: str, action: str, post_body: dict = None) -> Union[list
         :param action: The HTTP action (get, put, post, delete, patch) to perform.
         :param post_body: A python dict of the information to post, put, or patch. Not used for get/delete.
 
-        :return: the API's response as a python dict or list
+        :return: the API response as a python dict or list
 
         """
         return self._make_asset_call(url, action, post_body)
@@ -157,7 +157,7 @@ def create_product_type(self, name: str, description: str = None, parent=None, o
         :param description: A description of the type (optional)
         :param parent: A Type (dict) or Type ID to set as the parent type of this type (creates a subtype)(optional)
         :param order: Sort order for this type (optional, defaults to 1)
-        :param active: Boolean indicating whether or not the new type should be active (optional, default True)
+        :param active: Boolean indicating whether the new type should be active (optional, default True)
 
         :return: dict of created product type
 
@@ -276,7 +276,7 @@ def create_product_model(self, name: str, product_type: Union[str, dict], source
         :param source: The manufacturer or vendor the model is sourced from
         :param description: A description of the model (optional)
         :param part_number: Part number for this model (optional)
-        :param active: Boolean indicating whether or not the new model should be active (optional, default True)
+        :param active: Boolean indicating whether the new model should be active (optional, default True)
         :param attributes: Dict of custom attributes to set on the new model (no validation yet -- build this by hand)
 
         :return: dict of created product type
@@ -379,7 +379,7 @@ def add_asset_user(self, asset_id: str, user_uid: str) -> dict:
         :param asset_id: the ID of the asset to get users
         :param user_uid: the UID of the person to add
 
-        :return: the API's response (success/failure only)
+        :return: the API response (success/failure only)
         """
         return self.make_call(f'{asset_id}/users/{user_uid}', 'post', {'data': None})
 
@@ -604,7 +604,7 @@ def get_assets_by_requesting_department(self, dept: Union[dict, str], max_result
         """
         Gets all assets requested by a particular account/department in TDX
 
-        :param dept: the name or email of a account/department, or a dict containing its information
+        :param dept: the name or email of an account/department, or a dict containing its information
         :param max_results: an integer indicating the maximum number of results that should be returned (default: 25)
         :param full_record: boolean indicating whether to fetch the full Asset record, or just summary info
         :param retired: include retired assets in search if true
@@ -825,7 +825,8 @@ def change_asset_requesting_dept(self, asset: Union[dict, str, int, list], new_d
         return self.update_assets(asset, changed_attributes)
 
     def get_all_asset_custom_attributes(self):
-        return self.get_all_custom_attributes(TDXAssetIntegration.component_ids['asset'], app_id=self.asset_app_id)
+        return self.get_all_custom_attributes(TDXAssetIntegration.component_ids['asset'],
+                                              app_id=self.config.asset_app_id)
 
     def get_asset_custom_attribute_by_name_id(self, key: str) -> dict:
         """
@@ -837,15 +838,16 @@ def get_asset_custom_attribute_by_name_id(self, key: str) -> dict:
 
         """
         search_key = str(key) + "_asset_ci"
-        if self.caching and search_key in self.cache['ca_search']:
+        if self.config.caching and search_key in self.cache['ca_search']:
             return self.cache['ca_search'][search_key]
         # There is no API for searching attributes -- the only way is to get them all.
-        if self.caching:
+        if self.config.caching:
             custom_attributes = self.cache['custom_attributes']
         else:
             custom_attributes = self.get_all_asset_custom_attributes()
             custom_attributes.append(self.get_all_custom_attributes(
-                tdxlib.tdx_integration.TDXIntegration.component_ids['configuration_item'], app_id=self.asset_app_id))
+                tdxlib.tdx_integration.TDXIntegration.component_ids['configuration_item'],
+                app_id=self.config.asset_app_id))
         for item in custom_attributes:
             if type(item) == dict:
                 if str(key).lower() == item['Name'].lower() or str(key) == str(item['ID']):
@@ -1054,7 +1056,7 @@ def build_asset(self, asset_name: str, serial_number: str, status_name: str, loc
         if not acquisition_date:
             acquisition_date = datetime.datetime.today()
         if not requester:
-            requester = self.username
+            requester = self.config.username
         if not external_id:
             external_id = serial_number
 
@@ -1127,7 +1129,7 @@ def create_asset(self, asset: dict, check_duplicate: bool = True) -> dict:
         Creates an asset
 
         :param asset: a dict of asset info (maybe from make_asset_json()) to use in creation
-        :param check_duplicate: boolean of whether or not we should check to see if this is a duplicate asset
+        :param check_duplicate: boolean of whether we should check to see if this is a duplicate asset
 
         :return: dict of created asset details
 
diff --git a/tdxlib/tdx_config.py b/tdxlib/tdx_config.py
new file mode 100644
index 0000000..2a1c49a
--- /dev/null
+++ b/tdxlib/tdx_config.py
@@ -0,0 +1,266 @@
+import configparser
+import tdxlib.tdx_constants
+import getpass
+import os
+
+
+default_config = tdxlib.tdx_constants.default_config
+
+
+class TDXConfig:
+    def __init__(self, filename: str = None, config: dict = None):
+        self.filename = filename
+        self.config = configparser.ConfigParser()
+        self.api_url = None
+        self.token = None
+        self.token_exp = None
+        self.timezone = None
+        self.log_level = None
+        self.caching = True
+        self.sandbox = True
+        self.username = None
+        self.password = None
+        self.org_name = None
+        self.auth_type = None
+        self.ticket_app_id = None
+        self.asset_app_id = None
+        self.full_host = None
+        self.load_config_from_env()
+        if config:
+            self.set_config_from_dict(config)
+        self.load_config_from_file(filename)
+        if not self.config_complete():
+            self.run_setup_wizard()
+        self.config_to_attributes()
+        self.setup_from_attributes()
+
+    def config_complete(self):
+        if not self.get_value('org_name') and not self.get_value('orgname') and not self.get_value('full_host'):
+            return False
+        if self.get_value('authType') == 'password' and \
+                (not self.get_value('username')):
+            return False
+        return True
+
+    def set_config_from_dict(self, config: dict):
+        if 'TDX API Settings' in config.keys():
+            self.config['TDX API Settings'] = config['TDX API Settings']
+        else:
+            self.config['TDX API Settings'] = config
+
+    def load_config_from_file(self, filename: str):
+        # Read in configuration
+        if filename is None:
+            filename = tdxlib.tdx_constants.default_filename
+        self.config.read(filename)
+        return
+
+    def load_config_from_env(self):
+        self.config.add_section('TDX API Settings')
+        tdx_vars = {k:v for k,v in os.environ.items() if 'TDXLIB' in k}
+        for i in tdxlib.tdx_constants.config_keys.keys():
+            environ_key = f'TDXLIB_{i.upper()}'
+            if environ_key in tdx_vars.keys() and tdx_vars[environ_key]:
+                self.config.set('TDX API Settings', i, tdx_vars[environ_key])
+
+    def get_value(self, key: str, default=None):
+        if key and 'TDX API Settings' in self.config.keys():
+            if self.config['TDX API Settings'] and key in self.config['TDX API Settings'].keys():
+                t = tdxlib.tdx_constants.config_keys[key]
+                if t is str:
+                    return self.config['TDX API Settings'].get(key)
+                if t is bool:
+                    return self.config['TDX API Settings'].getboolean(key)
+                if t is int:
+                    return self.config['TDX API Settings'].getint(key)
+                if t is float:
+                    return self.config['TDX API Settings'].getfloat(key)
+            else:
+                if key in default_config.keys():
+                    return default_config[key]
+        return default
+
+    def config_to_attributes(self):
+        # Read settings in, respecting backwards compatibility
+        self.org_name = self.get_value('org_name')
+        if not self.org_name:
+            self.org_name = self.get_value('orgname')
+        self.sandbox = self.get_value('sandbox', bool)
+        self.auth_type = self.get_value('auth_type')
+        if not self.auth_type:
+            self.auth_type = self.get_value('authType')
+        if not self.auth_type or self.auth_type == 'password':
+            self.username = self.get_value('username')
+            self.password = self.get_value('password')
+        self.ticket_app_id = self.get_value('ticket_app_id')
+        if not self.ticket_app_id:
+            self.ticket_app_id = self.get_value('ticketAppId')
+        self.asset_app_id = self.get_value('asset_app_id')
+        if not self.ticket_app_id:
+            self.ticket_app_id = self.get_value('assetAppId')
+        self.caching = self.get_value('caching', bool)
+        self.timezone = self.get_value('timezone')
+        self.full_host = self.get_value('full_host')
+
+    def setup_from_attributes(self):
+        if not self.timezone:
+            self.timezone = 'Z'
+
+        if self.sandbox:
+            api_end = '/SBTDWebApi/api'
+        else:
+            api_end = '/TDWebApi/api'
+        if self.full_host is None:
+            self.api_url = 'https://' + self.org_name + '.teamdynamix.com' + api_end
+        else:
+            self.api_url = 'https://' + self.full_host + api_end
+
+        if not self.auth_type or self.auth_type == 'password':
+            if self.password == 'Prompt':
+                pass_prompt = 'Enter the TDX Password for user ' + self.username + \
+                              ' (this password will not be written to disk): '
+                self.password = getpass.getpass(pass_prompt)
+
+    def run_setup_wizard(self):
+        # Initialization wizard
+        print("\nIncomplete configuration. Please enter the following information: ")
+        self.set_fqdn_wizard()
+        self.set_sandbox_wizard()
+        self.set_auth_type_wizard()
+        self.set_ticket_app_id_wizard()
+        self.set_asset_app_id_wizard()
+        self.set_caching_wizard()
+        self.set_timezone_wizard()
+        self.set_logging_wizard()
+        self.save_config_wizard()
+
+    def set_fqdn_wizard(self):
+        fqdn_invalid = True
+        fqdn = False
+        while fqdn_invalid:
+            fqdn_choice = input("\nUse a Fully-Qualified DNS Name for your TDX Instance "
+                                "(if not *.teamdynamix.com)? [Y/N]: ")
+            if fqdn_choice.lower() in ['y', 'ye', 'yes', 'true']:
+                fqdn = True
+                fqdn_invalid = False
+            elif fqdn_choice.lower() in ['n', 'no', 'false']:
+                fqdn_invalid = False
+        if fqdn:
+            print("Enter the fully qualified DNS name of your TDX instance.")
+            init_full_host = input("FQDN (its.myuniversity.edu): ")
+            self.config.set('TDX API Settings', 'full_host', init_full_host)
+        else:
+            print("\n\nPlease enter your TeamDynamix organization name.")
+            print("This is the teamdynamix.com subdomain that you use to access TeamDynamix.")
+            init_org_name = input("Organization Name (<org_name>.teamdynamix.com): ")
+            self.config.set('TDX API Settings', 'org_name', init_org_name)
+
+    def set_sandbox_wizard(self):
+        sandbox_invalid = True
+        while sandbox_invalid:
+            sandbox_choice = input("\nUse TeamDynamix Sandbox? [Y/N]: ")
+            if sandbox_choice.lower() in ['y', 'ye', 'yes', 'true']:
+                self.config.set('TDX API Settings', 'sandbox', 'true')
+                sandbox_invalid = False
+            elif sandbox_choice.lower() in ['n', 'no', 'false']:
+                self.config.set('TDX API Settings', 'sandbox', 'false')
+                sandbox_invalid = False
+
+    def set_token_wizard(self):
+        provided_token = input("\nInput token now, or leave blank to fill programmatically later: ")
+        if provided_token != "":
+            self.token = provided_token
+
+    def set_password_wizard(self):
+        init_username = input("\nTDX API Username (tdxuser@company.com): ")
+        self.config.set('TDX API Settings', 'username', init_username)
+        print("\nTDXLib can store the password for the API user in the configuration file.")
+        print("This is convenient, but not very secure.")
+        password_invalid = True
+        while password_invalid:
+            password_choice = input("Store password for " + init_username + "? [Y/N]: ")
+            if password_choice.lower() in ['y', 'ye', 'yes', 'true']:
+                password_prompt = '\nEnter Password for ' + init_username + ": "
+                init_password = getpass.getpass(password_prompt)
+                self.config.set('TDX API Settings', 'password', init_password)
+                password_invalid = False
+            elif password_choice.lower() in ['n', 'no', 'false']:
+                self.config.set('TDX API Settings', 'password', 'Prompt')
+                password_invalid = False
+            if password_invalid:
+                print("\nInvalid Response.")
+
+    def set_auth_type_wizard(self):
+        auth_type_invalid = True
+        auth_type = "password"
+        while auth_type_invalid:
+            auth_type = input("\nAuthentication Type (Only password and token currently supported) [password]: ")
+            if auth_type == '':
+                auth_type = 'password'
+            if auth_type == "password":
+                self.set_password_wizard()
+                auth_type_invalid = False
+            elif auth_type == "token":
+                self.set_token_wizard()
+                auth_type_invalid = False
+        self.config.set("TDX API Settings", "authType", auth_type)
+
+    def set_asset_app_id_wizard(self):
+        init_asset_id = input("\nAssets App ID (optional): ")
+        self.config.set('TDX API Settings', 'asset_app_id', init_asset_id)
+
+    def set_ticket_app_id_wizard(self):
+        init_ticket_id = input("\nTickets App ID (optional): ")
+        self.config.set('TDX API Settings', 'ticket_app_id', init_ticket_id)
+
+    def set_caching_wizard(self):
+        print("\nTDXLib uses intelligent caching to speed up API calls on repetitive operations.")
+        print("In very dynamic environments, TDXLib's caching can cause issues.")
+        caching_invalid = True
+        while caching_invalid:
+            caching_choice = input("Disable Caching? Y/N [N]: ")
+            if caching_choice == '' or caching_choice.lower() in ['y', 'ye', 'yes', 'true']:
+                self.config.set('TDX API Settings', 'caching', 'true')
+                caching_invalid = False
+            elif caching_choice.lower() in ['n', 'no', 'false']:
+                self.config.set('TDX API Settings', 'caching', 'false')
+                caching_invalid = False
+            if caching_invalid:
+                print("Invalid Response.")
+
+    def set_timezone_wizard(self):
+        print("\nWhat timezone would you like to set for this integration (default: '-0500')?")
+        timezone_invalid = True
+        while timezone_invalid:
+            timezone_choice = input(f"Timezone (default: {default_config['timezone']} ) [+/-0000]: ")
+            if len(timezone_choice) == 5 and timezone_choice[:1] in ["+", "-"] and timezone_choice[1:].isdigit():
+                self.config.set('TDX API Settings', 'timezone', timezone_choice)
+                timezone_invalid = False
+            if len(timezone_choice) == 0:
+                self.config.set('TDX API Settings', 'timezone', default_config['timezone'])
+                timezone_invalid = False
+            if timezone_invalid:
+                print("Invalid Reponse.")
+
+    def set_logging_wizard(self):
+        logging_invalid = False
+        while logging_invalid:
+            logging_choice = input(f"Log Level (default: {default_config['logging_level']}) "
+                                   f"[CRITICAL, ERROR, WARNING, INFO, DEBUG]: ")
+            if logging_choice in ["WARNING", "ERROR", "INFO", "DEBUG", "CRITICAL"]:
+                self.config.set('TDX API Settings', 'log_level', logging_choice)
+                logging_invalid = False
+            if len(logging_choice) == 0:
+                self.config.set('TDX API Settings', 'log_level', default_config['log_level'])
+                logging_invalid = False
+            if logging_invalid:
+                print("Invalid Reponse")
+
+    def save_config_wizard(self):
+        filename = input(f"Enter config filename (default: {default_config['filename']}): ")
+        if filename == "":
+            filename = default_config['filename']
+        with open(filename, 'w') as configfile:
+            self.config.write(configfile)
+        print(f'\nSettings saved to {filename}')
+        self.filename = filename
diff --git a/tdxlib/tdx_constants.py b/tdxlib/tdx_constants.py
new file mode 100644
index 0000000..b36ede8
--- /dev/null
+++ b/tdxlib/tdx_constants.py
@@ -0,0 +1,57 @@
+# Hard-coded into TDX
+component_ids = {
+    'account': 14,
+    'asset': 27,
+    'configuration_item': 63,
+    'contract': 29,
+    'file_cabinet': 8,
+    'issue': 3,
+    'opportunity': 11,
+    'person': 31,
+    'product': 37,
+    'product_model': 30,
+    'project': 1,
+    'ticket': 9,
+    'vendor': 28
+}
+
+
+default_config = {
+    # 'org_name': '',
+    'sandbox': True,
+    'auth_type': 'password',
+    # 'username': '',
+    # 'password': '',
+    # 'ticket_app_id': '',
+    # 'asset_app_id': '',
+    'caching': False,
+    'timezone': '-0500',
+    'log_level': 'ERROR',
+    # 'full_host': ''
+
+}
+
+config_keys = {
+    'org_name': str,
+    'orgname': str,
+    'sandbox': bool,
+    'auth_type': str,
+    # backwards compatibility
+    'authType': str,
+    'password': str,
+    'username': str,
+    'ticket_app_id': str,
+    # backwards compatibility
+    'ticketAppId': str,
+    'asset_app_id': str,
+    # backwards compatibility
+    'assetAppId': str,
+    'caching': bool,
+    'timezone': str,
+    'log_level': str,
+    # backwards compatibility
+    'logLevel': str,
+    'full_host': str
+}
+
+default_filename = "tdxlib.ini"
diff --git a/tdxlib/tdx_integration.py b/tdxlib/tdx_integration.py
index 07dc4d9..db67f8f 100644
--- a/tdxlib/tdx_integration.py
+++ b/tdxlib/tdx_integration.py
@@ -1,8 +1,8 @@
-import configparser
 import requests
 import json
-import getpass
 import tdxlib.tdx_api_exceptions
+import tdxlib.tdx_constants
+import tdxlib.tdx_config
 import datetime
 import time
 from typing import BinaryIO
@@ -11,296 +11,51 @@
 
 
 class TDXIntegration:
+    component_ids = tdxlib.tdx_constants.component_ids
 
-    # Hard-coded into TDX
-    component_ids = {
-        'account': 14,
-        'asset': 27,
-        'configuration_item': 63,
-        'contract': 29,
-        'file_cabinet': 8,
-        'issue': 3,
-        'opportunity': 11,
-        'person': 31,
-        'product': 37,
-        'product_model': 30,
-        'project': 1,
-        'ticket': 9,
-        'vendor': 28
-    }
-    default_config = {
-        'filename': 'tdxlib.ini',
-        # 'orgname': '',
-        # 'sandbox': True,
-        # 'authType': 'password',
-        # 'ticketAppId': '',
-        # 'assetAppId': '',
-        'caching': False,
-        'timezone': '-0500',
-        'logLevel': 'ERROR'
-    }
-
-    def __init__(self, filename=None, config=None):
-        self.config_filename = None
-        self.settings = None
-        self.api_url = None
-        self.token = None
-        self.token_exp = None
-        self.timezone = None
-        self.log_level = None
-        self.caching = True
-        self.sandbox = True
-        self.username = None
-        self.password = None
-        self.org_name = None
-        self.auth_type = None
-        self.ticket_app_id = None
-        self.asset_app_id = None
+    def __init__(self, filename: str = None, config: dict = None):
         self.cache = dict()
         self.logger = logging.getLogger('tdx_integration')
-        self.config = configparser.ConfigParser()
-
-        self.load_config_from_file(filename)
-
-        if 'TDX API Settings' not in self.config:
-            self.set_config(config)
-            self.run_setup_wizard()
-
-        self.config_to_attributes()
-        # Default to UTC -- for backwards compatibility
-        self.setup_from_loaded_config()
+        self.config = tdxlib.tdx_config.TDXConfig(filename, config)
+        self.setup_logs()
         self.clean_cache()
+        self.check_auth_init()
 
-    def config_to_attributes(self):
-        # Read settings in
-        self.settings = self.config['TDX API Settings']
-        self.org_name = self.settings.get('orgname')
-        
-        self.sandbox = self.settings.getboolean('sandbox')
-        self.auth_type = self.settings.get('authType')
-        if not self.auth_type or self.auth_type == 'password':
-            self.username = self.settings.get('username')
-            self.password = self.settings.get('password')
-        self.ticket_app_id = self.settings.get('ticketAppId')
-        self.asset_app_id = self.settings.get('assetAppId')
-        self.caching = self.settings.getboolean('caching')
-        self.timezone = self.settings.get('timezone')
-
-    def setup_from_loaded_config(self):
-        if not self.timezone:
-            self.timezone = 'Z'
-
-        if self.sandbox:
-            api_end = '/SBTDWebApi/api'
-        else:
-            api_end = '/TDWebApi/api'
-
-        full_host = self.settings.get('full_host', None)
-        if full_host is None:
-            self.api_url = 'https://' + self.org_name + '.teamdynamix.com' + api_end
-        else:
-            self.api_url = 'https://' + full_host + api_end
-
-        if not self.auth_type or self.auth_type == 'password':
-            if self.password == 'Prompt':
-                pass_prompt = 'Enter the TDX Password for user ' + self.username + \
-                              ' (this password will not be stored): '
-                self.password = getpass.getpass(pass_prompt)
-
-        if self.log_level:
+    def setup_logs(self):
+        if self.config.log_level:
             self.logger = logging.getLogger('tdx_integration')
-            self.logger.setLevel(logging.getLevelName(self.log_level))
+            self.logger.setLevel(logging.getLevelName(self.config.log_level))
 
-        if not self.auth_type or self.auth_type == 'password':
+    def check_auth_init(self):
+        if not self.config.auth_type or self.config.auth_type == 'password':
             if not self.auth():
-                self.logger.error(f"Login Failed. Username or password in {self.config_filename} likely incorrect.")
-        elif self.auth_type == 'token':
-            if self.token is None:
-                self.token_exp = time.time()
+                self.logger.error(f"Login Failed. Username or password in {self.config.filename} likely incorrect.")
+        elif self.config.auth_type == 'token':
+            if self.config.token is None:
+                self.config.token_exp = time.time()
                 self.logger.info("Skipping initial authentication, no token provided yet.")
             else:
                 if not (self.auth()):
-                    self.logger.error(f"Login Failed. Username or password in {self.config_filename} likely incorrect.")
-
-    def load_config_from_file(self, filename: str):
-        # Read in configuration
-        if filename is None:
-            filename = self.default_config["filename"]
-        self.config.read(filename)
-        return
-
-    def set_config(self, config: dict):
-        if not config:
-            config = self.default_config
-        
-        self.config['TDX API Settings'] = config
-
-    def run_setup_wizard(self):
-        # Initialization wizard
-        print("\nNo configuration file found. Please enter the following information: ")
-
-        self.set_fqdn_wizard()
-        self.set_sandbox_wizard()
-        self.set_auth_type_wizard()
-        self.set_ticket_app_id_wizard()
-        self.set_asset_app_id_wizard()
-        self.set_caching_wizard()
-        self.set_timezone_wizard()
-        self.set_logging_wizard()
-        self.save_config_wizard()
-    
-    def set_fqdn_wizard(self):
-        fqdn_invalid = True
-        fqdn = False
-        while fqdn_invalid:
-            fqdn_choice = input("\nUse a Fully-Qualified DNS Name for your TDX Instance "
-                                "(if not *.teamdynamix.com)? [Y/N]: ")
-            if fqdn_choice.lower() in ['y', 'ye', 'yes', 'true']:
-                fqdn = True
-                fqdn_invalid = False
-            elif fqdn_choice.lower() in ['n', 'no', 'false']:
-                fqdn_invalid = False
-        if fqdn:
-            print("Enter the fully qualified DNS name of your TDX instance.")
-            init_full_host = input("FQDN (its.myuniversity.edu): ")
-            self.config.set('TDX API Settings', 'full_host', init_full_host)
-        else:
-            print("\n\nPlease enter your TeamDynamix organization name.")
-            print("This is the teamdynamix.com subdomain that you use to access TeamDynamix.")
-            init_org_name = input("Organization Name/FQDN (<orgname>.teamdynamix.com/<FQDN>): ")
-            self.config.set('TDX API Settings', 'orgname', init_org_name)
-
-    def set_sandbox_wizard(self):
-        sandbox_invalid = True
-        while sandbox_invalid:
-            sandbox_choice = input("\nUse TeamDynamix Sandbox? [Y/N]: ")
-            if sandbox_choice.lower() in ['y', 'ye', 'yes', 'true']:
-                self.config.set('TDX API Settings', 'sandbox', 'true')
-                sandbox_invalid = False
-            elif sandbox_choice.lower() in ['n', 'no', 'false']:
-                self.config.set('TDX API Settings', 'sandbox', 'false')
-                sandbox_invalid = False
-
-    def set_token_wizard(self):
-        provided_token = input("\nInput token now, or leave blank to fill programatically later: ")
-        if provided_token != "":
-            self.token = provided_token
-
-    def set_password_wizard(self):
-        init_username = input("\nTDX API Username (tdxuser@orgname.com): ")
-        self.config.set('TDX API Settings', 'username', init_username)
-        print("\nTDXLib can store the password for the API user in the configuration file.")
-        print("This is convenient, but not very secure.")
-        password_invalid = True
-        while password_invalid:
-            password_choice = input("Store password for " + init_username + "? [Y/N]: ")
-            if password_choice.lower() in ['y', 'ye', 'yes', 'true']:
-                password_prompt = '\nEnter Password for ' + init_username + ": "
-                init_password = getpass.getpass(password_prompt)
-                self.config.set('TDX API Settings', 'password', init_password)
-                password_invalid = False
-            elif password_choice.lower() in ['n', 'no', 'false']:
-                self.config.set('TDX API Settings', 'password', 'Prompt')
-                password_invalid = False
-            if password_invalid:
-                print("\nInvalid Response.")
-
-    def set_auth_type_wizard(self):
-        auth_type_invalid = True
-        auth_type = "password"
-        while auth_type_invalid:
-            auth_type = input("\nAuthentication Type (Only password and token currently supported) [password]: ")
-            if auth_type == '':
-                auth_type = 'password'
-            if auth_type == "password":
-                self.set_password_wizard()
-                auth_type_invalid = False
-            elif auth_type == "token":
-                self.set_token_wizard()
-                auth_type_invalid = False
-        self.config.set("TDX API Settings", "authType", auth_type)
-
-    def set_asset_app_id_wizard(self):
-        init_asset_id = input("\nAssets App ID (optional): ")
-        self.config.set('TDX API Settings', 'assetAppId', init_asset_id)
-    
-    def set_ticket_app_id_wizard(self):
-        init_ticket_id = input("\nTickets App ID (optional): ")
-        self.config.set('TDX API Settings', 'ticketAppId', init_ticket_id)
-
-    def set_caching_wizard(self):
-        print("\nTDXLib uses intelligent caching to speed up API calls on repetitive operations.")
-        print("In very dynamic environments, TDXLib's caching can cause issues.")
-        caching_invalid = True
-        while caching_invalid:
-            caching_choice = input("Disable Caching? Y/N [N]: ")
-            if caching_choice == '' or caching_choice.lower() in ['y', 'ye', 'yes', 'true']:
-                self.config.set('TDX API Settings', 'caching', 'true')
-                self.caching = False
-                caching_invalid = False
-            elif caching_choice.lower() in ['n', 'no', 'false']:
-                self.config.set('TDX API Settings', 'caching', 'false')
-                self.caching = True
-                caching_invalid = False
-            if caching_invalid:
-                print("Invalid Response.")
-                
-    def set_timezone_wizard(self):
-        print("\nWhat timezone would you like to set for this integration (default: '-0500')?")
-        timezone_invalid = True
-        while timezone_invalid:
-            timezone_choice = input(f"Timezone (default: {self.default_config['timezone']} ) [+/-0000]: ")
-            if len(timezone_choice) == 5 and timezone_choice[:1] in ["+", "-"] and timezone_choice[1:].isdigit():
-                self.config.set('TDX API Settings', 'timezone', timezone_choice)
-                self.timezone = timezone_choice
-                timezone_invalid = False
-            if len(timezone_choice) == 0:
-                self.config.set('TDX API Settings', 'timezone', '-0500')
-                self.timezone = self.default_config['timezone']
-                timezone_invalid = False
-            if timezone_invalid:
-                print("Invalid Reponse.")
-
-    def set_logging_wizard(self):
-        logging_invalid = False
-        while logging_invalid:
-            logging_choice = input(f"Log Level (default: {self.default_config['logging_level']}) "
-                                   f"[CRITICAL, ERROR, WARNING, INFO, DEBUG]: ")
-            if logging_choice in ["WARNING", "ERROR", "INFO", "DEBUG", "CRITICAL"]:
-                self.config.set('TDX API Settings', 'logLevel', logging_choice)
-                self.log_level = logging_choice
-                logging_invalid = False
-            if len(logging_choice) == 0:
-                self.config.set('TDX API Settings', 'logLevel', 'ERROR')
-                self.log_level = self.default_config['logging_level']
-                logging_invalid = False
-            if logging_invalid:
-                print("Invalid Reponse")
-    
-    def save_config_wizard(self):
-        filename = input(f"Enter config filename (default: {self.default_config['filename']}): ")
-        if filename == "":
-            filename = self.default_config['filename']
-        with open(filename, 'w') as configfile:
-            self.config.write(configfile)
-        print(f'\nSettings saved to {filename}')
-        self.config_filename = filename
+                    self.logger.error(f"Login Failed. Username or password in {self.config.filename} likely incorrect.")
+
+    def set_token(self, token: str):
+        self.config.token = token
 
     def auth(self) -> bool:
         """
         Internal method to authenticate to the TDX api using the selected method
         Stores a token in the token property, used for future calls. Returns true for success, false for failure.
         """
-        if not self.auth_type or self.auth_type == 'password':
+        if not self.config.auth_type or self.config.auth_type == 'password':
             try:
                 response = requests.post(
-                    url=str(self.api_url) + '/auth',
+                    url=str(self.config.api_url) + '/auth',
                     headers={
                         "Content-Type": "application/json; charset=utf-8",
                     },
                     data=json.dumps({
-                        "username": self.username,
-                        "password": self.password
+                        "username": self.config.username,
+                        "password": self.config.password
                     })
                 )
                 if response.status_code != 200:
@@ -308,14 +63,14 @@ def auth(self) -> bool:
                                                                     " " + response.reason + " " + " Returned: " +
                                                                     response.text)
                 else:
-                    self.token = response.text
+                    self.config.token = response.text
                     time.sleep(1)
                     # Decode token to identify expiration date
-                    decoded = jwt.decode(self.token,
+                    decoded = jwt.decode(self.config.token,
                                          algorithms=['HS256'],
                                          options={'verify_signature': False},
                                          audience="https://www.teamdynamix.com/")
-                    self.token_exp = decoded['exp']
+                    self.config.token_exp = decoded['exp']
                     return True
 
             except requests.exceptions.RequestException as e:
@@ -324,16 +79,16 @@ def auth(self) -> bool:
             except tdxlib.tdx_api_exceptions.TdxApiHTTPError as e:
                 self.logger.error(str(e))
                 return False
-        elif self.auth_type == 'token':
-            if self.token is None:
-                self.token_exp = time.time()
+        elif self.config.auth_type == 'token':
+            if self.config.token is None:
+                self.config.token_exp = time.time()
                 self.logger.info("Skipping initial authentication, no token provided yet.")
             # Decode token to identify expiration date
-            decoded = jwt.decode(self.token,
+            decoded = jwt.decode(self.config.token,
                                  algorithms=['HS256'],
                                  options={'verify_signature': False},
                                  audience="https://www.teamdynamix.com/")
-            self.token_exp = decoded['exp']
+            self.config.token_exp = decoded['exp']
             return True
         else:
             return False
@@ -344,9 +99,9 @@ def _check_auth_exp(self) -> bool:
         If it is expired, call auth() to get a new token.
         """
         # If token is expired or will expire in the next minute, get new token
-        if self.token_exp:
-            if self.token_exp < time.time() + 60:
-                self.logger.info(f"Token expires at {str(datetime.datetime.utcfromtimestamp(self.token_exp))}. "
+        if self.config.token_exp:
+            if self.config.token_exp < time.time() + 60:
+                self.logger.info(f"Token expires at {str(datetime.datetime.utcfromtimestamp(self.config.token_exp))}. "
                                  f"Getting new token...")
                 return self.auth()
             else:
@@ -374,7 +129,7 @@ def _rate_limit(self, skew_mitigation_secs=5):
 
     def make_get(self, request_url: str, retries: int = 3):
         """
-        Makes a HTTP GET request to the TDX Api.
+        Makes an HTTP GET request to the TDX Api.
 
         :param request_url: the path (everything after /TDWebAPI/api/) to call
         :param retries: the number of times to retry a failed request (defaults to 3)
@@ -383,7 +138,7 @@ def make_get(self, request_url: str, retries: int = 3):
 
         """
         self._rate_limit()
-        get_url = self.api_url + request_url
+        get_url = self.config.api_url + request_url
         response = None
         attempts = 0
         while attempts < retries:
@@ -394,7 +149,7 @@ def make_get(self, request_url: str, retries: int = 3):
                 response = requests.get(
                     url=get_url,
                     headers={
-                        "Authorization": 'Bearer ' + self.token,
+                        "Authorization": 'Bearer ' + self.config.token,
                         "Content-Type": "application/json; charset=utf-8",
                     }
                 )
@@ -422,7 +177,7 @@ def make_get(self, request_url: str, retries: int = 3):
 
     def make_post(self, request_url: str, body: dict):
         """
-        Makes a HTTP POST request to the TDX Api
+        Makes an HTTP POST request to the TDX Api
 
         :param request_url: the path (everything after /TDWebAPI/api/) to call
         :param body: dumped JSON data to send with the POST
@@ -431,7 +186,7 @@ def make_post(self, request_url: str, body: dict):
 
         """
         self._rate_limit()
-        post_url = self.api_url + request_url
+        post_url = self.config.api_url + request_url
         response = None
         try:
             if not (self._check_auth_exp()):
@@ -440,7 +195,7 @@ def make_post(self, request_url: str, body: dict):
             response = requests.post(
                 url=post_url,
                 headers={
-                    "Authorization": 'Bearer ' + self.token,
+                    "Authorization": 'Bearer ' + self.config.token,
                     "Content-Type": "application/json; charset=utf-8",
                 },
                 data=json.dumps(body))
@@ -468,7 +223,7 @@ def make_post(self, request_url: str, body: dict):
 
     def make_file_post(self, request_url: str, file: BinaryIO, filename: str = None):
         """
-        Makes a HTTP POST request to the TDX Api with a Multipart-Encoded File
+        Makes an HTTP POST request to the TDX Api with a Multipart-Encoded File
         
         :param request_url: the path (everything after /TDWebApi/api/) to call
         :param file: BinaryIO object opened in read mode to upload as attachment.
@@ -481,7 +236,7 @@ def make_file_post(self, request_url: str, file: BinaryIO, filename: str = None)
         :return: the API's response as a python dict
         """
         self._rate_limit()
-        post_url = self.api_url + request_url
+        post_url = self.config.api_url + request_url
         response = None
         if filename:
             files = {'file': (filename, file)}
@@ -494,7 +249,7 @@ def make_file_post(self, request_url: str, file: BinaryIO, filename: str = None)
             response = requests.post(
                 url=post_url,
                 headers={
-                    "Authorization": 'Bearer ' + self.token,
+                    "Authorization": 'Bearer ' + self.config.token,
                 },
                 files=files
             )
@@ -528,7 +283,7 @@ def make_put(self, request_url: str, body: dict):
 
         """
         self._rate_limit()
-        put_url = self.api_url + request_url
+        put_url = self.config.api_url + request_url
         response = None
         try:
             if not (self._check_auth_exp()):
@@ -537,7 +292,7 @@ def make_put(self, request_url: str, body: dict):
             response = requests.put(
                 url=put_url,
                 headers={
-                    "Authorization": 'Bearer ' + self.token,
+                    "Authorization": 'Bearer ' + self.config.token,
                     "Content-Type": "application/json; charset=utf-8",
                 },
                 data=json.dumps(body))
@@ -571,7 +326,7 @@ def make_delete(self, request_url: str):
         """
         self._rate_limit()
 
-        delete_url = self.api_url + request_url
+        delete_url = self.config.api_url + request_url
         try:
             if not (self._check_auth_exp()):
                 raise tdxlib.tdx_api_exceptions.TdxApiHTTPError(
@@ -579,7 +334,7 @@ def make_delete(self, request_url: str):
             response = requests.delete(
                 url=delete_url,
                 headers={
-                    "Authorization": 'Bearer ' + self.token,
+                    "Authorization": 'Bearer ' + self.config.token,
                     "Content-Type": "application/json; charset=utf-8",
                 })
             if response.status_code not in [200, 201]:
@@ -598,7 +353,7 @@ def make_patch(self, request_url: str, body: dict):
         """
         Makes an HTTP PATCH request to the TDX API.
 
-        The TeamDyanmix API supports limited PATCH functionality. Since TDX data is highly structured, items are
+        The TeamDynamix API supports limited PATCH functionality. Since TDX data is highly structured, items are
         referenced explicitly by their TDX ID, and not by their order in the object. Likewise, since the fields
         in a TDX object are all predefined, a PATCH call cannot add or remove any fields in the object.
 
@@ -609,7 +364,7 @@ def make_patch(self, request_url: str, body: dict):
 
         """
         self._rate_limit()
-        patch_url = self.api_url + request_url
+        patch_url = self.config.api_url + request_url
         response = None
         try:
             if not (self._check_auth_exp()):
@@ -618,7 +373,7 @@ def make_patch(self, request_url: str, body: dict):
             response = requests.patch(
                 url=patch_url,
                 headers={
-                    "Authorization": 'Bearer ' + self.token,
+                    "Authorization": 'Bearer ' + self.config.token,
                     "Content-Type": "application/json; charset=utf-8",
                 },
                 data=json.dumps(body)
@@ -714,7 +469,7 @@ def get_group_by_id(self, group_id: int) -> dict:
 
     def get_person_by_uid(self, uid: str) -> dict:
         """
-        Gets a a person by their UID.
+        Gets a person by their UID.
 
         :param uid: UID string corresponding to a person
 
@@ -899,7 +654,7 @@ def get_custom_attribute_by_name_id(self, key: str, object_type: int) -> dict:
 
         *NOTE: The best way to assign CA's is to test for an existing value (for choice-based CA's) using
         get_custom_attribute_value_by_name, and then if it returns false, directly assign the desired value to the CA.
-        Because of this, date-type and other format-specific attributes need to be in a TDX-acceptible format, this
+        Because of this, date-type and other format-specific attributes need to be in a TDX-acceptable format, this
         means that a field designated to hold person objects needs to be set to a UID.*
 
         :param key: a partial or full name of the custom attribute to search for
@@ -928,11 +683,11 @@ def get_custom_attribute_choice_by_name_id(self, attribute, key):
         Gets the choice item from a custom attribute, maybe from get_custom_attribute_by_name()
 
         *NOTE: The best way to assign CA's is to test for an existing value (for choice-based CA's), and then if this
-        method returns false, directly assign the desired value to the CA. Because of this, date-type and other format-
-        specific attributes need to be in a TDX-acceptible format, this means that a field designated to hold person
+        method returns false, directly assign the desired value to the CA. Because of this, date-type and other format
+        specific attributes need to be in a TDX-acceptable format, this means that a field designated to hold person
         objects needs to be set to a UID.*
 
-        :param key: a partial or full name name of the choice to look for
+        :param key: a partial or full name of the choice to look for
         :param attribute: a dict of custom attribute data (as retrieved from get_attribute_by_name())
 
         :return: the the choice object from this attribute whose name matches 'key', or False if none matches.
@@ -948,7 +703,7 @@ def get_custom_attribute_choice_by_name_id(self, attribute, key):
                 f"No custom attribute choice \"{str(key)}\" found in CA {attribute['Name']}")
         elif attribute['FieldType'] == 'datefield':
             value = tdxlib.tdx_utils.import_tdx_date(key)
-            return tdxlib.tdx_utils.export_tdx_date(value, self.timezone)
+            return tdxlib.tdx_utils.export_tdx_date(value, self.config.timezone)
         else:
             return str(key)
 
@@ -1004,7 +759,7 @@ def get_room_by_name(location: dict, room: str) -> dict:
         :param location: dict of location info
         :param room: partial or full name of a room to search for
 
-        :return: a dict with all the the information regarding the room. Use this to retrieve the ID attribute.
+        :return: a dict with all the information regarding the room. Use this to retrieve the ID attribute.
 
         :rtype: dict
 
diff --git a/tdxlib/tdx_ticket_integration.py b/tdxlib/tdx_ticket_integration.py
index f6dedd1..716242f 100644
--- a/tdxlib/tdx_ticket_integration.py
+++ b/tdxlib/tdx_ticket_integration.py
@@ -31,8 +31,8 @@ class TDXTicketIntegration(tdxlib.tdx_integration.TDXIntegration):
 
     def __init__(self, filename: str = None, config=None):
         tdxlib.tdx_integration.TDXIntegration.__init__(self, filename, config)
-        if self.ticket_app_id is None:
-            raise ValueError("Ticket App Id is required. Check your config file for 'ticketappid = 000'")
+        if self.config.ticket_app_id is None:
+            raise RuntimeError("Ticket App Id is required. Check your configuration.")
         self.clean_cache()
 
     def clean_cache(self):
@@ -52,7 +52,7 @@ def clean_cache(self):
         self.cache['ticket_form'] = {}
 
     def get_url_string(self):
-        return '/' + str(self.ticket_app_id) + '/tickets'
+        return '/' + str(self.config.ticket_app_id) + '/tickets'
 
     def _make_ticket_call(self, url: str, action: str, post_body: dict = None):
         url_string = self.get_url_string()
@@ -78,13 +78,14 @@ def make_call(self, url: str, action: str, post_body: dict = None):
         :param action: The HTTP action (get, put, post, delete, patch) to perform.
         :param post_body: A dict of the information to post, put, or patch. Not used for get/delete.
 
-        :return: the API's response as a python dict or list
+        :return: the API response as a python dict or list
 
         """
         return self._make_ticket_call(url, action, post_body)
 
     def get_all_ticket_custom_attributes(self):
-        return self.get_all_custom_attributes(TDXTicketIntegration.component_ids['ticket'], app_id=self.ticket_app_id)
+        return self.get_all_custom_attributes(TDXTicketIntegration.component_ids['ticket'],
+                                              app_id=self.config.ticket_app_id)
 
     def get_ticket_custom_attribute_by_name_id(self, key: str) -> dict:
         """
@@ -127,8 +128,8 @@ def build_ticket_custom_attribute_value(self, custom_attribute: Union[str, dict]
             value = tdxlib.tdx_utils.export_tdx_date(value)
         return {'ID': ca['ID'], 'Value': value}
 
-    def change_ticket_custom_attribute_value(self, ticket: Union[dict, str, int, list], custom_attributes: list) \
-        -> Union[tdxlib.tdx_ticket.TDXTicket,list]:
+    def change_ticket_custom_attribute_value(self, ticket: Union[dict, str, int, list],
+                                             custom_attributes: list) -> Union[tdxlib.tdx_ticket.TDXTicket, list]:
         """
         Takes a correctly formatted list of CA's (from build_ticket_custom_attribute_value, for instance)
         and updates one or more assets with the new values.
@@ -351,9 +352,9 @@ def update_ticket(self, ticket_id: Union[str, int], comments: str, new_status: s
 
         :param ticket_id: the ticket ID whose task to update
         :param comments: a string to provide as a comment to the update.
-        :param new_status: The name of the new status to set for the ticket (Default:
+        :param new_status: The name of the new status to set for the ticket (Default: The status whose ID is 0)
         :param notify: a list of strings containing email addresses to notify regarding this ticket. Default: None
-        :param private: boolean indicating whether or not the update to the task should be private. Default: True
+        :param private: boolean indicating whether the update to the task should be private. Default: True
 
         :return: python dict containing created ticket update information
 
@@ -564,7 +565,7 @@ def get_ticket_priority_by_name_id(self, key: Union[str, int]) -> dict:
 
     def get_all_ticket_urgencies(self) -> list:
         """
-        Gets all ticket urgencies from the tickets app.
+        Gets all ticket urgencies from the Tickets app.
 
         :return: list of priorities in python dict
 
@@ -672,7 +673,7 @@ def create_custom_ticket_status(self, name: str, order: float, status_class: str
         :param status_class:    A name of a status class. These values are hard-coded into the TDWebApi, and stored in
                                 this class as a class variable.
         :param description:     A string containing the description of the new status. (Default: Empty String)
-        :param active:          A bool indicating whether or not this new status should be active. (Default: True)
+        :param active:          A bool indicating whether this new status should be active. (Default: True)
 
         :return:                The new ticket status as a dict
 
@@ -792,7 +793,7 @@ def edit_ticket_task(self, ticket_id: int, task: Union[str, int, dict], changed_
         """
         Edits a ticket task with a set of new values.
 
-        :param ticket_id: The ticket Id on which the ticket task exists.
+        :param ticket_id: The ticket ID on which the ticket task exists.
         :param task: a single ticket task in dict (maybe from get_ticket_task_by_id), or a task ID
         :param changed_attributes: The new values ot set on the ticket task.
 
@@ -818,7 +819,7 @@ def delete_ticket_task(self, ticket_id: str, task_id: str) -> None:
         """
         Deletes a ticket task by ID
 
-        :param ticket_id: The ticket Id on which the ticket task exists.
+        :param ticket_id: The ticket ID on which the ticket task exists.
         :param task_id: The task ID of the task you want to delete.
 
         :return: none
@@ -831,7 +832,7 @@ def reassign_ticket_task(self, ticket_id: int, task: Union[str, dict, int], resp
         """
         Reassigns a ticket task to a person or group
 
-        :param ticket_id: The ticket Id on which the ticket task exists.
+        :param ticket_id: The ticket ID on which the ticket task exists.
         :param task: a single ticket task in dict (maybe from get_ticket_task_by_id), or a task ID
         :param responsible: a username, email, Full Name, or ID number to use to search for a person, or a group name.
         :param group: If this parameter is True, assign to group instead of individual
@@ -853,7 +854,7 @@ def reschedule_ticket_task(self, ticket_id, task,
         """
         Sets the start date and end date for a ticket task. This will affect the start & end dates of the parent ticket.
 
-        :param ticket_id: The ticket Id on which the ticket task exists.
+        :param ticket_id: The ticket ID on which the ticket task exists.
         :param task: a single ticket task in dict (maybe from get_ticket_task_by_id), or a task ID
         :param start_date: datetime.datetime object to use as the starting date for a task, defaults to now.
         :param end_date: datetime.datetime object to use as the starting date for a task, defaults to now + 1 day.
@@ -868,8 +869,8 @@ def reschedule_ticket_task(self, ticket_id, task,
         if not end_date:
             end_date = datetime.datetime.utcnow() + datetime.timedelta(days=1)
         new_dates = {
-            'StartDate': tdxlib.tdx_utils.export_tdx_date(start_date, self.timezone),
-            'EndDate': tdxlib.tdx_utils.export_tdx_date(end_date, self.timezone)
+            'StartDate': tdxlib.tdx_utils.export_tdx_date(start_date, self.config.timezone),
+            'EndDate': tdxlib.tdx_utils.export_tdx_date(end_date, self.config.timezone)
         }
         return self.edit_ticket_task(ticket_id, task, new_dates)
 
@@ -883,7 +884,7 @@ def update_ticket_task(self, ticket_id: int, task_id: int, percent: int, comment
         :param percent: the percent complete to set the task to after update
         :param comments: a string to provide as a comment to the update. Defaults to empty string.
         :param notify: a list of strings containing email addresses to notify regarding this ticket. Default: None
-        :param private: boolean indicating whether or not the update to the task should be private. Default: True
+        :param private: boolean indicating whether the update to the task should be private. Default: True
 
         :return: dict of update info
 
@@ -976,7 +977,7 @@ def generate_ticket(self, title_template: str, ticket_type: str, account: str, r
         :param requestor: name or email for requester of the ticket, defaults to username of integration (optional)
         :param classification: name of classification name for new ticket, default "Incident" (optional)
         :param form: name or ID of a form to use for the new ticket
-        :param responsible_is_group: Boolean indicating whether or not 'responsible' refers to a group. (Default: False)
+        :param responsible_is_group: Boolean indicating whether 'responsible' refers to a group. (Default: False)
         :param custom_attributes: dict of attribute names and values
 
         :return: TdxTicket object ready to be created via create_ticket()
@@ -988,7 +989,7 @@ def generate_ticket(self, title_template: str, ticket_type: str, account: str, r
 
         # set defaults
         if not requestor:
-            requestor = self.username
+            requestor = self.config.username
 
         # Required or defaulted parameters
         data = dict()
@@ -1037,8 +1038,8 @@ def generate_ticket(self, title_template: str, ticket_type: str, account: str, r
             else:
                 target_date = tdxlib.tdx_utils.import_tdx_date(due_date)
             start_date = target_date - datetime.timedelta(days=active_days)
-            data['StartDate'] = tdxlib.tdx_utils.export_tdx_date(start_date, self.timezone)
-            data['EndDate'] = tdxlib.tdx_utils.export_tdx_date(target_date, self.timezone)
+            data['StartDate'] = tdxlib.tdx_utils.export_tdx_date(start_date, self.config.timezone)
+            data['EndDate'] = tdxlib.tdx_utils.export_tdx_date(target_date, self.config.timezone)
 
         if location:
             building = self.get_location_by_name(location)
@@ -1084,8 +1085,8 @@ def generate_ticket_task(self, title: str, est_minutes: int = 30, description: s
         if description:
             data['Description'] = description
         if start and end:
-            data['StartDate'] = tdxlib.tdx_utils.export_tdx_date(start, self.timezone)
-            data['EndDate'] = tdxlib.tdx_utils.export_tdx_date(end, self.timezone)
+            data['StartDate'] = tdxlib.tdx_utils.export_tdx_date(start, self.config.timezone)
+            data['EndDate'] = tdxlib.tdx_utils.export_tdx_date(end, self.config.timezone)
         if completion_minutes:
             data['CompleteWithinMinutes'] = completion_minutes
         if group:


Test patch:
diff --git a/test/test_setup.py b/test/test_setup.py
new file mode 100644
index 0000000..5f50eb4
--- /dev/null
+++ b/test/test_setup.py
@@ -0,0 +1,28 @@
+import unittest
+
+from unittest.mock import patch, Mock
+
+from tdxlib import tdx_integration
+
+# python -m unittest test.test_setup.TdxSetupTesting.test_init
+
+class TdxSetupTesting(unittest.TestCase):
+
+	@patch.object(tdx_integration.TDXIntegration, "load_config_from_file")
+	@patch.object(tdx_integration.TDXIntegration, "run_setup_wizard")
+	def test_init(self, mock_wizzard, mock_load):
+		"""Assert that functions for building configuration 
+		interactively or from a file are not used when 
+		dictionary settings are provided.
+		"""
+		tdx = tdx_integration.TDXIntegration(config=
+			{
+				"orgname":'example.edu'
+			})
+		assert mock_load.call_count == 0
+		assert mock_wizzard.call_count == 0
+
+
+if __name__ == "__main__":
+    suite = unittest.TestLoader().loadTestsFromTestCase(TdxSetupTesting)
+    unittest.TextTestRunner(verbosity=2).run(suite)
\ No newline at end of file
diff --git a/testing/tdx_asset_testing.py b/testing/tdx_asset_testing.py
index 75f6470..13fdca6 100644
--- a/testing/tdx_asset_testing.py
+++ b/testing/tdx_asset_testing.py
@@ -13,7 +13,7 @@ def setUp(self):
         self.is_admin = False
         testing_vars_file = '../testing_vars.json'
         self.tax = tdx_asset_integration.TDXAssetIntegration('../tdxlib.ini')
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             print("Not in Sandbox... Aborting")
             quit()
         right_now = dt.today()
@@ -29,14 +29,14 @@ def setUp(self):
     def test_aaa(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
-        self.assertGreater(len(self.tax.token), 200)
+        self.assertGreater(len(self.tax.config.token), 200)
 
     def test_asset_forms(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         forms = self.tax.get_all_asset_forms()
         self.assertGreater(len(forms), 1)
@@ -44,7 +44,7 @@ def test_asset_forms(self):
     def test_asset_form_name(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         form = self.tax.get_asset_form_by_name_id(self.testing_vars['asset_form']['Name'])
         self.assertEqual(int(form['ID']), self.testing_vars['asset_form']['ID'])
@@ -52,7 +52,7 @@ def test_asset_form_name(self):
     def test_asset_form_id(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         form = self.tax.get_asset_form_by_name_id(self.testing_vars['asset_form']['ID'])
         self.assertEqual(form['Name'], self.testing_vars['asset_form']['Name'])
@@ -60,7 +60,7 @@ def test_asset_form_id(self):
     def test_asset_statuses(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         statuses = self.tax.get_all_asset_statuses()
         self.assertGreater(len(statuses),3)
@@ -68,7 +68,7 @@ def test_asset_statuses(self):
     def test_asset_status_id(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         status = self.tax.get_asset_status_by_name_id(self.testing_vars['asset_status1']['ID'])
         self.assertEqual(status['Name'], self.testing_vars['asset_status1']['Name'])
@@ -76,7 +76,7 @@ def test_asset_status_id(self):
     def test_asset_status_name(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         status = self.tax.get_asset_status_by_name_id(self.testing_vars['asset_status1']['Name'])
         self.assertEqual(int(status['ID']), self.testing_vars['asset_status1']['ID'])
@@ -84,7 +84,7 @@ def test_asset_status_name(self):
     def test_asset_product_types(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         product_types = self.tax.get_all_product_types()
         self.assertGreater(len(product_types),3)
@@ -92,7 +92,7 @@ def test_asset_product_types(self):
     def test_asset_product_type_id(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         product_type = self.tax.get_product_type_by_name_id(self.testing_vars['product_type']['ID'])
         self.assertEqual(product_type['Name'], self.testing_vars['product_type']['Name'])
@@ -100,7 +100,7 @@ def test_asset_product_type_id(self):
     def test_asset_product_type_name(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         product_type = self.tax.get_product_type_by_name_id(self.testing_vars['product_type']['Name'])
         self.assertEqual(int(product_type['ID']), self.testing_vars['product_type']['ID'])
@@ -108,7 +108,7 @@ def test_asset_product_type_name(self):
     def test_asset_product_models(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         product_models = self.tax.get_all_product_models()
         self.assertGreater(len(product_models),3)
@@ -116,7 +116,7 @@ def test_asset_product_models(self):
     def test_asset_product_model_id(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         product_model = self.tax.get_product_model_by_name_id(self.testing_vars['product_model']['ID'])
         self.assertEqual(product_model['Name'], self.testing_vars['product_model']['Name'])
@@ -124,7 +124,7 @@ def test_asset_product_model_id(self):
     def test_asset_product_model_name(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         product_model = self.tax.get_product_model_by_name_id(self.testing_vars['product_model']['Name'])
         self.assertEqual(int(product_model['ID']), self.testing_vars['product_model']['ID'])
@@ -132,7 +132,7 @@ def test_asset_product_model_name(self):
     def test_update_product_type(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         product_type = self.tax.get_product_type_by_name_id(self.testing_vars['product_type']['Name'])
         changed_attributes = {'Description': self.timestamp}
@@ -142,7 +142,7 @@ def test_update_product_type(self):
     def test_create_product_type(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         name = 'Temp Product Type ' + self.timestamp
         new_product_type = self.tax.create_product_type(name)
@@ -154,7 +154,7 @@ def test_create_product_type(self):
     def test_create_product_model(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         name = 'Temp Product Model ' + self.timestamp
         type = self.tax.get_product_type_by_name_id(self.testing_vars['product_type']['ID'])
@@ -166,7 +166,7 @@ def test_create_product_model(self):
     def test_get_all_vendors(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         vendors = self.tax.get_all_vendors()
         self.assertGreaterEqual(len(vendors), 5)
@@ -174,7 +174,7 @@ def test_get_all_vendors(self):
     def test_get_vendor_by_name_id(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         name = self.testing_vars['vendor']['Name']
         vendor = self.tax.get_vendor_by_name_id(name)
@@ -185,7 +185,7 @@ def test_get_vendor_by_name_id(self):
     def test_create_vendor(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         name = 'Temp Vendor ' + self.timestamp
         new_vendor = self.tax.create_vendor(name)
@@ -195,7 +195,7 @@ def test_create_vendor(self):
     def test_get_asset_id(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.get_asset_by_id(self.testing_vars['asset1']['ID'])
         self.assertEqual(asset['Name'], self.testing_vars['asset1']['Name'])
@@ -203,7 +203,7 @@ def test_get_asset_id(self):
     def test_search_asset(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         assets = self.tax.search_assets(self.testing_vars['asset_search']['Text'])
         self.assertGreaterEqual(len(assets), self.testing_vars['asset_search']['Count'])
@@ -211,7 +211,7 @@ def test_search_asset(self):
     def test_find_asset_tag(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.find_asset_by_tag(self.testing_vars['asset2']['Tag'])
         self.assertEqual(asset['SerialNumber'], self.testing_vars['asset2']['SerialNumber'])
@@ -219,7 +219,7 @@ def test_find_asset_tag(self):
     def test_find_asset_SN(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.find_asset_by_sn(self.testing_vars['asset2']['SerialNumber'])
         self.assertEqual(asset['Tag'].lstrip('0'), self.testing_vars['asset2']['Tag'].lstrip('0'))
@@ -233,7 +233,7 @@ def test_get_assets_by_location(self):
     def test_get_assets_by_locations(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         assets = self.tax.get_assets_by_location([self.testing_vars['location1'],self.testing_vars['location2']])
         self.assertGreaterEqual(len(assets),
@@ -242,7 +242,7 @@ def test_get_assets_by_locations(self):
     def test_get_assets_by_room(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         location = self.tax.get_location_by_name(self.testing_vars['location3']['Name'])
         room1 = self.tax.get_room_by_name(location, self.testing_vars['room1']['Name'])
@@ -252,7 +252,7 @@ def test_get_assets_by_room(self):
     def test_get_assets_by_owner(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         assets = self.tax.get_assets_by_owner(self.testing_vars['owner']['PrimaryEmail'], max_results=100, disposed=True)
         self.assertGreaterEqual(len(assets), self.testing_vars['owner']['asset_count'])
@@ -260,7 +260,7 @@ def test_get_assets_by_owner(self):
     def test_get_assets_by_requesting_dept(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         assets = self.tax.get_assets_by_requesting_department(self.testing_vars['department']['Name'], max_results=100, disposed=True)
         self.assertGreaterEqual(len(assets), self.testing_vars['department']['asset_count'])
@@ -268,7 +268,7 @@ def test_get_assets_by_requesting_dept(self):
     def test_get_assets_by_product_model(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         assets = self.tax.get_assets_by_product_model(self.testing_vars['product_model']['Name'], max_results=100, retired=True, disposed=True)
         self.assertGreaterEqual(len(assets), self.testing_vars['product_model']['asset_count'])
@@ -276,7 +276,7 @@ def test_get_assets_by_product_model(self):
     def test_get_assets_by_product_type(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         assets = self.tax.get_assets_by_product_type(self.testing_vars['product_type']['Name'], max_results=100, retired=True, disposed=True)
         self.assertGreaterEqual(len(assets), self.testing_vars['product_type']['asset_count'])
@@ -284,7 +284,7 @@ def test_get_assets_by_product_type(self):
     def test_update_asset_status(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         assets_to_update = self.tax.get_assets_by_owner(self.testing_vars['owner']['PrimaryEmail'], max_results=100, disposed=True)
         new_status = str(self.testing_vars['asset_status1']['ID'])
@@ -299,7 +299,7 @@ def test_update_asset_status(self):
     def test_update_asset_owner(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.get_asset_by_id(self.testing_vars['asset1']['ID'])
         new_owner = self.testing_vars['owner']
@@ -312,7 +312,7 @@ def test_update_asset_owner(self):
     def test_update_asset_requesting_dept(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.get_asset_by_id(self.testing_vars['asset1']['ID'])
         new_dept = self.testing_vars['department']
@@ -324,7 +324,7 @@ def test_update_asset_requesting_dept(self):
     def test_custom_attribute_updating_id(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.get_asset_by_id(self.testing_vars['asset1']['ID'])
         new_attributes = self.testing_vars['attributes1']
@@ -345,7 +345,7 @@ def test_custom_attribute_updating_id(self):
     def test_custom_attribute_updating_name(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.get_asset_by_id(self.testing_vars['asset1']['ID'])
         new_attributes = self.testing_vars['attributes1']
@@ -366,7 +366,7 @@ def test_custom_attribute_updating_name(self):
     def test_add_custom_attributes(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         asset = self.tax.get_asset_by_id(self.testing_vars['asset2']['ID'])
         change = {'Name': asset['Name']+'(Cleared)'}
@@ -391,7 +391,7 @@ def test_add_custom_attributes(self):
     def test_add_asset_user(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         self.tax.delete_asset_users(self.testing_vars['asset2']['ID'], self.testing_vars['person1']['UID'])
         validate1 = self.tax.get_asset_users(self.testing_vars['asset2']['ID'])
@@ -402,7 +402,7 @@ def test_add_asset_user(self):
     def test_get_asset_users(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         self.tax.add_asset_user(self.testing_vars['asset2']['ID'], self.testing_vars['person1']['UID'])
         validate = self.tax.get_asset_users(self.testing_vars['asset2']['ID'])
@@ -412,7 +412,7 @@ def test_get_asset_users(self):
     def test_delete_asset_users(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         self.tax.add_asset_user(self.testing_vars['asset2']['ID'], self.testing_vars['person1']['UID'])
         self.tax.delete_asset_users(self.testing_vars['asset2']['ID'], self.testing_vars['person1']['UID'])
@@ -434,7 +434,7 @@ def test_change_asset_location(self):
     def test_clear_asset_custom_attributes(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         new_asset =  self.tax.create_asset(
             self.tax.build_asset(f'testing2.{self.timestamp}', f'testing2.{self.timestamp}',
@@ -447,7 +447,7 @@ def test_clear_asset_custom_attributes(self):
     def test_get_asset_custom_attribute_value_by_name(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         new_asset = self.tax.create_asset(
             self.tax.build_asset(f'testing3.{self.timestamp}', f'testing3.{self.timestamp}',
@@ -460,7 +460,7 @@ def test_get_asset_custom_attribute_value_by_name(self):
     def test_copy_asset_attributes(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         sn1 = f'testing4.{self.timestamp}'
         new_asset1 = self.tax.create_asset(
@@ -479,7 +479,7 @@ def test_copy_asset_attributes(self):
     def test_change_asset_custom_attributes(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         sn1 = f'testing4.{self.timestamp}'
         new_asset1 = self.tax.create_asset(
@@ -499,7 +499,7 @@ def test_change_asset_custom_attributes(self):
     def test_move_child_assets(self):
         if not self.tax:
             self.setUp()
-        if not self.tax.sandbox:
+        if not self.tax.config.sandbox:
             return
         child_asset_id = self.testing_vars['child_asset']['ID']
         parent_asset_id = self.testing_vars['parent_asset']['ID']
diff --git a/testing/tdx_integration_testing.py b/testing/tdx_integration_testing.py
index 6e06d83..23888ec 100644
--- a/testing/tdx_integration_testing.py
+++ b/testing/tdx_integration_testing.py
@@ -23,11 +23,37 @@ def setUp(self):
             print('Testing variables need to be populated in file "testing_vars.json" in the working directory.',
                   'A sample file is available in testing/sample_ticket_testing_vars. Any *.json files are ignored by git.')
 
+    def test_config_by_file(self):
+        test_tdxlib = tdx_integration.TDXIntegration('../tdxlib.ini')
+        self.assertIsNotNone(test_tdxlib.config)
+        self.assertGreater(len(test_tdxlib.config.api_url), 13)
+        self.assertTrue(self.testing_vars['org_name'] in test_tdxlib.config.api_url)
+
+    def test_config_by_dict(self):
+        test_dict = self.testing_vars['test_config_dict']
+        test_tdxlib = tdx_integration.TDXIntegration(config=test_dict)
+        self.assertIsNotNone(test_tdxlib.config)
+        self.assertGreater(len(test_tdxlib.config.api_url), 13)
+        self.assertTrue(self.testing_vars['org_name'] in test_tdxlib.config.api_url)
+        self.assertFalse(test_tdxlib.config.caching)
+
+    def test_config_by_env(self):
+        test_dict = self.testing_vars['test_config_dict']['TDX API Settings']
+        for k,v in test_dict.items():
+            env_name = f'TDXLIB_{k.upper()}'
+            os.environ[env_name] = str(v)
+        os.environ.update()
+        test_tdxlib = tdx_integration.TDXIntegration()
+        self.assertIsNotNone(test_tdxlib.config)
+        self.assertGreater(len(test_tdxlib.config.api_url), 13)
+        self.assertTrue(self.testing_vars['org_name'] in test_tdxlib.config.api_url)
+        self.assertFalse(test_tdxlib.config.caching)
+
     def test_authentication(self):
         if not self.tdx:
             self.setUp()
-        self.assertIsNotNone(self.tdx.token)
-        self.assertGreater(len(self.tdx.token), 200)
+        self.assertIsNotNone(self.tdx.config.token)
+        self.assertGreater(len(self.tdx.config.token), 200)
 
     def test_check_auth_exp(self):
         if not self.tdx:
@@ -209,13 +235,13 @@ def test_get_room_by_partial_name(self):
     def test_create_account(self):
         if not self.tdx:
             self.setUp()
-        if not self.tdx.sandbox:
+        if not self.tdx.config.sandbox:
             return
         name = 'Testing Account ' + TdxIntegrationTesting.timestamp
         additional_info = {'Address1': '123 Main Street'}
         ca = self.testing_vars['account_ca']
         custom_attributes = {ca['Name']:ca['choice']['Name']}
-        account = self.tdx.create_account(name, self.tdx.username, additional_info,
+        account = self.tdx.create_account(name, self.tdx.config.username, additional_info,
                                           custom_attributes)
         self.assertTrue(account)
         self.assertEqual(account['Address1'], additional_info['Address1'])
@@ -225,7 +251,7 @@ def test_create_account(self):
     def test_edit_account(self):
         if not self.tdx:
             self.setUp()
-        if not self.tdx.sandbox:
+        if not self.tdx.config.sandbox:
             return
         # This will fail if test_create_account fails
         name = 'Testing Account ' + TdxIntegrationTesting.timestamp
@@ -236,7 +262,7 @@ def test_edit_account(self):
     def test_create_room(self):
         if not self.tdx:
             self.setUp()
-        if not self.tdx.sandbox:
+        if not self.tdx.config.sandbox:
             return
         if not self.is_admin:
             return
diff --git a/testing/tdx_ticket_testing.py b/testing/tdx_ticket_testing.py
index d9e180f..cf44027 100644
--- a/testing/tdx_ticket_testing.py
+++ b/testing/tdx_ticket_testing.py
@@ -27,7 +27,7 @@ def setUp(self):
     def test_authn(self):
         if not self.timestamp:
             self.setUp()
-        self.assertGreater(len(self.tix.token), 200)
+        self.assertGreater(len(self.tix.config.token), 200)
 
     def test_get_ticket_by_id(self):
         if not self.timestamp:
@@ -45,7 +45,7 @@ def test_edit_tickets(self):
         if not self.timestamp:
             self.setUp()
         # Protect production from deleting tasks
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_list = list()
         ticket_list.append(self.tix.get_ticket_by_id(self.testing_vars['ticket2']['ID']))
@@ -179,14 +179,14 @@ def test_create_ticket(self):
         if not self.timestamp:
             self.setUp()
         # Protect production from testing tickets
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         else:
             title_string = "Testing Ticket Created " + self.timestamp
             json_data = self.tix.generate_ticket(title_string,
                                                  self.testing_vars['ticket_type']['Name'],
                                                  self.testing_vars['account']['Name'],
-                                                 self.tix.username)
+                                                 self.tix.config.username)
             created_ticket = self.tix.create_ticket(json_data)
             self.assertTrue(created_ticket.get_id() > 10000)
 
@@ -209,7 +209,7 @@ def test_create_ticket_task(self):
         if not self.timestamp:
             self.setUp()
         # Protect production from testing tasks
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         else:
             ticket_id = self.testing_vars['ticket2']['ID']
@@ -223,7 +223,7 @@ def test_edit_ticket_task(self):
         if not self.timestamp:
             self.setUp()
         # Protect production from deleting tasks
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket2']['ID']
         task_id = self.testing_vars['ticket2']['task']['ID']
@@ -238,7 +238,7 @@ def test_edit_ticket_task(self):
     def test_delete_ticket_task(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         # Create the task
         ticket_id = self.testing_vars['ticket2']['ID']
@@ -256,7 +256,7 @@ def test_delete_ticket_task(self):
     def test_reassign_ticket_task(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket2']['ID']
         task_id = self.testing_vars['ticket2']['task']['ID']
@@ -271,7 +271,7 @@ def test_reassign_ticket_task(self):
     def test_reassign_ticket(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket2']['ID']
         ticket = self.tix.get_ticket_by_id(ticket_id)
@@ -285,7 +285,7 @@ def test_reassign_ticket(self):
     def test_reschedule_ticket(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket1']['ID']
         start = dt.utcnow()
@@ -296,7 +296,7 @@ def test_reschedule_ticket(self):
     def test_reschedule_ticket_task(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket2']['ID']
         task_id = self.testing_vars['ticket2']['task']['ID']
@@ -308,7 +308,7 @@ def test_reschedule_ticket_task(self):
     def test_generate_ticket_task(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         task = self.tix.generate_ticket_task("Testing Task", description="this is a test",start=dt.utcnow(),
                                              responsible=self.testing_vars['person1']['FullName'])
@@ -319,7 +319,7 @@ def test_create_custom_ticket_status(self):
             self.setUp()
         if not self.is_admin:
             return
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         status = self.tix.create_custom_ticket_status('Never Gonna Happen', 1, 'Cancelled')
         get_status = self.tix.get_ticket_status_by_id(status['ID'])
@@ -330,7 +330,7 @@ def test_edit_custom_ticket_status(self):
             self.setUp()
         if not self.is_admin:
             return
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         self.test_create_custom_ticket_status()
         to_change = {'Name': 'Never Gonna Give You Up'}
@@ -340,7 +340,7 @@ def test_edit_custom_ticket_status(self):
     def test_update_ticket_task(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket2']['ID']
         task_id = self.testing_vars['ticket2']['task']['ID']
@@ -350,7 +350,7 @@ def test_update_ticket_task(self):
     def test_update_ticket(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket1']['ID']
         update = self.tix.update_ticket(ticket_id, comments=str(self.timestamp), new_status='Open')
@@ -359,7 +359,7 @@ def test_update_ticket(self):
     def test_upload_attachment(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket1']['ID']
         filename = self.testing_vars['attachment']['Name']
@@ -370,7 +370,7 @@ def test_upload_attachment(self):
     def test_upload_attachment_filename(self):
         if not self.timestamp:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         ticket_id = self.testing_vars['ticket2']['ID']
         filepath = self.testing_vars['attachment']['Name']
@@ -397,7 +397,7 @@ def test_get_ticket_feed(self):
     def test_build_custom_attribute_value(self):
         if not self.tix:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         new_ca = self.tix.build_ticket_custom_attribute_value(self.testing_vars['ticket_ca']['Name'],
                                                               self.testing_vars['ticket_ca']['choice']['Name'])
@@ -407,7 +407,7 @@ def test_build_custom_attribute_value(self):
     def test_change_custom_attribute_value(self):
         if not self.tix:
             self.setUp()
-        if not self.tix.sandbox:
+        if not self.tix.config.sandbox:
             return
         new_ca = self.tix.build_ticket_custom_attribute_value(self.testing_vars['ticket_ca']['Name'],
                                                               self.testing_vars['ticket_ca']['choice2']['Name'])