Metadata-Version: 2.4
Name: mft-fortableau
Version: 1.0.768
Summary: Manager for Tableau - Custom Task Framework for building workflow tasks
Author: Laszlo Nemes
License: Proprietary
Project-URL: Homepage, https://products.fortableau.com/
Keywords: tableau,workflow,automation,manager-for-tableau
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Requires-Dist: tableauserverclient>=0.25
Requires-Dist: psycopg2-binary>=2.9
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"

# MFT - Manager for Tableau Custom Task Framework

Python framework for building custom workflow tasks that run inside **Manager for Tableau**.

## Quick Start

1. **Clone the Custom Task Template** repository and open it in VS Code.
2. **Reopen in Dev Container** — the container has Python, .NET runtime, and the `mft` package pre-installed.
3. **Edit `src/task-meta.json`** — define your task's inputs, outputs, and service dependencies.
4. **Generate a test input file:**
   ```bash
   mft generate-input
   ```
5. **Fill in `dev-files/input.json`** with test values (replace the `TODO:` markers).
6. **Validate your input file:**
   ```bash
   mft validate-input
   ```
7. **Write your task logic** in `main.py` inside the `run()` function.
8. **Test in dev mode:**
   ```bash
   python main.py dev --server https://your-mft-server --token YOUR_TOKEN --server-id SERVER_ID --site SITE
   ```
9. **Package for upload:**
   ```bash
   mft package
   ```
10. **Upload** the `.mft` file to Manager for Tableau.

## Installation

```bash
pip install mft-fortableau
```

The package includes the JsonConverter binary — no additional setup required.

## Project Structure

```
my-custom-task/
├── .devcontainer/
│   ├── Dockerfile          # Python 3.13 + .NET 8 runtime + mft
│   └── devcontainer.json   # VS Code dev container config
├── src/
│   ├── task-meta.json      # Task definition (inputs, outputs, uses)
│   └── requirements.txt    # Your pip dependencies
├── dev-files/
│   └── input.json          # Generated test input (gitignored)
└── main.py                 # Task entry point
```

## task-meta.json Reference

```json
{
  "display_name": "My Task",
  "description": "What this task does",
  "group": "Custom Tasks",
  "group_index": null,
  "major_version": 1,
  "version_description": null,
  "uses": ["RestApi", "Repository"],
  "inputs": [ ... ],
  "outputs": [ ... ]
}
```

### Top-Level Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `display_name` | string | Yes | Name shown in the workflow editor |
| `description` | string | No | Description of what the task does |
| `group` | string | No | Category group (default: `"Custom Tasks"`) |
| `group_index` | integer/null | No | Sort order within the group (default: `null`) |
| `major_version` | integer | No | Task version number (default: `1`) |
| `version_description` | string/null | No | Description of changes in this version (default: `null`) |
| `uses` | string[] | No | Services the task needs (see below) |
| `inputs` | object[] | No | Input parameter definitions |
| `outputs` | object[] | No | Output parameter definitions |

### Versioning

When you change a task's inputs or outputs or you implement a more significant logical change, increment `major_version` in `task-meta.json`. The new version is uploaded alongside existing versions — workflows using the old version continue to work, while new workflows can use the new version. Use `version_description` to document what changed.

Bug fixes that don't change the input/output schema keep the same `major_version` — delete the existing version in the Manager for Tableau UI first, then re-upload. Re-uploading without deleting first is rejected to prevent accidental overwrites.

### Uses (Service Dependencies)

| Value | Description |
|-------|-------------|
| `RestApi` | Tableau Server REST API via `tableauserverclient` |
| `Repository` | Read-only Tableau Repository (PostgreSQL) via `psycopg2` |

### Parameter Definition

Each entry in `inputs` or `outputs`:

```json
{
  "id": "myParam",
  "display_name": "My Parameter",
  "type": "String",
  "description": "Optional description",
  "required": true,
  "is_list": false,
  "enum_values": ["A", "B", "C"],
  "object_properties": [ ... ]
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Identifier (must match `^[a-zA-Z][a-zA-Z\d_]*$`) |
| `display_name` | string | Yes | Label shown in the UI |
| `type` | string | Yes | One of the 11 parameter types (see below) |
| `description` | string | No | Help text |
| `required` | boolean | No | Whether the parameter must be set (default: `false`) |
| `is_list` | boolean | No | Whether the parameter holds multiple values (default: `false`) |
| `enum_values` | string[] | Enum only | Allowed values for Enum type |
| `object_properties` | object[] | Object only | Nested parameter definitions for Object type |

## Parameter Types

| Type | JSON Value | Python Type | Input Method | Output Method |
|------|-----------|-------------|--------------|---------------|
| `String` | `"hello"` | `str` | `get_string()` | `set_string()` |
| `Integer` | `42` | `int` | `get_integer()` | `set_integer()` |
| `Double` | `3.14` | `float` | `get_double()` | `set_double()` |
| `Boolean` | `true` | `bool` | `get_boolean()` | `set_boolean()` |
| `Date` | `"2025-01-15"` | `datetime.date` | `get_date()` | `set_date()` |
| `DateTime` | `"2025-01-15T10:30:00"` | `datetime.datetime` | `get_datetime()` | `set_datetime()` |
| `TimeSpan` | `"01:30:00"` | `datetime.timedelta` | `get_timespan()` | `set_timespan()` |
| `Guid` | `"550e8400-..."` | `uuid.UUID` | `get_guid()` | `set_guid()` |
| `Enum` | `"ValueA"` | `str` | `get_enum()` | `set_enum()` |
| `Binary` | `"base64..."` | `bytes` | `get_binary()` | `set_binary()` |
| `Object` | `{ ... }` | `ParameterObjectHandler` | `get_object()` | `set_object()` |

Every type also has a list variant: `get_string_list()` / `set_string_list()`, etc.

## Writing Your Task

### Basic Pattern

```python
from mft import MFT


def run(mft: MFT) -> None:
    # Read inputs
    name = mft.input.get_string("name")
    count = mft.input.get_integer("count")
    tags = mft.input.get_string_list("tags")

    # Your logic here
    result = f"Processed {name} with {count} items"

    # Write outputs
    mft.output.set_string("result", result)
    mft.output.set_integer("processedCount", count)


# --- Boilerplate (do not modify) ---
if __name__ == "__main__":
    try:
        task = MFT.init()
    except Exception as e:
        MFT.Err(str(e), error_id="InitError")
    try:
        run(task)
        MFT.Ok()
    except Exception as e:
        MFT.Err(str(e))
```

### Using the Tableau REST API

Add `"RestApi"` to `uses` in `task-meta.json`, then:

```python
def run(mft: MFT) -> None:
    server = mft.tableau_api.server

    all_workbooks, pagination = server.workbooks.get()
    for wb in all_workbooks:
        print(f"{wb.name} (owner: {wb.owner_id})")
```

The `server` object is a connected `tableauserverclient.Server` instance (included with `mft`).

### Using the Repository

Add `"Repository"` to `uses` in `task-meta.json`, then:

```python
def run(mft: MFT) -> None:
    rows = mft.repository.execute(
        "SELECT name, owner_name FROM workbooks WHERE site_id = %s",
        (site_id,),
    )
    for name, owner in rows:
        print(f"{name} owned by {owner}")

    # Or get results as dicts:
    dicts = mft.repository.execute_dict(
        "SELECT name, owner_name FROM workbooks LIMIT 10"
    )
```

`psycopg2-binary` is included with `mft`.

### Working with Objects

For nested Object parameters, use callback functions:

```python
# Reading nested objects
address = mft.input.get_object("address")
city = address.get_string("city")
zip_code = address.get_string("zipCode")

# Reading a list of objects
for item in mft.input.get_objects("items"):
    name = item.get_string("name")
    qty = item.get_integer("quantity")

# Writing nested objects
def write_address(obj):
    obj.set_string("city", "New York")
    obj.set_string("zipCode", "10001")

mft.output.set_object("address", write_address)

# Writing a list of objects
writers = []
for item in items:
    def make_writer(i):
        def writer(obj):
            obj.set_string("name", i["name"])
            obj.set_integer("quantity", i["qty"])
        return writer
    writers.append(make_writer(item))

mft.output.set_object_list("items", writers)
```

## Dev Mode

Dev mode lets you test your task locally without deploying to Manager for Tableau.

### Running in Dev Mode

```bash
python main.py dev \
    --server https://your-mft-server \
    --token YOUR_REFRESH_TOKEN \
    --server-id SERVER_ID \
    --site SITE_CONTENT_URL
```

Optional: specify a custom input file path:
```bash
python main.py dev --server ... --input-file ./my-input.json
```

### What Happens in Dev Mode

1. `input.json` is **validated** against `task-meta.json` (type checks, format checks, required fields, leftover TODO markers).
2. `input.json` is converted from JSON to the binary parameter format.
3. Your `run()` function executes, reading inputs and writing outputs.
4. The binary output file is converted back to `output.json` for inspection.
5. A `status.json` file is written with the completion status.

If validation fails in step 1, the task exits immediately with a clear error listing every problem. This is the same validation that `mft validate-input` performs.

All files are written to `./dev-files/` by default.

### Token Persistence

The `--token` refresh token is single-use — each API call consumes it and returns a new one. The `mft` package handles this automatically: after each token refresh, the new token is saved to `dev-files/.mft_token`. On subsequent runs with the same `--token` value, the persisted token is used. This means you provide the token once and can re-run your task as many times as needed.

If you generate a new PAT in the MFT web UI, pass the new value via `--token` and the old persisted token is replaced automatically.

### Debugging with Logs

To enable MFT internal logs (token refreshes, API connections, warnings), add this at the top of your `main.py`:

```python
import logging
logging.basicConfig()
logging.getLogger("mft").setLevel(logging.DEBUG)
```

This will print debug output to the console without noise from third-party libraries.

## Prod Mode

Prod mode is how tasks run when deployed to Manager for Tableau. You do not invoke prod mode yourself — the MFT workflow engine handles it automatically.

When your packaged `.mft` task is uploaded and triggered by a workflow, Manager for Tableau:

1. Starts a Docker container with your task code and `mft` pre-installed.
2. Sets environment variables with the runtime configuration.
3. Runs `python main.py` (without the `dev` argument), which activates prod mode.

### How It Differs from Dev Mode

| | Dev Mode | Prod Mode |
|---|---|---|
| Activation | `python main.py dev --server ...` | `python main.py` (no arguments) |
| Configuration | CLI arguments | Environment variables |
| Input/Output | JSON files converted to/from binary | Binary parameter files used directly |
| Authentication | Refresh token (long-lived) | Action token (short-lived, set by engine) |
| Status output | Written to `./dev-files/status.json` + stdout | Written to `MFT_STATUS_FILE` (or stdout) |

### Environment Variables (set by the engine)

| Variable | Description |
|----------|-------------|
| `MFT_SERVER_URL` | Manager for Tableau server URL |
| `MFT_AUTH_TOKEN` | Short-lived action token for API calls |
| `MFT_SERVER_ID` | Tableau Server ID |
| `MFT_SITE_CONTENT_URL` | Tableau site content URL |
| `MFT_TASK_NODE_ID` | Workflow task node identifier |
| `MFT_JOB_ID` | Current workflow job identifier |
| `MFT_INPUT_FILE` | Path to the binary input parameter file (created by the engine before the task starts) |
| `MFT_OUTPUT_FILE` | Path where the task writes the binary output parameter file (read by the engine after completion) |
| `MFT_STATUS_FILE` | *(optional)* Path to write the status JSON |

Your task code does not need to read these variables directly — `MFT.init()` handles everything. The same `run()` function works identically in both modes.

## CLI Commands

### `mft validate-meta`

Validate a `task-meta.json` file and print a summary.

```bash
mft validate-meta                          # auto-detect in ./src/ or ./
mft validate-meta path/to/task-meta.json   # explicit path
```

### `mft generate-input`

Generate a sample `input.json` template from task metadata.

```bash
mft generate-input                                        # auto-detect meta, output to ./dev-files/input.json
mft generate-input path/to/task-meta.json ./my-input.json # explicit paths
```

The generated file contains `TODO:` markers for string values and type-appropriate defaults for other types. A parameter guide is printed to stdout.

### `mft validate-input`

Validate an `input.json` file against task metadata. Catches type mismatches, leftover `TODO:` markers, missing required fields, invalid formats (dates, GUIDs, etc.), unknown keys, and bad enum values -- all before the data reaches the converter or binary layer.

```bash
mft validate-input                                                  # auto-detect meta, validate ./dev-files/input.json
mft validate-input ./dev-files/input.json                           # explicit input path, auto-detect meta
mft validate-input ./dev-files/input.json path/to/task-meta.json    # explicit both
```

Typical workflow:

```bash
mft generate-input          # creates ./dev-files/input.json with TODO markers
# edit input.json...
mft validate-input          # catches mistakes before running the task
```

**Auto-validation in dev mode:** `MFT.init()` automatically validates the input JSON before converting it to binary format. If validation fails, the task exits immediately with a clear error message listing every problem. This means you get the same validation whether you run `mft validate-input` manually or just start your task in dev mode -- bad input never reaches the converter.

### `mft package`

Package the task into a `.mft` ZIP archive for upload to Manager for Tableau.

```bash
mft package              # filename derived from display_name
mft package my_task.mft  # custom filename
```

The archive contains the files the MFT engine needs to run the task:

- `task-meta.json` — task definition (always at archive root)
- `main.py` — entry point (from project root)
- `requirements.txt` — pip dependencies (from `src/` or project root)
- Additional `.py` files at the project root (helper modules)
- All files under `src/` except `task-meta.json` and `requirements.txt` (subdirectory modules)

Non-runtime directories (`.devcontainer`, `dev-files`, `docs`, `__pycache__`, `.git`) are excluded automatically.

## API Reference

### `MFT` (class)

| Method | Description |
|--------|-------------|
| `MFT.init(meta_path=None)` | Initialize the runtime, returns an `MFT` instance |
| `MFT.Ok(message="...")` | Signal success, write status, exit with code 0 |
| `MFT.Err(message, error_id="TaskError")` | Signal failure, write status, exit with code 1 |

| Property | Type | Description |
|----------|------|-------------|
| `mft.input` | `Input` | Read-only input parameters |
| `mft.output` | `Output` | Writable output parameters |
| `mft.tableau_api` | `TableauApi` | Tableau REST API client (requires `RestApi` in `uses`) |
| `mft.repository` | `Repository` | Read-only Tableau Repository (requires `Repository` in `uses`) |

### `Input` (read methods)

All methods take a `param_id: str` argument matching the `id` in `task-meta.json`.

| Method | Returns |
|--------|---------|
| `get_string(id)` | `str \| None` |
| `get_string_list(id)` | `list[str]` |
| `get_integer(id)` | `int \| None` |
| `get_integer_list(id)` | `list[int]` |
| `get_double(id)` | `float \| None` |
| `get_double_list(id)` | `list[float]` |
| `get_boolean(id)` | `bool \| None` |
| `get_boolean_list(id)` | `list[bool]` |
| `get_date(id)` | `datetime.date \| None` |
| `get_date_list(id)` | `list[datetime.date]` |
| `get_datetime(id)` | `datetime.datetime \| None` |
| `get_datetime_list(id)` | `list[datetime.datetime]` |
| `get_timespan(id)` | `datetime.timedelta \| None` |
| `get_timespan_list(id)` | `list[datetime.timedelta]` |
| `get_guid(id)` | `uuid.UUID \| None` |
| `get_guid_list(id)` | `list[uuid.UUID]` |
| `get_enum(id)` | `str \| None` |
| `get_enum_list(id)` | `list[str]` |
| `get_binary(id)` | `bytes \| None` |
| `get_binary_list(id)` | `list[bytes]` |
| `get_object(id)` | `ParameterObjectHandler \| None` |
| `get_objects(id)` | `Iterator[ParameterObjectHandler]` |

### `Output` (write methods)

All methods take a `param_id: str` and the value to write.

| Method | Value Type |
|--------|-----------|
| `set_string(id, value)` | `str` |
| `set_string_list(id, values)` | `list[str]` |
| `set_integer(id, value)` | `int` |
| `set_integer_list(id, values)` | `list[int]` |
| `set_double(id, value)` | `float` |
| `set_double_list(id, values)` | `list[float]` |
| `set_boolean(id, value)` | `bool` |
| `set_boolean_list(id, values)` | `list[bool]` |
| `set_date(id, value)` | `datetime.date` |
| `set_date_list(id, values)` | `list[datetime.date]` |
| `set_datetime(id, value)` | `datetime.datetime` |
| `set_datetime_list(id, values)` | `list[datetime.datetime]` |
| `set_timespan(id, value)` | `datetime.timedelta` |
| `set_timespan_list(id, values)` | `list[datetime.timedelta]` |
| `set_guid(id, value)` | `uuid.UUID` |
| `set_guid_list(id, values)` | `list[uuid.UUID]` |
| `set_enum(id, value)` | `str` |
| `set_enum_list(id, values)` | `list[str]` |
| `set_binary(id, value)` | `bytes` |
| `set_binary_list(id, values)` | `list[bytes]` |
| `set_object(id, writer)` | `Callable[[ParameterObjectHandler], None]` |
| `set_object_list(id, writers)` | `list[Callable[[ParameterObjectHandler], None]]` |

### `TableauApi`

| Member | Description |
|--------|-------------|
| `.server` | Connected `tableauserverclient.Server` instance |
| `.connect()` | Authenticate (called automatically) |
| `.disconnect()` | Sign out (called automatically on `Ok`/`Err`) |

### `Repository`

| Method | Description |
|--------|-------------|
| `.execute(sql, params=None)` | Execute query, return rows as `list[tuple]` |
| `.execute_dict(sql, params=None)` | Execute query, return rows as `list[dict]` |
| `.connection` | Raw `psycopg2` connection |
| `.connect()` | Open connection (called automatically) |
| `.disconnect()` | Close connection (called automatically on `Ok`/`Err`) |
