typeline 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.
CONTRIBUTING.md ADDED
@@ -0,0 +1,94 @@
1
+ # Development and Testing
2
+
3
+ ## Primary Development Commands
4
+
5
+ To check and resolve linting issues in the codebase, run:
6
+
7
+ ```console
8
+ poetry run ruff check --fix
9
+ ```
10
+
11
+ To check and resolve formatting issues in the codebase, run:
12
+
13
+ ```console
14
+ poetry run ruff format
15
+ ```
16
+
17
+ To check the unit tests in the codebase, run:
18
+
19
+ ```console
20
+ poetry run pytest
21
+ ```
22
+
23
+ To check the typing in the codebase, run:
24
+
25
+ ```console
26
+ poetry run mypy
27
+ ```
28
+
29
+ To generate a code coverage report after testing locally, run:
30
+
31
+ ```console
32
+ poetry run coverage html
33
+ ```
34
+
35
+ To check the lock file is up-to-date:
36
+
37
+ ```console
38
+ poetry check --lock
39
+ ```
40
+
41
+ ## Shortcut Task Commands
42
+
43
+ To be able to run shortcut task commands, first install the Poetry plugin [`poethepoet`](https://poethepoet.natn.io/index.html):
44
+
45
+ ```console
46
+ poetry self add 'poethepoet[poetry_plugin]'
47
+ ```
48
+
49
+ > [!NOTE]
50
+ > Upon the release of Poetry [v2.0.0](https://github.com/orgs/python-poetry/discussions/9793#discussioncomment-11043205), Poetry will automatically support bootstrap installation of [project-specific plugins](https://github.com/python-poetry/poetry/pull/9547) and installation of the task runner will become automatic for this project.
51
+ > The `pyproject.toml` syntax will be:
52
+ >
53
+ > ```toml
54
+ > [tool.poetry]
55
+ > requires-poetry = ">=2.0"
56
+ >
57
+ > [tool.poetry.requires-plugins]
58
+ > poethepoet = ">=0.29"
59
+ > ```
60
+
61
+ ### For Running Individual Checks
62
+
63
+ ```console
64
+ poetry task check-lock
65
+ poetry task check-format
66
+ poetry task check-lint
67
+ poetry task check-tests
68
+ poetry task check-typing
69
+ ```
70
+
71
+ ### For Running All Checks
72
+
73
+ ```console
74
+ poetry task check-all
75
+ ```
76
+
77
+ ### For Running Individual Fixes
78
+
79
+ ```console
80
+ poetry task fix-format
81
+ poetry task fix-lint
82
+ ```
83
+
84
+ ### For Running All Fixes
85
+
86
+ ```console
87
+ poetry task fix-all
88
+ ```
89
+
90
+ ### For Running All Fixes and Checks
91
+
92
+ ```console
93
+ poetry task fix-and-check-all
94
+ ```
LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2024 Clint Valentine
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.
typeline/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ # ruff: noqa: F401
2
+ from ._data_types import RecordType
3
+ from ._reader import CsvStructReader
4
+ from ._reader import DelimitedStructReader
5
+ from ._reader import TsvStructReader
6
+ from ._writer import CsvStructWriter
7
+ from ._writer import DelimitedStructWriter
8
+ from ._writer import TsvStructWriter
9
+
10
+ __all__ = [
11
+ "CsvStructReader",
12
+ "DelimitedStructReader",
13
+ "TsvStructReader",
14
+ "CsvStructWriter",
15
+ "DelimitedStructWriter",
16
+ "TsvStructWriter",
17
+ "RecordType",
18
+ ]
@@ -0,0 +1,21 @@
1
+ from dataclasses import Field
2
+ from typing import Any
3
+ from typing import ClassVar
4
+ from typing import Protocol
5
+ from typing import TypeAlias
6
+ from typing import TypeVar
7
+ from typing import runtime_checkable
8
+
9
+
10
+ @runtime_checkable
11
+ class DataclassInstance(Protocol):
12
+ """A protocol for objects that are dataclass instances."""
13
+
14
+ __dataclass_fields__: ClassVar[dict[str, Field[Any]]]
15
+
16
+
17
+ JsonType: TypeAlias = dict[str, "JsonType"] | list["JsonType"] | str | int | float | bool | None
18
+ """A JSON-like data type."""
19
+
20
+ RecordType = TypeVar("RecordType", bound=DataclassInstance)
21
+ """A type variable for the type of record (of dataclass type) for reading and writing."""
typeline/_reader.py ADDED
@@ -0,0 +1,267 @@
1
+ import csv
2
+ import json
3
+ from abc import ABC
4
+ from abc import abstractmethod
5
+ from collections.abc import Iterable
6
+ from collections.abc import Iterator
7
+ from contextlib import AbstractContextManager
8
+ from csv import DictReader
9
+ from dataclasses import Field
10
+ from dataclasses import fields as fields_of
11
+ from dataclasses import is_dataclass
12
+ from io import TextIOWrapper
13
+ from pathlib import Path
14
+ from types import NoneType
15
+ from types import TracebackType
16
+ from types import UnionType
17
+ from typing import Any
18
+ from typing import Generic
19
+ from typing import final
20
+ from typing import get_args
21
+ from typing import get_origin
22
+
23
+ from msgspec import convert
24
+ from typing_extensions import Self
25
+ from typing_extensions import override
26
+
27
+ from ._data_types import JsonType
28
+ from ._data_types import RecordType
29
+
30
+
31
+ class DelimitedStructReader(
32
+ AbstractContextManager["DelimitedStructReader[RecordType]"],
33
+ Iterable[RecordType],
34
+ Generic[RecordType],
35
+ ABC,
36
+ ):
37
+ """A reader for reading delimited data into dataclasses."""
38
+
39
+ def __init__(
40
+ self,
41
+ handle: TextIOWrapper,
42
+ record_type: type[RecordType],
43
+ /,
44
+ has_header: bool = True,
45
+ ):
46
+ """Instantiate a new delimited struct reader.
47
+
48
+ Args:
49
+ handle: a file-like object to read records from.
50
+ record_type: the type of the object we will be writing.
51
+ has_header: whether we expect the first line to be a header or not.
52
+ """
53
+ if not is_dataclass(record_type):
54
+ raise ValueError("record_type is not a dataclass but must be!")
55
+
56
+ self._record_type: type[RecordType] = record_type
57
+ self._handle: TextIOWrapper = handle
58
+ self._fields: tuple[Field[Any], ...] = fields_of(record_type)
59
+ self._header: list[str] = [field.name for field in self._fields]
60
+ self._types: list[type | str | Any] = [field.type for field in self._fields]
61
+ self._reader: DictReader[str] = DictReader(
62
+ self._filter_out_comments(handle),
63
+ fieldnames=self._header if not has_header else None,
64
+ delimiter=self.delimiter,
65
+ quotechar="'",
66
+ quoting=csv.QUOTE_MINIMAL,
67
+ )
68
+
69
+ if self._reader.fieldnames is not None and set(self._reader.fieldnames) != set(
70
+ self._header
71
+ ):
72
+ raise ValueError("Fields of header do not match fields of dataclass!")
73
+
74
+ @property
75
+ @abstractmethod
76
+ def delimiter(self) -> str:
77
+ """Delimiter character to use in the output."""
78
+
79
+ @override
80
+ def __enter__(self) -> Self:
81
+ """Enter this context."""
82
+ _ = super().__enter__()
83
+ return self
84
+
85
+ @override
86
+ def __exit__(
87
+ self,
88
+ __exc_type: type[BaseException] | None,
89
+ __exc_value: BaseException | None,
90
+ __traceback: TracebackType | None,
91
+ ) -> bool | None:
92
+ """Close and exit this context."""
93
+ self.close()
94
+ return None
95
+
96
+ def _filter_out_comments(self, lines: Iterator[str]) -> Iterator[str]:
97
+ """Yield only lines in an iterator that do not start with a comment character."""
98
+ for line in lines:
99
+ stripped: str = line.strip()
100
+ if stripped and not any(stripped.startswith(char) for char in self.comment_prefixes):
101
+ yield line
102
+
103
+ def _value_to_builtin(self, name: str, value: Any, field_type: type | str | Any) -> Any:
104
+ type_args: tuple[type, ...] = get_args(field_type)
105
+ type_origin: type | None = get_origin(field_type)
106
+ is_union: bool = isinstance(field_type, UnionType)
107
+
108
+ if value is None:
109
+ return f'"{name}":null'
110
+ elif value == "" and is_union and NoneType in type_args:
111
+ return f'"{name}":null'
112
+ elif field_type is bool or (is_union and bool in type_args):
113
+ return f'"{name}":{value.lower()}'
114
+ elif field_type is int or (is_union and int in type_args):
115
+ return f'"{name}":{value}'
116
+ elif field_type is float or (is_union and float in type_args):
117
+ return f'"{name}":{value}'
118
+ elif field_type is str or (is_union and str in type_args):
119
+ return f'"{name}":"{value}"'
120
+ elif type_origin in (dict, frozenset, list, set, tuple):
121
+ return f'"{name}":{value}'
122
+ elif is_union and len(type_args) >= 2 and NoneType in type_args:
123
+ other_types: set[type] = set(type_args) - {NoneType}
124
+ return self._value_to_builtin(name, value, other_types)
125
+ else:
126
+ return f'"{name}":{value}'
127
+
128
+ def _csv_dict_to_json(self, record: dict[str, str]) -> JsonType:
129
+ """Build a list of builtin-like objects from a string-only dictionary."""
130
+ key_values: list[str] = []
131
+
132
+ for (name, value), field_type in zip(record.items(), self._types, strict=True):
133
+ decoded: Any = self._decode(field_type, value)
134
+
135
+ key_value = self._value_to_builtin(name, decoded, field_type)
136
+
137
+ key_value = key_value.replace("\t", "\\t")
138
+ key_value = key_value.replace("\r", "\\r")
139
+ key_value = key_value.replace("\n", "\\n")
140
+
141
+ key_values.append(key_value)
142
+
143
+ json_string: str = f"{{{','.join(key_values)}}}"
144
+
145
+ try:
146
+ as_builtins: JsonType = json.loads(json_string)
147
+ except json.decoder.JSONDecodeError as exception:
148
+ raise json.decoder.JSONDecodeError(
149
+ msg=(
150
+ "Could not load delimited data line into JSON-like format."
151
+ + f" Built improperly formatted JSON: {json_string}."
152
+ + f" Originally formatted message: {exception.msg}."
153
+ ),
154
+ doc=exception.doc,
155
+ pos=exception.pos,
156
+ ) from exception
157
+
158
+ return as_builtins
159
+
160
+ @override
161
+ def __iter__(self) -> Iterator[RecordType]:
162
+ """Yield converted records from the delimited data file."""
163
+ for record in self._reader:
164
+ as_builtins = self._csv_dict_to_json(record)
165
+ try:
166
+ yield convert(as_builtins, self._record_type, strict=False)
167
+ except ValueError as exception:
168
+ raise ValueError(
169
+ f"Could not parse {record} as {self._record_type.__name__}!"
170
+ + f" Intermediate structure formed is: {as_builtins}."
171
+ + f" Original error: {exception}"
172
+ ) from exception
173
+
174
+ @staticmethod
175
+ def _decode(record_type: type[Any] | str | Any, item: Any) -> Any: # noqa: ARG004 # pyright: ignore[reportUnusedParameter]
176
+ """A callback for overriding the decoding of builtin types and custom types."""
177
+ return item
178
+
179
+ @property
180
+ def comment_prefixes(self) -> set[str]:
181
+ """Any string that when one prefixes a line, marks it as a comment."""
182
+ return {"#"}
183
+
184
+ def close(self) -> None:
185
+ """Close all opened resources."""
186
+ self._handle.close()
187
+ return None
188
+
189
+ @classmethod
190
+ def from_path(
191
+ cls,
192
+ path: Path | str,
193
+ record_type: type[RecordType],
194
+ /,
195
+ has_header: bool = True,
196
+ ) -> Self:
197
+ """Construct a delimited struct reader from a file path."""
198
+ reader = cls(Path(path).open("r"), record_type, has_header=has_header)
199
+ return reader
200
+
201
+
202
+ class CsvStructReader(DelimitedStructReader[RecordType]):
203
+ r"""A reader for reading comma-delimited data into dataclasses.
204
+
205
+ Example:
206
+ ```pycon
207
+ >>> from pathlib import Path
208
+ >>> from dataclasses import dataclass
209
+ >>> from tempfile import NamedTemporaryFile
210
+ >>>
211
+ >>> @dataclass
212
+ ... class MyData:
213
+ ... field1: str
214
+ ... field2: float | None
215
+ >>>
216
+ >>> from typeline import CsvStructReader
217
+ >>>
218
+ >>> with NamedTemporaryFile(mode="w+t") as tmpfile:
219
+ ... _ = tmpfile.write("field1,field2\nmy-name,0.2\n")
220
+ ... _ = tmpfile.flush()
221
+ ... with CsvStructReader.from_path(tmpfile.name, MyData) as reader:
222
+ ... for record in reader:
223
+ ... print(record)
224
+ MyData(field1='my-name', field2=0.2)
225
+
226
+ ```
227
+ """
228
+
229
+ @property
230
+ @override
231
+ @final
232
+ def delimiter(self) -> str:
233
+ return ","
234
+
235
+
236
+ class TsvStructReader(DelimitedStructReader[RecordType]):
237
+ r"""A reader for reading tab-delimited data into dataclasses.
238
+
239
+ Example:
240
+ ```pycon
241
+ >>> from pathlib import Path
242
+ >>> from dataclasses import dataclass
243
+ >>> from tempfile import NamedTemporaryFile
244
+ >>>
245
+ >>> @dataclass
246
+ ... class MyData:
247
+ ... field1: str
248
+ ... field2: float | None
249
+ >>>
250
+ >>> from typeline import TsvStructReader
251
+ >>>
252
+ >>> with NamedTemporaryFile(mode="w+t") as tmpfile:
253
+ ... _ = tmpfile.write("field1\tfield2\nmy-name\t0.2\n")
254
+ ... _ = tmpfile.flush()
255
+ ... with TsvStructReader.from_path(tmpfile.name, MyData) as reader:
256
+ ... for record in reader:
257
+ ... print(record)
258
+ MyData(field1='my-name', field2=0.2)
259
+
260
+ ```
261
+ """
262
+
263
+ @property
264
+ @override
265
+ @final
266
+ def delimiter(self) -> str:
267
+ return "\t"
typeline/_writer.py ADDED
@@ -0,0 +1,182 @@
1
+ import csv
2
+ import json
3
+ from abc import ABC
4
+ from abc import abstractmethod
5
+ from contextlib import AbstractContextManager
6
+ from csv import DictWriter
7
+ from dataclasses import Field
8
+ from dataclasses import fields as fields_of
9
+ from dataclasses import is_dataclass
10
+ from io import TextIOWrapper
11
+ from pathlib import Path
12
+ from types import TracebackType
13
+ from typing import Any
14
+ from typing import Generic
15
+ from typing import cast
16
+ from typing import final
17
+
18
+ from msgspec import to_builtins
19
+ from typing_extensions import Self
20
+ from typing_extensions import override
21
+
22
+ from ._data_types import RecordType
23
+
24
+
25
+ class DelimitedStructWriter(
26
+ AbstractContextManager["DelimitedStructWriter[RecordType]"],
27
+ Generic[RecordType],
28
+ ABC,
29
+ ):
30
+ """A writer for writing dataclasses into delimited data."""
31
+
32
+ def __init__(self, handle: TextIOWrapper, record_type: type[RecordType]) -> None:
33
+ """Instantiate a new delimited struct writer.
34
+
35
+ Args:
36
+ handle: a file-like object to write records to.
37
+ record_type: the type of the object we will be writing.
38
+ """
39
+ if not is_dataclass(record_type):
40
+ raise ValueError("record_type is not a dataclass but must be!")
41
+
42
+ self._record_type: type[RecordType] = record_type
43
+ self._handle: TextIOWrapper = handle
44
+ self._fields: tuple[Field[Any], ...] = fields_of(record_type)
45
+ self._header: list[str] = [field.name for field in fields_of(record_type)]
46
+ self._writer: DictWriter[str] = DictWriter(
47
+ handle,
48
+ fieldnames=self._header,
49
+ delimiter=self.delimiter,
50
+ quotechar="'",
51
+ quoting=csv.QUOTE_MINIMAL,
52
+ )
53
+
54
+ @property
55
+ @abstractmethod
56
+ def delimiter(self) -> str:
57
+ """Delimiter character to use in the output."""
58
+
59
+ @override
60
+ def __enter__(self) -> Self:
61
+ """Enter this context."""
62
+ _ = super().__enter__()
63
+ return self
64
+
65
+ @override
66
+ def __exit__(
67
+ self,
68
+ __exc_type: type[BaseException] | None,
69
+ __exc_value: BaseException | None,
70
+ __traceback: TracebackType | None,
71
+ ) -> bool | None:
72
+ """Close and exit this context."""
73
+ self.close()
74
+ return None
75
+
76
+ @staticmethod
77
+ def _encode(item: Any) -> Any:
78
+ """A callback for overriding the encoding of builtin types and custom types."""
79
+ if isinstance(item, tuple):
80
+ return list(item) # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType]
81
+ return item
82
+
83
+ def write(self, record: RecordType) -> None:
84
+ """Write the record to the open file-like object."""
85
+ if not isinstance(record, self._record_type):
86
+ raise ValueError(
87
+ f"Expected {self._record_type.__name__} but found"
88
+ + f" {record.__class__.__qualname__}!"
89
+ )
90
+
91
+ encoded = {name: self._encode(getattr(record, name)) for name in self._header}
92
+ builtin = {
93
+ name: (json.dumps(value) if not isinstance(value, str) else value)
94
+ for name, value in cast(dict[str, Any], to_builtins(encoded, str_keys=True)).items()
95
+ }
96
+ self._writer.writerow(builtin)
97
+
98
+ return None
99
+
100
+ def write_header(self) -> None:
101
+ """Write the header line to the open file-like object."""
102
+ self._writer.writeheader()
103
+ return None
104
+
105
+ def close(self) -> None:
106
+ """Close all opened resources."""
107
+ self._handle.close()
108
+ return None
109
+
110
+ @classmethod
111
+ def from_path(
112
+ cls, path: Path | str, record_type: type[RecordType]
113
+ ) -> "DelimitedStructWriter[RecordType]":
114
+ """Construct a delimited struct writer from a file path."""
115
+ writer = cls(Path(path).open("w"), record_type)
116
+ return writer
117
+
118
+
119
+ class CsvStructWriter(DelimitedStructWriter[RecordType]):
120
+ r"""A writer for writing dataclasses into comma-delimited data.
121
+
122
+ Example:
123
+ ```pycon
124
+ >>> from pathlib import Path
125
+ >>> from dataclasses import dataclass
126
+ >>> from tempfile import NamedTemporaryFile
127
+ >>>
128
+ >>> @dataclass
129
+ ... class MyData:
130
+ ... field1: str
131
+ ... field2: float | None
132
+ >>>
133
+ >>> from typeline import CsvStructWriter
134
+ >>>
135
+ >>> with NamedTemporaryFile(mode="w+t") as tmpfile:
136
+ ... with CsvStructWriter.from_path(tmpfile.name, MyData) as writer:
137
+ ... writer.write_header()
138
+ ... writer.write(MyData(field1="my-name", field2=0.2))
139
+ ... Path(tmpfile.name).read_text()
140
+ 'field1,field2\nmy-name,0.2\n'
141
+
142
+ ```
143
+ """
144
+
145
+ @property
146
+ @override
147
+ @final
148
+ def delimiter(self) -> str:
149
+ return ","
150
+
151
+
152
+ class TsvStructWriter(DelimitedStructWriter[RecordType]):
153
+ r"""A writer for writing dataclasses into tab-delimited data.
154
+
155
+ Example:
156
+ ```pycon
157
+ >>> from pathlib import Path
158
+ >>> from dataclasses import dataclass
159
+ >>> from tempfile import NamedTemporaryFile
160
+ >>>
161
+ >>> @dataclass
162
+ ... class MyData:
163
+ ... field1: str
164
+ ... field2: float | None
165
+ >>>
166
+ >>> from typeline import TsvStructWriter
167
+ >>>
168
+ >>> with NamedTemporaryFile(mode="w+t") as tmpfile:
169
+ ... with TsvStructWriter.from_path(tmpfile.name, MyData) as writer:
170
+ ... writer.write_header()
171
+ ... writer.write(MyData(field1="my-name", field2=0.2))
172
+ ... Path(tmpfile.name).read_text()
173
+ 'field1\tfield2\nmy-name\t0.2\n'
174
+
175
+ ```
176
+ """
177
+
178
+ @property
179
+ @override
180
+ @final
181
+ def delimiter(self) -> str:
182
+ return "\t"
typeline/py.typed ADDED
File without changes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2024 Clint Valentine
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,100 @@
1
+ Metadata-Version: 2.1
2
+ Name: typeline
3
+ Version: 0.1.0
4
+ Summary: Write dataclasses to delimited text formats and read them back again.
5
+ Home-page: https://github.com/clintval/typeline
6
+ License: MIT
7
+ Keywords: dataclass,msgspec,IO,delimited,CSV,TSV
8
+ Author: Clint Valentine
9
+ Author-email: valentine.clint@gmail.com
10
+ Requires-Python: >=3.10.0,<4.0.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: File Formats
24
+ Classifier: Topic :: Software Development :: Documentation
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Dist: msgspec (>=0.18,<0.19)
28
+ Requires-Dist: typing-extensions (>=4.12,<5.0)
29
+ Project-URL: Repository, https://github.com/clintval/typeline
30
+ Description-Content-Type: text/markdown
31
+
32
+ # typeline
33
+
34
+ [![PyPi Release](https://badge.fury.io/py/typeline.svg)](https://badge.fury.io/py/typeline)
35
+ [![CI](https://github.com/clintval/typeline/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/clintval/typeline/actions/workflows/tests.yml?query=branch%3Amain)
36
+ [![Python Versions](https://img.shields.io/badge/python-3.10_|_3.11_|_3.12-blue)](https://github.com/clintval/typeline)
37
+ [![basedpyright](https://img.shields.io/badge/basedpyright-checked-42b983)](https://docs.basedpyright.com/latest/)
38
+ [![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
39
+ [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
40
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff/)
41
+
42
+ Write dataclasses to delimited text formats and read them back again.
43
+
44
+ Features type-safe parsing, optional field support, and an intuitive API for working with structured data.
45
+
46
+ ## Installation
47
+
48
+ The package can be installed with `pip`:
49
+
50
+ ```console
51
+ pip install typeline
52
+ ```
53
+
54
+ ## Quickstart
55
+
56
+ ### Building a Test Dataclass
57
+
58
+ ```pycon
59
+ >>> from dataclasses import dataclass
60
+ >>>
61
+ >>> @dataclass
62
+ ... class MyData:
63
+ ... field1: int
64
+ ... field2: str
65
+ ... field3: float | None
66
+
67
+ ```
68
+
69
+ ### Writing
70
+
71
+ ```pycon
72
+ >>> from tempfile import NamedTemporaryFile
73
+ >>> from typeline import TsvStructWriter
74
+ >>>
75
+ >>> temp_file = NamedTemporaryFile(mode="w+t", suffix=".txt")
76
+ >>>
77
+ >>> with TsvStructWriter.from_path(temp_file.name, MyData) as writer:
78
+ ... writer.write_header()
79
+ ... writer.write(MyData(10, "test1", 0.2))
80
+ ... writer.write(MyData(20, "test2", None))
81
+
82
+ ```
83
+
84
+ ### Reading
85
+
86
+ ```pycon
87
+ >>> from typeline import TsvStructReader
88
+ >>>
89
+ >>> with TsvStructReader.from_path(temp_file.name, MyData) as reader:
90
+ ... for record in reader:
91
+ ... print(record)
92
+ MyData(field1=10, field2='test1', field3=0.2)
93
+ MyData(field1=20, field2='test2', field3=None)
94
+
95
+ ```
96
+
97
+ ## Development and Testing
98
+
99
+ See the [contributing guide](./CONTRIBUTING.md) for more information.
100
+
@@ -0,0 +1,11 @@
1
+ CONTRIBUTING.md,sha256=selnIsM_6KTwdFyc0B3u4n2bM_sBAeSbQV-Fd7mzHuE,1803
2
+ LICENSE,sha256=nN9Rgc41QgFz1uJfX7AsXRQJxlcJotVEPTNPwUAqfys,1070
3
+ typeline/__init__.py,sha256=oCm5b8hOuUvXu00MTxJuwGlHMnDF4-PwdBikrmbZhTE,472
4
+ typeline/_data_types.py,sha256=oTJnX5prKrOY1IxSudTdckgZNTIQdbmdc6Hk2lzR75U,659
5
+ typeline/_reader.py,sha256=Mt3SCdF_RYaO0K6SaaAzIrknJr4h0DNNeMpKvBPvTTY,9236
6
+ typeline/_writer.py,sha256=scHpJZQxoJsKq4DOtGB7SxV8TNPEIXNOWEL0frn-jNA,5666
7
+ typeline/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ typeline-0.1.0.dist-info/LICENSE,sha256=nN9Rgc41QgFz1uJfX7AsXRQJxlcJotVEPTNPwUAqfys,1070
9
+ typeline-0.1.0.dist-info/METADATA,sha256=DikIcUX9An9lfDp1ItuhQgIT_wpRV9PLuTl6cIs342o,3408
10
+ typeline-0.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
11
+ typeline-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any