Skip to content

Commit

Permalink
Merge branch 'master' into pv/taco-version-name
Browse files Browse the repository at this point in the history
  • Loading branch information
pvanderknyff authored Oct 27, 2023
2 parents 31a1e1c + 6c6bcf8 commit 1a51e52
Show file tree
Hide file tree
Showing 201 changed files with 7,958 additions and 2,877 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ labels: ''
assignees: ''

---
Thanks for filing a bug report! Please check out our docs on [Filing a Bug with the SDK](https://tableau.github.io/connector-plugin-sdk/docs/bug-artifacts) for guidance on what to include with your report.

**Describe the bug**
A clear and concise description of what the bug is.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.9
- name: Set up Java
uses: actions/setup-java@v1
with:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tdvt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ jobs:

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.9
- name: Install TDVT
run: |
python -m pip install --upgrade pip
pip install -e tdvt/
- name: Run TDVT unit tests
run: |
cd tdvt/test
python tdvt_test.py -v CommandLineTest ConfigTest DiffTest PrintConfigurationsTest ResultsTest ResultsExceptionTest
python tdvt_test.py -v CommandLineTest ConfigTest DiffTest PrintConfigurationsTest ResultsTest ResultsExceptionTest TestCreatorTest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ dmypy.json
packager_tests_logs.txt
packaging_logs.txt

# MacOS
.DS_Store

# TDVT logs
tabquery_logs.zip
tdvt_actuals_combined.zip
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# Tableau Connector SDK Changelog
## 2023-04-10
### Changed
- Update `oauth_config.xsd` to include `configLabel` field and update `min-version-tableau` to be 2023.2 if present
## 2023-04-04
### Changed
- Update `oauth_config.xsd` to include `instanceUrlSuffix` field and update `min-version-tableau` to be 2023.1 if present
## 2022-12-16
### Changed
- Convert database impersonation sample to Connection Dialog V2
## 2022-11-21
### Changed
- Update `connector_plugin_manifest_latest.xsd` to allow multple `oauth-config` fields and update `min-version-tableau` to be 2021.4 if present
## 2022-11-16
### Changed
- Update `oauth_config.xsd` to include `oauthConfigId` field and update `min-version-tableau` to be 2021.4 if present
## 2021-09-02
### Changed
- Validate max length of `name` in Company-G in `connector_plugin_manifest_latest.xsd`
Expand Down
40 changes: 23 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@

This project consists of documentation, example files, the Tableau Datasource Verification Tool (TDVT) test harness, and a packaging tool that you can use to build and customize a Tableau Connector that uses an ODBC or JDBC driver.

The latest version of the SDK is always targeted towards the latest, non-beta version of Tableau. Right now, this is **Tableau Desktop/Server 2021.3**. This means that the samples may not work on older versions of Tableau, and connectors packaged with newer versions of the SDK may not work in older versions of Tableau. You can download past releases of the SDK to work with older versions of Tableau if necessary.

| Tool | Latest Version |
|--------------------------------------------------|--------------------|
| Connector SDK for Tableau 2021.2 | 09-09-2021 |
| Connector SDK for Tableau 2021.1 | 07-14-2021 |
| Connector SDK for Tableau 2020.4 | 03-29-2021 |
| Connector SDK for Tableau 2020.3 | 12-07-2020 |
| Connector SDK for Tableau 2020.2 | 8-12-2020 |
| Connector SDK for Tableau 2020.1 | 5-08-2020 |
| Connector SDK for Tableau 2019.4 | 3-13-2020 |
| Connector Packager SDK (Beta) for Tableau 2019.3 | 12-11-2019 |
| TDVT | 2.5.0 (12-01-2021) |
| | 1.5.24 (04-13-2020)|
| Connector Packager | 2.1.0 (05-08-2020) |
The latest version of the SDK is always targeted towards the latest, non-beta version of Tableau. Right now, this is **Tableau Desktop/Server 2023.2**. This means that the samples may not work on older versions of Tableau, and connectors packaged with newer versions of the SDK may not work in older versions of Tableau. You can download past releases of the SDK to work with older versions of Tableau if necessary.

| Tool | Latest Version |
|--------------------------------------------------|---------------------|
| Connector SDK for Tableau 2023.1 | 06-15-2023 |
| Connector SDK for Tableau 2022.4 | 03-15-2023 |
| Connector SDK for Tableau 2022.3 | 12-14-2022 |
| Connector SDK for Tableau 2022.2 | 10-18-2022 |
| Connector SDK for Tableau 2022.1 | 06-29-2022 |
| Connector SDK for Tableau 2021.4 | 03-31-2022 |
| Connector SDK for Tableau 2021.3 | 12-15-2021 |
| Connector SDK for Tableau 2021.2 | 09-09-2021 |
| Connector SDK for Tableau 2021.1 | 07-14-2021 |
| Connector SDK for Tableau 2020.4 | 03-29-2021 |
| Connector SDK for Tableau 2020.3 | 12-07-2020 |
| Connector SDK for Tableau 2020.2 | 8-12-2020 |
| Connector SDK for Tableau 2020.1 | 5-08-2020 |
| Connector SDK for Tableau 2019.4 | 3-13-2020 |
| Connector Packager SDK (Beta) for Tableau 2019.3 | 12-11-2019 |
| TDVT | 2.7.0 (10-28-2022) |
| | 1.5.24 (04-13-2020) |
| Connector Packager | 2.1.0 (05-08-2020) |

* [Why Connectors?](#why-connectors)
* [Get started](#get-started)
Expand Down Expand Up @@ -48,14 +55,13 @@ The SDK includes several [standalone example connectors](https://github.com/tabl
To work with connectors, you need the following:

* Windows or Mac
* Tableau Desktop or Server 2019.1 Beta 2 or higher
* Tableau Desktop or Server 2020.4
* Python 3.7 or higher
* An ODBC or JDBC data source and driver
* The provided test data loaded in your data source

To package the connector into a .taco file, you will also need:

* Tableau Desktop or Server 2019.4 Beta 1 or higher
* JDK 8 or higher

For a JDBC connector, your driver must fulfill the following requirements:
Expand Down
28 changes: 27 additions & 1 deletion connector-packager/connector_packager/jar_jdk_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,22 @@ def get_min_support_version(file_list: List[ConnectorFile], cur_min_version_tabl
min_version_tableau = "2020.3"
reasons.append("Connector uses Connection Dialogs V2, which was added in the 2020.3 release")
elif connector_file.file_type == "oauth-config":
if 2021.1 > float(min_version_tableau):
# Check to see if we're using oauthConfigId, which needs 2021.4+
pluginOAuthConfigRoot = ET.parse(input_dir / connector_file.file_name).getroot()

oauthConfigId = pluginOAuthConfigRoot.find('.//oauthConfigId')
instanceUrlSuffix = pluginOAuthConfigRoot.find('.//instanceUrlSuffix')
configLabel = pluginOAuthConfigRoot.find('.//configLabel')
if (configLabel is not None and 2023.2 > float(min_version_tableau)):
min_version_tableau = "2023.2"
reasons.append("Connector uses configLabel field, which was added in the 2023.2 release")
elif (instanceUrlSuffix is not None and 2023.1 > float(min_version_tableau)):
min_version_tableau = "2023.1"
reasons.append("Connector uses instanceUrlSuffix field, which was added in the 2023.1 release")
elif (oauthConfigId is not None and 2021.4 > float(min_version_tableau)):
min_version_tableau = "2021.4"
reasons.append("Connector contains an oauthConfigId field, which was added in the 2021.4 release")
elif 2021.1 > float(min_version_tableau):
min_version_tableau = "2021.1"
reasons.append("Connector uses OAuth, which was added in the 2021.1 release")
elif connector_file.file_type == "connection-resolver":
Expand All @@ -67,6 +82,17 @@ def get_min_support_version(file_list: List[ConnectorFile], cur_min_version_tabl
if 2021.1 > float(min_version_tableau):
min_version_tableau = "2021.1"
reasons.append("Connector uses inferred connection resolver, which was added in the 2021.1 release")
elif connector_file.file_type == "manifest":
manifestRoot = ET.parse(input_dir / connector_file.file_name).getroot()
oauthConfigs = manifestRoot.findall('.//oauth-config')
if (oauthConfigs is not None and len(oauthConfigs) > 1 and 2023.1 > float(min_version_tableau)):
min_version_tableau = "2023.1"
reasons.append("Support for multiple OAuth configs was added in the 2023.1 release")
elif (oauthConfigs is not None and len(oauthConfigs) == 1):
firstConfig = oauthConfigs[0]
if firstConfig.attrib['file'] == "null_config" and 2024.1 > float(min_version_tableau):
min_version_tableau = "2024.1"
reasons.append("Connector uses Null OAuth Config, which was added in the 2024.1 release")

if version.parse(cur_min_version_tableau) > version.parse(min_version_tableau):
reasons.append("min-tableau-version set to " + cur_min_version_tableau + ", since that is higher than calculated version of " + min_version_tableau)
Expand Down
96 changes: 61 additions & 35 deletions connector-packager/connector_packager/xml_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
MAX_FILE_SIZE = 1024 * 256 # This is based on the max file size we will load on the Tableau side
HTTPS_STRING = "https://"
TRANSLATABLE_STRING_PREFIX = "@string/"
TABLEAU_FALLBACK_LANGUAGE = "en_US" # If localizing a connector, US English must be translated since we'll fall back to the English strings if we can't find one for the correct language
# If localizing a connector, US English must be translated since we'll fall back to the English strings if we can't
# find one for the correct language
TABLEAU_FALLBACK_LANGUAGE = "en_US"
TABLEAU_SUPPORTED_LANGUAGES = ["de_DE", "en_GB", "es_ES", "fr_CA", "fr_FR", "ga_IE", "it_IT", "ja_JP", "ko_KR",
"pt_BR", "zh_CN", "zh_TW"]
"pt_BR", "sv_SE", "th_TH", "zh_CN", "zh_TW"]


class XMLParser:
Expand All @@ -38,6 +40,8 @@ def __init__(self, path_to_folder: Path):
self.file_list = [] # list of files to package
self.loc_strings = [] # list of loc strings so we can make sure they are covered in the resource files.
self.connector_version = None # Get ths from the plugin-version attribute in the manifest
self.null_oauth_config_found = False # whether or not we found a null oauth config
self.num_oauth_configs_found = 0 # number of oauth configs found, so we can fail the connector if there are non-null cconfigs and a null config

def generate_file_list(self) -> Optional[List[ConnectorFile]]:
"""
Expand Down Expand Up @@ -87,7 +91,9 @@ def generate_file_list(self) -> Optional[List[ConnectorFile]]:
self.file_list.append(ConnectorFile(fallback_resource_file_name, "resource"))
logging.debug("Adding file to list (name = " + fallback_resource_file_name + ", type = resource)")
else:
logger.error("Error: Found localized strings but " + fallback_resource_file_name + " does not exist. US English translations are required to fall back on if other languages are not translated.")
logger.error("Error: Found localized strings but " + fallback_resource_file_name +
" does not exist. US English translations are required to fall back on if other languages"
" are not translated.")
return None

# Check for files for each of the languages we suport
Expand Down Expand Up @@ -125,7 +131,7 @@ def parse_file(self, file_to_parse: ConnectorFile) -> bool:

# If the file is too big, we shouldn't try and parse it, just log the violation and move on
if path_to_file.stat().st_size > MAX_FILE_SIZE:
logging.error(file_to_parse.file_name + " exceeds maximum size of " + str(int(MAX_FILE_SIZE / 1024)) + " KB")
logging.error(file_to_parse.file_name + " exceeds maximum size of " + str(int(MAX_FILE_SIZE / 1024)) + "KB")
return False

# Get XML file ready for parsing
Expand All @@ -144,7 +150,8 @@ def parse_file(self, file_to_parse: ConnectorFile) -> bool:
for child in root.iter():

# Check the tag
# Oauth config file uses dbclass tag instead of class attribute. We need to make sure that matches the class name as well.
# Oauth config file uses dbclass tag instead of class attribute. We need to make sure that matches the
# class name as well.
if child.tag == "dbclass":
if child.text != self.class_name:
logging.error("Error: dbclass in file " + file_to_parse.file_name +
Expand All @@ -153,46 +160,65 @@ def parse_file(self, file_to_parse: ConnectorFile) -> bool:
file_to_parse.file_name)
return False

# If oauth-config attribute, keep track of how many we find. Enforce that if null oauth config only one config is defined
if child.tag == "oauth-config":
if self.null_oauth_config_found:
logger.error("Error: cannot declare a null OAuth config in connector with non-null configs")
return False

if 'file' in child.attrib and child.attrib['file'] == "null_config":

# If we already found oauth configs and then found a null config, reject the connector
if self.num_oauth_configs_found > 0:
logger.error("Error: cannot declare a null OAuth config in connector with non-null configs")
return False
else:
logger.debug("Null OAuth config found")
self.null_oauth_config_found = True

self.num_oauth_configs_found+=1



# Check the attributes
# If xml element has file attribute, add it to the file list. If it's not a script, parse that file too.
if 'file' in child.attrib:
if 'file' in child.attrib and not (child.tag == "oauth-config" and child.attrib['file'] == "null_config"):
# Check to make sure the file actually exists
new_file_path = str(self.path_to_folder / child.attrib['file'])

# Check to make sure the file actually exists
new_file_path = str(self.path_to_folder / child.attrib['file'])
if not os.path.isfile(new_file_path):
logger.error("Error: " + new_file_path + " does not exist but is referenced in " +
str(file_to_parse.file_name))
return False

if not os.path.isfile(new_file_path):
logger.debug("Error: " + new_file_path + " does not exist but is referenced in " +
str(file_to_parse.file_name))
return False
# Make new connector file object
logging.debug("Adding file to list (name = " + child.attrib['file'] + ", type = " + child.tag + ")")
new_file = ConnectorFile(child.attrib['file'], child.tag)

# Make new connector file object
logging.debug("Adding file to list (name = " + child.attrib['file'] + ", type = " + child.tag + ")")
new_file = ConnectorFile(child.attrib['file'], child.tag)
# figure out if new file is in the list
already_in_list = new_file in self.file_list

# figure out if new file is in the list
already_in_list = new_file in self.file_list
# add new file to list
self.file_list.append(new_file)

# add new file to list
self.file_list.append(new_file)
# If connection-metadata, make sure that connection-fields file exists
if child.tag == 'connection-metadata':
connection_fields_exists = False

# If connection-metadata, make sure that connection-fields file exists
if child.tag == 'connection-metadata':
connection_fields_exists = False
for xml_file in self.file_list:
if xml_file.file_type == 'connection-fields':
connection_fields_exists = True
break

for xml_file in self.file_list:
if xml_file.file_type == 'connection-fields':
connection_fields_exists = True
break
if not connection_fields_exists:
logger.debug("Error: connection-metadata file requires a connection-fields file")
return False

if not connection_fields_exists:
logger.debug("Error: connection-metadata file requires a connection-fields file")
return False

# If not a script and not in list, parse the file for more files to include
if child.tag != 'script' and not already_in_list:
children_valid = self.parse_file(new_file)
if not children_valid:
return False
# If not a script and not in list, parse the file for more files to include
if child.tag != 'script' and not already_in_list:
children_valid = self.parse_file(new_file)
if not children_valid:
return False

if 'url' in child.attrib:

Expand Down
13 changes: 10 additions & 3 deletions connector-packager/connector_packager/xsd_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def validate_file_specific_rules_connection_fields(file_to_test: ConnectorFile,
if field_name in field_names:
xml_violations_buffer.append("A field with the field name = " + field_name +
" already exists. Cannot have multiple fields with the same name.")

return False
if field_name == 'instanceurl':
used_for_oauth = False
Expand Down Expand Up @@ -354,11 +354,18 @@ def warn_file_specific_rules_tdr(path_to_file: Path):
return

authentication_attr_exists = False
server_attr_exists = False
for attr in attribute_list.iter('attr'):
if attr.text == 'authentication':
authentication_attr_exists = True
break

if attr.text == 'server':
server_attr_exists = True

if not authentication_attr_exists:
logger.warning("Warning: 'authentication' attribute is missing from "
"<connection-normalizer>/<required-attributes>/<attribute-list> in " + str(path_to_file) + ".")
"<connection-normalizer>/<required-attributes>/<attribute-list> in " + str(path_to_file) + ".")

if not server_attr_exists:
logger.warning("Warning: 'server' attribute is missing from "
"<connection-normalizer>/<required-attributes>/<attribute-list> in " + str(path_to_file) + ".")
Loading

0 comments on commit 1a51e52

Please sign in to comment.