excel-orm 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {excel_orm-0.1.0 → excel_orm-0.1.2}/.gitignore +2 -1
- excel_orm-0.1.2/.pre-commit-config.yaml +80 -0
- excel_orm-0.1.2/.python-version +1 -0
- {excel_orm-0.1.0 → excel_orm-0.1.2}/PKG-INFO +1 -1
- {excel_orm-0.1.0 → excel_orm-0.1.2}/pyproject.toml +9 -11
- {excel_orm-0.1.0 → excel_orm-0.1.2/src}/excel_orm/orm.py +42 -19
- excel_orm-0.1.2/tests/__init__.py +0 -0
- excel_orm-0.1.2/tests/conftest.py +86 -0
- excel_orm-0.1.2/tests/test_columns.py +110 -0
- excel_orm-0.1.2/tests/test_orm.py +307 -0
- excel_orm-0.1.2/tests/test_orm_helpers.py +47 -0
- excel_orm-0.1.2/uv.lock +709 -0
- {excel_orm-0.1.0 → excel_orm-0.1.2}/README.md +0 -0
- {excel_orm-0.1.0 → excel_orm-0.1.2/src}/excel_orm/__init__.py +0 -0
- {excel_orm-0.1.0 → excel_orm-0.1.2/src}/excel_orm/column.py +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# .pre-commit-config.yaml
|
|
2
|
+
default_stages: [pre-commit]
|
|
3
|
+
|
|
4
|
+
minimum_pre_commit_version: "3.7.0"
|
|
5
|
+
|
|
6
|
+
exclude: |
|
|
7
|
+
(?x)(
|
|
8
|
+
^\.venv/|
|
|
9
|
+
^dist/|
|
|
10
|
+
^build/|
|
|
11
|
+
^\.mypy_cache/|
|
|
12
|
+
^\.ruff_cache/|
|
|
13
|
+
^\.pytest_cache/|
|
|
14
|
+
^\.tox/|
|
|
15
|
+
^node_modules/|
|
|
16
|
+
^docs/_build/
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
repos:
|
|
20
|
+
# -------------------------
|
|
21
|
+
# Baseline hygiene hooks
|
|
22
|
+
# -------------------------
|
|
23
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
24
|
+
rev: v5.0.0
|
|
25
|
+
hooks:
|
|
26
|
+
- id: check-yaml
|
|
27
|
+
- id: check-toml
|
|
28
|
+
- id: end-of-file-fixer
|
|
29
|
+
- id: trailing-whitespace
|
|
30
|
+
args: [--markdown-linebreak-ext=md]
|
|
31
|
+
- id: check-added-large-files
|
|
32
|
+
args: ["--maxkb=1500"]
|
|
33
|
+
- id: detect-private-key
|
|
34
|
+
- id: debug-statements
|
|
35
|
+
|
|
36
|
+
# -------------------------
|
|
37
|
+
# uv: lockfile / export consistency
|
|
38
|
+
# Official hook repo: astral-sh/uv-pre-commit :contentReference[oaicite:1]{index=1}
|
|
39
|
+
# -------------------------
|
|
40
|
+
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
41
|
+
rev: 0.9.18
|
|
42
|
+
hooks:
|
|
43
|
+
# Ensure uv.lock matches pyproject.toml changes
|
|
44
|
+
- id: uv-lock
|
|
45
|
+
stages: [pre-commit]
|
|
46
|
+
|
|
47
|
+
# -------------------------
|
|
48
|
+
# Ruff (lint + format)
|
|
49
|
+
# Official integration: astral-sh/ruff-pre-commit :contentReference[oaicite:2]{index=2}
|
|
50
|
+
# Order note when using --fix: lint before format :contentReference[oaicite:3]{index=3}
|
|
51
|
+
# -------------------------
|
|
52
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
53
|
+
rev: v0.14.10
|
|
54
|
+
hooks:
|
|
55
|
+
- id: ruff-check
|
|
56
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
57
|
+
stages: [pre-commit]
|
|
58
|
+
- id: ruff-format
|
|
59
|
+
stages: [pre-commit]
|
|
60
|
+
|
|
61
|
+
# -------------------------
|
|
62
|
+
# Local hooks that run via uv so they use your pinned toolchain.
|
|
63
|
+
# This is the cleanest way to integrate ty + pytest today.
|
|
64
|
+
# ty CLI: `ty check` :contentReference[oaicite:4]{index=4}
|
|
65
|
+
# -------------------------
|
|
66
|
+
- repo: local
|
|
67
|
+
hooks:
|
|
68
|
+
- id: ty-check
|
|
69
|
+
name: ty (type check)
|
|
70
|
+
entry: uv run ty check
|
|
71
|
+
language: system
|
|
72
|
+
pass_filenames: false
|
|
73
|
+
stages: [pre-push]
|
|
74
|
+
|
|
75
|
+
- id: pytest
|
|
76
|
+
name: pytest
|
|
77
|
+
entry: uv run pytest -q
|
|
78
|
+
language: system
|
|
79
|
+
pass_filenames: false
|
|
80
|
+
stages: [pre-push]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: excel-orm
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A lightweight Excel ORM for generating templates and parsing typed row models.
|
|
5
5
|
Project-URL: Homepage, https://github.com/acdelrusso/excel-orm
|
|
6
6
|
Project-URL: Repository, https://github.com/acdelrusso/excel-orm
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "excel-orm"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "A lightweight Excel ORM for generating templates and parsing typed row models."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -15,18 +15,16 @@ classifiers = [
|
|
|
15
15
|
"License :: OSI Approved :: MIT License",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
17
17
|
]
|
|
18
|
-
dependencies = [
|
|
19
|
-
"openpyxl>=3.1.5",
|
|
20
|
-
]
|
|
18
|
+
dependencies = ["openpyxl>=3.1.5"]
|
|
21
19
|
|
|
22
20
|
[dependency-groups]
|
|
23
21
|
dev = [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
"build>=1.3.0",
|
|
23
|
+
"pre-commit>=4.5.1",
|
|
24
|
+
"pytest>=9.0.2",
|
|
25
|
+
"ruff>=0.14.10",
|
|
26
|
+
"twine>=6.2.0",
|
|
27
|
+
"ty>=0.0.8",
|
|
30
28
|
]
|
|
31
29
|
|
|
32
30
|
[tool.ruff]
|
|
@@ -58,5 +56,5 @@ Issues = "https://github.com/acdelrusso/excel-orm/issues"
|
|
|
58
56
|
requires = ["hatchling>=1.25"]
|
|
59
57
|
build-backend = "hatchling.build"
|
|
60
58
|
|
|
61
|
-
[tool.hatch.build]
|
|
59
|
+
[tool.hatch.build.targets.wheel]
|
|
62
60
|
packages = ["src/excel_orm"]
|
|
@@ -23,6 +23,8 @@ def _pluralize(s: str) -> str:
|
|
|
23
23
|
# keep deliberately simple; can be swapped for inflect later
|
|
24
24
|
if s.endswith("s"):
|
|
25
25
|
return s
|
|
26
|
+
if s.endswith("y") and len(s) >= 2 and s[-2] not in "aeiou":
|
|
27
|
+
return s[:-1] + "ies"
|
|
26
28
|
return s + "s"
|
|
27
29
|
|
|
28
30
|
|
|
@@ -84,7 +86,7 @@ class PivotSheetSpec:
|
|
|
84
86
|
|
|
85
87
|
# field names on the model
|
|
86
88
|
pivot_field: str
|
|
87
|
-
|
|
89
|
+
row_fields: list[str]
|
|
88
90
|
value_field: str
|
|
89
91
|
|
|
90
92
|
# layout
|
|
@@ -92,7 +94,6 @@ class PivotSheetSpec:
|
|
|
92
94
|
header_row: int = 2 # pivot headers
|
|
93
95
|
row_header_col: int = 1
|
|
94
96
|
data_start_row: int = 3
|
|
95
|
-
data_start_col: int = 2
|
|
96
97
|
|
|
97
98
|
# template: define the pivot column values (dates) to render across the top
|
|
98
99
|
pivot_values: list[Any] | None = None # e.g., list[date]; required for generation
|
|
@@ -102,6 +103,10 @@ class PivotSheetSpec:
|
|
|
102
103
|
|
|
103
104
|
include_blanks: bool = False # whether to load blank cells as data points
|
|
104
105
|
|
|
106
|
+
@property
|
|
107
|
+
def data_start_col(self) -> int:
|
|
108
|
+
return len(self.row_fields) + 1
|
|
109
|
+
|
|
105
110
|
|
|
106
111
|
AnySheetSpec = PivotSheetSpec | SheetSpec
|
|
107
112
|
|
|
@@ -274,39 +279,54 @@ class ExcelFile:
|
|
|
274
279
|
tcell.font = title_font
|
|
275
280
|
tcell.alignment = title_alignment
|
|
276
281
|
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
282
|
+
# Row field headers (left block)
|
|
283
|
+
for i, rf in enumerate(spec.row_fields):
|
|
284
|
+
c = spec.row_header_col + i
|
|
285
|
+
cell = ws.cell(spec.header_row, c, rf.title())
|
|
286
|
+
cell.font = header_font
|
|
287
|
+
col_letter = cell.column_letter
|
|
288
|
+
ws.column_dimensions[col_letter].width = max(12, min(40, len(str(rf)) + 4))
|
|
280
289
|
|
|
281
290
|
# Pivot headers across the top
|
|
282
291
|
for j, pv in enumerate(spec.pivot_values):
|
|
283
292
|
c = spec.data_start_col + j
|
|
284
293
|
cell = ws.cell(spec.header_row, c, pv)
|
|
285
294
|
cell.font = header_font
|
|
286
|
-
|
|
287
|
-
col_letter = ws.cell(spec.header_row, c).column_letter
|
|
295
|
+
col_letter = cell.column_letter
|
|
288
296
|
ws.column_dimensions[col_letter].width = 14
|
|
289
297
|
|
|
290
|
-
# Seed row keys (optional)
|
|
291
298
|
if spec.row_values:
|
|
292
299
|
for i, rv in enumerate(spec.row_values):
|
|
293
300
|
r = spec.data_start_row + i
|
|
294
|
-
|
|
301
|
+
|
|
302
|
+
if len(spec.row_fields) == 1:
|
|
303
|
+
# Back-compat: rv is a single value
|
|
304
|
+
ws.cell(r, spec.row_header_col, rv)
|
|
305
|
+
else:
|
|
306
|
+
# Expect rv to be a tuple/list matching row_fields
|
|
307
|
+
if not isinstance(rv, (tuple, list)) or len(rv) != len(spec.row_fields):
|
|
308
|
+
raise ValueError(
|
|
309
|
+
"For multi-row_fields, PivotSheetSpec.row_values must be a list of "
|
|
310
|
+
f"tuples/lists with length {len(spec.row_fields)}."
|
|
311
|
+
)
|
|
312
|
+
for k, part in enumerate(rv):
|
|
313
|
+
ws.cell(r, spec.row_header_col + k, part)
|
|
295
314
|
|
|
296
315
|
def _parse_pivot_sheet(self, ws: Worksheet, spec: PivotSheetSpec) -> None:
|
|
297
316
|
model = spec.model
|
|
298
317
|
cols = {c.name: c for c in _get_model_columns(model)} # Column descriptors by field name
|
|
299
318
|
|
|
300
319
|
# Validate fields exist
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
)
|
|
320
|
+
required = [spec.pivot_field, spec.value_field, *spec.row_fields]
|
|
321
|
+
missing = [f for f in required if f not in cols]
|
|
322
|
+
if missing:
|
|
323
|
+
raise ValueError(
|
|
324
|
+
f"{model.__name__} is missing Column field(s) required by PivotSheetSpec: {missing}"
|
|
325
|
+
)
|
|
306
326
|
|
|
307
327
|
pivot_col = cols[spec.pivot_field]
|
|
308
|
-
row_col = cols[spec.row_field]
|
|
309
328
|
val_col = cols[spec.value_field]
|
|
329
|
+
row_cols = [cols[f] for f in spec.row_fields]
|
|
310
330
|
|
|
311
331
|
# Determine pivot headers from sheet (or trust spec.pivot_values)
|
|
312
332
|
pivot_headers: list[Any] = []
|
|
@@ -326,11 +346,13 @@ class ExcelFile:
|
|
|
326
346
|
|
|
327
347
|
r = spec.data_start_row
|
|
328
348
|
while r <= ws.max_row:
|
|
329
|
-
|
|
330
|
-
|
|
349
|
+
raw_parts = [
|
|
350
|
+
ws.cell(r, spec.row_header_col + i).value for i in range(len(spec.row_fields))
|
|
351
|
+
]
|
|
352
|
+
if _row_is_blank(raw_parts):
|
|
331
353
|
break
|
|
332
354
|
|
|
333
|
-
|
|
355
|
+
row_parts = [row_cols[i].parse_cell(raw_parts[i]) for i in range(len(row_cols))]
|
|
334
356
|
|
|
335
357
|
for j, pivot_value in enumerate(pivot_headers):
|
|
336
358
|
c = spec.data_start_col + j
|
|
@@ -340,7 +362,8 @@ class ExcelFile:
|
|
|
340
362
|
|
|
341
363
|
obj = _instantiate_model(model)
|
|
342
364
|
|
|
343
|
-
|
|
365
|
+
for fname, parsed in zip(spec.row_fields, row_parts, strict=False):
|
|
366
|
+
setattr(obj, fname, parsed)
|
|
344
367
|
setattr(obj, spec.pivot_field, pivot_value)
|
|
345
368
|
setattr(obj, spec.value_field, val_col.parse_cell(raw_val))
|
|
346
369
|
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from src.excel_orm import (
|
|
6
|
+
Column,
|
|
7
|
+
ExcelFile,
|
|
8
|
+
PivotSheetSpec,
|
|
9
|
+
SheetSpec,
|
|
10
|
+
date_column,
|
|
11
|
+
int_column,
|
|
12
|
+
text_column,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def models():
|
|
18
|
+
class Car:
|
|
19
|
+
make: Column[str] = text_column(header="Make", not_null=True)
|
|
20
|
+
model: Column[str] = text_column(header="Model", not_null=True)
|
|
21
|
+
year: Column[int] = int_column(header="Year", not_null=True)
|
|
22
|
+
|
|
23
|
+
class ManufacturingPlant:
|
|
24
|
+
name: Column[str] = text_column(header="Factory Name", not_null=True)
|
|
25
|
+
location: Column[str] = text_column(header="Location")
|
|
26
|
+
|
|
27
|
+
return Car, ManufacturingPlant
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def excel_file(models):
|
|
32
|
+
Car, ManufacturingPlant = models
|
|
33
|
+
sheet = SheetSpec(
|
|
34
|
+
name="Cars",
|
|
35
|
+
models=[Car, ManufacturingPlant],
|
|
36
|
+
title_row=1,
|
|
37
|
+
header_row=2,
|
|
38
|
+
data_start_row=3,
|
|
39
|
+
template_table_gap=2,
|
|
40
|
+
)
|
|
41
|
+
return ExcelFile(sheets=[sheet])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def demand_model():
|
|
46
|
+
class Demand:
|
|
47
|
+
dt: Column[date] = date_column(header="Date")
|
|
48
|
+
region: Column[str] = text_column(header="Region", not_null=True)
|
|
49
|
+
value: Column[int] = int_column(header="Value", not_null=True)
|
|
50
|
+
|
|
51
|
+
return Demand
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def demand_two_pivot():
|
|
56
|
+
class Demand:
|
|
57
|
+
dt: Column[date] = date_column(header="Date")
|
|
58
|
+
region: Column[str] = text_column(header="Region", not_null=True)
|
|
59
|
+
product: Column[str] = text_column(header="Product", not_null=True)
|
|
60
|
+
value: Column[int] = int_column(header="Value", not_null=True)
|
|
61
|
+
|
|
62
|
+
return Demand
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def pivot_excel_file(demand_model):
|
|
67
|
+
Demand = demand_model
|
|
68
|
+
|
|
69
|
+
# Pre-defined pivot values across the top of the sheet
|
|
70
|
+
pivot_values = [
|
|
71
|
+
date(2025, 6, 1),
|
|
72
|
+
date(2025, 7, 1),
|
|
73
|
+
date(2025, 8, 1),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
spec = PivotSheetSpec(
|
|
77
|
+
name="Demand",
|
|
78
|
+
model=Demand,
|
|
79
|
+
pivot_field="dt",
|
|
80
|
+
row_fields=["region"],
|
|
81
|
+
value_field="value",
|
|
82
|
+
pivot_values=pivot_values,
|
|
83
|
+
row_values=["NA", "EU"], # seed some regions
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return ExcelFile(sheets=[spec])
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from src.excel_orm import Column, bool_column, date_column, int_column, text_column
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_descriptor_stores_in_values_dict():
|
|
11
|
+
class Foo:
|
|
12
|
+
a: Column[str] = text_column(header="A")
|
|
13
|
+
|
|
14
|
+
f = Foo()
|
|
15
|
+
f._values = {} # ty:ignore[unresolved-attribute]
|
|
16
|
+
|
|
17
|
+
f.a = "hello"
|
|
18
|
+
assert f._values["a"] == "hello" # ty:ignore[unresolved-attribute]
|
|
19
|
+
assert f.a == "hello"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_descriptor_class_level_access_returns_column():
|
|
23
|
+
class Foo:
|
|
24
|
+
a: Column[str] = text_column(header="A")
|
|
25
|
+
|
|
26
|
+
assert isinstance(Foo.a, Column)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_not_null_validation_raises_on_none_or_empty():
|
|
30
|
+
class Foo:
|
|
31
|
+
a: Column[str] = text_column(header="A", not_null=True)
|
|
32
|
+
|
|
33
|
+
f = Foo()
|
|
34
|
+
f._values = {} # ty:ignore[unresolved-attribute]
|
|
35
|
+
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
f.a = None # type: ignore[assignment]
|
|
38
|
+
|
|
39
|
+
with pytest.raises(ValueError):
|
|
40
|
+
f.a = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_text_column_strip_and_none_to_empty_string():
|
|
44
|
+
col = text_column(header="X", strip=True)
|
|
45
|
+
assert col.parse_cell(None) == ""
|
|
46
|
+
assert col.parse_cell(" hi ") == "hi"
|
|
47
|
+
assert col.parse_cell(123) == "123"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_text_column_no_strip():
|
|
51
|
+
col = text_column(header="X", strip=False)
|
|
52
|
+
assert col.parse_cell(" hi ") == " hi "
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_int_column_parsing():
|
|
56
|
+
col = int_column(header="Y")
|
|
57
|
+
assert col.parse_cell(None) == 0
|
|
58
|
+
assert col.parse_cell("") == 0
|
|
59
|
+
assert col.parse_cell("42") == 42
|
|
60
|
+
assert col.parse_cell(7) == 7
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_bool_column_parsing_valid_values():
|
|
64
|
+
col = bool_column(header="B")
|
|
65
|
+
assert col.parse_cell(None) is False
|
|
66
|
+
assert col.parse_cell("") is False
|
|
67
|
+
assert col.parse_cell(True) is True
|
|
68
|
+
assert col.parse_cell("YES") is True
|
|
69
|
+
assert col.parse_cell("0") is False
|
|
70
|
+
assert col.parse_cell("n") is False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_bool_column_invalid_raises():
|
|
74
|
+
col = bool_column(header="B")
|
|
75
|
+
with pytest.raises(ValueError):
|
|
76
|
+
col.parse_cell("maybe")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize(
|
|
80
|
+
"raw, expected",
|
|
81
|
+
[
|
|
82
|
+
("01-JUN-2025", date(2025, 6, 1)),
|
|
83
|
+
("01-jun-2025", date(2025, 6, 1)), # case-insensitive month
|
|
84
|
+
("2025-06-01", date(2025, 6, 1)),
|
|
85
|
+
("2025/06/01", date(2025, 6, 1)),
|
|
86
|
+
("06/01/2025", date(2025, 6, 1)), # per your formats list (US)
|
|
87
|
+
(datetime(2025, 6, 1, 12, 30), date(2025, 6, 1)),
|
|
88
|
+
(date(2025, 6, 1), date(2025, 6, 1)),
|
|
89
|
+
("2025-06-01T13:45:00", date(2025, 6, 1)), # ISO datetime
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
def test_date_column_tryparse_cascade(raw, expected):
|
|
93
|
+
col = date_column(header="D")
|
|
94
|
+
assert col.parse_cell(raw) == expected
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_date_column_empty_raises():
|
|
98
|
+
col = date_column(header="D")
|
|
99
|
+
with pytest.raises(ValueError):
|
|
100
|
+
col.parse_cell(None)
|
|
101
|
+
with pytest.raises(ValueError):
|
|
102
|
+
col.parse_cell("")
|
|
103
|
+
with pytest.raises(ValueError):
|
|
104
|
+
col.parse_cell(" ")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_date_column_invalid_raises():
|
|
108
|
+
col = date_column(header="D")
|
|
109
|
+
with pytest.raises(ValueError):
|
|
110
|
+
col.parse_cell("not-a-date")
|