flat-file-renderers 0.1.0__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.
- flat_file_renderers-0.1.0/AUTHORS.md +3 -0
- flat_file_renderers-0.1.0/LICENSE.md +23 -0
- flat_file_renderers-0.1.0/PKG-INFO +146 -0
- flat_file_renderers-0.1.0/README.md +78 -0
- flat_file_renderers-0.1.0/flat_file_renderers/__init__.py +32 -0
- flat_file_renderers-0.1.0/flat_file_renderers/base.py +114 -0
- flat_file_renderers-0.1.0/flat_file_renderers/flat_dict_list_helper.py +205 -0
- flat_file_renderers-0.1.0/flat_file_renderers/html.py +88 -0
- flat_file_renderers-0.1.0/flat_file_renderers/json.py +75 -0
- flat_file_renderers-0.1.0/flat_file_renderers/parquet.py +68 -0
- flat_file_renderers-0.1.0/flat_file_renderers/separated_value.py +112 -0
- flat_file_renderers-0.1.0/flat_file_renderers/text.py +7 -0
- flat_file_renderers-0.1.0/flat_file_renderers/xlsx.py +64 -0
- flat_file_renderers-0.1.0/flat_file_renderers.egg-info/PKG-INFO +146 -0
- flat_file_renderers-0.1.0/flat_file_renderers.egg-info/SOURCES.txt +27 -0
- flat_file_renderers-0.1.0/flat_file_renderers.egg-info/dependency_links.txt +1 -0
- flat_file_renderers-0.1.0/flat_file_renderers.egg-info/requires.txt +21 -0
- flat_file_renderers-0.1.0/flat_file_renderers.egg-info/top_level.txt +1 -0
- flat_file_renderers-0.1.0/pyproject.toml +71 -0
- flat_file_renderers-0.1.0/setup.cfg +4 -0
- flat_file_renderers-0.1.0/tests/test_base.py +53 -0
- flat_file_renderers-0.1.0/tests/test_flat_dict_list_helper.py +149 -0
- flat_file_renderers-0.1.0/tests/test_html.py +51 -0
- flat_file_renderers-0.1.0/tests/test_init.py +19 -0
- flat_file_renderers-0.1.0/tests/test_json.py +158 -0
- flat_file_renderers-0.1.0/tests/test_parquet.py +65 -0
- flat_file_renderers-0.1.0/tests/test_separated_value.py +74 -0
- flat_file_renderers-0.1.0/tests/test_text.py +12 -0
- flat_file_renderers-0.1.0/tests/test_xlsx.py +58 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
## The MIT License (MIT)
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 python-by-kelsoncm
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flat-file-renderers
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Render tabular/nested datasets (list/dict/QuerySet-like) into xlsx, csv, tsv, json, html or parquet files, zipped, with zero required dependencies
|
|
5
|
+
Author-email: Kelson da Costa Medeiros <kelsoncm@gmail.com>
|
|
6
|
+
License: # License
|
|
7
|
+
|
|
8
|
+
## The MIT License (MIT)
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 python-by-kelsoncm
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Homepage, https://github.com/python-by-kelsoncm/python-flat-file-renderers
|
|
31
|
+
Project-URL: Bug Tracker, https://github.com/python-by-kelsoncm/python-flat-file-renderers/issues
|
|
32
|
+
Project-URL: Download, https://github.com/python-by-kelsoncm/python-flat-file-renderers/releases/
|
|
33
|
+
Project-URL: Docs, https://python-by-kelsoncm.github.io/python-flat-file-renderers/
|
|
34
|
+
Keywords: export,xlsx,csv,tsv,json,html,parquet,renderer,flatten
|
|
35
|
+
Classifier: Development Status :: 4 - Beta
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
45
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
46
|
+
Requires-Python: >=3.10
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
License-File: LICENSE.md
|
|
49
|
+
License-File: AUTHORS.md
|
|
50
|
+
Provides-Extra: xlsx
|
|
51
|
+
Requires-Dist: openpyxl>=3.1; extra == "xlsx"
|
|
52
|
+
Provides-Extra: parquet
|
|
53
|
+
Requires-Dist: pyarrow>=15; extra == "parquet"
|
|
54
|
+
Provides-Extra: all
|
|
55
|
+
Requires-Dist: openpyxl>=3.1; extra == "all"
|
|
56
|
+
Requires-Dist: pyarrow>=15; extra == "all"
|
|
57
|
+
Provides-Extra: dev
|
|
58
|
+
Requires-Dist: openpyxl>=3.1; extra == "dev"
|
|
59
|
+
Requires-Dist: pyarrow>=15; extra == "dev"
|
|
60
|
+
Requires-Dist: pre-commit>=4.6.0; extra == "dev"
|
|
61
|
+
Requires-Dist: black>=26.3.1; extra == "dev"
|
|
62
|
+
Requires-Dist: ruff>=0.15.11; extra == "dev"
|
|
63
|
+
Requires-Dist: doc8>=2.0.0; extra == "dev"
|
|
64
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
65
|
+
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
66
|
+
Requires-Dist: pytest-coverage-gate>=1.0.3; extra == "dev"
|
|
67
|
+
Dynamic: license-file
|
|
68
|
+
|
|
69
|
+
# flat-file-renderers
|
|
70
|
+
|
|
71
|
+
[](https://opensource.org/licenses/MIT)
|
|
72
|
+
[](https://pypi.org/project/flat-file-renderers/)
|
|
73
|
+
[](https://github.com/python-by-kelsoncm/python-flat-file-renderers/actions/workflows/qa.yml)
|
|
74
|
+
[](https://codecov.io/gh/python-by-kelsoncm/python-flat-file-renderers)
|
|
75
|
+
[](https://github.com/python-by-kelsoncm/python-flat-file-renderers/actions/workflows/publish.yml)
|
|
76
|
+
[](https://python-by-kelsoncm.github.io/python-flat-file-renderers/)
|
|
77
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
78
|
+
|
|
79
|
+
Render tabular/nested datasets - a `list` of dicts, a `dict` of `{"rows": [...], "cols": [...]}`,
|
|
80
|
+
a QuerySet-like iterable, or record/model objects - into **xlsx, csv, tsv, json, html or parquet**
|
|
81
|
+
files, zipped. Nested lists of dicts are flattened automatically into dot+index-notation columns
|
|
82
|
+
(`contacts.1.phone`, `contacts.2.phone`, ...), and a `dict[str, dataset]` renders one file per key
|
|
83
|
+
into the same zip (one sheet per key for xlsx, one file per key for the others).
|
|
84
|
+
|
|
85
|
+
No required dependencies for csv/tsv/json/html. `xlsx` and `parquet` are optional extras.
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install flat-file-renderers # csv, tsv, json, html - zero extra dependencies
|
|
91
|
+
pip install flat-file-renderers[xlsx] # + openpyxl, for XlsxRenderer
|
|
92
|
+
pip install flat-file-renderers[parquet] # + pyarrow, for ParquetRenderer
|
|
93
|
+
pip install flat-file-renderers[all] # everything
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quick start
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from flat_file_renderers import CsvRenderer
|
|
100
|
+
|
|
101
|
+
renderer = CsvRenderer({})
|
|
102
|
+
zip_path = renderer.render({"dataset": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]})
|
|
103
|
+
# zip_path -> Path to a .zip containing one .csv
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from flat_file_renderers.xlsx import XlsxRenderer # requires the "xlsx" extra
|
|
108
|
+
|
|
109
|
+
renderer = XlsxRenderer({})
|
|
110
|
+
zip_path = renderer.render({"dataset": {"Students": [{"name": "Alice"}], "Courses": [{"title": "Python"}]}})
|
|
111
|
+
# zip_path -> Path to a .zip containing one .xlsx with two sheets
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Every renderer shares the same interface (`BaseRenderer.render(context) -> Path`, where
|
|
115
|
+
`context["dataset"]` holds the data), so switching output formats is a one-line change:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from flat_file_renderers import CsvRenderer, TsvRenderer, JsonRenderer, HtmlRenderer
|
|
119
|
+
from flat_file_renderers.xlsx import XlsxRenderer
|
|
120
|
+
from flat_file_renderers.parquet import ParquetRenderer
|
|
121
|
+
|
|
122
|
+
for Renderer in (CsvRenderer, TsvRenderer, JsonRenderer, HtmlRenderer, XlsxRenderer, ParquetRenderer):
|
|
123
|
+
zip_path = Renderer({}).render({"dataset": rows})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Modules
|
|
127
|
+
|
|
128
|
+
* `flat_file_renderers.base` - `BaseRenderer`, `ZipFileEntry`
|
|
129
|
+
* `flat_file_renderers.flat_dict_list_helper` - `FlatDictListHelper`, the flattening engine shared
|
|
130
|
+
by every renderer
|
|
131
|
+
* `flat_file_renderers.separated_value` - `CsvRenderer`, `TsvRenderer`
|
|
132
|
+
* `flat_file_renderers.json` - `JsonRenderer`
|
|
133
|
+
* `flat_file_renderers.html` - `HtmlRenderer`
|
|
134
|
+
* `flat_file_renderers.xlsx` - `XlsxRenderer` (extra: `xlsx`)
|
|
135
|
+
* `flat_file_renderers.parquet` - `ParquetRenderer` (extra: `parquet`)
|
|
136
|
+
|
|
137
|
+
See the [documentation](https://python-by-kelsoncm.github.io/python-flat-file-renderers/) for
|
|
138
|
+
details and more examples.
|
|
139
|
+
|
|
140
|
+
## Security
|
|
141
|
+
|
|
142
|
+
Please report vulnerabilities according to [SECURITY.md](SECURITY.md).
|
|
143
|
+
|
|
144
|
+
## Author
|
|
145
|
+
|
|
146
|
+
Kelson da Costa Medeiros <kelsoncm@gmail.com>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# flat-file-renderers
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://pypi.org/project/flat-file-renderers/)
|
|
5
|
+
[](https://github.com/python-by-kelsoncm/python-flat-file-renderers/actions/workflows/qa.yml)
|
|
6
|
+
[](https://codecov.io/gh/python-by-kelsoncm/python-flat-file-renderers)
|
|
7
|
+
[](https://github.com/python-by-kelsoncm/python-flat-file-renderers/actions/workflows/publish.yml)
|
|
8
|
+
[](https://python-by-kelsoncm.github.io/python-flat-file-renderers/)
|
|
9
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
10
|
+
|
|
11
|
+
Render tabular/nested datasets - a `list` of dicts, a `dict` of `{"rows": [...], "cols": [...]}`,
|
|
12
|
+
a QuerySet-like iterable, or record/model objects - into **xlsx, csv, tsv, json, html or parquet**
|
|
13
|
+
files, zipped. Nested lists of dicts are flattened automatically into dot+index-notation columns
|
|
14
|
+
(`contacts.1.phone`, `contacts.2.phone`, ...), and a `dict[str, dataset]` renders one file per key
|
|
15
|
+
into the same zip (one sheet per key for xlsx, one file per key for the others).
|
|
16
|
+
|
|
17
|
+
No required dependencies for csv/tsv/json/html. `xlsx` and `parquet` are optional extras.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install flat-file-renderers # csv, tsv, json, html - zero extra dependencies
|
|
23
|
+
pip install flat-file-renderers[xlsx] # + openpyxl, for XlsxRenderer
|
|
24
|
+
pip install flat-file-renderers[parquet] # + pyarrow, for ParquetRenderer
|
|
25
|
+
pip install flat-file-renderers[all] # everything
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from flat_file_renderers import CsvRenderer
|
|
32
|
+
|
|
33
|
+
renderer = CsvRenderer({})
|
|
34
|
+
zip_path = renderer.render({"dataset": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]})
|
|
35
|
+
# zip_path -> Path to a .zip containing one .csv
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from flat_file_renderers.xlsx import XlsxRenderer # requires the "xlsx" extra
|
|
40
|
+
|
|
41
|
+
renderer = XlsxRenderer({})
|
|
42
|
+
zip_path = renderer.render({"dataset": {"Students": [{"name": "Alice"}], "Courses": [{"title": "Python"}]}})
|
|
43
|
+
# zip_path -> Path to a .zip containing one .xlsx with two sheets
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Every renderer shares the same interface (`BaseRenderer.render(context) -> Path`, where
|
|
47
|
+
`context["dataset"]` holds the data), so switching output formats is a one-line change:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from flat_file_renderers import CsvRenderer, TsvRenderer, JsonRenderer, HtmlRenderer
|
|
51
|
+
from flat_file_renderers.xlsx import XlsxRenderer
|
|
52
|
+
from flat_file_renderers.parquet import ParquetRenderer
|
|
53
|
+
|
|
54
|
+
for Renderer in (CsvRenderer, TsvRenderer, JsonRenderer, HtmlRenderer, XlsxRenderer, ParquetRenderer):
|
|
55
|
+
zip_path = Renderer({}).render({"dataset": rows})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Modules
|
|
59
|
+
|
|
60
|
+
* `flat_file_renderers.base` - `BaseRenderer`, `ZipFileEntry`
|
|
61
|
+
* `flat_file_renderers.flat_dict_list_helper` - `FlatDictListHelper`, the flattening engine shared
|
|
62
|
+
by every renderer
|
|
63
|
+
* `flat_file_renderers.separated_value` - `CsvRenderer`, `TsvRenderer`
|
|
64
|
+
* `flat_file_renderers.json` - `JsonRenderer`
|
|
65
|
+
* `flat_file_renderers.html` - `HtmlRenderer`
|
|
66
|
+
* `flat_file_renderers.xlsx` - `XlsxRenderer` (extra: `xlsx`)
|
|
67
|
+
* `flat_file_renderers.parquet` - `ParquetRenderer` (extra: `parquet`)
|
|
68
|
+
|
|
69
|
+
See the [documentation](https://python-by-kelsoncm.github.io/python-flat-file-renderers/) for
|
|
70
|
+
details and more examples.
|
|
71
|
+
|
|
72
|
+
## Security
|
|
73
|
+
|
|
74
|
+
Please report vulnerabilities according to [SECURITY.md](SECURITY.md).
|
|
75
|
+
|
|
76
|
+
## Author
|
|
77
|
+
|
|
78
|
+
Kelson da Costa Medeiros <kelsoncm@gmail.com>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""flat_file_renderers - render tabular/nested datasets into xlsx, csv, tsv, json, html or
|
|
2
|
+
parquet files, zipped.
|
|
3
|
+
|
|
4
|
+
Only the dependency-free renderers (csv, tsv, json, html) are exported here, so importing this
|
|
5
|
+
package never requires openpyxl or pyarrow. XlsxRenderer and ParquetRenderer are available from
|
|
6
|
+
their own submodules and require the matching optional extra:
|
|
7
|
+
|
|
8
|
+
pip install flat-file-renderers[xlsx] # from flat_file_renderers.xlsx import XlsxRenderer
|
|
9
|
+
pip install flat-file-renderers[parquet] # from flat_file_renderers.parquet import ParquetRenderer
|
|
10
|
+
pip install flat-file-renderers[all] # both
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from flat_file_renderers.base import BaseRenderer, ZipFileEntry
|
|
14
|
+
from flat_file_renderers.flat_dict_list_helper import FlatDictListHelper
|
|
15
|
+
from flat_file_renderers.html import HtmlRenderer
|
|
16
|
+
from flat_file_renderers.json import JsonRenderer
|
|
17
|
+
from flat_file_renderers.separated_value import CsvRenderer, FlatDelimitedRenderer, TsvRenderer
|
|
18
|
+
from flat_file_renderers.text import replace_crlf
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BaseRenderer",
|
|
24
|
+
"ZipFileEntry",
|
|
25
|
+
"FlatDictListHelper",
|
|
26
|
+
"replace_crlf",
|
|
27
|
+
"CsvRenderer",
|
|
28
|
+
"TsvRenderer",
|
|
29
|
+
"FlatDelimitedRenderer",
|
|
30
|
+
"JsonRenderer",
|
|
31
|
+
"HtmlRenderer",
|
|
32
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tempfile import NamedTemporaryFile
|
|
7
|
+
from zipfile import ZIP_DEFLATED, ZipFile
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ZipFileEntry:
|
|
13
|
+
def __init__(self, zipfilealias: str, osfilepath: Path):
|
|
14
|
+
self.zipfilealias = zipfilealias
|
|
15
|
+
self.osfilepath = osfilepath
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseRenderer:
|
|
19
|
+
"""
|
|
20
|
+
Base class for dataset renderers.
|
|
21
|
+
|
|
22
|
+
Defines the interface and basic structure for concrete renderers (CSV, Excel, Parquet, etc).
|
|
23
|
+
Subclasses must implement the abstract `render` method to provide the file-generation logic.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
context (dict): Context dict used to render the file. May hold input data, a validated
|
|
27
|
+
form, the requesting user, etc.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
class CsvRenderer(BaseRenderer):
|
|
31
|
+
def render(self, *args, **kwargs) -> BytesIO:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
renderer = CsvRenderer({'columns': ["Name", "Age"], 'rows': [{"Name": "Alice", "Age": "30"}]})
|
|
35
|
+
csv_file = renderer.render()
|
|
36
|
+
with open("data.csv", "wb") as f:
|
|
37
|
+
f.write(csv_file.getvalue())
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, context: dict[str, any]):
|
|
41
|
+
"""
|
|
42
|
+
Initializes the base renderer with the given context.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
context (dict): Context dict holding whatever the renderer needs to produce the file.
|
|
46
|
+
"""
|
|
47
|
+
self.context = context
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def default_filename(self) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Builds a default filename from the current date/time and the renderer's extension.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: Suggested filename.
|
|
56
|
+
"""
|
|
57
|
+
return f"dataset_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.{self.extension}"
|
|
58
|
+
|
|
59
|
+
def write_zipfile(self, entries: list["ZipFileEntry"]) -> Path:
|
|
60
|
+
"""
|
|
61
|
+
Compresses one or more files into a temporary ZIP file.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
entries (list[ZipFileEntry]): Entries to compress.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Path: Path to the generated ZIP file.
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
```python
|
|
71
|
+
zip_path = renderer.write_zipfile([ZipFileEntry("data.csv", Path("/path/to/data.csv"))])
|
|
72
|
+
|
|
73
|
+
with open(zip_path, "rb") as f:
|
|
74
|
+
zip_content = f.read()
|
|
75
|
+
|
|
76
|
+
from zipfile import ZipFile
|
|
77
|
+
with ZipFile(zip_path, 'r') as zipf:
|
|
78
|
+
print(zipf.namelist())
|
|
79
|
+
# ['data.csv']
|
|
80
|
+
```
|
|
81
|
+
"""
|
|
82
|
+
with NamedTemporaryFile(delete=False, suffix=".zip") as zipfilehandler:
|
|
83
|
+
logger.info(
|
|
84
|
+
"Creating temporary zip file to compress: %s",
|
|
85
|
+
[entry.zipfilealias for entry in entries],
|
|
86
|
+
)
|
|
87
|
+
with ZipFile(zipfilehandler, mode="w", compression=ZIP_DEFLATED) as archive:
|
|
88
|
+
for entry in entries:
|
|
89
|
+
logger.info("Adding file %s to zip as %s", entry.osfilepath, entry.zipfilealias)
|
|
90
|
+
archive.write(entry.osfilepath, arcname=entry.zipfilealias)
|
|
91
|
+
logger.info("Zip file created successfully: %s", zipfilehandler.name)
|
|
92
|
+
return Path(zipfilehandler.name)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def extension(self) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Returns the default file extension produced by this renderer.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: File extension (e.g. 'csv', 'xlsx').
|
|
101
|
+
"""
|
|
102
|
+
return getattr(self, "_extension", "dat")
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def render(self, *args, **kwargs) -> BytesIO:
|
|
106
|
+
"""
|
|
107
|
+
Abstract method that renders the data and produces the file.
|
|
108
|
+
|
|
109
|
+
Subclasses must implement this to provide the format-specific generation logic.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
BytesIO: In-memory generated file.
|
|
113
|
+
"""
|
|
114
|
+
...
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _as_plain_dict(row: Any) -> Any:
|
|
5
|
+
"""Returns `row` as a plain dict when it looks like a model/record instance.
|
|
6
|
+
|
|
7
|
+
Duck-typed on purpose: rather than importing Django (or any other ORM) to `isinstance`-check
|
|
8
|
+
for a `Model`, any non-dict object exposing `__dict__` (Django models, dataclasses without
|
|
9
|
+
`__slots__`, plain objects, etc.) is treated as a record and flattened via its `__dict__`.
|
|
10
|
+
Dicts and anything without `__dict__` (namedtuples, plain values) pass through unchanged.
|
|
11
|
+
"""
|
|
12
|
+
if not isinstance(row, dict) and hasattr(row, "__dict__"):
|
|
13
|
+
return row.__dict__
|
|
14
|
+
return row
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlatDictListHelper:
|
|
18
|
+
"""
|
|
19
|
+
Helper for renderers that deal with lists of plain dicts (or dict-like records), without
|
|
20
|
+
deeply nested structures.
|
|
21
|
+
|
|
22
|
+
Provides methods to expand lists of dicts into flat columns and to normalize keys, so that
|
|
23
|
+
renderers can handle tabular data consistently and without naming collisions. Particularly
|
|
24
|
+
useful for renderers like CSV/TSV, which need to turn complex data into simple tabular rows.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def extract_rows_and_columns(context: dict[str, Any]) -> tuple[list[Any], list[str]]:
|
|
29
|
+
"""
|
|
30
|
+
Extracts the rows and columns of a dataset present in the rendering context.
|
|
31
|
+
|
|
32
|
+
Accepts a dataset shaped as a dict (with 'rows'/'cols'), a list, or any other iterable
|
|
33
|
+
(e.g. a Django QuerySet or generator) - duck-typed via `__iter__`, no ORM import needed.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
context (dict): Context containing the 'dataset' key.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
tuple[list, list]: Tuple of (rows, column names - if available).
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If the context has no 'dataset' key, or its type isn't supported.
|
|
43
|
+
"""
|
|
44
|
+
if "dataset" not in context:
|
|
45
|
+
raise ValueError("context must contain a 'dataset' key")
|
|
46
|
+
|
|
47
|
+
dataset = context.get("dataset", None)
|
|
48
|
+
|
|
49
|
+
if isinstance(dataset, dict):
|
|
50
|
+
rows: list[Any] = dataset.get("rows", [])
|
|
51
|
+
elif isinstance(dataset, (list, tuple)):
|
|
52
|
+
rows = list(dataset)
|
|
53
|
+
elif dataset is not None and not isinstance(dataset, (str, bytes)) and hasattr(dataset, "__iter__"):
|
|
54
|
+
rows = list(dataset)
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError("dataset must be a QuerySet-like iterable, dict, or list")
|
|
57
|
+
|
|
58
|
+
cols: list[str] = dataset.get("cols", []) if isinstance(dataset, dict) else []
|
|
59
|
+
return rows, cols
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def deep_flatten(row: dict, parent_key: str = "") -> dict:
|
|
63
|
+
"""
|
|
64
|
+
Recursively expands nested dicts and lists of dicts into a flat dict, using dot notation
|
|
65
|
+
for nested keys.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
row (dict): Data row, possibly containing plain values, lists of dicts, or nested dicts.
|
|
69
|
+
parent_key (str): Key prefix (used during recursion).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
dict: Flat dict with all keys expanded.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
deep_flatten({"name": "Alice", "contacts": [{"phone": "1234"}, {"phone": "5678"}]})
|
|
76
|
+
-> {"name": "Alice", "contacts.1.phone": "1234", "contacts.2.phone": "5678"}
|
|
77
|
+
"""
|
|
78
|
+
items = {}
|
|
79
|
+
flat = FlatDictListHelper.expand_row(row)
|
|
80
|
+
for k, v in flat.items():
|
|
81
|
+
new_key = f"{parent_key}.{k}" if parent_key else str(k)
|
|
82
|
+
if isinstance(v, dict):
|
|
83
|
+
items.update(FlatDictListHelper.deep_flatten(v, new_key))
|
|
84
|
+
else:
|
|
85
|
+
items[new_key] = v
|
|
86
|
+
return items
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def extract_flat_columns(rows: list) -> list[str]:
|
|
90
|
+
"""
|
|
91
|
+
Given a list of dicts (or record objects), returns the list of flat, normalized columns,
|
|
92
|
+
including columns expanded from lists of dicts.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
rows (list): List of dicts or record/model objects representing the data rows.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
list[str]: List of column names, including columns expanded from lists of dicts,
|
|
99
|
+
with normalized keys.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
rows = [
|
|
103
|
+
{"name": "Alice", "contacts": [{"phone": "1234"}, {"phone": "5678"}]},
|
|
104
|
+
{"name": "Bob", "addresses": [{"street": "Street A"}, {"street": "Street B"}]}
|
|
105
|
+
]
|
|
106
|
+
# Output: ["name", "contacts.1.phone", "contacts.2.phone", "addresses.1.street", "addresses.2.street"]
|
|
107
|
+
"""
|
|
108
|
+
list_dict_max = {} # key: max_len
|
|
109
|
+
list_dict_keys = {} # key: set(subkeys)
|
|
110
|
+
for row in rows:
|
|
111
|
+
row = _as_plain_dict(row)
|
|
112
|
+
for key, value in row.items():
|
|
113
|
+
if isinstance(value, list) and value and all(isinstance(x, dict) for x in value):
|
|
114
|
+
list_dict_max[key] = max(list_dict_max.get(key, 0), len(value))
|
|
115
|
+
subkeys = set()
|
|
116
|
+
for item in value:
|
|
117
|
+
subkeys.update(item.keys())
|
|
118
|
+
if key not in list_dict_keys:
|
|
119
|
+
list_dict_keys[key] = set()
|
|
120
|
+
list_dict_keys[key].update(subkeys)
|
|
121
|
+
|
|
122
|
+
cols = set()
|
|
123
|
+
for row in rows:
|
|
124
|
+
row = _as_plain_dict(row)
|
|
125
|
+
flat = FlatDictListHelper.expand_row(row)
|
|
126
|
+
for key in flat.keys():
|
|
127
|
+
if key not in list_dict_max:
|
|
128
|
+
cols.add(FlatDictListHelper.normalize_key(key))
|
|
129
|
+
for key, max_len in list_dict_max.items():
|
|
130
|
+
for idx in range(1, max_len + 1):
|
|
131
|
+
for subkey in list_dict_keys[key]:
|
|
132
|
+
cols.add(FlatDictListHelper.normalize_key(f"{key}.{idx}.{subkey}"))
|
|
133
|
+
return sorted(cols)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def expand_row(row: dict) -> dict:
|
|
137
|
+
"""
|
|
138
|
+
Expands lists of dicts into flat columns.
|
|
139
|
+
|
|
140
|
+
Discovers all columns, including expanded lists of dicts, and normalizes keys to avoid
|
|
141
|
+
collisions. Lists of dicts are expanded into dot+index-notation columns, and keys are
|
|
142
|
+
normalized by replacing '__' with '.'.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
row (dict): Data row, holding plain values or lists of dicts.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict: Flat dict where lists of dicts have been expanded into dot+index-notation columns.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
expand_row({"name": "Alice", "contacts": [{"phone": "1234"}, {"phone": "5678"}]})
|
|
152
|
+
-> {"name": "Alice", "contacts.1.phone": "1234", "contacts.2.phone": "5678"}
|
|
153
|
+
"""
|
|
154
|
+
flat = {}
|
|
155
|
+
for key, value in row.items():
|
|
156
|
+
if isinstance(value, list) and value and all(isinstance(x, dict) for x in value):
|
|
157
|
+
for idx, item in enumerate(value, 1):
|
|
158
|
+
for subkey, subval in item.items():
|
|
159
|
+
flat[f"{key}.{idx}.{subkey}"] = subval
|
|
160
|
+
else:
|
|
161
|
+
flat[key] = value
|
|
162
|
+
return flat
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def normalize_key(key: str) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Replaces '__' with '.' in keys, to avoid clashing with the list-of-dicts dot notation.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
key (str): Key to normalize.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
str: Normalized key, with '__' replaced by '.'.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
normalize_key("address__street") -> "address.street"
|
|
177
|
+
normalize_key("person__contacts__1__phone") -> "person.contacts.1.phone"
|
|
178
|
+
"""
|
|
179
|
+
return key.replace("__", ".")
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def flatten_dataset(context: dict[str, Any]) -> tuple[list[dict], list[str]]:
|
|
183
|
+
"""
|
|
184
|
+
Returns a flat version of the context's dataset, with all rows and columns expanded and
|
|
185
|
+
normalized.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
context (dict): Context containing the 'dataset' key.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
tuple[list[dict], list[str]]: List of flat rows and list of column names.
|
|
192
|
+
"""
|
|
193
|
+
flattened_rows = []
|
|
194
|
+
all_keys = set()
|
|
195
|
+
rows, _ = FlatDictListHelper.extract_rows_and_columns(context)
|
|
196
|
+
for row in rows:
|
|
197
|
+
row = _as_plain_dict(row)
|
|
198
|
+
flat = FlatDictListHelper.deep_flatten(row)
|
|
199
|
+
normalized_flat = {FlatDictListHelper.normalize_key(k): v for k, v in flat.items()}
|
|
200
|
+
flattened_rows.append(normalized_flat)
|
|
201
|
+
all_keys.update(normalized_flat.keys())
|
|
202
|
+
|
|
203
|
+
cols = list(all_keys)
|
|
204
|
+
cols.sort()
|
|
205
|
+
return flattened_rows, cols
|