Polished API for Behave Data Tables — as_dicts, as_models, select, sort, to_csv, to_json, to_jsonl
Project description
behave-tables
Polished API for Behave Data Tables — convert to dicts, Pydantic models, CSV, JSON & JSON Lines. Zero deps · Type-safe · Python 3.11+ · 100% test coverage
Why?
Behave's Table requires manual iteration:
# Without behave-tables — repetitive, error-prone
for row in context.table.rows:
name = row["name"]
age = row["age"]
# No dicts, no models, no CSV, no JSON...
Every project reimplements the same helpers. behave-tables fixes this with a single wrap() call.
Install
pip install behave-tables
# Optional: pydantic for as_models() with validation & type coercion
pip install behave-tables[pydantic]
Quick start
from behave_tables import TableWrapper, wrap
@then("the users should be")
def step_impl(context):
table = wrap(context.table)
# Convert to dicts
users = table.as_dicts()
# [{"name": "Alice", "age": "30"}, {"name": "Bob", "age": "25"}]
# Convert to models (Pydantic v2 or dataclass)
users = table.as_models(User)
# Extract a single column
names = table.column("name") # ["Alice", "Bob"]
# Find the first matching row
alice = table.find_row(name="Alice") # {"name": "Alice", "age": "30"}
# Find all matching rows
thirty_somethings = table.find_all_rows(age="30")
# Validate columns (strict mode detects extras too)
table.validate_columns("name", "age", strict=True)
# Export
csv_str = table.to_csv(delimiter=";")
json_str = table.to_json(sort_keys=True)
# Transpose rows and columns
transposed = table.transpose()
# Transform
selected = table.select("name", "age")
cleaned = table.drop("internal_id")
renamed = table.rename_columns({"name": "full_name"})
sorted_wt = table.sort("age", reverse=True)
deduped = table.distinct()
# Query
cities = table.unique("city")
count = table.count(age="30")
first = table.first()
last = table.last()
# Export & import (round-trip)
jsonl = table.to_jsonl()
restored = TableWrapper.from_csv(table.to_csv())
restored = TableWrapper.from_json(table.to_json())
# Iterate, index, and measure
for row in table:
print(row["name"])
table[0] # {"name": "Alice", "age": "30"}
table[0:2] # [{"name": "Alice", "age": "30"}, {"name": "Bob", "age": "25"}]
len(table) # 2
# Compare and check membership
assert table == wrap(other_table)
assert {"name": "Alice", "age": "30"} in table
API
wrap(table)
Wrap a behave.model.Table (or any table-like object) with TableWrapper.
TableWrapper
| Method | Returns | Description |
|---|---|---|
as_dicts() |
list[dict[str, str]] |
All rows as dicts (copies) |
as_models(model) |
list[Model] |
Rows as Pydantic v2 or dataclass instances |
column(name) |
list[str] |
Values for a single column |
find_row(**filters) |
dict[str, str] | None |
First row matching all filters |
find_all_rows(**filters) |
list[dict[str, str]] |
All rows matching all filters (copies) |
validate_columns(*expected, strict=False) |
None |
Raise ColumnMismatchError if columns missing or unexpected |
transpose() |
TableWrapper |
New wrapper with rows and columns swapped |
to_csv(delimiter=",", quoting=QUOTE_MINIMAL) |
str |
CSV string with configurable delimiter and quoting |
to_json(indent=2, sort_keys=False, default=None) |
str |
JSON string (list of objects) |
to_jsonl() |
str |
JSON Lines string (one object per line) |
select(*columns) |
TableWrapper |
New wrapper with only the specified columns |
drop(*columns) |
TableWrapper |
New wrapper without the specified columns |
rename_columns(mapping) |
TableWrapper |
New wrapper with renamed columns |
sort(key, reverse=False) |
TableWrapper |
New wrapper with rows sorted by column or callable |
unique(column) |
list[str] |
Unique values for a column (first-seen order) |
distinct() |
TableWrapper |
New wrapper with duplicate rows removed |
count(**filters) |
int |
Count rows matching all filters |
first() |
dict[str, str] | None |
First row as dict copy, or None if empty |
last() |
dict[str, str] | None |
Last row as dict copy, or None if empty |
from_csv(csv_string, delimiter=",") |
TableWrapper |
Classmethod: create wrapper from CSV string |
from_json(json_string) |
TableWrapper |
Classmethod: create wrapper from JSON string |
headers |
list[str] |
Column names |
__iter__ |
Iterator[dict] |
Iterate rows as dicts |
__getitem__(i) |
dict[str, str] | list[dict[str, str]] |
Row at index as dict, or list of dicts for a slice |
__len__ |
int |
Number of rows |
__eq__ |
bool |
Compare by headers and rows |
__contains__ |
bool |
Check if a row dict is present |
__repr__ |
str |
Unambiguous representation |
ColumnMismatchError
Raised by validate_columns() when expected columns are missing or unexpected columns are found. Subclass of ValueError.
from behave_tables import wrap, ColumnMismatchError
table = wrap(context.table)
try:
table.validate_columns("name", "email")
except ColumnMismatchError as e:
print(e.missing) # ["email"]
# Strict mode: also detects unexpected columns
try:
table.validate_columns("name", strict=True)
except ColumnMismatchError as e:
print(e.missing) # []
print(e.extra) # ["age", "email"]
Examples
With dataclasses
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
@then("the users should be")
def step_impl(context):
users = wrap(context.table).as_models(User)
assert users[0].name == "Alice"
With Pydantic v2
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
@then("the users should be")
def step_impl(context):
users = wrap(context.table).as_models(User)
# Pydantic validates and coerces types automatically
assert users[0].age == 30 # int, not str
Validate columns
@then("the user list should be")
def step_impl(context):
table = wrap(context.table)
table.validate_columns("name", "email", "role")
# Proceed with confidence that columns exist
# Strict mode: ensure no extra columns
@then("the user list has exactly these columns")
def step_impl(context):
table = wrap(context.table)
table.validate_columns("name", "email", strict=True)
Transpose
@then("the data should be")
def step_impl(context):
table = wrap(context.table)
transposed = table.transpose()
# Original headers become the first column ("_column")
# Original row indices become the new headers ("0", "1", ...)
Find rows
@then("users aged 30 should exist")
def step_impl(context):
table = wrap(context.table)
users = table.find_all_rows(age="30")
assert len(users) == 2
# Find first match
alice = table.find_row(name="Alice")
Compare and check membership
@then("the table matches expected data")
def step_impl(context):
table = wrap(context.table)
other = wrap(other_table)
assert table == other
# Check if a row exists
assert {"name": "Alice", "age": "30"} in table
Export & import
@then("the report is generated")
def step_impl(context):
table = wrap(context.table)
csv_output = table.to_csv(delimiter=";")
json_output = table.to_json(indent=4, sort_keys=True)
jsonl_output = table.to_jsonl() # one JSON object per line
# Round-trip: export then re-import
restored = TableWrapper.from_csv(csv_output)
restored = TableWrapper.from_json(json_output)
Transform
@then("the user names are extracted")
def step_impl(context):
table = wrap(context.table)
names_only = table.select("name")
without_id = table.drop("internal_id")
renamed = table.rename_columns({"name": "full_name"})
sorted_by_age = table.sort("age", reverse=True)
deduped = table.distinct()
Query
@then("the statistics are computed")
def step_impl(context):
table = wrap(context.table)
cities = table.unique("city")
count = table.count(age="30")
first_user = table.first()
last_user = table.last()
Design principles
- Zero dependencies — no required packages.
as_models()works with stdlibdataclassesout of the box. Installpydanticoptionally for validation and type coercion. - Immutable returns — all public methods return copies of internal data. Modifying results never affects the wrapper state.
- Type-safe — ships with
py.typed(PEP 561) for full type checker support (mypy, pyright). - Defensive by default —
to_csv()handles missing values and extra keys gracefully.is_pydantic_model()won't crash on non-class input. - Protocol-based —
TableLikeprotocol accepts any object withheadingsandrows, not justbehave.model.Table.
Development
make dev # install with dev dependencies
make test # run tests
make test-cov # run tests with coverage
make lint # check code style
make lint-fix # auto-fix lint issues
make format # format code with ruff
make format-check # verify code is formatted
See CONTRIBUTING.md for details.
License
MIT
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file behave_tables-1.3.1.tar.gz.
File metadata
- Download URL: behave_tables-1.3.1.tar.gz
- Upload date:
- Size: 23.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2469d7a5c686ea8b3bf4939825ffeb20b6121b43ea8c48c49b6cb36180d84e8
|
|
| MD5 |
6fc63a84e3e0b4621b3a8cae909f20a9
|
|
| BLAKE2b-256 |
14359302ba81504057ea479af5b01259d1ba5885e991c5888927c5228146e5cd
|
Provenance
The following attestation bundles were made for behave_tables-1.3.1.tar.gz:
Publisher:
release.yml on MathiasPaulenko/behave-tables
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
behave_tables-1.3.1.tar.gz -
Subject digest:
e2469d7a5c686ea8b3bf4939825ffeb20b6121b43ea8c48c49b6cb36180d84e8 - Sigstore transparency entry: 2160414257
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/behave-tables@c5562c51b7bdedc775e1c771c0e2c47513de01b0 -
Branch / Tag:
refs/tags/v1.3.1 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c5562c51b7bdedc775e1c771c0e2c47513de01b0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file behave_tables-1.3.1-py3-none-any.whl.
File metadata
- Download URL: behave_tables-1.3.1-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8f5cd976cf02c2cfd376f9fbc85511ef03b275903a6cd07b75a651d83d9e8c9
|
|
| MD5 |
4faad5bcc0a3f76a3fd5527ef9dea382
|
|
| BLAKE2b-256 |
581d523c932744609c31c407612897acbdd3dcc3fab174e10e276443ce6a1160
|
Provenance
The following attestation bundles were made for behave_tables-1.3.1-py3-none-any.whl:
Publisher:
release.yml on MathiasPaulenko/behave-tables
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
behave_tables-1.3.1-py3-none-any.whl -
Subject digest:
c8f5cd976cf02c2cfd376f9fbc85511ef03b275903a6cd07b75a651d83d9e8c9 - Sigstore transparency entry: 2160414376
- Sigstore integration time:
-
Permalink:
MathiasPaulenko/behave-tables@c5562c51b7bdedc775e1c771c0e2c47513de01b0 -
Branch / Tag:
refs/tags/v1.3.1 - Owner: https://github.com/MathiasPaulenko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c5562c51b7bdedc775e1c771c0e2c47513de01b0 -
Trigger Event:
push
-
Statement type: