nm-tool-forge 0.2.3__tar.gz → 0.2.5__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.
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/PKG-INFO +53 -17
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/README.md +98 -62
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/pyproject.toml +1 -1
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/csvchunking/__init__.py +1 -1
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/csvchunking/chunker.py +42 -25
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/csvchunking/cli.py +9 -9
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/__init__.py +1 -1
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/nm_tool_forge.egg-info/PKG-INFO +53 -17
- nm_tool_forge-0.2.5/tests/test_csvchunking.py +153 -0
- nm_tool_forge-0.2.3/tests/test_csvchunking.py +0 -63
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/LICENSE +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/setup.cfg +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/csvchunking/__main__.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/__main__.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/analysis.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/cli.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/constants.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/converters.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/csv_export.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/encoding.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/filesystem.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/models.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/normalization.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/parsing.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/report_html.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/report_markdown.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/report_models.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/report_pdf.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/loganalysis/selftest.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/nm_tool_forge.egg-info/SOURCES.txt +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/nm_tool_forge.egg-info/dependency_links.txt +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/nm_tool_forge.egg-info/entry_points.txt +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/nm_tool_forge.egg-info/requires.txt +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/src/nm_tool_forge.egg-info/top_level.txt +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/tests/test_analysis.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/tests/test_normalization.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/tests/test_parsing.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/tests/test_report_html.py +0 -0
- {nm_tool_forge-0.2.3 → nm_tool_forge-0.2.5}/tests/test_report_markdown.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nm-tool-forge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Analyze MigMan log files and generate aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
5
5
|
Author-email: Stefan Ewald <s.ew@outlook.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,7 +31,7 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
# nm-tool-forge
|
|
33
33
|
|
|
34
|
-
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
34
|
+
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports. The package also includes `csvchunking`, a small helper for splitting large CSV files into migration-friendly chunks.
|
|
35
35
|
|
|
36
36
|
The project uses a package-ready `src` layout. The legacy `log_analysis.py` file remains available as a thin compatibility entry point for older local setups.
|
|
37
37
|
|
|
@@ -43,6 +43,7 @@ The project uses a package-ready `src` layout. The legacy `log_analysis.py` file
|
|
|
43
43
|
- Generate Markdown summary reports
|
|
44
44
|
- Optionally convert reports to HTML and PDF
|
|
45
45
|
- Keep a backup copy of analyzed log files
|
|
46
|
+
- Split large CSV files into numbered chunks while preserving the header row
|
|
46
47
|
- Run built-in self-tests from the CLI
|
|
47
48
|
|
|
48
49
|
## Installation
|
|
@@ -61,12 +62,14 @@ python -m pip install .[pdf,dev]
|
|
|
61
62
|
|
|
62
63
|
## Command-line usage
|
|
63
64
|
|
|
64
|
-
After installation,
|
|
65
|
+
After installation, the CLI entry points are available:
|
|
65
66
|
|
|
66
67
|
```powershell
|
|
67
68
|
python -m loganalysis --help
|
|
69
|
+
python -m csvchunking --help
|
|
68
70
|
loganalysis --help
|
|
69
71
|
nm-tool-forge --help
|
|
72
|
+
csvchunking --help
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
Typical analysis run:
|
|
@@ -89,29 +92,30 @@ python -m loganalysis --self-test
|
|
|
89
92
|
|
|
90
93
|
Legacy compatibility call:
|
|
91
94
|
|
|
95
|
+
```powershell
|
|
96
|
+
python .\log_analysis.py --convert
|
|
97
|
+
```
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
To publish a new release, always test on TestPyPI first, then upload to PyPI only after successful Conda/Smoke-Tests:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
export TWINE_USERNAME="__token__"
|
|
99
|
-
export TWINE_PASSWORD="pypi-..."
|
|
99
|
+
CSV chunking run:
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
```powershell
|
|
102
|
+
csvchunking "data\large_export.csv" --chunk-size 5000
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
The command creates an output directory next to the input file named after the CSV stem. For example, `data\large_export.csv` is split into files such as `data\large_export\large_export_01.csv`, `data\large_export\large_export_02.csv`, and so on.
|
|
106
|
+
|
|
107
|
+
CSV chunking with an explicit encoding:
|
|
108
108
|
|
|
109
109
|
```powershell
|
|
110
|
-
python
|
|
110
|
+
python -m csvchunking "data\large_export.csv" --chunk-size 5000 --encoding utf-8-sig
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
Each chunk contains the original header row plus up to `--chunk-size` data rows. The delimiter is detected automatically; if detection fails, semicolon-separated CSV is used.
|
|
114
|
+
|
|
113
115
|
## Supported CLI options
|
|
114
116
|
|
|
117
|
+
Log analysis options:
|
|
118
|
+
|
|
115
119
|
- `--logs-dir`
|
|
116
120
|
- `--out-dir`
|
|
117
121
|
- `--backup-dir`
|
|
@@ -119,6 +123,28 @@ python .\log_analysis.py --convert
|
|
|
119
123
|
- `--convert`
|
|
120
124
|
- `--self-test`
|
|
121
125
|
|
|
126
|
+
CSV chunking options:
|
|
127
|
+
|
|
128
|
+
- `input_file` - path to the CSV file to split
|
|
129
|
+
- `--chunk-size` - required number of data rows per output file; must be greater than zero
|
|
130
|
+
- `--encoding` - input and output encoding; defaults to `utf-8-sig`
|
|
131
|
+
|
|
132
|
+
## Release process
|
|
133
|
+
|
|
134
|
+
To publish a new release, always test on TestPyPI first, then upload to PyPI only after successful Conda smoke tests:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
export TWINE_USERNAME="__token__"
|
|
138
|
+
export TWINE_PASSWORD="pypi-..."
|
|
139
|
+
|
|
140
|
+
bash scripts/release_testpypi.sh --bump patch
|
|
141
|
+
bash scripts/release_pypi.sh --yes
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Notes:**
|
|
145
|
+
- Run and verify the TestPyPI release first, then upload the final package to PyPI.
|
|
146
|
+
- PyPI versions cannot be overwritten or reused.
|
|
147
|
+
|
|
122
148
|
## Library usage
|
|
123
149
|
|
|
124
150
|
```python
|
|
@@ -130,6 +156,7 @@ from loganalysis import (
|
|
|
130
156
|
iter_logical_entries,
|
|
131
157
|
normalize_message,
|
|
132
158
|
)
|
|
159
|
+
from csvchunking import split_csv
|
|
133
160
|
|
|
134
161
|
result = analyze_file(Path("logs/app.txt"))
|
|
135
162
|
print(result["norm_counts"])
|
|
@@ -146,14 +173,21 @@ convert_report_md_to_html_pdf(
|
|
|
146
173
|
Path("log_analyse_out/report.html"),
|
|
147
174
|
Path("log_analyse_out/report.pdf"),
|
|
148
175
|
)
|
|
176
|
+
|
|
177
|
+
chunk_result = split_csv(Path("data/large_export.csv"), chunk_size=5000)
|
|
178
|
+
print(chunk_result.output_dir)
|
|
179
|
+
print(chunk_result.output_files)
|
|
149
180
|
```
|
|
150
181
|
|
|
182
|
+
`split_csv()` returns a `ChunkResult` with the input file, output directory, chunk size, processed data-row count, created file count, and generated output file paths.
|
|
183
|
+
|
|
151
184
|
## Project structure
|
|
152
185
|
|
|
153
186
|
```text
|
|
154
187
|
.
|
|
155
188
|
├─ pyproject.toml
|
|
156
189
|
├─ src/loganalysis/
|
|
190
|
+
├─ src/csvchunking/
|
|
157
191
|
├─ tests/
|
|
158
192
|
├─ docs/
|
|
159
193
|
└─ log_analysis.py
|
|
@@ -168,7 +202,9 @@ Important modules:
|
|
|
168
202
|
- `report_html.py` - HTML/CSS rendering
|
|
169
203
|
- `report_pdf.py` - PDF engine selection and fallback handling
|
|
170
204
|
- `converters.py` - Markdown-to-HTML/PDF conversion
|
|
171
|
-
- `cli.py` - command-line entry point
|
|
205
|
+
- `loganalysis/cli.py` - log analysis command-line entry point
|
|
206
|
+
- `csvchunking/chunker.py` - CSV splitting logic and `ChunkResult`
|
|
207
|
+
- `csvchunking/cli.py` - CSV chunking command-line entry point
|
|
172
208
|
|
|
173
209
|
## HTML/PDF conversion
|
|
174
210
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nm-tool-forge
|
|
2
2
|
|
|
3
|
-
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
3
|
+
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports. The package also includes `csvchunking`, a small helper for splitting large CSV files into migration-friendly chunks.
|
|
4
4
|
|
|
5
5
|
The project uses a package-ready `src` layout. The legacy `log_analysis.py` file remains available as a thin compatibility entry point for older local setups.
|
|
6
6
|
|
|
@@ -9,10 +9,11 @@ The project uses a package-ready `src` layout. The legacy `log_analysis.py` file
|
|
|
9
9
|
- Parse logical log entries from multi-line text logs
|
|
10
10
|
- Normalize recurring error patterns for better aggregation
|
|
11
11
|
- Generate aggregated CSV reports
|
|
12
|
-
- Generate Markdown summary reports
|
|
13
|
-
- Optionally convert reports to HTML and PDF
|
|
14
|
-
- Keep a backup copy of analyzed log files
|
|
15
|
-
-
|
|
12
|
+
- Generate Markdown summary reports
|
|
13
|
+
- Optionally convert reports to HTML and PDF
|
|
14
|
+
- Keep a backup copy of analyzed log files
|
|
15
|
+
- Split large CSV files into numbered chunks while preserving the header row
|
|
16
|
+
- Run built-in self-tests from the CLI
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -30,13 +31,15 @@ python -m pip install .[pdf,dev]
|
|
|
30
31
|
|
|
31
32
|
## Command-line usage
|
|
32
33
|
|
|
33
|
-
After installation,
|
|
34
|
-
|
|
35
|
-
```powershell
|
|
36
|
-
python -m loganalysis --help
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
After installation, the CLI entry points are available:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
python -m loganalysis --help
|
|
38
|
+
python -m csvchunking --help
|
|
39
|
+
loganalysis --help
|
|
40
|
+
nm-tool-forge --help
|
|
41
|
+
csvchunking --help
|
|
42
|
+
```
|
|
40
43
|
|
|
41
44
|
Typical analysis run:
|
|
42
45
|
|
|
@@ -50,18 +53,54 @@ Analysis with HTML/PDF conversion:
|
|
|
50
53
|
nm-tool-forge --logs-dir logs --out-dir log_analyse_out --convert
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
Self-test mode:
|
|
54
|
-
|
|
55
|
-
```powershell
|
|
56
|
-
python -m loganalysis --self-test
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Legacy compatibility call:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
Self-test mode:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
python -m loganalysis --self-test
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Legacy compatibility call:
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
python .\log_analysis.py --convert
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
CSV chunking run:
|
|
69
|
+
|
|
70
|
+
```powershell
|
|
71
|
+
csvchunking "data\large_export.csv" --chunk-size 5000
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The command creates an output directory next to the input file named after the CSV stem. For example, `data\large_export.csv` is split into files such as `data\large_export\large_export_01.csv`, `data\large_export\large_export_02.csv`, and so on.
|
|
75
|
+
|
|
76
|
+
CSV chunking with an explicit encoding:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
python -m csvchunking "data\large_export.csv" --chunk-size 5000 --encoding utf-8-sig
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Each chunk contains the original header row plus up to `--chunk-size` data rows. The delimiter is detected automatically; if detection fails, semicolon-separated CSV is used.
|
|
83
|
+
|
|
84
|
+
## Supported CLI options
|
|
85
|
+
|
|
86
|
+
Log analysis options:
|
|
87
|
+
|
|
88
|
+
- `--logs-dir`
|
|
89
|
+
- `--out-dir`
|
|
90
|
+
- `--backup-dir`
|
|
91
|
+
- `--top-examples`
|
|
92
|
+
- `--convert`
|
|
93
|
+
- `--self-test`
|
|
94
|
+
|
|
95
|
+
CSV chunking options:
|
|
96
|
+
|
|
97
|
+
- `input_file` - path to the CSV file to split
|
|
98
|
+
- `--chunk-size` - required number of data rows per output file; must be greater than zero
|
|
99
|
+
- `--encoding` - input and output encoding; defaults to `utf-8-sig`
|
|
100
|
+
|
|
101
|
+
## Release process
|
|
102
|
+
|
|
103
|
+
To publish a new release, always test on TestPyPI first, then upload to PyPI only after successful Conda smoke tests:
|
|
65
104
|
|
|
66
105
|
```bash
|
|
67
106
|
export TWINE_USERNAME="__token__"
|
|
@@ -71,37 +110,25 @@ bash scripts/release_testpypi.sh --bump patch
|
|
|
71
110
|
bash scripts/release_pypi.sh --yes
|
|
72
111
|
```
|
|
73
112
|
|
|
74
|
-
**
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Supported CLI options
|
|
83
|
-
|
|
84
|
-
- `--logs-dir`
|
|
85
|
-
- `--out-dir`
|
|
86
|
-
- `--backup-dir`
|
|
87
|
-
- `--top-examples`
|
|
88
|
-
- `--convert`
|
|
89
|
-
- `--self-test`
|
|
90
|
-
|
|
91
|
-
## Library usage
|
|
92
|
-
|
|
93
|
-
```python
|
|
113
|
+
**Notes:**
|
|
114
|
+
- Run and verify the TestPyPI release first, then upload the final package to PyPI.
|
|
115
|
+
- PyPI versions cannot be overwritten or reused.
|
|
116
|
+
|
|
117
|
+
## Library usage
|
|
118
|
+
|
|
119
|
+
```python
|
|
94
120
|
from pathlib import Path
|
|
95
121
|
|
|
96
122
|
from loganalysis import (
|
|
97
123
|
analyze_file,
|
|
98
124
|
convert_report_md_to_html_pdf,
|
|
99
|
-
iter_logical_entries,
|
|
100
|
-
normalize_message,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
iter_logical_entries,
|
|
126
|
+
normalize_message,
|
|
127
|
+
)
|
|
128
|
+
from csvchunking import split_csv
|
|
129
|
+
|
|
130
|
+
result = analyze_file(Path("logs/app.txt"))
|
|
131
|
+
print(result["norm_counts"])
|
|
105
132
|
|
|
106
133
|
print(normalize_message(
|
|
107
134
|
'Conversion: X =3100110. 138 The record was not found in table "Teile".'
|
|
@@ -112,20 +139,27 @@ for entry in iter_logical_entries(Path("logs/app.txt")):
|
|
|
112
139
|
|
|
113
140
|
convert_report_md_to_html_pdf(
|
|
114
141
|
Path("log_analyse_out/report.md"),
|
|
115
|
-
Path("log_analyse_out/report.html"),
|
|
116
|
-
Path("log_analyse_out/report.pdf"),
|
|
117
|
-
)
|
|
118
|
-
|
|
142
|
+
Path("log_analyse_out/report.html"),
|
|
143
|
+
Path("log_analyse_out/report.pdf"),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
chunk_result = split_csv(Path("data/large_export.csv"), chunk_size=5000)
|
|
147
|
+
print(chunk_result.output_dir)
|
|
148
|
+
print(chunk_result.output_files)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`split_csv()` returns a `ChunkResult` with the input file, output directory, chunk size, processed data-row count, created file count, and generated output file paths.
|
|
119
152
|
|
|
120
153
|
## Project structure
|
|
121
154
|
|
|
122
155
|
```text
|
|
123
156
|
.
|
|
124
|
-
├─ pyproject.toml
|
|
125
|
-
├─ src/loganalysis/
|
|
126
|
-
├─
|
|
127
|
-
├─
|
|
128
|
-
|
|
157
|
+
├─ pyproject.toml
|
|
158
|
+
├─ src/loganalysis/
|
|
159
|
+
├─ src/csvchunking/
|
|
160
|
+
├─ tests/
|
|
161
|
+
├─ docs/
|
|
162
|
+
└─ log_analysis.py
|
|
129
163
|
```
|
|
130
164
|
|
|
131
165
|
Important modules:
|
|
@@ -135,9 +169,11 @@ Important modules:
|
|
|
135
169
|
- `normalization.py` - message normalization
|
|
136
170
|
- `report_markdown.py` - Markdown report model and rendering
|
|
137
171
|
- `report_html.py` - HTML/CSS rendering
|
|
138
|
-
- `report_pdf.py` - PDF engine selection and fallback handling
|
|
139
|
-
- `converters.py` - Markdown-to-HTML/PDF conversion
|
|
140
|
-
- `cli.py` - command-line entry point
|
|
172
|
+
- `report_pdf.py` - PDF engine selection and fallback handling
|
|
173
|
+
- `converters.py` - Markdown-to-HTML/PDF conversion
|
|
174
|
+
- `loganalysis/cli.py` - log analysis command-line entry point
|
|
175
|
+
- `csvchunking/chunker.py` - CSV splitting logic and `ChunkResult`
|
|
176
|
+
- `csvchunking/cli.py` - CSV chunking command-line entry point
|
|
141
177
|
|
|
142
178
|
## HTML/PDF conversion
|
|
143
179
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nm-tool-forge"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.5"
|
|
8
8
|
description = "Analyze MigMan log files and generate aggregated CSV, Markdown, HTML, and optional PDF reports."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,33 +1,50 @@
|
|
|
1
|
-
import csv
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import csv
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class ChunkResult:
|
|
8
9
|
input_file: Path
|
|
9
10
|
output_dir: Path
|
|
10
|
-
chunk_size: int
|
|
11
|
-
data_rows_processed: int
|
|
12
|
-
files_created: int
|
|
13
|
-
output_files: tuple[Path, ...]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
chunk_size: int
|
|
12
|
+
data_rows_processed: int
|
|
13
|
+
files_created: int
|
|
14
|
+
output_files: tuple[Path, ...]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cleanup_existing_chunks(output_dir: Path, input_file: Path) -> None:
|
|
18
|
+
output_dir = Path(output_dir)
|
|
19
|
+
if not output_dir.exists():
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
input_file = Path(input_file)
|
|
23
|
+
pattern = re.compile(
|
|
24
|
+
rf"^{re.escape(input_file.stem)}_\d{{2,}}{re.escape(input_file.suffix)}$"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
for existing_file in output_dir.iterdir():
|
|
28
|
+
if existing_file.is_file() and pattern.fullmatch(existing_file.name):
|
|
29
|
+
existing_file.unlink()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def split_csv(
|
|
33
|
+
input_file: Path,
|
|
34
|
+
chunk_size: int,
|
|
35
|
+
encoding: str = "utf-8-sig",
|
|
36
|
+
) -> ChunkResult:
|
|
21
37
|
if not Path(input_file).is_file():
|
|
22
|
-
raise FileNotFoundError(f"
|
|
38
|
+
raise FileNotFoundError(f"Input file not found: {input_file}")
|
|
23
39
|
if chunk_size <= 0:
|
|
24
|
-
raise ValueError("chunk_size
|
|
40
|
+
raise ValueError("chunk_size must be greater than 0")
|
|
25
41
|
|
|
26
42
|
input_file = Path(input_file)
|
|
27
43
|
output_dir = input_file.parent / input_file.stem
|
|
28
44
|
output_dir.mkdir(exist_ok=True)
|
|
45
|
+
cleanup_existing_chunks(output_dir, input_file)
|
|
29
46
|
|
|
30
|
-
#
|
|
47
|
+
# Detect the delimiter automatically.
|
|
31
48
|
with open(input_file, encoding=encoding, newline="") as f:
|
|
32
49
|
sample = f.read(4096)
|
|
33
50
|
f.seek(0)
|
|
@@ -38,10 +55,10 @@ def split_csv(
|
|
|
38
55
|
dialect = csv.excel
|
|
39
56
|
dialect.delimiter = ";"
|
|
40
57
|
reader = csv.reader(f, dialect)
|
|
41
|
-
try:
|
|
42
|
-
header = next(reader)
|
|
43
|
-
except StopIteration as exc:
|
|
44
|
-
raise ValueError("
|
|
58
|
+
try:
|
|
59
|
+
header = next(reader)
|
|
60
|
+
except StopIteration as exc:
|
|
61
|
+
raise ValueError("Input file is empty.") from exc
|
|
45
62
|
chunk = []
|
|
46
63
|
file_count = 0
|
|
47
64
|
data_rows = 0
|
|
@@ -7,22 +7,22 @@ from .chunker import split_csv
|
|
|
7
7
|
|
|
8
8
|
def main() -> None:
|
|
9
9
|
parser = argparse.ArgumentParser(
|
|
10
|
-
description="
|
|
10
|
+
description="Split a large CSV file into smaller chunks with a header row.",
|
|
11
11
|
)
|
|
12
|
-
parser.add_argument("input_file", help="
|
|
12
|
+
parser.add_argument("input_file", help="Path to the CSV file")
|
|
13
13
|
parser.add_argument(
|
|
14
14
|
"--chunk-size",
|
|
15
15
|
type=int,
|
|
16
16
|
required=True,
|
|
17
|
-
help="
|
|
17
|
+
help="Number of data rows per output file; must be greater than 0",
|
|
18
18
|
)
|
|
19
|
-
parser.add_argument("--encoding", default="utf-8-sig", help="
|
|
19
|
+
parser.add_argument("--encoding", default="utf-8-sig", help="Input and output encoding (Default: utf-8-sig)")
|
|
20
20
|
args = parser.parse_args()
|
|
21
|
-
try:
|
|
22
|
-
result = split_csv(Path(args.input_file), args.chunk_size, encoding=args.encoding)
|
|
23
|
-
except Exception as e:
|
|
24
|
-
print(f"
|
|
25
|
-
sys.exit(1)
|
|
21
|
+
try:
|
|
22
|
+
result = split_csv(Path(args.input_file), args.chunk_size, encoding=args.encoding)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
25
|
+
sys.exit(1)
|
|
26
26
|
print("CSV chunking completed.")
|
|
27
27
|
print(f"- Input: {result.input_file}")
|
|
28
28
|
print(f"- Output directory: {result.output_dir}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nm-tool-forge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Analyze MigMan log files and generate aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
5
5
|
Author-email: Stefan Ewald <s.ew@outlook.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,7 +31,7 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
# nm-tool-forge
|
|
33
33
|
|
|
34
|
-
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports.
|
|
34
|
+
`nm-tool-forge` analyzes MigMan text log files with severity tokens such as `INFO`, `ERROR`, and `WARNING` and generates aggregated CSV, Markdown, HTML, and optional PDF reports. The package also includes `csvchunking`, a small helper for splitting large CSV files into migration-friendly chunks.
|
|
35
35
|
|
|
36
36
|
The project uses a package-ready `src` layout. The legacy `log_analysis.py` file remains available as a thin compatibility entry point for older local setups.
|
|
37
37
|
|
|
@@ -43,6 +43,7 @@ The project uses a package-ready `src` layout. The legacy `log_analysis.py` file
|
|
|
43
43
|
- Generate Markdown summary reports
|
|
44
44
|
- Optionally convert reports to HTML and PDF
|
|
45
45
|
- Keep a backup copy of analyzed log files
|
|
46
|
+
- Split large CSV files into numbered chunks while preserving the header row
|
|
46
47
|
- Run built-in self-tests from the CLI
|
|
47
48
|
|
|
48
49
|
## Installation
|
|
@@ -61,12 +62,14 @@ python -m pip install .[pdf,dev]
|
|
|
61
62
|
|
|
62
63
|
## Command-line usage
|
|
63
64
|
|
|
64
|
-
After installation,
|
|
65
|
+
After installation, the CLI entry points are available:
|
|
65
66
|
|
|
66
67
|
```powershell
|
|
67
68
|
python -m loganalysis --help
|
|
69
|
+
python -m csvchunking --help
|
|
68
70
|
loganalysis --help
|
|
69
71
|
nm-tool-forge --help
|
|
72
|
+
csvchunking --help
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
Typical analysis run:
|
|
@@ -89,29 +92,30 @@ python -m loganalysis --self-test
|
|
|
89
92
|
|
|
90
93
|
Legacy compatibility call:
|
|
91
94
|
|
|
95
|
+
```powershell
|
|
96
|
+
python .\log_analysis.py --convert
|
|
97
|
+
```
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
To publish a new release, always test on TestPyPI first, then upload to PyPI only after successful Conda/Smoke-Tests:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
export TWINE_USERNAME="__token__"
|
|
99
|
-
export TWINE_PASSWORD="pypi-..."
|
|
99
|
+
CSV chunking run:
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
```powershell
|
|
102
|
+
csvchunking "data\large_export.csv" --chunk-size 5000
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
The command creates an output directory next to the input file named after the CSV stem. For example, `data\large_export.csv` is split into files such as `data\large_export\large_export_01.csv`, `data\large_export\large_export_02.csv`, and so on.
|
|
106
|
+
|
|
107
|
+
CSV chunking with an explicit encoding:
|
|
108
108
|
|
|
109
109
|
```powershell
|
|
110
|
-
python
|
|
110
|
+
python -m csvchunking "data\large_export.csv" --chunk-size 5000 --encoding utf-8-sig
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
Each chunk contains the original header row plus up to `--chunk-size` data rows. The delimiter is detected automatically; if detection fails, semicolon-separated CSV is used.
|
|
114
|
+
|
|
113
115
|
## Supported CLI options
|
|
114
116
|
|
|
117
|
+
Log analysis options:
|
|
118
|
+
|
|
115
119
|
- `--logs-dir`
|
|
116
120
|
- `--out-dir`
|
|
117
121
|
- `--backup-dir`
|
|
@@ -119,6 +123,28 @@ python .\log_analysis.py --convert
|
|
|
119
123
|
- `--convert`
|
|
120
124
|
- `--self-test`
|
|
121
125
|
|
|
126
|
+
CSV chunking options:
|
|
127
|
+
|
|
128
|
+
- `input_file` - path to the CSV file to split
|
|
129
|
+
- `--chunk-size` - required number of data rows per output file; must be greater than zero
|
|
130
|
+
- `--encoding` - input and output encoding; defaults to `utf-8-sig`
|
|
131
|
+
|
|
132
|
+
## Release process
|
|
133
|
+
|
|
134
|
+
To publish a new release, always test on TestPyPI first, then upload to PyPI only after successful Conda smoke tests:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
export TWINE_USERNAME="__token__"
|
|
138
|
+
export TWINE_PASSWORD="pypi-..."
|
|
139
|
+
|
|
140
|
+
bash scripts/release_testpypi.sh --bump patch
|
|
141
|
+
bash scripts/release_pypi.sh --yes
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Notes:**
|
|
145
|
+
- Run and verify the TestPyPI release first, then upload the final package to PyPI.
|
|
146
|
+
- PyPI versions cannot be overwritten or reused.
|
|
147
|
+
|
|
122
148
|
## Library usage
|
|
123
149
|
|
|
124
150
|
```python
|
|
@@ -130,6 +156,7 @@ from loganalysis import (
|
|
|
130
156
|
iter_logical_entries,
|
|
131
157
|
normalize_message,
|
|
132
158
|
)
|
|
159
|
+
from csvchunking import split_csv
|
|
133
160
|
|
|
134
161
|
result = analyze_file(Path("logs/app.txt"))
|
|
135
162
|
print(result["norm_counts"])
|
|
@@ -146,14 +173,21 @@ convert_report_md_to_html_pdf(
|
|
|
146
173
|
Path("log_analyse_out/report.html"),
|
|
147
174
|
Path("log_analyse_out/report.pdf"),
|
|
148
175
|
)
|
|
176
|
+
|
|
177
|
+
chunk_result = split_csv(Path("data/large_export.csv"), chunk_size=5000)
|
|
178
|
+
print(chunk_result.output_dir)
|
|
179
|
+
print(chunk_result.output_files)
|
|
149
180
|
```
|
|
150
181
|
|
|
182
|
+
`split_csv()` returns a `ChunkResult` with the input file, output directory, chunk size, processed data-row count, created file count, and generated output file paths.
|
|
183
|
+
|
|
151
184
|
## Project structure
|
|
152
185
|
|
|
153
186
|
```text
|
|
154
187
|
.
|
|
155
188
|
├─ pyproject.toml
|
|
156
189
|
├─ src/loganalysis/
|
|
190
|
+
├─ src/csvchunking/
|
|
157
191
|
├─ tests/
|
|
158
192
|
├─ docs/
|
|
159
193
|
└─ log_analysis.py
|
|
@@ -168,7 +202,9 @@ Important modules:
|
|
|
168
202
|
- `report_html.py` - HTML/CSS rendering
|
|
169
203
|
- `report_pdf.py` - PDF engine selection and fallback handling
|
|
170
204
|
- `converters.py` - Markdown-to-HTML/PDF conversion
|
|
171
|
-
- `cli.py` - command-line entry point
|
|
205
|
+
- `loganalysis/cli.py` - log analysis command-line entry point
|
|
206
|
+
- `csvchunking/chunker.py` - CSV splitting logic and `ChunkResult`
|
|
207
|
+
- `csvchunking/cli.py` - CSV chunking command-line entry point
|
|
172
208
|
|
|
173
209
|
## HTML/PDF conversion
|
|
174
210
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from csvchunking.chunker import split_csv
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def make_csv(tmp_path, name, header, rows, encoding="utf-8-sig", delimiter=";"):
|
|
7
|
+
file = tmp_path / name
|
|
8
|
+
with open(file, "w", encoding=encoding, newline="") as f:
|
|
9
|
+
f.write(delimiter.join(header) + "\n")
|
|
10
|
+
for row in rows:
|
|
11
|
+
f.write(delimiter.join(row) + "\n")
|
|
12
|
+
return file
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_regular_split(tmp_path):
|
|
16
|
+
header = ["col1", "col2"]
|
|
17
|
+
rows = [["A", "1"], ["B", "2"], ["C", "3"], ["D", "4"], ["E", "5"]]
|
|
18
|
+
file = make_csv(tmp_path, "sample.csv", header, rows)
|
|
19
|
+
result = split_csv(file, chunk_size=2)
|
|
20
|
+
assert result.files_created == 3
|
|
21
|
+
for out in result.output_files:
|
|
22
|
+
with open(out, encoding="utf-8-sig") as f:
|
|
23
|
+
lines = f.read().splitlines()
|
|
24
|
+
assert lines[0] == "col1;col2"
|
|
25
|
+
assert (result.output_dir / "sample_01.csv").exists()
|
|
26
|
+
assert (result.output_dir / "sample_02.csv").exists()
|
|
27
|
+
assert (result.output_dir / "sample_03.csv").exists()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_header_in_each_file(tmp_path):
|
|
31
|
+
header = ["foo", "bar"]
|
|
32
|
+
rows = [["x", "1"], ["y", "2"], ["z", "3"]]
|
|
33
|
+
file = make_csv(tmp_path, "test.csv", header, rows)
|
|
34
|
+
result = split_csv(file, chunk_size=1)
|
|
35
|
+
for out in result.output_files:
|
|
36
|
+
with open(out, encoding="utf-8-sig") as f:
|
|
37
|
+
assert f.readline().strip() == "foo;bar"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_filename_with_spaces(tmp_path):
|
|
41
|
+
header = ["a", "b"]
|
|
42
|
+
rows = [["1", "2"]]
|
|
43
|
+
file = make_csv(tmp_path, "Part-Storage Areas Relationships.csv", header, rows)
|
|
44
|
+
result = split_csv(file, chunk_size=1)
|
|
45
|
+
assert result.output_dir.name == "Part-Storage Areas Relationships"
|
|
46
|
+
assert (result.output_dir / "Part-Storage Areas Relationships_01.csv").exists()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_cleanup_removes_stale_matching_chunk_files(tmp_path):
|
|
50
|
+
header = ["col1", "col2"]
|
|
51
|
+
rows = [["A", "1"], ["B", "2"], ["C", "3"], ["D", "4"]]
|
|
52
|
+
file = make_csv(tmp_path, "sample.csv", header, rows)
|
|
53
|
+
output_dir = tmp_path / "sample"
|
|
54
|
+
output_dir.mkdir()
|
|
55
|
+
for name in ("sample_01.csv", "sample_02.csv", "sample_03.csv"):
|
|
56
|
+
(output_dir / name).write_text("old chunk\n", encoding="utf-8-sig")
|
|
57
|
+
|
|
58
|
+
result = split_csv(file, chunk_size=2)
|
|
59
|
+
|
|
60
|
+
assert result.files_created == 2
|
|
61
|
+
assert (result.output_dir / "sample_01.csv").exists()
|
|
62
|
+
assert (result.output_dir / "sample_02.csv").exists()
|
|
63
|
+
assert not (result.output_dir / "sample_03.csv").exists()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_cleanup_keeps_non_matching_csv_files_and_subdirectories(tmp_path):
|
|
67
|
+
header = ["col1", "col2"]
|
|
68
|
+
rows = [["A", "1"], ["B", "2"]]
|
|
69
|
+
file = make_csv(tmp_path, "sample.csv", header, rows)
|
|
70
|
+
output_dir = tmp_path / "sample"
|
|
71
|
+
output_dir.mkdir()
|
|
72
|
+
preserved_files = [
|
|
73
|
+
"notes.csv",
|
|
74
|
+
"sample_backup.csv",
|
|
75
|
+
"sample_old.csv",
|
|
76
|
+
"other_01.csv",
|
|
77
|
+
"sample_1.csv",
|
|
78
|
+
]
|
|
79
|
+
for name in preserved_files:
|
|
80
|
+
(output_dir / name).write_text("keep\n", encoding="utf-8-sig")
|
|
81
|
+
matching_subdir = output_dir / "sample_99.csv"
|
|
82
|
+
matching_subdir.mkdir()
|
|
83
|
+
(matching_subdir / "nested.txt").write_text("keep nested\n", encoding="utf-8-sig")
|
|
84
|
+
|
|
85
|
+
result = split_csv(file, chunk_size=1)
|
|
86
|
+
|
|
87
|
+
for name in preserved_files:
|
|
88
|
+
assert (result.output_dir / name).exists()
|
|
89
|
+
assert matching_subdir.is_dir()
|
|
90
|
+
assert (matching_subdir / "nested.txt").exists()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_cleanup_filename_with_spaces_uses_exact_chunk_pattern(tmp_path):
|
|
94
|
+
header = ["a", "b"]
|
|
95
|
+
rows = [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"]]
|
|
96
|
+
filename = "Part-Storage Areas Relationships.csv"
|
|
97
|
+
file = make_csv(tmp_path, filename, header, rows)
|
|
98
|
+
output_dir = tmp_path / "Part-Storage Areas Relationships"
|
|
99
|
+
output_dir.mkdir()
|
|
100
|
+
for name in (
|
|
101
|
+
"Part-Storage Areas Relationships_01.csv",
|
|
102
|
+
"Part-Storage Areas Relationships_02.csv",
|
|
103
|
+
"Part-Storage Areas Relationships_99.csv",
|
|
104
|
+
):
|
|
105
|
+
(output_dir / name).write_text("old chunk\n", encoding="utf-8-sig")
|
|
106
|
+
backup_file = output_dir / "Part-Storage Areas Relationships_backup.csv"
|
|
107
|
+
backup_file.write_text("keep\n", encoding="utf-8-sig")
|
|
108
|
+
|
|
109
|
+
result = split_csv(file, chunk_size=2)
|
|
110
|
+
|
|
111
|
+
assert result.output_dir == output_dir
|
|
112
|
+
assert (result.output_dir / "Part-Storage Areas Relationships_01.csv").exists()
|
|
113
|
+
assert (result.output_dir / "Part-Storage Areas Relationships_02.csv").exists()
|
|
114
|
+
assert not (result.output_dir / "Part-Storage Areas Relationships_99.csv").exists()
|
|
115
|
+
assert backup_file.exists()
|
|
116
|
+
assert "old chunk" not in (
|
|
117
|
+
result.output_dir / "Part-Storage Areas Relationships_01.csv"
|
|
118
|
+
).read_text(encoding="utf-8-sig")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_cleanup_repeated_run_removes_extra_chunks(tmp_path):
|
|
122
|
+
header = ["col1", "col2"]
|
|
123
|
+
first_rows = [["A", "1"], ["B", "2"], ["C", "3"], ["D", "4"], ["E", "5"]]
|
|
124
|
+
file = make_csv(tmp_path, "sample.csv", header, first_rows)
|
|
125
|
+
first_result = split_csv(file, chunk_size=2)
|
|
126
|
+
assert first_result.files_created == 3
|
|
127
|
+
assert (first_result.output_dir / "sample_03.csv").exists()
|
|
128
|
+
|
|
129
|
+
second_rows = [["A", "1"], ["B", "2"]]
|
|
130
|
+
make_csv(tmp_path, "sample.csv", header, second_rows)
|
|
131
|
+
second_result = split_csv(file, chunk_size=2)
|
|
132
|
+
|
|
133
|
+
assert second_result.files_created == 1
|
|
134
|
+
assert (second_result.output_dir / "sample_01.csv").exists()
|
|
135
|
+
assert not (second_result.output_dir / "sample_02.csv").exists()
|
|
136
|
+
assert not (second_result.output_dir / "sample_03.csv").exists()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_invalid_chunk_size(tmp_path):
|
|
140
|
+
header = ["a", "b"]
|
|
141
|
+
rows = [["1", "2"]]
|
|
142
|
+
file = make_csv(tmp_path, "fail.csv", header, rows)
|
|
143
|
+
with pytest.raises(ValueError):
|
|
144
|
+
split_csv(file, chunk_size=0)
|
|
145
|
+
with pytest.raises(ValueError):
|
|
146
|
+
split_csv(file, chunk_size=-1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_empty_file(tmp_path):
|
|
150
|
+
file = tmp_path / "empty.csv"
|
|
151
|
+
file.write_text("")
|
|
152
|
+
with pytest.raises(ValueError):
|
|
153
|
+
split_csv(file, chunk_size=1)
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from csvchunking.chunker import split_csv
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def make_csv(tmp_path, name, header, rows, encoding="utf-8-sig", delimiter=";"):
|
|
7
|
-
file = tmp_path / name
|
|
8
|
-
with open(file, "w", encoding=encoding, newline="") as f:
|
|
9
|
-
f.write(delimiter.join(header) + "\n")
|
|
10
|
-
for row in rows:
|
|
11
|
-
f.write(delimiter.join(row) + "\n")
|
|
12
|
-
return file
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_normale_aufteilung(tmp_path):
|
|
16
|
-
header = ["col1", "col2"]
|
|
17
|
-
rows = [["A", "1"], ["B", "2"], ["C", "3"], ["D", "4"], ["E", "5"]]
|
|
18
|
-
file = make_csv(tmp_path, "sample.csv", header, rows)
|
|
19
|
-
result = split_csv(file, chunk_size=2)
|
|
20
|
-
assert result.files_created == 3
|
|
21
|
-
for out in result.output_files:
|
|
22
|
-
with open(out, encoding="utf-8-sig") as f:
|
|
23
|
-
lines = f.read().splitlines()
|
|
24
|
-
assert lines[0] == "col1;col2"
|
|
25
|
-
assert (result.output_dir / "sample_01.csv").exists()
|
|
26
|
-
assert (result.output_dir / "sample_02.csv").exists()
|
|
27
|
-
assert (result.output_dir / "sample_03.csv").exists()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_header_in_jeder_datei(tmp_path):
|
|
31
|
-
header = ["foo", "bar"]
|
|
32
|
-
rows = [["x", "1"], ["y", "2"], ["z", "3"]]
|
|
33
|
-
file = make_csv(tmp_path, "test.csv", header, rows)
|
|
34
|
-
result = split_csv(file, chunk_size=1)
|
|
35
|
-
for out in result.output_files:
|
|
36
|
-
with open(out, encoding="utf-8-sig") as f:
|
|
37
|
-
assert f.readline().strip() == "foo;bar"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_dateiname_mit_leerzeichen(tmp_path):
|
|
41
|
-
header = ["a", "b"]
|
|
42
|
-
rows = [["1", "2"]]
|
|
43
|
-
file = make_csv(tmp_path, "Part-Storage Areas Relationships.csv", header, rows)
|
|
44
|
-
result = split_csv(file, chunk_size=1)
|
|
45
|
-
assert result.output_dir.name == "Part-Storage Areas Relationships"
|
|
46
|
-
assert (result.output_dir / "Part-Storage Areas Relationships_01.csv").exists()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_ungueltige_chunkgroesse(tmp_path):
|
|
50
|
-
header = ["a", "b"]
|
|
51
|
-
rows = [["1", "2"]]
|
|
52
|
-
file = make_csv(tmp_path, "fail.csv", header, rows)
|
|
53
|
-
with pytest.raises(ValueError):
|
|
54
|
-
split_csv(file, chunk_size=0)
|
|
55
|
-
with pytest.raises(ValueError):
|
|
56
|
-
split_csv(file, chunk_size=-1)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_leere_datei(tmp_path):
|
|
60
|
-
file = tmp_path / "empty.csv"
|
|
61
|
-
file.write_text("")
|
|
62
|
-
with pytest.raises(ValueError):
|
|
63
|
-
split_csv(file, chunk_size=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|