Skip to main content

Typed, validated schemas for BDD data tables

Project description

talika

talika adds small, dataclass-style schemas to BDD data tables. It parses the raw list-of-lists supplied by tools such as pytest-bdd, validates the table shape, converts cells with project-defined parsers, and returns typed schema records.

It does not prescribe a table DSL or perform business actions. Projects define their own readable table vocabulary while talika handles the repeatable parts: shape validation, conversion, source-aware diagnostics, and optional static checks for .feature files.

Links

Installation

talika supports Python 3.10 and newer. The core package has no runtime dependencies:

pip install talika

Install optional extras only for integrations you use:

pip install "talika[cli]"       # static Gherkin feature-file checks
pip install "talika[pydantic]"  # Pydantic output models
pip install "talika[test]"      # test/integration dependencies

The command-line tool is available as both talika and python -m talika.

Quick Start

from talika import RowTable, field


def parse_bool(value, context):
    return value.lower() == "true"


class UserTable(RowTable):
    name = field("name", required=True)
    role = field("role", required=True)
    active = field("active", parser=parse_bool, default=True)


users = UserTable.parse(
    [
        ["name", "role", "active"],
        ["Alice", "admin", "true"],
    ]
)

assert users[0].name == "Alice"
assert users[0].active is True

Table Shapes

Row-oriented tables use the first row as labels and every following row as one record:

from talika import RowTable, field


class ProductTable(RowTable):
    sku = field("sku", required=True)
    name = field("name", required=True)

Column-oriented tables use the first column as labels and every following column as one record:

from talika import ColumnTable, field, id_field


class ContentTable(ColumnTable):
    id = id_field("IDs")
    content_type = field("Type*", required=True)
    headline = field("Headline*", required=True)
    category = field("Category")


items = ContentTable.parse(
    [
        ["IDs", "1", "2"],
        ["Type*", "Article", "Poll"],
        ["Headline*", "Hello", "QA Poll"],
    ]
)

assert items[0].id == "1"
assert items[1].content_type == "Poll"
assert items[0].category is None

Use parse_records() when you specifically want schema instances for static typing or when a schema has an output_model but a test needs the intermediate validated record:

records: list[ContentTable] = ContentTable.parse_records(datatable)

Functional helpers are also available:

from talika import parse_table, parse_table_records

items = parse_table(ContentTable, datatable)
records = parse_table_records(ContentTable, datatable)

Conversion

Common field conversion does not require custom functions:

from talika import RowTable, boolean, compose, decimal, each, field, split, string


class ProductTable(RowTable):
    price = field("price", parser=decimal())
    active = field("active", parser=boolean())
    tags = field("tags", parser=compose(split(","), each(string(strip=True))))

The package provides string, integer, floating, decimal, boolean, choice, split, map_value, optional, compose, and each.

Supported annotations infer a parser when the field has no explicit parser:

class UserTable(RowTable):
    name: str = field("name")
    age: int | None = field("age")
    active: bool = field("active")

Inference supports str, int, float, bool, Decimal, enums, string Literal values, and simple optionals. Collection annotations such as list[str] do not imply a cell syntax; use an explicit parser such as split(",") when one cell should become several values.

Defaults, Aliases, And Policies

Missing optional fields can use static defaults or context-aware factories:

headline = field(
    "Headline",
    default_factory=lambda context: (
        context.user_data["generator"].headline(context.item_id)
    ),
)

Aliases support intentional wording changes:

headline = field("Headline", aliases=("Title", "Name"))

Unknown fields currently accept only the default unknown_fields = "forbid" policy. Discriminated schemas also support inapplicable_fields = "forbid" and inapplicable_fields = "preserve" for variant-specific values.

Variants

Variants let one BDD table contain several related record shapes. The base schema declares shared fields and a discriminator used to select the applicable fields and behavior.

from talika import ColumnTable, TableFields, discriminator, field, id_field, split


class ArticleFields(TableFields):
    body = field("Body*", required=True)


class PollFields(TableFields):
    options: list[str] = field("Options*", required=True, parser=split(","))


class ContentTable(ColumnTable):
    id = id_field("IDs")
    content_type = discriminator(
        "Type*",
        variants={
            "Article": ArticleFields,
            "Poll": PollFields,
        },
    )
    headline = field("Headline*", required=True)

The explicit decorator form is also available with discriminator_field() and @ContentTable.variant(...).

Custom Cell Syntax

CellDSL groups exact tokens and full-match regular-expression rules into a reusable field parser. The package owns dispatch; your project owns the meaning.

from talika import CellDSL, ColumnTable, field, id_field

content_cells = CellDSL()


@content_cells.token("random")
def random_value(context):
    return context.user_data["generator"].random_for(context.field_name)


@content_cells.pattern(r"(?P<count>\d+):word")
def generated_words(match, context):
    count = int(match["count"])
    return context.user_data["generator"].words(count)


class ContentTable(ColumnTable):
    id = id_field("IDs")
    headline = field("Headline*", required=True, parser=content_cells)

Exact tokens run before patterns. Patterns are tried in registration order and must match the whole cell. Values that match no rule pass through unchanged. Rules may be scoped by schema attribute name, and several DSLs may be composed with compose_cell_dsls(...).

Validation And Diagnostics

Override validate_record() to check rules involving one parsed record:

class ContentTable(ColumnTable):
    id = id_field("IDs")
    content_type = field("Type*", required=True)
    headline = field("Headline*", required=True)

    def validate_record(self, context):
        if self.content_type == "Poll" and not self.headline.endswith("?"):
            raise ValueError("Poll headlines must end with a question mark")

Use validate_records() for relationships involving several records. Failures become TableError instances with stable error codes and source coordinates. In collect mode, independent diagnostics are grouped into TableErrors:

from talika import TableErrors

try:
    ContentTable.parse(datatable, error_mode="collect")
except TableErrors as errors:
    for error in errors:
        print(error.code, error.row, error.column, error.message)

Schema records expose immutable source metadata:

record.table_source.row
record.table_source.column
record.table_source.item_id
record.source_for("headline")

Output Models

Set output_model to return project objects after schema and table validation:

from dataclasses import dataclass


@dataclass(frozen=True)
class User:
    name: str
    age: int


class UserTable(RowTable):
    output_model = User

    name = field("name")
    age: int = field("age")

Dataclasses and other keyword-constructed classes need no integration dependency. Pydantic v2 works through the optional talika[pydantic] extra. Override build_output(record, context) when construction needs a custom signature, selected fields, source metadata, or project services.

Table Transformations

ColumnGroupExpander handles a common compact table convention where one source column describes one item or a group of items:

from talika import ColumnGroupExpander, NumericRange, PrefixRepeat


class ContentTable(ColumnTable):
    table_transformer = ColumnGroupExpander(
        key_row="IDs",
        range_rule=NumericRange(separator=".."),
        repeat_rule=PrefixRepeat(separator=":"),
    )

    id = id_field("IDs")
    content_type = field("Type*", required=True)

Projects can also implement compatible RangeRule and RepeatRule objects or override transform_table() for table syntax that does not fit the reusable grouped-column shape.

pytest Integration

Installing the package registers a talika fixture:

def content_exists(datatable, talika, faker):
    return talika.parse(
        datatable,
        schema=ContentTable,
        context={"faker": faker},
    )

The fixture also exposes parse_records() for type-checker-friendly schema records.

Static Feature Checking

Install the optional CLI extra to validate feature tables without executing pytest scenarios:

pip install "talika[cli]"
talika check features/content.feature `
  --schema tests/support/content_schema.py:ContentTable `
  --step "the following content exists:"

Machine-readable diagnostics are available for CI and editor integrations:

talika check features/content.feature `
  --schema tests/support/content_schema.py:ContentTable `
  --step "the following content exists:" `
  --format json

Use describe to inspect a schema without parsing a feature file:

talika describe tests/support/content_schema.py:ContentTable
talika describe tests/support/content_schema.py:ContentTable --format json

The checker uses the official Gherkin parser and reports exact feature-file coordinates. Importable module:Schema references are also supported. Use --context-factory module:function for deterministic parser dependencies. Scenario-outline substitutions are not expanded.

Development

uv sync --all-extras --dev
uv run pytest -p no:cacheprovider
uv run ruff check .
uv run ruff format --check .
uv run mypy src tests/typing/public_api.py
uv build

The documentation site is built with Zensical:

uv run --group docs zensical build --strict

GitHub Pages is configured for https://talikadev.github.io/talika/ through the Docs workflow in .github/workflows/docs.yml.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

talika-0.1.0.tar.gz (73.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

talika-0.1.0-py3-none-any.whl (67.9 kB view details)

Uploaded Python 3

File details

Details for the file talika-0.1.0.tar.gz.

File metadata

  • Download URL: talika-0.1.0.tar.gz
  • Upload date:
  • Size: 73.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for talika-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e884e2ca5faa6c25fc3238dbfa4325bdec0c072cb727e36822aec7566196fafa
MD5 3a551ce754c9ae44a23fb9bd86be451b
BLAKE2b-256 bd1cacc9717f5a2f2b6a8600e110be69f937603c24ed5ed26021818ee342462c

See more details on using hashes here.

Provenance

The following attestation bundles were made for talika-0.1.0.tar.gz:

Publisher: publish-pypi.yml on talikadev/talika

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file talika-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: talika-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 67.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for talika-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ef9767568b0f1ec64b82cbd47636b2ffc35963b40b07c5c2088149c1e8c3e805
MD5 7d7d1d3ce2cad0bffd5cfef856bcb7be
BLAKE2b-256 8ce34d9be254ef556a7e7c2553f833533c048086dbedd894269e0b2f647858eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for talika-0.1.0-py3-none-any.whl:

Publisher: publish-pypi.yml on talikadev/talika

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page