software-dlc-tile 0.2.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.
- software_dlc_tile-0.2.2/.gitignore +14 -0
- software_dlc_tile-0.2.2/LICENSE +21 -0
- software_dlc_tile-0.2.2/PKG-INFO +51 -0
- software_dlc_tile-0.2.2/README.md +29 -0
- software_dlc_tile-0.2.2/pyproject.toml +41 -0
- software_dlc_tile-0.2.2/src/software_dlc_tile/__init__.py +10 -0
- software_dlc_tile-0.2.2/src/software_dlc_tile/first_class.py +176 -0
- software_dlc_tile-0.2.2/src/software_dlc_tile/text.py +77 -0
- software_dlc_tile-0.2.2/tests/test_first_class.py +69 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SoftwareDLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: software-dlc-tile
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Python helpers for first-class TILE prompt tables.
|
|
5
|
+
Project-URL: Homepage, https://github.com/SoftwareDLC/tile#readme
|
|
6
|
+
Project-URL: Repository, https://github.com/SoftwareDLC/tile
|
|
7
|
+
Project-URL: Issues, https://github.com/SoftwareDLC/tile/issues
|
|
8
|
+
Author: SoftwareDLC
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: json,llm,prompt-engineering,prompt-optimization,structured-data,tile
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Text Processing
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# software-dlc-tile
|
|
24
|
+
|
|
25
|
+
Python helpers for first-class TILE prompt tables.
|
|
26
|
+
|
|
27
|
+
This package is an incubating Python surface for `@software-dlc/tile`. It starts
|
|
28
|
+
with first-class TILE because that is the smallest useful API for Python, data,
|
|
29
|
+
and LLM users who already have structured rows and want compact prompt context.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from software_dlc_tile import encode_first_class_tables_to_tile
|
|
33
|
+
|
|
34
|
+
tile = encode_first_class_tables_to_tile({
|
|
35
|
+
"delimiter": "pipe",
|
|
36
|
+
"tables": [
|
|
37
|
+
{
|
|
38
|
+
"id": "packages",
|
|
39
|
+
"columns": ["name", {"embedded_columns": ["dependency"]}],
|
|
40
|
+
"rows": [
|
|
41
|
+
["fastapi", "pydantic"],
|
|
42
|
+
["", "starlette"],
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This first Python package does not yet implement lossless JSON-to-TILE encoding
|
|
50
|
+
or TILE-to-JSON decoding. Those should come after shared conformance coverage is
|
|
51
|
+
expanded for the full wire format.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# software-dlc-tile
|
|
2
|
+
|
|
3
|
+
Python helpers for first-class TILE prompt tables.
|
|
4
|
+
|
|
5
|
+
This package is an incubating Python surface for `@software-dlc/tile`. It starts
|
|
6
|
+
with first-class TILE because that is the smallest useful API for Python, data,
|
|
7
|
+
and LLM users who already have structured rows and want compact prompt context.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from software_dlc_tile import encode_first_class_tables_to_tile
|
|
11
|
+
|
|
12
|
+
tile = encode_first_class_tables_to_tile({
|
|
13
|
+
"delimiter": "pipe",
|
|
14
|
+
"tables": [
|
|
15
|
+
{
|
|
16
|
+
"id": "packages",
|
|
17
|
+
"columns": ["name", {"embedded_columns": ["dependency"]}],
|
|
18
|
+
"rows": [
|
|
19
|
+
["fastapi", "pydantic"],
|
|
20
|
+
["", "starlette"],
|
|
21
|
+
],
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This first Python package does not yet implement lossless JSON-to-TILE encoding
|
|
28
|
+
or TILE-to-JSON decoding. Those should come after shared conformance coverage is
|
|
29
|
+
expanded for the full wire format.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "software-dlc-tile"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Python helpers for first-class TILE prompt tables."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "SoftwareDLC" }
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"json",
|
|
18
|
+
"llm",
|
|
19
|
+
"prompt-engineering",
|
|
20
|
+
"prompt-optimization",
|
|
21
|
+
"structured-data",
|
|
22
|
+
"tile"
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
"Topic :: Text Processing"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/SoftwareDLC/tile#readme"
|
|
37
|
+
Repository = "https://github.com/SoftwareDLC/tile"
|
|
38
|
+
Issues = "https://github.com/SoftwareDLC/tile/issues"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/software_dlc_tile"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .first_class import encode_first_class_tables_to_tile
|
|
2
|
+
from .text import escape_tile_text, resolve_tile_delimiter, unescape_tile_text
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"encode_first_class_tables_to_tile",
|
|
6
|
+
"escape_tile_text",
|
|
7
|
+
"resolve_tile_delimiter",
|
|
8
|
+
"unescape_tile_text",
|
|
9
|
+
]
|
|
10
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .text import escape_tile_text, resolve_tile_delimiter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
JsonTileFirstClassCell = str | int | float | bool | None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def encode_first_class_tables_to_tile(document: Mapping[str, Any]) -> str:
|
|
14
|
+
delimiter = resolve_tile_delimiter(str(document.get("delimiter", "tab")))
|
|
15
|
+
tables = document.get("tables")
|
|
16
|
+
if not isinstance(tables, Sequence) or isinstance(tables, (str, bytes)):
|
|
17
|
+
raise TypeError("First-class TILE document requires a tables sequence")
|
|
18
|
+
|
|
19
|
+
return "\n\n".join(_encode_first_class_table(table, delimiter) for table in tables)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _encode_first_class_table(table: Any, delimiter: str) -> str:
|
|
23
|
+
if not isinstance(table, Mapping):
|
|
24
|
+
raise TypeError("First-class TILE tables must be mappings")
|
|
25
|
+
|
|
26
|
+
columns = table.get("columns")
|
|
27
|
+
rows = table.get("rows")
|
|
28
|
+
if not isinstance(columns, Sequence) or isinstance(columns, (str, bytes)):
|
|
29
|
+
raise TypeError("First-class TILE table columns must be a sequence")
|
|
30
|
+
|
|
31
|
+
if not isinstance(rows, Sequence) or isinstance(rows, (str, bytes)):
|
|
32
|
+
raise TypeError("First-class TILE table rows must be a sequence")
|
|
33
|
+
|
|
34
|
+
lines = [
|
|
35
|
+
_encode_first_class_table_definition(table, delimiter),
|
|
36
|
+
delimiter.join(_format_first_class_table_column(column, delimiter) for column in columns),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
for row in rows:
|
|
40
|
+
if not isinstance(row, Sequence) or isinstance(row, (str, bytes)):
|
|
41
|
+
raise TypeError("First-class TILE table rows must contain cell sequences")
|
|
42
|
+
lines.append(delimiter.join(_format_first_class_table_cell(cell, delimiter) for cell in row))
|
|
43
|
+
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_first_class_table_column(column: Any, delimiter: str) -> str:
|
|
48
|
+
if isinstance(column, str):
|
|
49
|
+
return escape_tile_text(column, delimiter)
|
|
50
|
+
|
|
51
|
+
if not isinstance(column, Mapping):
|
|
52
|
+
raise TypeError("First-class TILE columns must be strings or embedded column mappings")
|
|
53
|
+
|
|
54
|
+
embedded_columns = column.get("embedded_columns")
|
|
55
|
+
if (
|
|
56
|
+
not isinstance(embedded_columns, Sequence)
|
|
57
|
+
or isinstance(embedded_columns, (str, bytes))
|
|
58
|
+
or len(embedded_columns) == 0
|
|
59
|
+
):
|
|
60
|
+
raise ValueError("First-class TILE embedded column groups cannot be empty")
|
|
61
|
+
|
|
62
|
+
join_delimiter = ";" if delimiter == "," else ","
|
|
63
|
+
return (
|
|
64
|
+
"["
|
|
65
|
+
+ join_delimiter.join(
|
|
66
|
+
_format_first_class_embedded_column_name(embedded_column, delimiter)
|
|
67
|
+
for embedded_column in embedded_columns
|
|
68
|
+
)
|
|
69
|
+
+ "]"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _format_first_class_embedded_column_name(column: Any, delimiter: str) -> str:
|
|
74
|
+
if not isinstance(column, str):
|
|
75
|
+
raise TypeError("First-class TILE embedded column names must be strings")
|
|
76
|
+
|
|
77
|
+
if "[" in column or "]" in column or (delimiter != "," and "," in column):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"First-class TILE embedded column names cannot contain comma or brackets"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return escape_tile_text(column, delimiter)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _encode_first_class_table_definition(table: Mapping[str, Any], delimiter: str) -> str:
|
|
86
|
+
table_id = table.get("id")
|
|
87
|
+
if not isinstance(table_id, str):
|
|
88
|
+
raise TypeError("First-class TILE table id must be a string")
|
|
89
|
+
|
|
90
|
+
definition_parts = [table_id]
|
|
91
|
+
kind = table.get("kind")
|
|
92
|
+
path = table.get("path")
|
|
93
|
+
|
|
94
|
+
if kind is not None and not isinstance(kind, str):
|
|
95
|
+
raise TypeError("First-class TILE table kind must be a string")
|
|
96
|
+
|
|
97
|
+
if path is not None and not isinstance(path, str):
|
|
98
|
+
raise TypeError("First-class TILE table path must be a string")
|
|
99
|
+
|
|
100
|
+
if kind or path:
|
|
101
|
+
definition_parts.append(kind or "")
|
|
102
|
+
|
|
103
|
+
if path:
|
|
104
|
+
definition_parts.append(path)
|
|
105
|
+
|
|
106
|
+
return delimiter.join(escape_tile_text(part, delimiter) for part in definition_parts)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _format_first_class_table_cell(cell: Any, delimiter: str) -> str:
|
|
110
|
+
if cell is None:
|
|
111
|
+
return ""
|
|
112
|
+
|
|
113
|
+
return escape_tile_text(_format_cell_value(cell), delimiter)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _format_cell_value(cell: JsonTileFirstClassCell) -> str:
|
|
117
|
+
if isinstance(cell, bool):
|
|
118
|
+
return "true" if cell else "false"
|
|
119
|
+
|
|
120
|
+
if isinstance(cell, str):
|
|
121
|
+
return cell
|
|
122
|
+
|
|
123
|
+
if isinstance(cell, int):
|
|
124
|
+
return str(cell)
|
|
125
|
+
|
|
126
|
+
if isinstance(cell, float):
|
|
127
|
+
if not math.isfinite(cell):
|
|
128
|
+
raise ValueError("First-class TILE only supports finite numbers")
|
|
129
|
+
return _format_javascript_number(cell)
|
|
130
|
+
|
|
131
|
+
raise TypeError("First-class TILE cells must be strings, numbers, booleans, or None")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _format_javascript_number(value: float) -> str:
|
|
135
|
+
if value == 0:
|
|
136
|
+
return "0"
|
|
137
|
+
|
|
138
|
+
sign = "-" if value < 0 else ""
|
|
139
|
+
digits, decimal_exponent = _decimal_parts(abs(value))
|
|
140
|
+
digit_count = len(digits)
|
|
141
|
+
|
|
142
|
+
if digit_count <= decimal_exponent <= 21:
|
|
143
|
+
return f"{sign}{digits}{'0' * (decimal_exponent - digit_count)}"
|
|
144
|
+
|
|
145
|
+
if 0 < decimal_exponent <= 21:
|
|
146
|
+
return f"{sign}{digits[:decimal_exponent]}.{digits[decimal_exponent:]}"
|
|
147
|
+
|
|
148
|
+
if -6 < decimal_exponent <= 0:
|
|
149
|
+
return f"{sign}0.{'0' * (-decimal_exponent)}{digits}"
|
|
150
|
+
|
|
151
|
+
mantissa = digits if digit_count == 1 else f"{digits[0]}.{digits[1:]}"
|
|
152
|
+
exponent = decimal_exponent - 1
|
|
153
|
+
exponent_sign = "+" if exponent >= 0 else ""
|
|
154
|
+
return f"{sign}{mantissa}e{exponent_sign}{exponent}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _decimal_parts(value: float) -> tuple[str, int]:
|
|
158
|
+
text = repr(value)
|
|
159
|
+
if "e" in text or "E" in text:
|
|
160
|
+
mantissa_text, exponent_text = text.lower().split("e", 1)
|
|
161
|
+
exponent = int(exponent_text)
|
|
162
|
+
integer_part, _, fractional_part = mantissa_text.partition(".")
|
|
163
|
+
digits = f"{integer_part}{fractional_part}".lstrip("0")
|
|
164
|
+
return digits, exponent + len(integer_part)
|
|
165
|
+
|
|
166
|
+
integer_part, _, fractional_part = text.partition(".")
|
|
167
|
+
if integer_part == "0":
|
|
168
|
+
trimmed_fractional_part = fractional_part.rstrip("0")
|
|
169
|
+
leading_zero_count = len(trimmed_fractional_part) - len(
|
|
170
|
+
trimmed_fractional_part.lstrip("0")
|
|
171
|
+
)
|
|
172
|
+
digits = trimmed_fractional_part.lstrip("0")
|
|
173
|
+
return digits or "0", -leading_zero_count
|
|
174
|
+
|
|
175
|
+
digits = f"{integer_part}{fractional_part}".rstrip("0")
|
|
176
|
+
return digits, len(integer_part)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def escape_tile_text(value: str, delimiter: str = "\t") -> str:
|
|
5
|
+
result: list[str] = []
|
|
6
|
+
|
|
7
|
+
for char in value:
|
|
8
|
+
if char == "\\":
|
|
9
|
+
result.append("\\\\")
|
|
10
|
+
elif char == "\t":
|
|
11
|
+
result.append("\\t")
|
|
12
|
+
elif char == "\n":
|
|
13
|
+
result.append("\\n")
|
|
14
|
+
elif char == "\r":
|
|
15
|
+
result.append("\\r")
|
|
16
|
+
elif char == " " and delimiter == " ":
|
|
17
|
+
result.append("\\s")
|
|
18
|
+
elif char == delimiter and delimiter != "\t":
|
|
19
|
+
result.append(f"\\{char}")
|
|
20
|
+
else:
|
|
21
|
+
result.append(char)
|
|
22
|
+
|
|
23
|
+
return "".join(result)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def unescape_tile_text(value: str) -> str:
|
|
27
|
+
result: list[str] = []
|
|
28
|
+
index = 0
|
|
29
|
+
|
|
30
|
+
while index < len(value):
|
|
31
|
+
char = value[index]
|
|
32
|
+
if char != "\\":
|
|
33
|
+
result.append(char)
|
|
34
|
+
index += 1
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
if index + 1 >= len(value):
|
|
38
|
+
raise ValueError("Invalid TILE escape: trailing backslash")
|
|
39
|
+
|
|
40
|
+
escaped_char = value[index + 1]
|
|
41
|
+
if escaped_char == "t":
|
|
42
|
+
result.append("\t")
|
|
43
|
+
elif escaped_char == "n":
|
|
44
|
+
result.append("\n")
|
|
45
|
+
elif escaped_char == "r":
|
|
46
|
+
result.append("\r")
|
|
47
|
+
elif escaped_char == "\\":
|
|
48
|
+
result.append("\\")
|
|
49
|
+
elif escaped_char == "s":
|
|
50
|
+
result.append(" ")
|
|
51
|
+
elif escaped_char == ",":
|
|
52
|
+
result.append(",")
|
|
53
|
+
elif escaped_char == "|":
|
|
54
|
+
result.append("|")
|
|
55
|
+
elif escaped_char == "@":
|
|
56
|
+
result.append("@")
|
|
57
|
+
elif escaped_char == ":":
|
|
58
|
+
result.append(":")
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f"Invalid TILE escape: \\{escaped_char}")
|
|
61
|
+
|
|
62
|
+
index += 2
|
|
63
|
+
|
|
64
|
+
return "".join(result)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_tile_delimiter(delimiter: str) -> str:
|
|
68
|
+
if delimiter == "comma":
|
|
69
|
+
return ","
|
|
70
|
+
|
|
71
|
+
if delimiter == "pipe":
|
|
72
|
+
return "|"
|
|
73
|
+
|
|
74
|
+
if delimiter == "space":
|
|
75
|
+
return " "
|
|
76
|
+
|
|
77
|
+
return "\t"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
ROOT_DIR = pathlib.Path(__file__).resolve().parents[3]
|
|
10
|
+
sys.path.insert(0, str(ROOT_DIR / "packages/python/src"))
|
|
11
|
+
|
|
12
|
+
from software_dlc_tile import ( # noqa: E402
|
|
13
|
+
encode_first_class_tables_to_tile,
|
|
14
|
+
escape_tile_text,
|
|
15
|
+
unescape_tile_text,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FirstClassTileTests(unittest.TestCase):
|
|
20
|
+
def test_first_class_conformance_cases(self) -> None:
|
|
21
|
+
cases_path = ROOT_DIR / "conformance/first-class-cases.json"
|
|
22
|
+
cases = json.loads(cases_path.read_text(encoding="utf8"))
|
|
23
|
+
|
|
24
|
+
for case in cases:
|
|
25
|
+
with self.subTest(case["id"]):
|
|
26
|
+
self.assertEqual(
|
|
27
|
+
encode_first_class_tables_to_tile(case["document"]),
|
|
28
|
+
case["expected"],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def test_rejects_ambiguous_embedded_column_names(self) -> None:
|
|
32
|
+
with self.assertRaisesRegex(ValueError, "comma or brackets"):
|
|
33
|
+
encode_first_class_tables_to_tile(
|
|
34
|
+
{
|
|
35
|
+
"tables": [
|
|
36
|
+
{
|
|
37
|
+
"id": "nodes",
|
|
38
|
+
"columns": [{"embedded_columns": ["edge,id"]}],
|
|
39
|
+
"rows": [],
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def test_rejects_non_finite_numbers(self) -> None:
|
|
46
|
+
with self.assertRaisesRegex(ValueError, "finite numbers"):
|
|
47
|
+
encode_first_class_tables_to_tile(
|
|
48
|
+
{
|
|
49
|
+
"tables": [
|
|
50
|
+
{
|
|
51
|
+
"id": "metrics",
|
|
52
|
+
"columns": ["value"],
|
|
53
|
+
"rows": [[math.inf]],
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def test_tile_text_escaping_round_trip(self) -> None:
|
|
60
|
+
escaped = escape_tile_text("a\tb\nc\\d")
|
|
61
|
+
self.assertEqual(escaped, "a\\tb\\nc\\\\d")
|
|
62
|
+
self.assertEqual(unescape_tile_text(escaped), "a\tb\nc\\d")
|
|
63
|
+
|
|
64
|
+
with self.assertRaisesRegex(ValueError, "Invalid TILE escape"):
|
|
65
|
+
unescape_tile_text("bad\\x")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
unittest.main()
|