From 0382a1d8ff97effca8db01fbae0cffc359039cbf Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 15 May 2025 16:55:59 -0700 Subject: [PATCH 1/6] update publish workflow --- .github/workflows/publish-pypi.yml | 11 +++++++---- publish.sh | 11 ----------- 2 files changed, 7 insertions(+), 15 deletions(-) delete mode 100755 publish.sh diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index cae0f409c..052113e7c 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,9 +1,12 @@ name: Publish to PyPi -# This will publish a package to TestPyPi (and real Pypi if run on master) with a version -# number generated by versioneer from the most recent tag looking like v____ -# TODO: maybe move this into the package job so all release-based actions are together +# This will build a package with a version set by versioneer from the most recent tag matching v____ +# It will publish to TestPyPi, and to real Pypi *if* run on master where head has a release tag +# For a live run, this should only need to be triggered by a newly published repo release. +# This can also be run manually for testing on: + release: + types: [published] workflow_dispatch: push: tags: @@ -23,7 +26,7 @@ jobs: - name: Build dist files run: | python -m pip install --upgrade pip - pip install -e .[test] build + python -m pip install -e .[test] build python -m build git describe --tag --dirty --always diff --git a/publish.sh b/publish.sh deleted file mode 100755 index 46d54a1ee..000000000 --- a/publish.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# tag the release version and confirm a clean version number -git tag vxxxx -git describe --tag --dirty --always - -set -e - -rm -rf dist -python setup.py sdist bdist_wheel -twine upload dist/* From 80f06cf30eb12db00610c8bc8501fd4ebf851f97 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 15 May 2025 18:06:59 -0700 Subject: [PATCH 2/6] Update pyproject.toml and setup --- .gitattributes | 1 + MANIFEST.in | 3 --- pyproject.toml | 12 +++++++++--- setup.py | 13 +++---------- tableauserverclient/__init__.py | 4 ---- tableauserverclient/bin/__init__.py | 3 +++ tableauserverclient/bin/_version.py | 5 ++--- 7 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 tableauserverclient/bin/__init__.py diff --git a/.gitattributes b/.gitattributes index ade44ab7c..040321c04 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ tableauserverclient/_version.py export-subst +tableauserverclient/bin/_version.py export-subst diff --git a/MANIFEST.in b/MANIFEST.in index 9b7512fb9..7acbed103 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,8 +4,6 @@ include CONTRIBUTORS.md include LICENSE include LICENSE.versioneer include README.md -include tableauserverclient/_version.py -include versioneer.py recursive-include docs *.md recursive-include samples *.py recursive-include samples *.txt @@ -18,5 +16,4 @@ recursive-include test *.png recursive-include test *.py recursive-include test *.xml recursive-include test *.tde -global-include *.pyi global-include *.typed diff --git a/pyproject.toml b/pyproject.toml index 68f7589ca..3118182cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=75.0", "versioneer[toml]==0.29", "wheel"] +requires = ["setuptools>=77.0", "versioneer[toml]==0.29", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,7 +8,7 @@ name="tableauserverclient" dynamic = ["version"] description='A Python module for working with the Tableau Server REST API.' authors = [{name="Tableau", email="github@tableau.com"}] -license = {file = "LICENSE"} +license-files = ["LICENSE"] readme = "README.md" dependencies = [ @@ -35,6 +35,12 @@ repository = "https://github.com/tableau/server-client-python" test = ["black==24.8", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", "requests-mock>=1.0,<2.0"] +[tool.setuptools.packages.find] +where = ["tableauserverclient", "tableauserverclient.helpers", "tableauserverclient.models", "tableauserverclient.server", "tableauserverclient.server.endpoint"] + +[tool.setuptools.dynamic] +version = {attr = "versioneer.get_version"} + [tool.black] line-length = 120 target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] @@ -61,5 +67,5 @@ addopts = "--junitxml=./test.junit.xml" VCS = "git" style = "pep440-pre" versionfile_source = "tableauserverclient/bin/_version.py" -versionfile_build = "tableauserverclient/bin/_version.py" +versionfile_build = "_version.py" tag_prefix = "v" diff --git a/setup.py b/setup.py index bdce51f2e..b52ba267e 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,7 @@ from setuptools import setup setup( - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - # not yet sure how to move this to pyproject.toml - packages=[ - "tableauserverclient", - "tableauserverclient.helpers", - "tableauserverclient.models", - "tableauserverclient.server", - "tableauserverclient.server.endpoint", - ], + # This line is required to set the version number when building the wheel + # not yet sure how to move this to pyproject.toml - it may require work in versioneer + cmdclass=versioneer.get_cmdclass() ) diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 21e2c4760..6de77943a 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -139,7 +139,3 @@ "WeeklyInterval", "WorkbookItem", ] - -from .bin import _version - -__version__ = _version.get_versions()["version"] diff --git a/tableauserverclient/bin/__init__.py b/tableauserverclient/bin/__init__.py new file mode 100644 index 000000000..e4605a43b --- /dev/null +++ b/tableauserverclient/bin/__init__.py @@ -0,0 +1,3 @@ +# generated during initial setup of versioneer +from . import _version +__version__ = _version.get_versions()['version'] diff --git a/tableauserverclient/bin/_version.py b/tableauserverclient/bin/_version.py index f23819e86..680304c7d 100644 --- a/tableauserverclient/bin/_version.py +++ b/tableauserverclient/bin/_version.py @@ -46,14 +46,13 @@ class VersioneerConfig: def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py + # these strings are filled in from pyproject.toml at file generation time cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "pep440-pre" cfg.tag_prefix = "v" cfg.parentdir_prefix = "None" - cfg.versionfile_source = "tableauserverclient/_version.py" + cfg.versionfile_source = "tableauserverclient/bin/_version.py" cfg.verbose = False return cfg From a588d18c7ab3b04140346f29fdeee77af67aecbc Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 15 May 2025 22:02:29 -0700 Subject: [PATCH 3/6] add init files to find test_repr, fix it to pass black --- tableauserverclient/models/column_item.py | 6 ++-- test/http/__init__.py | 0 test/models/__init__.py | 0 test/models/test_repr.py | 41 ++++++++++++++++++----- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 test/http/__init__.py create mode 100644 test/models/__init__.py diff --git a/tableauserverclient/models/column_item.py b/tableauserverclient/models/column_item.py index 3a7416e28..9691bbef1 100644 --- a/tableauserverclient/models/column_item.py +++ b/tableauserverclient/models/column_item.py @@ -4,13 +4,13 @@ class ColumnItem: - def __init__(self, name, description=None): + def __init__(self, name=None, description=None): self._id = None self.description = description - self.name = name + self._name = name def __repr__(self): - return f"<{self.__class__.__name__} {self._id} {self.name} {self.description}>" + return f"<{self.__class__.__name__} {self._id} {self._name} {self.description}>" @property def id(self): diff --git a/test/http/__init__.py b/test/http/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/models/__init__.py b/test/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/models/test_repr.py b/test/models/test_repr.py index 92d11978f..ed8f5ce9b 100644 --- a/test/models/test_repr.py +++ b/test/models/test_repr.py @@ -1,14 +1,13 @@ import inspect from unittest import TestCase -import _models # type: ignore # did not set types for this +import tableauserverclient.models as TSC_models # type: ignore # did not set types for this import tableauserverclient as TSC from typing import Any -# ensure that all models that don't need parameters can be instantiated -# todo.... +# ensure that all models can be instantiated def instantiate_class(name: str, obj: Any): # Get the constructor (init) of the class constructor = getattr(obj, "__init__", None) @@ -31,14 +30,38 @@ def instantiate_class(name: str, obj: Any): print(f"Class '{name}' does not have a constructor (__init__ method).") +not_yet_done = [ + "DQWItem", + "UnpopulatedPropertyError", + "FavoriteItem", + "FileuploadItem", + "FlowRunItem", + "IntervalItem", + "LinkedTaskItem", + "LinkedTaskStepItem", + "LinkedTaskFlowRunItem", + "Permission", + "SiteAuthConfiguration", + "Resource", + "TagItem", + "ExtractItem", +] + + class TestAllModels(TestCase): - # not all models have __repr__ yet: see above list + + # confirm that all models can be instantiated without params, and have __repr__ implemented + # not all do have __repr__ yet: see above list 'not_yet_done' def test_repr_is_implemented(self): - m = _models.get_defined_models() - for model in m: - with self.subTest(model.__name__, model=model): - print(model.__name__, type(model.__repr__).__name__) - self.assertEqual(type(model.__repr__).__name__, "function") + m = TSC_models + for type_name in m.__dict__: + if type_name in not_yet_done: + continue + model = getattr(m, type_name) + if inspect.isclass(model): + with self.subTest(type_name): + self.assertTrue(hasattr(model, "__repr__")) + self.assertEqual(type(model.__repr__).__name__, "function") # 2 - Iterate through the objects in the module def test_by_reflection(self): From 3791e8a2c70b9b9cff88938d06b5c18d4c19adef Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 20 Jan 2026 12:58:03 -0800 Subject: [PATCH 4/6] fix test/syntax failures from merge --- tableauserverclient/models/column_item.py | 8 +-- test/models/_models.py | 59 ++++++----------- test/models/test_repr.py | 77 +++++++---------------- 3 files changed, 46 insertions(+), 98 deletions(-) diff --git a/tableauserverclient/models/column_item.py b/tableauserverclient/models/column_item.py index 9691bbef1..dd1f6d555 100644 --- a/tableauserverclient/models/column_item.py +++ b/tableauserverclient/models/column_item.py @@ -4,13 +4,13 @@ class ColumnItem: - def __init__(self, name=None, description=None): + def __init__(self, name, description=None): self._id = None self.description = description - self._name = name + self.name = name def __repr__(self): - return f"<{self.__class__.__name__} {self._id} {self._name} {self.description}>" + return f"<{self.__class__.__name__} {self._id} {self.name} {self.description}>" @property def id(self): @@ -68,4 +68,4 @@ def _parse_element(column_xml, ns): description = column_xml.get("description", None) remote_type = column_xml.get("remoteType", None) - return id, name, description, remote_type + return id, name, description, remote_type \ No newline at end of file diff --git a/test/models/_models.py b/test/models/_models.py index 59011c6c3..9be97a87b 100644 --- a/test/models/_models.py +++ b/test/models/_models.py @@ -10,48 +10,27 @@ ) -def get_defined_models(): - # nothing clever here: list was manually copied from tsc/models/__init__.py - return [ - BackgroundJobItem, - ConnectionItem, - DataAccelerationReportItem, - DataAlertItem, - DatasourceItem, - FlowItem, - GroupItem, - JobItem, - MetricItem, - PermissionsRule, - ProjectItem, - RevisionItem, - ScheduleItem, - SubscriptionItem, - Credentials, - JWTAuth, - TableauAuth, - PersonalAccessTokenAuth, - ServerInfoItem, - SiteItem, - TaskItem, - UserItem, - ViewItem, - WebhookItem, - WorkbookItem, - PaginationItem, - Permission.Mode, - Permission.Capability, - DailyInterval, - WeeklyInterval, - MonthlyInterval, - HourlyInterval, - TableItem, - Target, - ] - - def get_unimplemented_models(): return [ + # these items should have repr , please fix + CollectionItem, + DQWItem, + ExtensionsServer, + ExtensionsSiteSettings, + FileuploadItem, + FlowRunItem, + LinkedTaskFlowRunItem, + LinkedTaskItem, + LinkedTaskStepItem, + SafeExtension, + # these should be implemented together for consistency + CSVRequestOptions, + ExcelRequestOptions, + ImageRequestOptions, + PDFRequestOptions, + PPTXRequestOptions, + RequestOptions, + # these don't need it FavoriteItem, # no repr because there is no state Resource, # list of type names TableauItem, # should be an interface diff --git a/test/models/test_repr.py b/test/models/test_repr.py index 6347ea2c9..9dd9108fc 100644 --- a/test/models/test_repr.py +++ b/test/models/test_repr.py @@ -1,15 +1,30 @@ import inspect from typing import Any - -from unittest import TestCase -import tableauserverclient.models as TSC_models # type: ignore # did not set types for this +from test.models._models import get_unimplemented_models import tableauserverclient as TSC import pytest -# ensure that all models can be instantiated -def instantiate_class(name: str, obj: Any): +def is_concrete(obj: Any): + return inspect.isclass(obj) and not inspect.isabstract(obj) + +@pytest.mark.parametrize("class_name, obj", inspect.getmembers(TSC, is_concrete)) +def test_by_reflection(class_name, obj): + instance = try_instantiate_class(class_name, obj) + if instance: + class_type = type(instance) + if class_type in get_unimplemented_models(): + print(f"Class '{class_name}' has no repr defined, skipping test") + return + else: + assert type(instance.__repr__).__name__ == "method" + print(instance.__repr__.__name__) + + + +# Instantiate a class if it doesn't require any parameters +def try_instantiate_class(name: str, obj: Any) -> Any | None: # Get the constructor (init) of the class constructor = getattr(obj, "__init__", None) if constructor: @@ -22,58 +37,12 @@ def instantiate_class(name: str, obj: Any): print(f"Class '{name}' requires the following parameters for instantiation:") for param in required_parameters: print(f"- {param.name}") + return None else: print(f"Class '{name}' does not require any parameters for instantiation.") # Instantiate the class instance = obj() - print(f"Instantiated: {name} -> {instance}") + return instance else: print(f"Class '{name}' does not have a constructor (__init__ method).") - - -not_yet_done = [ - "DQWItem", - "UnpopulatedPropertyError", - "FavoriteItem", - "FileuploadItem", - "FlowRunItem", - "IntervalItem", - "LinkedTaskItem", - "LinkedTaskStepItem", - "LinkedTaskFlowRunItem", - "Permission", - "SiteAuthConfiguration", - "Resource", - "TagItem", - "ExtractItem", -] - - -class TestAllModels(TestCase): - - # confirm that all models can be instantiated without params, and have __repr__ implemented - # not all do have __repr__ yet: see above list 'not_yet_done' - def test_repr_is_implemented(self): - m = TSC_models - for type_name in m.__dict__: - if type_name in not_yet_done: - continue - model = getattr(m, type_name) - if inspect.isclass(model): - with self.subTest(type_name): - self.assertTrue(hasattr(model, "__repr__")) - self.assertEqual(type(model.__repr__).__name__, "function") - -def is_concrete(obj: Any): - return inspect.isclass(obj) and not inspect.isabstract(obj) - - -@pytest.mark.parametrize("class_name, obj", inspect.getmembers(TSC, is_concrete)) -def test_by_reflection(class_name, obj): - instantiate_class(class_name, obj) - - -@pytest.mark.parametrize("model", _models.get_defined_models()) -def test_repr_is_implemented(model): - print(model.__name__, type(model.__repr__).__name__) - assert type(model.__repr__).__name__ == "function" + return None From 92119c5f2021d8971abe5809ea2223f3a7efb3c9 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 20 Jan 2026 13:45:44 -0800 Subject: [PATCH 5/6] formatting, mypy --- tableauserverclient/models/column_item.py | 2 +- test/models/test_repr.py | 2 +- test/test_custom_view.py | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/models/column_item.py b/tableauserverclient/models/column_item.py index dd1f6d555..3a7416e28 100644 --- a/tableauserverclient/models/column_item.py +++ b/tableauserverclient/models/column_item.py @@ -68,4 +68,4 @@ def _parse_element(column_xml, ns): description = column_xml.get("description", None) remote_type = column_xml.get("remoteType", None) - return id, name, description, remote_type \ No newline at end of file + return id, name, description, remote_type diff --git a/test/models/test_repr.py b/test/models/test_repr.py index 9dd9108fc..34f8509a7 100644 --- a/test/models/test_repr.py +++ b/test/models/test_repr.py @@ -9,6 +9,7 @@ def is_concrete(obj: Any): return inspect.isclass(obj) and not inspect.isabstract(obj) + @pytest.mark.parametrize("class_name, obj", inspect.getmembers(TSC, is_concrete)) def test_by_reflection(class_name, obj): instance = try_instantiate_class(class_name, obj) @@ -22,7 +23,6 @@ def test_by_reflection(class_name, obj): print(instance.__repr__.__name__) - # Instantiate a class if it doesn't require any parameters def try_instantiate_class(name: str, obj: Any) -> Any | None: # Get the constructor (init) of the class diff --git a/test/test_custom_view.py b/test/test_custom_view.py index b2117358a..345a38c12 100644 --- a/test/test_custom_view.py +++ b/test/test_custom_view.py @@ -175,6 +175,7 @@ def test_publish_filepath(server: TSC.Server) -> None: cv._owner = TSC.UserItem() cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with requests_mock.mock() as m: m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) @@ -191,6 +192,7 @@ def test_publish_file_str(server: TSC.Server) -> None: cv._owner = TSC.UserItem() cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with requests_mock.mock() as m: m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) @@ -207,6 +209,7 @@ def test_publish_file_io(server: TSC.Server) -> None: cv._owner = TSC.UserItem() cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" data = io.BytesIO(CUSTOM_VIEW_DOWNLOAD.read_bytes()) with requests_mock.mock() as m: @@ -223,6 +226,7 @@ def test_publish_missing_owner_id(server: TSC.Server) -> None: cv = TSC.CustomViewItem(name="test") cv._owner = TSC.UserItem() cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with requests_mock.mock() as m: m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) @@ -244,8 +248,10 @@ def test_publish_missing_wb_id(server: TSC.Server) -> None: def test_large_publish(server: TSC.Server): cv = TSC.CustomViewItem(name="test") cv._owner = TSC.UserItem() + assert cv.workbook is not None cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" with ExitStack() as stack: temp_dir = stack.enter_context(TemporaryDirectory()) From 7be1b67bbad1ec5205c77059780c861aa1d819d1 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 20 Jan 2026 14:07:17 -0800 Subject: [PATCH 6/6] too many corrections --- test/test_custom_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_custom_view.py b/test/test_custom_view.py index 345a38c12..2a3932726 100644 --- a/test/test_custom_view.py +++ b/test/test_custom_view.py @@ -248,7 +248,6 @@ def test_publish_missing_wb_id(server: TSC.Server) -> None: def test_large_publish(server: TSC.Server): cv = TSC.CustomViewItem(name="test") cv._owner = TSC.UserItem() - assert cv.workbook is not None cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" cv.workbook = TSC.WorkbookItem() assert cv.workbook is not None