behave-data 0.1.0__py3-none-any.whl
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.
- behave_data/__init__.py +61 -0
- behave_data/config.py +137 -0
- behave_data/diff.py +182 -0
- behave_data/errors.py +100 -0
- behave_data/examples.py +107 -0
- behave_data/hooks.py +80 -0
- behave_data/loaders/__init__.py +109 -0
- behave_data/loaders/csv.py +26 -0
- behave_data/loaders/excel.py +54 -0
- behave_data/loaders/http.py +61 -0
- behave_data/loaders/json.py +34 -0
- behave_data/loaders/sql.py +46 -0
- behave_data/loaders/yaml.py +45 -0
- behave_data/manager.py +20 -0
- behave_data/null.py +65 -0
- behave_data/patch.py +153 -0
- behave_data/py.typed +0 -0
- behave_data/raw_table.py +118 -0
- behave_data/secrets.py +62 -0
- behave_data/typed_table.py +162 -0
- behave_data/types.py +134 -0
- behave_data-0.1.0.dist-info/METADATA +87 -0
- behave_data-0.1.0.dist-info/RECORD +26 -0
- behave_data-0.1.0.dist-info/WHEEL +4 -0
- behave_data-0.1.0.dist-info/entry_points.txt +5 -0
- behave_data-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Loader registry and Protocol for behave-data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from behave_data.config import Config
|
|
9
|
+
from behave_data.errors import LoaderNotFoundError
|
|
10
|
+
|
|
11
|
+
_EXTENSION_MAP: dict[str, str] = {
|
|
12
|
+
".csv": "csv",
|
|
13
|
+
".json": "json",
|
|
14
|
+
".yaml": "yaml",
|
|
15
|
+
".yml": "yaml",
|
|
16
|
+
".xlsx": "xlsx",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class Loader(Protocol):
|
|
22
|
+
"""Protocol for data loaders."""
|
|
23
|
+
|
|
24
|
+
def load(self, source: str, config: Config) -> list[dict[str, Any]]:
|
|
25
|
+
"""Load data from a source and return a list of dicts."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_LOADERS: dict[str, str] = {
|
|
30
|
+
"csv": "behave_data.loaders.csv:CsvLoader",
|
|
31
|
+
"json": "behave_data.loaders.json:JsonLoader",
|
|
32
|
+
"yaml": "behave_data.loaders.yaml:YamlLoader",
|
|
33
|
+
"xlsx": "behave_data.loaders.excel:ExcelLoader",
|
|
34
|
+
"sql": "behave_data.loaders.sql:SqlLoader",
|
|
35
|
+
"http": "behave_data.loaders.http:HttpLoader",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_loader(schema: str) -> Loader:
|
|
40
|
+
"""Lazy-import and return a loader instance for the given schema."""
|
|
41
|
+
if schema not in _LOADERS:
|
|
42
|
+
raise LoaderNotFoundError(schema)
|
|
43
|
+
module_path, class_name = _LOADERS[schema].split(":")
|
|
44
|
+
import importlib
|
|
45
|
+
|
|
46
|
+
module = importlib.import_module(module_path)
|
|
47
|
+
loader_cls = getattr(module, class_name)
|
|
48
|
+
return loader_cls()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _detect_schema(source: str) -> str | None:
|
|
52
|
+
"""Detect the schema prefix from a source string.
|
|
53
|
+
|
|
54
|
+
If source contains a schema prefix (e.g. ``csv:``), return it.
|
|
55
|
+
Otherwise, detect from file extension.
|
|
56
|
+
"""
|
|
57
|
+
if ":" in source:
|
|
58
|
+
prefix = source.split(":", 1)[0].lower()
|
|
59
|
+
if prefix in _LOADERS:
|
|
60
|
+
return prefix
|
|
61
|
+
p = Path(source)
|
|
62
|
+
ext = p.suffix.lower()
|
|
63
|
+
if ext in _EXTENSION_MAP:
|
|
64
|
+
return _EXTENSION_MAP[ext]
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_path(source: str, config: Config) -> str:
|
|
69
|
+
"""Resolve a source path, using config.load_base_dir for relative paths."""
|
|
70
|
+
p = Path(source)
|
|
71
|
+
if p.is_absolute() or ":" in source:
|
|
72
|
+
return source
|
|
73
|
+
return str(Path(config.load_base_dir) / source)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load(source: str, config: Config | None = None) -> list[dict[str, Any]]:
|
|
77
|
+
"""Load data from a source string.
|
|
78
|
+
|
|
79
|
+
Detects the schema prefix (e.g. ``csv:``) or file extension,
|
|
80
|
+
resolves relative paths against ``config.load_base_dir``,
|
|
81
|
+
and delegates to the appropriate loader.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
source: Source string with optional schema prefix.
|
|
85
|
+
config: Configuration with load_base_dir and connection settings.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of dicts representing loaded data.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
LoaderNotFoundError: If no loader matches the source.
|
|
92
|
+
"""
|
|
93
|
+
cfg = config if config is not None else Config()
|
|
94
|
+
|
|
95
|
+
schema = _detect_schema(source)
|
|
96
|
+
if schema is None:
|
|
97
|
+
raise LoaderNotFoundError(source)
|
|
98
|
+
|
|
99
|
+
loader = _resolve_loader(schema)
|
|
100
|
+
|
|
101
|
+
if ":" in source and source.split(":", 1)[0].lower() == schema:
|
|
102
|
+
path = source.split(":", 1)[1]
|
|
103
|
+
else:
|
|
104
|
+
path = source
|
|
105
|
+
|
|
106
|
+
if schema not in ("sql", "http"):
|
|
107
|
+
path = _resolve_path(path, cfg)
|
|
108
|
+
|
|
109
|
+
return loader.load(path, cfg)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""CSV loader for behave-data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from behave_data.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CsvLoader:
|
|
12
|
+
"""Load data from CSV files using csv.DictReader."""
|
|
13
|
+
|
|
14
|
+
def load(self, source: str, config: Config) -> list[dict[str, Any]]:
|
|
15
|
+
"""Load CSV data from a file path.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
source: Path to the CSV file.
|
|
19
|
+
config: Configuration (unused for CSV, but required by Protocol).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of dicts, one per row. Empty list if file is empty.
|
|
23
|
+
"""
|
|
24
|
+
with open(source, encoding="utf-8") as f:
|
|
25
|
+
reader = csv.DictReader(f)
|
|
26
|
+
return list(reader)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Excel loader for behave-data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from behave_data.config import Config
|
|
8
|
+
from behave_data.errors import OptionalDependencyError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExcelLoader:
|
|
12
|
+
"""Load data from Excel (.xlsx) files using openpyxl."""
|
|
13
|
+
|
|
14
|
+
def load(self, source: str, config: Config) -> list[dict[str, Any]]:
|
|
15
|
+
"""Load Excel data from a file path.
|
|
16
|
+
|
|
17
|
+
First row is treated as headers.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
source: Path to the Excel file.
|
|
21
|
+
config: Configuration (unused for Excel, but required by Protocol).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
List of dicts, one per data row.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
OptionalDependencyError: If openpyxl is not installed.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
import openpyxl
|
|
31
|
+
except ImportError:
|
|
32
|
+
raise OptionalDependencyError(
|
|
33
|
+
"openpyxl",
|
|
34
|
+
"Excel loading",
|
|
35
|
+
"pip install behave-data[excel]",
|
|
36
|
+
) from None
|
|
37
|
+
|
|
38
|
+
wb = openpyxl.load_workbook(source, read_only=True)
|
|
39
|
+
ws = wb.active
|
|
40
|
+
rows = list(ws.iter_rows(values_only=True))
|
|
41
|
+
wb.close()
|
|
42
|
+
|
|
43
|
+
if not rows:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
headers = [str(h) if h is not None else "" for h in rows[0]]
|
|
47
|
+
result: list[dict[str, Any]] = []
|
|
48
|
+
for row in rows[1:]:
|
|
49
|
+
row_dict: dict[str, Any] = {}
|
|
50
|
+
for i, cell in enumerate(row):
|
|
51
|
+
key = headers[i] if i < len(headers) else f"col_{i}"
|
|
52
|
+
row_dict[key] = cell
|
|
53
|
+
result.append(row_dict)
|
|
54
|
+
return result
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""HTTP loader for behave-data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from behave_data.config import Config
|
|
8
|
+
from behave_data.errors import OptionalDependencyError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpLoader:
|
|
12
|
+
"""Load data from HTTP endpoints using requests."""
|
|
13
|
+
|
|
14
|
+
def load(self, source: str, config: Config) -> list[dict[str, Any]]:
|
|
15
|
+
"""Fetch data from an HTTP endpoint.
|
|
16
|
+
|
|
17
|
+
Source format: ``GET https://api.example.com/users`` or
|
|
18
|
+
``POST https://api.example.com/users``.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
source: HTTP method + URL string.
|
|
22
|
+
config: Configuration (unused for HTTP, but required by Protocol).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
List of dicts from the JSON response.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
OptionalDependencyError: If requests is not installed.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
import requests
|
|
32
|
+
except ImportError:
|
|
33
|
+
raise OptionalDependencyError(
|
|
34
|
+
"requests",
|
|
35
|
+
"HTTP loading",
|
|
36
|
+
"pip install behave-data[http]",
|
|
37
|
+
) from None
|
|
38
|
+
|
|
39
|
+
parts = source.strip().split(None, 1)
|
|
40
|
+
if len(parts) == 2:
|
|
41
|
+
method, url = parts
|
|
42
|
+
else:
|
|
43
|
+
method = "GET"
|
|
44
|
+
url = parts[0]
|
|
45
|
+
|
|
46
|
+
method = method.upper()
|
|
47
|
+
if method == "GET":
|
|
48
|
+
response = requests.get(url, timeout=30)
|
|
49
|
+
elif method == "POST":
|
|
50
|
+
response = requests.post(url, timeout=30)
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
53
|
+
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
data = response.json()
|
|
56
|
+
|
|
57
|
+
if isinstance(data, list):
|
|
58
|
+
return data
|
|
59
|
+
if isinstance(data, dict):
|
|
60
|
+
return [data]
|
|
61
|
+
raise ValueError(f"HTTP response must be a list or dict, got {type(data).__name__}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""JSON loader for behave-data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from behave_data.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JsonLoader:
|
|
12
|
+
"""Load data from JSON files."""
|
|
13
|
+
|
|
14
|
+
def load(self, source: str, config: Config) -> list[dict[str, Any]]:
|
|
15
|
+
"""Load JSON data from a file path.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
source: Path to the JSON file.
|
|
19
|
+
config: Configuration (unused for JSON, but required by Protocol).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of dicts. If the JSON is a dict, wraps it in a list.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If the JSON data is not a list or dict.
|
|
26
|
+
"""
|
|
27
|
+
with open(source, encoding="utf-8") as f:
|
|
28
|
+
data = json.load(f)
|
|
29
|
+
|
|
30
|
+
if isinstance(data, list):
|
|
31
|
+
return data
|
|
32
|
+
if isinstance(data, dict):
|
|
33
|
+
return [data]
|
|
34
|
+
raise ValueError(f"JSON data must be a list or dict, got {type(data).__name__}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""SQL loader for behave-data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from behave_data.config import Config
|
|
8
|
+
from behave_data.errors import OptionalDependencyError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SqlLoader:
|
|
12
|
+
"""Load data from SQL queries using SQLAlchemy."""
|
|
13
|
+
|
|
14
|
+
def load(self, source: str, config: Config) -> list[dict[str, Any]]:
|
|
15
|
+
"""Execute a SQL query and return results as list of dicts.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
source: SQL query string.
|
|
19
|
+
config: Configuration with db_connections mapping.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of dicts, one per row.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
OptionalDependencyError: If SQLAlchemy is not installed.
|
|
26
|
+
KeyError: If the database name is not in config.db_connections.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
import sqlalchemy
|
|
30
|
+
except ImportError:
|
|
31
|
+
raise OptionalDependencyError(
|
|
32
|
+
"sqlalchemy",
|
|
33
|
+
"SQL loading",
|
|
34
|
+
"pip install behave-data[sql]",
|
|
35
|
+
) from None
|
|
36
|
+
|
|
37
|
+
db_name = getattr(config, "_sql_db_name", None) or "default"
|
|
38
|
+
connection_string = config.db_connections.get(db_name)
|
|
39
|
+
if connection_string is None:
|
|
40
|
+
raise KeyError(f"Database '{db_name}' not found in config.db_connections")
|
|
41
|
+
|
|
42
|
+
engine = sqlalchemy.create_engine(connection_string)
|
|
43
|
+
with engine.connect() as conn:
|
|
44
|
+
result = conn.execute(sqlalchemy.text(source))
|
|
45
|
+
columns = list(result.keys())
|
|
46
|
+
return [dict(zip(columns, row, strict=False)) for row in result]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""YAML loader for behave-data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from behave_data.config import Config
|
|
8
|
+
from behave_data.errors import OptionalDependencyError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class YamlLoader:
|
|
12
|
+
"""Load data from YAML files."""
|
|
13
|
+
|
|
14
|
+
def load(self, source: str, config: Config) -> list[dict[str, Any]]:
|
|
15
|
+
"""Load YAML data from a file path.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
source: Path to the YAML file.
|
|
19
|
+
config: Configuration (unused for YAML, but required by Protocol).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of dicts. If the YAML is a dict, wraps it in a list.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
OptionalDependencyError: If PyYAML is not installed.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
import yaml
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise OptionalDependencyError(
|
|
31
|
+
"pyyaml",
|
|
32
|
+
"YAML loading",
|
|
33
|
+
"pip install behave-data[yaml]",
|
|
34
|
+
) from None
|
|
35
|
+
|
|
36
|
+
with open(source, encoding="utf-8") as f:
|
|
37
|
+
data = yaml.safe_load(f)
|
|
38
|
+
|
|
39
|
+
if isinstance(data, list):
|
|
40
|
+
return data
|
|
41
|
+
if isinstance(data, dict):
|
|
42
|
+
return [data]
|
|
43
|
+
if data is None:
|
|
44
|
+
return []
|
|
45
|
+
raise ValueError(f"YAML data must be a list or dict, got {type(data).__name__}")
|
behave_data/manager.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Minimal DataManager stub for MVP.
|
|
2
|
+
|
|
3
|
+
The full implementation comes in Fase 3.3.
|
|
4
|
+
For now, it just stores the config.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from behave_data.config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataManager:
|
|
13
|
+
"""Minimal data manager that stores configuration.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
config: The behave-data configuration.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: Config) -> None:
|
|
20
|
+
self.config = config
|
behave_data/null.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Null handling for behave-data table cells."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from behave_data.config import DEFAULT_NULL_MARKERS, Config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_column_markers(column_name: str, config: Config) -> frozenset[str] | None:
|
|
9
|
+
"""Get the per-column null markers for a specific column.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
column_name: The column name.
|
|
13
|
+
config: The Config to query.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A frozenset of null markers for the column, or None if no
|
|
17
|
+
per-column markers are defined.
|
|
18
|
+
"""
|
|
19
|
+
return config.null_markers_by_column.get(column_name)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_null(
|
|
23
|
+
value: str,
|
|
24
|
+
markers: frozenset[str] | None = None,
|
|
25
|
+
column_markers: frozenset[str] | None = None,
|
|
26
|
+
) -> bool:
|
|
27
|
+
"""Check if a value is a null marker.
|
|
28
|
+
|
|
29
|
+
Priority: ``column_markers`` > ``markers`` > ``DEFAULT_NULL_MARKERS``.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
value: The string value to check.
|
|
33
|
+
markers: Global null markers. If None, defaults are used.
|
|
34
|
+
column_markers: Per-column markers that override global markers.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if the value is a null marker, False otherwise.
|
|
38
|
+
"""
|
|
39
|
+
if column_markers is not None:
|
|
40
|
+
return value in column_markers
|
|
41
|
+
if markers is not None:
|
|
42
|
+
return value in markers
|
|
43
|
+
return value in DEFAULT_NULL_MARKERS
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve_null(
|
|
47
|
+
value: str,
|
|
48
|
+
markers: frozenset[str] | None = None,
|
|
49
|
+
column_markers: frozenset[str] | None = None,
|
|
50
|
+
) -> str | None:
|
|
51
|
+
"""Resolve a null marker to None, or return the value unchanged.
|
|
52
|
+
|
|
53
|
+
Priority: ``column_markers`` > ``markers`` > ``DEFAULT_NULL_MARKERS``.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
value: The string value to resolve.
|
|
57
|
+
markers: Global null markers. If None, defaults are used.
|
|
58
|
+
column_markers: Per-column markers that override global markers.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
None if the value is a null marker, otherwise the original value.
|
|
62
|
+
"""
|
|
63
|
+
if is_null(value, markers, column_markers):
|
|
64
|
+
return None
|
|
65
|
+
return value
|
behave_data/patch.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Patch behave.model.Table with behave-data convenience methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from behave_data.config import Config
|
|
8
|
+
from behave_data.diff import diff as _diff
|
|
9
|
+
from behave_data.raw_table import RawTable
|
|
10
|
+
from behave_data.typed_table import TypedTableWrapper
|
|
11
|
+
from behave_data.typed_table import typed_wrap as _typed_wrap
|
|
12
|
+
|
|
13
|
+
_ORIGINAL_METHODS: dict[str, Any] = {}
|
|
14
|
+
_IS_PATCHED = False
|
|
15
|
+
|
|
16
|
+
_METHOD_NAMES = [
|
|
17
|
+
"typed_wrap",
|
|
18
|
+
"typed_dicts",
|
|
19
|
+
"typed_objects",
|
|
20
|
+
"clean_headers",
|
|
21
|
+
"to_dict",
|
|
22
|
+
"to_lists",
|
|
23
|
+
"to_pandas",
|
|
24
|
+
"raw_table",
|
|
25
|
+
"diff",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _typed_wrap_method(self: Any, config: Config | None = None) -> TypedTableWrapper:
|
|
30
|
+
return _typed_wrap(self, config)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _typed_dicts(self: Any, config: Config | None = None) -> list[dict[str, Any]]:
|
|
34
|
+
return TypedTableWrapper(self, config).typed_dicts()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _typed_objects(self: Any, cls: type, config: Config | None = None) -> list[Any]:
|
|
38
|
+
return TypedTableWrapper(self, config).typed_objects(cls, config)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _clean_headers(self: Any) -> list[str]:
|
|
42
|
+
return TypedTableWrapper(self).clean_headers()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _to_dict(self: Any, config: Config | None = None) -> dict[Any, Any]:
|
|
46
|
+
return TypedTableWrapper(self, config).to_dict()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _to_lists(self: Any, config: Config | None = None) -> list[list[Any]]:
|
|
50
|
+
return TypedTableWrapper(self, config).to_lists()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _to_pandas(self: Any, config: Config | None = None) -> Any:
|
|
54
|
+
return TypedTableWrapper(self, config).to_pandas()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _raw_table(self: Any) -> RawTable:
|
|
58
|
+
return RawTable(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _diff_method(
|
|
62
|
+
self: Any,
|
|
63
|
+
other: Any,
|
|
64
|
+
*,
|
|
65
|
+
ordered: bool = True,
|
|
66
|
+
ignore_columns: list[str] | None = None,
|
|
67
|
+
surplus_columns: bool = True,
|
|
68
|
+
) -> None:
|
|
69
|
+
_diff(
|
|
70
|
+
self,
|
|
71
|
+
other,
|
|
72
|
+
ordered=ordered,
|
|
73
|
+
ignore_columns=ignore_columns,
|
|
74
|
+
surplus_columns=surplus_columns,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_IMPLEMENTATIONS = {
|
|
79
|
+
"typed_wrap": _typed_wrap_method,
|
|
80
|
+
"typed_dicts": _typed_dicts,
|
|
81
|
+
"typed_objects": _typed_objects,
|
|
82
|
+
"clean_headers": _clean_headers,
|
|
83
|
+
"to_dict": _to_dict,
|
|
84
|
+
"to_lists": _to_lists,
|
|
85
|
+
"to_pandas": _to_pandas,
|
|
86
|
+
"raw_table": _raw_table,
|
|
87
|
+
"diff": _diff_method,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def apply_patches() -> None:
|
|
92
|
+
"""Add behave-data convenience methods to behave.model.Table.
|
|
93
|
+
|
|
94
|
+
This function is idempotent: calling it multiple times has no effect.
|
|
95
|
+
Each method is added as a bound method to the Table class.
|
|
96
|
+
|
|
97
|
+
Adds the following methods:
|
|
98
|
+
- typed_wrap(config=None)
|
|
99
|
+
- typed_dicts(config=None)
|
|
100
|
+
- typed_objects(cls, config=None)
|
|
101
|
+
- clean_headers()
|
|
102
|
+
- to_dict(config=None)
|
|
103
|
+
- to_lists(config=None)
|
|
104
|
+
- to_pandas(config=None)
|
|
105
|
+
- raw_table()
|
|
106
|
+
- diff(other, *, ordered=True, ignore_columns=None, surplus_columns=True)
|
|
107
|
+
|
|
108
|
+
Sets ``Table._behave_data_patched = True``.
|
|
109
|
+
"""
|
|
110
|
+
global _IS_PATCHED
|
|
111
|
+
if _IS_PATCHED:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
from behave.model import Table
|
|
116
|
+
except ImportError:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
for name in _METHOD_NAMES:
|
|
120
|
+
if hasattr(Table, name):
|
|
121
|
+
_ORIGINAL_METHODS[name] = getattr(Table, name)
|
|
122
|
+
setattr(Table, name, _IMPLEMENTATIONS[name])
|
|
123
|
+
|
|
124
|
+
Table._behave_data_patched = True # type: ignore[attr-defined]
|
|
125
|
+
_IS_PATCHED = True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def revert_patches() -> None:
|
|
129
|
+
"""Restore the original behave.model.Table methods.
|
|
130
|
+
|
|
131
|
+
This function is idempotent: calling it when not patched has no effect.
|
|
132
|
+
"""
|
|
133
|
+
global _IS_PATCHED
|
|
134
|
+
if not _IS_PATCHED:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
from behave.model import Table
|
|
139
|
+
except ImportError:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
for name in _METHOD_NAMES:
|
|
143
|
+
if name in _ORIGINAL_METHODS:
|
|
144
|
+
setattr(Table, name, _ORIGINAL_METHODS[name])
|
|
145
|
+
else:
|
|
146
|
+
if hasattr(Table, name):
|
|
147
|
+
delattr(Table, name)
|
|
148
|
+
|
|
149
|
+
if hasattr(Table, "_behave_data_patched"):
|
|
150
|
+
delattr(Table, "_behave_data_patched")
|
|
151
|
+
|
|
152
|
+
_ORIGINAL_METHODS.clear()
|
|
153
|
+
_IS_PATCHED = False
|
behave_data/py.typed
ADDED
|
File without changes
|