Skip to content

Add structural Protocol types for TSC item classes - #1802

Open
jacalata wants to merge 10 commits into
developmentfrom
jac/base-item-class
Open

Add structural Protocol types for TSC item classes#1802
jacalata wants to merge 10 commits into
developmentfrom
jac/base-item-class

Conversation

@jacalata

@jacalata jacalata commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Introduces base_item.py with four runtime_checkable Protocol classes:
    TableauItem (id, name), OwnedItem (+ owner_id), TaggableItem (+ tags),
    and ContentItem (+ created_at, updated_at), forming a composable hierarchy.
  • Exports all four from tableauserverclient.models and from the top-level
    tableauserverclient package.
  • Replaces the ad-hoc Taggable protocol in resource_tagger.py with a
    private _TaggableWithInitial and fully annotates all _ResourceTagger
    method signatures.
  • Renames local BaseItem type alias in default_permissions_endpoint.py to
    DefaultPermissionsTarget to avoid shadowing the new public Protocol.
  • Narrows _initial_tags: set to set[str] in DatasourceItem and
    WorkbookItem for Protocol invariance compliance.
  • Removes pre-existing duplicate SiteOIDCConfiguration entry from
    models.__all__.

Behavior note

TableauItem is a structural Protocol, replacing the TableauItem Union
type formerly in tableau_types.py. This widens the accepted type surface:
any object with id and name now satisfies annotations that formerly
required one of the Union members. Callers who relied on the narrow Union
get the same coverage plus more; callers who require narrower typing can
continue to name concrete item classes directly.

Items without a name attribute (e.g. TaskItem, DataAlertItem) do NOT
satisfy TableauItem, and this is now covered by tests.

Schema compliance

All five primary content types in ts-api_3_29.xsd (workbookType,
dataSourceType, viewType, flowType, metricType) carry id, name, owner, tags,
and createdAt/updatedAt attributes. The Protocol hierarchy matches the schema
exactly. MetricItem correctly satisfies ContentItem per schema confirmation.

Test plan

  • python -m pytest test/test_protocols.py -v -- all pass
  • Full suite: 918 passed, 1 skipped
  • mypy: no issues in 211 source files
  • TSC.TableauItem, TSC.ContentItem, TSC.OwnedItem, TSC.TaggableItem
    are all reachable from the top-level package.
  • isinstance(TSC.WorkbookItem(...), ContentItem) returns True at runtime

🤖 Generated with Claude Code

jacalata and others added 7 commits June 25, 2026 17:31
…gs type

- Rename DefaultPermissionsEndpoint's local BaseItem alias to
  DefaultPermissionsTarget to avoid shadowing the new public Protocol
- Remove _initial_tags from TaggableItem (internal dirty-tracking detail,
  not a public contract); update ContentItem docstring to include MetricItem
- Narrow WorkbookItem._initial_tags and DatasourceItem._initial_tags
  annotations from bare set to set[str] for Protocol invariance compliance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… __all__

Taggable in resource_tagger.py was never used as a type bound -- TaggableItem
in base_item.py now covers the public contract. Also remove runtime_checkable
import which became unused. Fix pre-existing duplicate SiteOIDCConfiguration
entry in models/__init__.__all__.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ViewItem.owner_id is not independently writable (it tracks the parent
workbook's owner), so a plain writable Protocol attribute annotation would
mislead mypy. A @Property annotation satisfies both ViewItem's read-only
property and the writable instance attributes on other item classes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add test/test_protocols.py covering isinstance() checks for BaseItem,
  OwnedItem, TaggableItem, and ContentItem against all representative item
  classes (WorkbookItem, DatasourceItem, ViewItem, FlowItem, ProjectItem,
  MetricItem, UserItem) and plain structural objects, including negative
  cases and protocol-hierarchy checks.
- Add _TaggableWithInitial private Protocol in resource_tagger.py to
  capture the _initial_tags implementation detail alongside the public
  TaggableItem surface; use it to fully annotate _ResourceTagger._add_tags,
  _delete_tag, and update_tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…test

Add section comments to __init__.__all__ to distinguish the new
structural protocols from the alphabetical concrete-model list.

Move the inline `import datetime` in test_protocols.py to module
level, matching the import style used throughout the test suite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
tableauserverclient
   __init__.py50100% 
   config.py150100% 
   datetime_helpers.py2511 96%
   exponential_backoff.py200100% 
   filesys_helpers.py310100% 
   namespace.py2633 88%
tableauserverclient/bin
   __init__.py20100% 
   _version.py358212212 41%
tableauserverclient/helpers
   __init__.py10100% 
   logging.py20100% 
   strings.py3111 97%
tableauserverclient/models
   __init__.py470100% 
   base_item.py140100% 
   collection_item.py4177 83%
   column_item.py553232 42%
   connection_credentials.py351111 69%
   connection_item.py941414 85%
   custom_view_item.py1442121 85%
   data_acceleration_report_item.py5411 98%
   data_alert_item.py15844 97%
   data_freshness_policy_item.py1551515 90%
   database_item.py2073636 83%
   datasource_item.py3001212 96%
   dqw_item.py10455 95%
   exceptions.py40100% 
   extensions_item.py13244 97%
   extract_item.py4444 91%
   favorites_item.py6988 88%
   fileupload_item.py190100% 
   flow_item.py1491010 93%
   flow_run_item.py710100% 
   group_item.py8966 93%
   groupset_item.py4977 86%
   interval_item.py1823232 82%
   job_item.py1871010 95%
   linked_tasks_item.py7911 99%
   location_item.py2922 93%
   metric_item.py1291313 90%
   oidc_item.py6333 95%
   pagination_item.py3411 97%
   permissions_item.py1111212 89%
   project_item.py2073131 85%
   property_decorators.py1001818 82%
   reference_item.py2622 92%
   revision_item.py5911 98%
   schedule_item.py20966 97%
   server_info_item.py3777 81%
   site_item.py6361313 98%
   subscription_item.py10122 98%
   table_item.py1191818 85%
   tableau_auth.py612525 59%
   tableau_types.py1711 94%
   tag_item.py150100% 
   target.py60100% 
   task_item.py5622 96%
   user_item.py3101818 94%
   view_item.py2201616 93%
   virtual_connection_item.py6488 88%
   webhook_item.py6911 99%
   workbook_item.py3621616 96%
tableauserverclient/server
   __init__.py90100% 
   exceptions.py40100% 
   filter.py2911 97%
   pager.py3311 97%
   query.py1431515 90%
   request_factory.py1335195195 85%
   request_options.py38655 99%
   server.py1882323 88%
   sort.py60100% 
tableauserverclient/server/endpoint
   __init__.py350100% 
   auth_endpoint.py771111 86%
   custom_views_endpoint.py1521212 92%
   data_acceleration_report_endpoint.py210100% 
   data_alert_endpoint.py942323 76%
   databases_endpoint.py1113030 73%
   datasources_endpoint.py3233333 90%
   default_permissions_endpoint.py4433 93%
   dqw_endpoint.py451616 64%
   endpoint.py2122020 91%
   exceptions.py7766 92%
   extensions_endpoint.py310100% 
   favorites_endpoint.py942222 77%
   fileuploads_endpoint.py510100% 
   flow_runs_endpoint.py6299 85%
   flow_task_endpoint.py2122 90%
   flows_endpoint.py1985353 73%
   groups_endpoint.py12699 93%
   groupsets_endpoint.py7277 90%
   jobs_endpoint.py6799 87%
   linked_tasks_endpoint.py370100% 
   metadata_endpoint.py881414 84%
   metrics_endpoint.py5566 89%
   oidc_endpoint.py4211 98%
   permissions_endpoint.py4533 93%
   projects_endpoint.py1782424 87%
   resource_tagger.py1243434 73%
   schedules_endpoint.py1191111 91%
   server_info_endpoint.py361010 72%
   sites_endpoint.py1302727 79%
   subscriptions_endpoint.py561414 75%
   tables_endpoint.py1103636 67%
   tasks_endpoint.py6366 90%
   users_endpoint.py18388 96%
   views_endpoint.py15099 94%
   virtual_connections_endpoint.py1131010 91%
   webhooks_endpoint.py5499 83%
   workbooks_endpoint.py3382222 93%
TOTAL12005142288% 

@jorwoods

Copy link
Copy Markdown
Contributor

This is great and will make other typing easier. Should they be named differently to clearly differentiate them?

Something like _BaseItem or BaseItemProtocol

@jacalata

Copy link
Copy Markdown
Contributor Author

Hm, I think underscores would make some more helpful IDEs hide/limit them in autocomplete etc, so not that. A common approach is something like "SupportsOwner", but that doesn't quite work for "ContentItem". I think the current item works ok and is pretty standard.
But thinking about it did make me remember to absorb the half-implemented TableauItem, so good question :)

…es.py

BaseItem is renamed to TableauItem to match the existing name in
tableau_types.py and fulfill its TODO comment ("should define TableauItem
as an interface"). The Union type alias is replaced with an import of the
Protocol, removing the need to enumerate concrete types.

id and name are now declared as @Property on the Protocol so that concrete
classes with read-only property implementations (and VirtualConnectionItem
whose name: str is narrower than str | None) satisfy it under mypy's
covariant property checking.

A private _PermissibleItem Protocol is added to permissions_endpoint.py
to type the populate() method's _set_permissions call, which is an
implementation detail not appropriate for the public TableauItem Protocol.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jacalata

Copy link
Copy Markdown
Contributor Author

Apparently an underscore prefix can get them ignored by some IDEs, so not that. I spent a while looking at other projects and it seems the standard two choices are "Thingable/SupportsThing" for atomic Protocols (like SupportsInt and SupportsFloat) or just "Thing". I think the current naming is ok.
But looking into it did remind me of the existing TableauItem type, so I've re-used that instead of creating BaseItem.

jacalata and others added 2 commits July 28, 2026 00:26
- base_item.py TableauItem docstring: fix the stale first paragraph
  that claimed "plain Protocol attributes (not @Property)". The code
  uses @Property; the docstring now matches. Also documents that TaskItem
  and DataAlertItem (no `name` attribute) don't satisfy the protocol,
  and calls out the issubclass-raises-TypeError gotcha for data-attribute
  Protocols.

- __init__.py: export ContentItem, OwnedItem, TaggableItem from the
  top-level package. Previously only TableauItem was accessible as
  TSC.TableauItem; the other three were only under models/__init__.py,
  which meant TSC.ContentItem raised AttributeError.

- test_protocols.py: add coverage for previously-untested types the
  original Union included. DatabaseItem, TableItem, VirtualConnectionItem
  positive on TableauItem; VirtualConnectionItem positive on OwnedItem
  and negative on TaggableItem (no `tags` attribute). TaskItem negative
  on TableauItem (has `id` but no `name`), validating the docstring's
  explicit exclusion. Plus an issubclass-raises-TypeError test to lock
  in the data-attribute Protocol limitation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants