ramtable 0.3.1__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.
- ramtable/__init__.py +15 -0
- ramtable/columns.py +181 -0
- ramtable/rows.py +397 -0
- ramtable/table.py +58 -0
- ramtable/types.py +181 -0
- ramtable-0.3.1.dist-info/METADATA +232 -0
- ramtable-0.3.1.dist-info/RECORD +9 -0
- ramtable-0.3.1.dist-info/WHEEL +4 -0
- ramtable-0.3.1.dist-info/licenses/LICENSE +21 -0
ramtable/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Public RAMTable API.
|
|
2
|
+
|
|
3
|
+
Expose :class:`Columns`, :class:`Rows`, and :class:`Table` from the
|
|
4
|
+
``ramtable`` package as a concise, stable import path.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .columns import Columns
|
|
8
|
+
from .rows import Rows
|
|
9
|
+
from .table import Table
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Columns",
|
|
13
|
+
"Rows",
|
|
14
|
+
"Table",
|
|
15
|
+
]
|
ramtable/columns.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
|
|
2
|
+
"""Mutable column schema and validation of schema changes."""
|
|
3
|
+
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .types import TypeValidator
|
|
8
|
+
|
|
9
|
+
_MISSING = object()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Columns:
|
|
13
|
+
"""Manage the names and types associated with a table's columns.
|
|
14
|
+
|
|
15
|
+
The instance keeps a copy of the supplied schema and shares its owning
|
|
16
|
+
table's row storage so schema changes remain consistent with existing
|
|
17
|
+
records.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
columns: dict[str, Any],
|
|
23
|
+
rows_storage: list[dict[str, Any]],
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Create a non-empty schema associated with row storage.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
columns: Mapping of column names to supported types.
|
|
29
|
+
rows_storage: Shared list of table records.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If the schema is empty or a name is invalid.
|
|
33
|
+
TypeError: If a type annotation is unsupported.
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(columns, dict) or not columns:
|
|
36
|
+
raise ValueError("Table must have at least one column.")
|
|
37
|
+
|
|
38
|
+
self._columns = columns.copy()
|
|
39
|
+
self._rows_storage = rows_storage
|
|
40
|
+
|
|
41
|
+
for name, column_type in self._columns.items():
|
|
42
|
+
self._validate_name(name)
|
|
43
|
+
self._validate_type(column_type)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _validate_name(name: str) -> None:
|
|
47
|
+
"""Reject column names that are not non-empty strings."""
|
|
48
|
+
if not isinstance(name, str) or not name.strip():
|
|
49
|
+
raise ValueError("Column names must be non-empty strings.")
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _validate_type(column_type: object) -> None:
|
|
53
|
+
"""Reject annotations unsupported by RAMTable's type validator."""
|
|
54
|
+
if not TypeValidator.is_supported_type(column_type):
|
|
55
|
+
raise TypeError(
|
|
56
|
+
f"Unsupported column type: {column_type!r}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def add(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
column_type: Any,
|
|
63
|
+
default: Any = _MISSING,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Add a column and initialize existing rows when necessary.
|
|
66
|
+
|
|
67
|
+
If records already exist, ``default`` is required. A deep copy of the
|
|
68
|
+
value is created for each record so mutable defaults are not shared
|
|
69
|
+
between rows. If there are no rows, an optional default is not stored.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If the name is empty, already exists, or a required
|
|
73
|
+
default is missing.
|
|
74
|
+
TypeError: If the type or default value is invalid.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
self._validate_name(name)
|
|
78
|
+
self._validate_type(column_type)
|
|
79
|
+
|
|
80
|
+
if name in self._columns:
|
|
81
|
+
raise ValueError(f"Column '{name}' already exists.")
|
|
82
|
+
|
|
83
|
+
if self._rows_storage and default is _MISSING:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"A default value is required when adding a column "
|
|
86
|
+
"to a table containing rows."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if default is not _MISSING:
|
|
90
|
+
if not TypeValidator.matches_type(default, column_type):
|
|
91
|
+
raise TypeError(
|
|
92
|
+
f"Default value does not match {column_type!r}."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Prepare all default values before changing the table.
|
|
96
|
+
# This prevents partial updates if copying fails.
|
|
97
|
+
if self._rows_storage:
|
|
98
|
+
default_values = [
|
|
99
|
+
deepcopy(default) for _ in self._rows_storage
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
for row, value in zip(self._rows_storage, default_values):
|
|
103
|
+
row[name] = value
|
|
104
|
+
|
|
105
|
+
self._columns[name] = column_type
|
|
106
|
+
|
|
107
|
+
def remove(self, name: str) -> None:
|
|
108
|
+
"""Remove a column from the schema and all records.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If the column does not exist or is the last column.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
if name not in self._columns:
|
|
115
|
+
raise ValueError(f"Column '{name}' does not exist.")
|
|
116
|
+
|
|
117
|
+
if len(self._columns) == 1:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"A table must have at least one column."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
for row in self._rows_storage:
|
|
123
|
+
del row[name]
|
|
124
|
+
|
|
125
|
+
del self._columns[name]
|
|
126
|
+
|
|
127
|
+
def update(self, name: str, new_type: Any) -> None:
|
|
128
|
+
"""Change a column's declared type if all current values match.
|
|
129
|
+
|
|
130
|
+
Stored values are not converted. The change is applied only after
|
|
131
|
+
every record has been validated.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If the column does not exist.
|
|
135
|
+
TypeError: If the new type is unsupported or any current value
|
|
136
|
+
does not match it.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if name not in self._columns:
|
|
140
|
+
raise ValueError(f"Column '{name}' does not exist.")
|
|
141
|
+
|
|
142
|
+
self._validate_type(new_type)
|
|
143
|
+
|
|
144
|
+
for row in self._rows_storage:
|
|
145
|
+
value = row[name]
|
|
146
|
+
|
|
147
|
+
if not TypeValidator.matches_type(value, new_type):
|
|
148
|
+
raise TypeError(
|
|
149
|
+
f"Existing value in column '{name}' "
|
|
150
|
+
f"does not match {new_type!r}."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
self._columns[name] = new_type
|
|
154
|
+
|
|
155
|
+
def get(self, name: str) -> Any:
|
|
156
|
+
"""Return the type annotation of a column.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If the column does not exist.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
if name not in self._columns:
|
|
163
|
+
raise ValueError(f"Column '{name}' does not exist.")
|
|
164
|
+
|
|
165
|
+
return self._columns[name]
|
|
166
|
+
|
|
167
|
+
def get_all(self) -> dict[str, Any]:
|
|
168
|
+
"""Return a shallow copy of the complete schema."""
|
|
169
|
+
return self._columns.copy()
|
|
170
|
+
|
|
171
|
+
def __contains__(self, name: str) -> bool:
|
|
172
|
+
"""Return whether ``name`` is declared in the schema."""
|
|
173
|
+
return name in self._columns
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int:
|
|
176
|
+
"""Return the number of declared columns."""
|
|
177
|
+
return len(self._columns)
|
|
178
|
+
|
|
179
|
+
def __repr__(self) -> str:
|
|
180
|
+
"""Return a debugging representation of the schema."""
|
|
181
|
+
return f"Columns({self._columns!r})"
|
ramtable/rows.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
|
|
2
|
+
"""Row insertion, retrieval, filtering, update, and removal operations."""
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from .columns import Columns
|
|
7
|
+
from .types import TypeValidator
|
|
8
|
+
|
|
9
|
+
_FILTER_OPERATORS: dict[str, Callable[[Any, Any], bool]] = {
|
|
10
|
+
"==": lambda value, operand: value == operand,
|
|
11
|
+
"!=": lambda value, operand: value != operand,
|
|
12
|
+
">": lambda value, operand: value > operand,
|
|
13
|
+
">=": lambda value, operand: value >= operand,
|
|
14
|
+
"<": lambda value, operand: value < operand,
|
|
15
|
+
"<=": lambda value, operand: value <= operand,
|
|
16
|
+
"in": lambda value, operand: value in operand,
|
|
17
|
+
"not_in": lambda value, operand: value not in operand,
|
|
18
|
+
"contains": lambda value, operand: operand in value,
|
|
19
|
+
"startswith": lambda value, operand: value.startswith(operand),
|
|
20
|
+
"endswith": lambda value, operand: value.endswith(operand),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Rows:
|
|
25
|
+
"""Provide operations on a table's rows.
|
|
26
|
+
|
|
27
|
+
Rows are stored as dictionaries in storage shared with :class:`Columns`.
|
|
28
|
+
Returned copies are shallow: the dictionaries are new, but mutable
|
|
29
|
+
objects they contain may still be shared.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
columns: Columns,
|
|
35
|
+
storage: list[dict[str, Any]],
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Bind the row view to its schema and storage.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
columns: Schema that each row must satisfy.
|
|
41
|
+
storage: Shared list used to store rows.
|
|
42
|
+
"""
|
|
43
|
+
self._columns = columns
|
|
44
|
+
self._storage = storage
|
|
45
|
+
|
|
46
|
+
def _validate(self, row: dict[str, Any]) -> None:
|
|
47
|
+
"""Validate that a row has exactly the required columns and types.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
TypeError: If ``row`` is not a dictionary or a value does not
|
|
51
|
+
match its declared type.
|
|
52
|
+
ValueError: If columns are missing or undeclared.
|
|
53
|
+
"""
|
|
54
|
+
if not isinstance(row, dict):
|
|
55
|
+
raise TypeError("Row must be a dictionary.")
|
|
56
|
+
|
|
57
|
+
schema = self._columns.get_all()
|
|
58
|
+
|
|
59
|
+
expected = set(schema)
|
|
60
|
+
received = set(row)
|
|
61
|
+
|
|
62
|
+
if expected != received:
|
|
63
|
+
missing = expected - received
|
|
64
|
+
unexpected = received - expected
|
|
65
|
+
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Invalid row structure. "
|
|
68
|
+
f"Missing columns: {missing}. "
|
|
69
|
+
f"Unexpected columns: {unexpected}."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for column, column_type in schema.items():
|
|
73
|
+
if not TypeValidator.matches_type(
|
|
74
|
+
row[column], column_type
|
|
75
|
+
):
|
|
76
|
+
raise TypeError(
|
|
77
|
+
f"Invalid value for column '{column}'. "
|
|
78
|
+
f"Expected {column_type!r}, "
|
|
79
|
+
f"received {type(row[column]).__name__}."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _filter(self, where: dict[str, Any]) -> list[dict[str, Any]]:
|
|
83
|
+
"""Return the stored rows that satisfy every condition in ``where``.
|
|
84
|
+
|
|
85
|
+
This is the shared engine behind :meth:`filter`, :meth:`delete`, and
|
|
86
|
+
:meth:`update`. Each key is a column name and each condition is one
|
|
87
|
+
of:
|
|
88
|
+
|
|
89
|
+
* a plain value, matched with ``==``;
|
|
90
|
+
* a dictionary of operators whose operands must all match;
|
|
91
|
+
* a callable that receives the cell value and returns a truthy
|
|
92
|
+
result when the row matches.
|
|
93
|
+
|
|
94
|
+
Supported operators are ``==``, ``!=``, ``>``, ``>=``, ``<``,
|
|
95
|
+
``<=``, ``in``, ``not_in``, ``contains``, ``startswith``, and
|
|
96
|
+
``endswith``. All conditions are combined with ``and``.
|
|
97
|
+
|
|
98
|
+
Returned rows are internal references; callers copy them before
|
|
99
|
+
exposing them outside the class.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: If ``where`` is not a non-empty dictionary, names a
|
|
103
|
+
column that does not exist, or uses an unknown operator.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if not isinstance(where, dict) or not where:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"Filter conditions must be a non-empty dictionary."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
schema = self._columns.get_all()
|
|
112
|
+
|
|
113
|
+
for column, condition in where.items():
|
|
114
|
+
if column not in schema:
|
|
115
|
+
raise ValueError(f"Column '{column}' does not exist.")
|
|
116
|
+
|
|
117
|
+
if isinstance(condition, dict):
|
|
118
|
+
if not condition:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Filter for column '{column}' must not be empty."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for operator in condition:
|
|
124
|
+
if operator not in _FILTER_OPERATORS:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Unknown filter operator {operator!r}. "
|
|
127
|
+
"Supported operators: "
|
|
128
|
+
f"{', '.join(_FILTER_OPERATORS)}."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def matches(value: Any, condition: Any) -> bool:
|
|
132
|
+
if isinstance(condition, dict):
|
|
133
|
+
return all(
|
|
134
|
+
_FILTER_OPERATORS[operator](value, operand)
|
|
135
|
+
for operator, operand in condition.items()
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if callable(condition) and not isinstance(condition, type):
|
|
139
|
+
return bool(condition(value))
|
|
140
|
+
|
|
141
|
+
return value == condition
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
row
|
|
145
|
+
for row in self._storage
|
|
146
|
+
if all(
|
|
147
|
+
matches(row[column], condition)
|
|
148
|
+
for column, condition in where.items()
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
def insert(self, row: dict[str, Any]) -> None:
|
|
153
|
+
"""Validate and append a shallow copy of ``row``.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
TypeError: If the row or one of its values is invalid.
|
|
157
|
+
ValueError: If the row does not have exactly the schema columns.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
self._validate(row)
|
|
161
|
+
self._storage.append(row.copy())
|
|
162
|
+
|
|
163
|
+
def remove(self, row: dict[str, Any]) -> None:
|
|
164
|
+
"""Remove the first row equal to ``row`` after validating its shape.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
TypeError: If the row or one of its values is invalid.
|
|
168
|
+
ValueError: If the row does not match the schema or does not exist.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
self._validate(row)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
self._storage.remove(row)
|
|
175
|
+
except ValueError:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
"Row does not exist in this table."
|
|
178
|
+
) from None
|
|
179
|
+
|
|
180
|
+
def delete(self, where: dict[str, Any]) -> int:
|
|
181
|
+
"""Delete every row that matches ``where``.
|
|
182
|
+
|
|
183
|
+
Conditions use the same format as :meth:`filter` and :meth:`update`.
|
|
184
|
+
To remove one specific row by its full content, use :meth:`remove`.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
where: Non-empty dictionary mapping column names to conditions.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Number of deleted rows; may be zero.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ValueError: If ``where`` is not a non-empty dictionary, names a
|
|
194
|
+
column that does not exist, or uses an unknown operator.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
matches = self._filter(where)
|
|
198
|
+
|
|
199
|
+
if matches:
|
|
200
|
+
matched = {id(row) for row in matches}
|
|
201
|
+
self._storage[:] = [
|
|
202
|
+
row for row in self._storage
|
|
203
|
+
if id(row) not in matched
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
return len(matches)
|
|
207
|
+
|
|
208
|
+
def update(
|
|
209
|
+
self,
|
|
210
|
+
where: dict[str, Any],
|
|
211
|
+
values: dict[str, Any],
|
|
212
|
+
) -> int:
|
|
213
|
+
"""Update fields in every row that satisfies ``where``.
|
|
214
|
+
|
|
215
|
+
Both ``where`` and ``values`` must be non-empty dictionaries containing
|
|
216
|
+
existing column names. Conditions use the same flexible format as
|
|
217
|
+
:meth:`filter`; new values are validated against the schema, but
|
|
218
|
+
condition values are not. The entire request is validated before any
|
|
219
|
+
rows are modified.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
where: Conditions that a row must satisfy (see :meth:`filter`).
|
|
223
|
+
values: Columns and values to assign to each matching row.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Number of updated rows; may be zero.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
TypeError: If an assigned value does not match its column type.
|
|
230
|
+
ValueError: If either argument is empty or not a dictionary, or
|
|
231
|
+
if a non-existent column or filter operator is named.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if not isinstance(values, dict) or not values:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"Update values must be a non-empty dictionary."
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
schema = self._columns.get_all()
|
|
240
|
+
|
|
241
|
+
# Validate update columns and their values.
|
|
242
|
+
for column, value in values.items():
|
|
243
|
+
if column not in schema:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Column '{column}' does not exist."
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not TypeValidator.matches_type(value, schema[column]):
|
|
249
|
+
raise TypeError(
|
|
250
|
+
f"Invalid value for column '{column}'. "
|
|
251
|
+
f"Expected {schema[column]!r}, "
|
|
252
|
+
f"received {type(value).__name__}."
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Find matching rows before modifying anything. ``_filter`` validates
|
|
256
|
+
# the conditions and returns references to stored rows.
|
|
257
|
+
matching_rows = self._filter(where)
|
|
258
|
+
|
|
259
|
+
# Apply changes.
|
|
260
|
+
for row in matching_rows:
|
|
261
|
+
row.update(values)
|
|
262
|
+
|
|
263
|
+
return len(matching_rows)
|
|
264
|
+
|
|
265
|
+
def filter(self, where: dict[str, Any]) -> list[dict[str, Any]]:
|
|
266
|
+
"""Return shallow copies of the rows that match ``where``.
|
|
267
|
+
|
|
268
|
+
Conditions use the same format as :meth:`update` and :meth:`delete`;
|
|
269
|
+
see :meth:`_filter` for the supported operators and callables. Rows
|
|
270
|
+
are returned in storage order.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
where: Non-empty dictionary mapping column names to conditions.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
New list of shallow row copies; empty when nothing matches.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
ValueError: If ``where`` is not a non-empty dictionary, names a
|
|
280
|
+
column that does not exist, or uses an unknown operator.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
return [row.copy() for row in self._filter(where)]
|
|
284
|
+
|
|
285
|
+
def clear(self) -> None:
|
|
286
|
+
"""Remove all rows without changing the schema."""
|
|
287
|
+
self._storage.clear()
|
|
288
|
+
|
|
289
|
+
def get(self, *indices: int) -> list[dict[str, Any]]:
|
|
290
|
+
"""Return all rows or copies of the requested positions.
|
|
291
|
+
|
|
292
|
+
With no arguments, return all rows. Indexes support negative values as
|
|
293
|
+
Python lists do; the requested order is preserved and repeated
|
|
294
|
+
indexes are allowed.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
TypeError: If an index is not an integer or is a boolean.
|
|
298
|
+
IndexError: If an index is out of range.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
if not indices:
|
|
302
|
+
return self.get_all()
|
|
303
|
+
|
|
304
|
+
selected = []
|
|
305
|
+
|
|
306
|
+
for index in indices:
|
|
307
|
+
if isinstance(index, bool) or not isinstance(index, int):
|
|
308
|
+
raise TypeError("Row indices must be integers.")
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
selected.append(self._storage[index].copy())
|
|
312
|
+
except IndexError:
|
|
313
|
+
raise IndexError(
|
|
314
|
+
f"Row index {index} is out of range."
|
|
315
|
+
) from None
|
|
316
|
+
|
|
317
|
+
return selected
|
|
318
|
+
|
|
319
|
+
def get_one(self, index: int) -> dict[str, Any]:
|
|
320
|
+
"""Return a shallow copy of the row at ``index``.
|
|
321
|
+
|
|
322
|
+
Negative indexes are supported. ``bool`` is not accepted as an index,
|
|
323
|
+
although it is a subclass of ``int`` in Python.
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
TypeError: If the index is not an integer or is a boolean.
|
|
327
|
+
IndexError: If the index is out of range.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
if isinstance(index, bool) or not isinstance(index, int):
|
|
331
|
+
raise TypeError("Row index must be an integer.")
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
return self._storage[index].copy()
|
|
335
|
+
except IndexError:
|
|
336
|
+
raise IndexError(
|
|
337
|
+
f"Row index {index} is out of range."
|
|
338
|
+
) from None
|
|
339
|
+
|
|
340
|
+
def get_all(self) -> list[dict[str, Any]]:
|
|
341
|
+
"""Return a new list containing a shallow copy of each row."""
|
|
342
|
+
return [row.copy() for row in self._storage]
|
|
343
|
+
|
|
344
|
+
def get_columns(self, *columns: str) -> dict[str, list[Any]]:
|
|
345
|
+
"""Return value lists for the requested columns.
|
|
346
|
+
|
|
347
|
+
If no names are given, include every schema column. The dictionary and
|
|
348
|
+
lists are new, but their values are not copied.
|
|
349
|
+
|
|
350
|
+
Raises:
|
|
351
|
+
ValueError: If a requested column does not exist.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
schema = self._columns.get_all()
|
|
355
|
+
|
|
356
|
+
if not columns:
|
|
357
|
+
columns = tuple(schema.keys())
|
|
358
|
+
|
|
359
|
+
result = {}
|
|
360
|
+
|
|
361
|
+
for column in columns:
|
|
362
|
+
if column not in schema:
|
|
363
|
+
raise ValueError(f"Column '{column}' does not exist.")
|
|
364
|
+
|
|
365
|
+
result[column] = [
|
|
366
|
+
row[column] for row in self._storage
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
def get_column(self, column: str) -> list[Any]:
|
|
372
|
+
"""Return a new list containing the values of one column.
|
|
373
|
+
|
|
374
|
+
Values are not copied.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
ValueError: If the column does not exist.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
if column not in self._columns:
|
|
381
|
+
raise ValueError(f"Column '{column}' does not exist.")
|
|
382
|
+
|
|
383
|
+
return [
|
|
384
|
+
row[column] for row in self._storage
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
def __len__(self) -> int:
|
|
388
|
+
"""Return the current number of rows."""
|
|
389
|
+
return len(self._storage)
|
|
390
|
+
|
|
391
|
+
def __iter__(self):
|
|
392
|
+
"""Iterate over shallow copies of the current rows."""
|
|
393
|
+
return iter(self.get_all())
|
|
394
|
+
|
|
395
|
+
def __repr__(self) -> str:
|
|
396
|
+
"""Return a short representation with the number of rows."""
|
|
397
|
+
return f"Rows(count={len(self)})"
|
ramtable/table.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
"""Main container coordinating a table's schema and rows."""
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .columns import Columns
|
|
7
|
+
from .rows import Rows
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Table:
|
|
11
|
+
"""Represent an in-memory table with a typed schema and rows.
|
|
12
|
+
|
|
13
|
+
The table does not persist data to disk. Its ``columns`` and ``rows``
|
|
14
|
+
attributes provide operations on the schema and records, respectively.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
name: str,
|
|
20
|
+
columns: dict[str, Any],
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Create a table with a non-empty name and schema.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
name: Descriptive table name.
|
|
26
|
+
columns: Dictionary of column names and supported annotations.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If the name is not a non-empty string, the schema is
|
|
30
|
+
empty, or a column name is invalid.
|
|
31
|
+
TypeError: If a column annotation is unsupported.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(name, str) or not name.strip():
|
|
34
|
+
raise ValueError("Table name must be a non-empty string.")
|
|
35
|
+
|
|
36
|
+
self.name = name
|
|
37
|
+
|
|
38
|
+
# Shared internal storage.
|
|
39
|
+
storage: list[dict[str, Any]] = []
|
|
40
|
+
|
|
41
|
+
self.columns = Columns(columns, storage)
|
|
42
|
+
self.rows = Rows(self.columns, storage)
|
|
43
|
+
|
|
44
|
+
def clear(self) -> None:
|
|
45
|
+
"""Remove all records while preserving the column schema."""
|
|
46
|
+
self.rows.clear()
|
|
47
|
+
|
|
48
|
+
def __len__(self) -> int:
|
|
49
|
+
"""Return the current number of rows."""
|
|
50
|
+
return len(self.rows)
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
"""Return a representation with the name, columns, and row count."""
|
|
54
|
+
return (
|
|
55
|
+
f"Table(name={self.name!r}, "
|
|
56
|
+
f"columns={list(self.columns.get_all())!r}, "
|
|
57
|
+
f"rows={len(self.rows)})"
|
|
58
|
+
)
|
ramtable/types.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
|
|
2
|
+
"""Recursive validation of type annotations for RAMTable."""
|
|
3
|
+
|
|
4
|
+
import types as std_types
|
|
5
|
+
from collections import abc
|
|
6
|
+
from typing import Annotated, Any, Literal, Union, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TypeValidator:
|
|
10
|
+
"""Check supported annotations and whether values match them.
|
|
11
|
+
|
|
12
|
+
Validation is performed at runtime; it does not convert values or
|
|
13
|
+
implement every static type-checking rule.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def is_supported_type(cls, value_type: object) -> bool:
|
|
18
|
+
"""Return whether ``value_type`` is an annotation RAMTable can check.
|
|
19
|
+
|
|
20
|
+
Supported forms include classes, ``Any``, ``None``, ``Annotated``,
|
|
21
|
+
unions, ``Literal``, common parameterized collections, fixed or
|
|
22
|
+
variadic tuples, and ``type[T]``. Other annotation forms return
|
|
23
|
+
``False``.
|
|
24
|
+
"""
|
|
25
|
+
if value_type is Any or value_type is None:
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
if isinstance(value_type, type):
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
origin = get_origin(value_type)
|
|
32
|
+
args = get_args(value_type)
|
|
33
|
+
|
|
34
|
+
if origin is Annotated:
|
|
35
|
+
return bool(args) and cls.is_supported_type(args[0])
|
|
36
|
+
|
|
37
|
+
if origin in (Union, std_types.UnionType):
|
|
38
|
+
return bool(args) and all(
|
|
39
|
+
cls.is_supported_type(arg) for arg in args
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if origin is Literal:
|
|
43
|
+
return bool(args)
|
|
44
|
+
|
|
45
|
+
if origin in (list, set, frozenset, abc.Sequence, abc.Set):
|
|
46
|
+
return not args or (
|
|
47
|
+
len(args) == 1
|
|
48
|
+
and cls.is_supported_type(args[0])
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if origin in (dict, abc.Mapping):
|
|
52
|
+
return not args or (
|
|
53
|
+
len(args) == 2
|
|
54
|
+
and all(cls.is_supported_type(arg) for arg in args)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if origin is tuple:
|
|
58
|
+
if not args:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
|
62
|
+
return cls.is_supported_type(args[0])
|
|
63
|
+
|
|
64
|
+
return all(cls.is_supported_type(arg) for arg in args)
|
|
65
|
+
|
|
66
|
+
if origin is type:
|
|
67
|
+
return (
|
|
68
|
+
len(args) == 1
|
|
69
|
+
and isinstance(args[0], type)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def matches_type(cls, value: object, value_type: object) -> bool:
|
|
76
|
+
"""Recursively check whether ``value`` matches ``value_type``.
|
|
77
|
+
|
|
78
|
+
Simple types follow Python's ``isinstance`` semantics (for example,
|
|
79
|
+
``bool`` also matches ``int``). ``Annotated`` checks its base type;
|
|
80
|
+
``Literal`` requires equality and an exact type match. Elements and
|
|
81
|
+
keys in parameterized containers are also checked.
|
|
82
|
+
"""
|
|
83
|
+
if value_type is Any:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
if value_type is None:
|
|
87
|
+
return value is None
|
|
88
|
+
|
|
89
|
+
origin = get_origin(value_type)
|
|
90
|
+
args = get_args(value_type)
|
|
91
|
+
|
|
92
|
+
if origin is Annotated:
|
|
93
|
+
return cls.matches_type(value, args[0])
|
|
94
|
+
|
|
95
|
+
if origin in (Union, std_types.UnionType):
|
|
96
|
+
return any(
|
|
97
|
+
cls.matches_type(value, arg) for arg in args
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if origin is Literal:
|
|
101
|
+
return any(
|
|
102
|
+
value == literal and type(value) is type(literal)
|
|
103
|
+
for literal in args
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if origin in (list, set, frozenset):
|
|
107
|
+
if not isinstance(value, origin):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
return not args or all(
|
|
111
|
+
cls.matches_type(item, args[0])
|
|
112
|
+
for item in value
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if origin is tuple:
|
|
116
|
+
if not isinstance(value, tuple):
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
if not args:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
|
123
|
+
return all(
|
|
124
|
+
cls.matches_type(item, args[0])
|
|
125
|
+
for item in value
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
len(value) == len(args)
|
|
130
|
+
and all(
|
|
131
|
+
cls.matches_type(item, expected)
|
|
132
|
+
for item, expected in zip(value, args)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if origin in (dict, abc.Mapping):
|
|
137
|
+
if not isinstance(value, origin):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
if not args:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
key_type, item_type = args
|
|
144
|
+
|
|
145
|
+
return all(
|
|
146
|
+
cls.matches_type(key, key_type)
|
|
147
|
+
and cls.matches_type(item, item_type)
|
|
148
|
+
for key, item in value.items()
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if origin is abc.Sequence:
|
|
152
|
+
if not isinstance(value, abc.Sequence):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
return not args or all(
|
|
156
|
+
cls.matches_type(item, args[0])
|
|
157
|
+
for item in value
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if origin is abc.Set:
|
|
161
|
+
if not isinstance(value, abc.Set):
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
return not args or all(
|
|
165
|
+
cls.matches_type(item, args[0])
|
|
166
|
+
for item in value
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if origin is type:
|
|
170
|
+
if not isinstance(value, type):
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
return issubclass(value, args[0])
|
|
175
|
+
except TypeError:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
return isinstance(value, value_type)
|
|
180
|
+
except TypeError:
|
|
181
|
+
return False
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ramtable
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: A lightweight in-memory table library for Python.
|
|
5
|
+
Author: Thiago Valentín Stilo Limarino
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: data-structure,in-memory,python,table
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# RAMTable
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+

|
|
24
|
+
[](https://pypi.org/project/ramtable/)
|
|
25
|
+
[](https://test.pypi.org/project/ramtable/)
|
|
26
|
+
|
|
27
|
+
RAMTable provides typed in-memory tables for data that only needs to live
|
|
28
|
+
while a program is running. It gives records an explicit schema and common
|
|
29
|
+
operations, without requiring you to manage lists and dictionaries by hand
|
|
30
|
+
or set up a persistent SQL database.
|
|
31
|
+
|
|
32
|
+
RAMTable is a lightweight Python library with no runtime dependencies. Data
|
|
33
|
+
is kept in memory and is lost when the process ends. RAMTable is not a
|
|
34
|
+
database: it does not execute SQL and does not write data to disk.
|
|
35
|
+
|
|
36
|
+
## Where it fits
|
|
37
|
+
|
|
38
|
+
Reach for RAMTable when a plain `list` or `dict` does not provide enough
|
|
39
|
+
structure, but a persistent database is too much:
|
|
40
|
+
|
|
41
|
+
| | Plain `list` / `dict` | RAMTable | SQL database |
|
|
42
|
+
|---|:---:|:---:|:---:|
|
|
43
|
+
| Schema with declared types | — | ✓ | ✓ |
|
|
44
|
+
| Rows validated on write | — | ✓ | ✓ |
|
|
45
|
+
| Schema changes with data present | manual | ✓ | ✓ |
|
|
46
|
+
| Query language, indexes, constraints | — | — | ✓ |
|
|
47
|
+
| Survives process exit | — | — | ✓ |
|
|
48
|
+
| Setup | none | none | server or files |
|
|
49
|
+
|
|
50
|
+
Typical scenarios:
|
|
51
|
+
|
|
52
|
+
- intermediate results of a multi-step process;
|
|
53
|
+
- temporary records staged during a calculation or import;
|
|
54
|
+
- the working set of a long-running task;
|
|
55
|
+
- prototypes that want typed rows before a real database exists.
|
|
56
|
+
|
|
57
|
+
For simple collections, a list or dictionary is still the right answer. If
|
|
58
|
+
data must survive restarts, be shared between processes, or be queried with
|
|
59
|
+
SQL, use an appropriate persistence solution instead. For analysis and
|
|
60
|
+
vectorized operations, use a data-frame library: RAMTable targets structured
|
|
61
|
+
temporary storage, not analytics.
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
RAMTable requires Python 3.10 or later.
|
|
66
|
+
|
|
67
|
+
Install the latest stable release from PyPI:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m pip install ramtable
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Pre-releases are published to TestPyPI. To try one:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m pip install --pre --index-url https://test.pypi.org/simple/ ramtable
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
TestPyPI is a separate testing index, not the official package index. Stable
|
|
80
|
+
releases are published to PyPI under the name `ramtable`.
|
|
81
|
+
|
|
82
|
+
Install from a local checkout:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m pip install .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Install in editable mode for development:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m pip install --editable .
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from ramtable import Table
|
|
98
|
+
|
|
99
|
+
people = Table(
|
|
100
|
+
name="people",
|
|
101
|
+
columns={
|
|
102
|
+
"id": int,
|
|
103
|
+
"name": str,
|
|
104
|
+
"age": int,
|
|
105
|
+
"email": str,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Every row must contain exactly the columns declared above.
|
|
110
|
+
people.rows.insert(
|
|
111
|
+
{"id": 1, "name": "Ada", "age": 36, "email": "ada@example.com"}
|
|
112
|
+
)
|
|
113
|
+
people.rows.insert(
|
|
114
|
+
{"id": 2, "name": "Linus", "age": 28, "email": "linus@example.com"}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
print(people.rows.get_column("name"))
|
|
118
|
+
# ['Ada', 'Linus']
|
|
119
|
+
|
|
120
|
+
# Update every row matching the conditions; returns the number of rows changed.
|
|
121
|
+
updated = people.rows.update(
|
|
122
|
+
where={"id": 1},
|
|
123
|
+
values={"name": "Ada Lovelace"},
|
|
124
|
+
)
|
|
125
|
+
print(updated)
|
|
126
|
+
# 1
|
|
127
|
+
|
|
128
|
+
print(people.rows.get_one(0))
|
|
129
|
+
# {'id': 1, 'name': 'Ada Lovelace', 'age': 36, 'email': 'ada@example.com'}
|
|
130
|
+
|
|
131
|
+
# Conditions go beyond equality in filter(), update(), and delete().
|
|
132
|
+
older = people.rows.filter({"age": {">": 30}})
|
|
133
|
+
print([row["name"] for row in older])
|
|
134
|
+
# ['Ada Lovelace']
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A `Table` exposes two views over the same storage:
|
|
138
|
+
|
|
139
|
+
- `table.rows`: the data. Insert, read, update, and remove rows.
|
|
140
|
+
- `table.columns`: the schema. Add, remove, and retype columns.
|
|
141
|
+
|
|
142
|
+
The [user guide](docs/guide.md) walks through every operation step by step.
|
|
143
|
+
|
|
144
|
+
## Features
|
|
145
|
+
|
|
146
|
+
- **Explicit schema:** declare column names and types when the table is
|
|
147
|
+
created.
|
|
148
|
+
- **Runtime validation:** every row must contain exactly the schema's
|
|
149
|
+
columns, and each value must match its declared type.
|
|
150
|
+
- **Row operations:** insert, retrieve, update, filter, and delete rows.
|
|
151
|
+
- **Flexible conditions:** one condition format powers `filter`, `update`,
|
|
152
|
+
and `delete`, with comparison, membership, and string operators.
|
|
153
|
+
- **Schema evolution:** add, remove, or retype columns while accounting for
|
|
154
|
+
existing rows.
|
|
155
|
+
- **Validate-then-write:** update requests are checked in full before any
|
|
156
|
+
row is modified, so a rejected request never leaves a partial change.
|
|
157
|
+
- **Zero dependencies:** no server, no SQL setup, nothing beyond the package
|
|
158
|
+
itself.
|
|
159
|
+
|
|
160
|
+
## Supported types
|
|
161
|
+
|
|
162
|
+
Besides plain classes, RAMTable understands `Any`, `None`, unions
|
|
163
|
+
(`str | None`), `Annotated`, `Literal`, `type[T]`, and parameterized lists,
|
|
164
|
+
sets, frozensets, sequences, mappings, dictionaries, and tuples. Nested
|
|
165
|
+
arguments are checked recursively:
|
|
166
|
+
|
|
167
|
+
| Annotation | Example | Rule |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| Classes | `int`, `str`, `bytes` | `isinstance` |
|
|
170
|
+
| Unions | `str \| None` | any alternative may match |
|
|
171
|
+
| `Literal` | `Literal["pending", "done"]` | value and exact type |
|
|
172
|
+
| Sequence types | `list[str]`, `set[int]`, `Sequence[float]` | container class, then every item |
|
|
173
|
+
| Mapping types | `dict[str, int]`, `Mapping[str, list[int]]` | container class, then every key and value |
|
|
174
|
+
| Tuples | `tuple[int, str]`, `tuple[int, ...]` | fixed positions or one repeated type |
|
|
175
|
+
| `Annotated` | `Annotated[int, "id"]` | base type only; metadata is ignored |
|
|
176
|
+
| Class objects | `type[int]` | subclasses of `T` |
|
|
177
|
+
| `Any` / `None` | — | any value / only `None` |
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from typing import Literal
|
|
181
|
+
from ramtable import Table
|
|
182
|
+
|
|
183
|
+
events = Table(
|
|
184
|
+
"events",
|
|
185
|
+
{
|
|
186
|
+
"name": str,
|
|
187
|
+
"status": Literal["pending", "complete"],
|
|
188
|
+
"tags": list[str],
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Simple classes follow `isinstance` semantics, so a `bool` value also matches
|
|
194
|
+
`int`. `Literal` is stricter: it compares both value and exact type. See
|
|
195
|
+
[types and validation](docs/types-and-validation.md) for the complete rules
|
|
196
|
+
and known limitations.
|
|
197
|
+
|
|
198
|
+
## Copies and lifecycle
|
|
199
|
+
|
|
200
|
+
Rows are stored as dictionaries. Inserting a row stores a shallow copy of
|
|
201
|
+
the dictionary; retrieving a row returns a new dictionary containing the same
|
|
202
|
+
values. Mutable objects used as cell values may therefore remain shared
|
|
203
|
+
with the code that supplied or retrieved them.
|
|
204
|
+
|
|
205
|
+
`table.clear()` removes all rows and preserves the schema. No operation
|
|
206
|
+
writes to disk: records disappear when the process ends.
|
|
207
|
+
|
|
208
|
+
## Documentation
|
|
209
|
+
|
|
210
|
+
| Document | Contents |
|
|
211
|
+
|---|---|
|
|
212
|
+
| [User guide](docs/guide.md) | Tables, rows, filtering, schema changes, errors, and limits. |
|
|
213
|
+
| [API reference](docs/api.md) | Every class and method, with arguments, returns, and exceptions. |
|
|
214
|
+
| [Types and validation](docs/types-and-validation.md) | Supported annotations and runtime checking rules. |
|
|
215
|
+
| [Development and releases](docs/development.md) | Project layout, checks, and the release process. |
|
|
216
|
+
| [CHANGELOG](CHANGELOG.md) | Release history. |
|
|
217
|
+
| [LICENSE](LICENSE) | MIT license terms. |
|
|
218
|
+
|
|
219
|
+
## Project status
|
|
220
|
+
|
|
221
|
+
The current development version is **0.3.1rc0**. RAMTable is published as
|
|
222
|
+
`ramtable` on both [PyPI](https://pypi.org/project/ramtable/) and
|
|
223
|
+
[TestPyPI](https://test.pypi.org/project/ramtable/). Use PyPI for stable
|
|
224
|
+
releases and TestPyPI to try release candidates and other pre-releases.
|
|
225
|
+
|
|
226
|
+
The project is at an early stage and will continue to evolve. See the
|
|
227
|
+
[CHANGELOG](CHANGELOG.md) for release history.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
RAMTable is distributed under the MIT License. See [LICENSE](LICENSE) for
|
|
232
|
+
the terms.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ramtable/__init__.py,sha256=HVrTgXXmZ5jb93ZdGU4jgKx077cjGmDiDUGNQq4Pe38,285
|
|
2
|
+
ramtable/columns.py,sha256=xkHZwX0KuiauJET1ugql8i1LMU5AevclOmLG5ZQA4dU,5805
|
|
3
|
+
ramtable/rows.py,sha256=Jaw4cLI1tppTXZOALBjOKHyIpIk5dfBhTPFg9RoqJVU,13359
|
|
4
|
+
ramtable/table.py,sha256=cshsx6GHY0_MQu2wvupXg3DdiM5lhEdrp0Be7d5MHAc,1731
|
|
5
|
+
ramtable/types.py,sha256=9eB0C7pKA5TkOJdV_VkT3yUO9-D3j8CLnGueeRP-rKA,5290
|
|
6
|
+
ramtable-0.3.1.dist-info/METADATA,sha256=B4kygAq1sZLUhyGgkCqDcO3WZxQUC5gL_t-n8qKgndQ,8120
|
|
7
|
+
ramtable-0.3.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
ramtable-0.3.1.dist-info/licenses/LICENSE,sha256=0o1fe5xZ9sY7cVfXYqFCCRH_25mSui25kyUBbYLvQQ0,1078
|
|
9
|
+
ramtable-0.3.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RAMTable contributors
|
|
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.
|