ExcelAlchemy 2.2.4__tar.gz → 2.2.6__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.
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/PKG-INFO +70 -2
- excelalchemy-2.2.6/README-pypi.md +159 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/__init__.py +15 -2
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/base.py +64 -1
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/date.py +9 -6
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/date_range.py +12 -4
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/email.py +4 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/multi_checkbox.py +43 -6
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/number.py +4 -13
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/number_range.py +8 -15
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/organization.py +10 -1
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/phone_number.py +4 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/radio.py +33 -2
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/staff.py +10 -1
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/tree.py +8 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/url.py +4 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/alchemy.py +9 -9
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/headers.py +2 -3
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/import_session.py +16 -10
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/rows.py +10 -14
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/schema.py +8 -2
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/writer.py +9 -3
- excelalchemy-2.2.6/src/excelalchemy/exceptions.py +145 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/helper/pydantic.py +144 -22
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/i18n/messages.py +46 -3
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/metadata.py +9 -3
- excelalchemy-2.2.6/src/excelalchemy/results.py +231 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/converter.py +2 -3
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/file.py +21 -5
- excelalchemy-2.2.4/README-pypi.md +0 -91
- excelalchemy-2.2.4/src/excelalchemy/exceptions.py +0 -80
- excelalchemy-2.2.4/src/excelalchemy/results.py +0 -91
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/LICENSE +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/pyproject.toml +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/boolean.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/storage.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/convertor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ExcelAlchemy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: Schema-driven Python library for typed Excel import/export workflows with Pydantic and locale-aware workbooks.
|
|
5
5
|
Keywords: excel,openpyxl,pydantic,minio,schema
|
|
6
6
|
Author: Ray
|
|
@@ -49,7 +49,9 @@ ExcelAlchemy turns Pydantic models into typed workbook contracts:
|
|
|
49
49
|
- render workbook-facing output in `zh-CN` or `en`
|
|
50
50
|
- keep storage pluggable through `ExcelStorage`
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
The current stable release is `2.2.6`, which continues the 2.x line with stronger result-object guidance, a copyable FastAPI reference project, more robust smoke verification, and clearer codec fallback diagnostics.
|
|
53
|
+
|
|
54
|
+
[GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
|
|
53
55
|
|
|
54
56
|
## Screenshots
|
|
55
57
|
|
|
@@ -114,6 +116,72 @@ alchemy = ExcelAlchemy(ImporterConfig(Importer, locale='en'))
|
|
|
114
116
|
template = alchemy.download_template_artifact(filename='people-template.xlsx')
|
|
115
117
|
```
|
|
116
118
|
|
|
119
|
+
## Example Outputs
|
|
120
|
+
|
|
121
|
+
These fixed outputs are generated from the repository examples by
|
|
122
|
+
[`scripts/generate_example_output_assets.py`](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/scripts/generate_example_output_assets.py).
|
|
123
|
+
|
|
124
|
+
Import workflow:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
Employee import workflow completed
|
|
128
|
+
Result: SUCCESS
|
|
129
|
+
Success rows: 1
|
|
130
|
+
Failed rows: 0
|
|
131
|
+
Result workbook URL: None
|
|
132
|
+
Created rows: 1
|
|
133
|
+
Uploaded artifacts: []
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Export workflow:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
Export workflow completed
|
|
140
|
+
Artifact filename: employees-export.xlsx
|
|
141
|
+
Artifact bytes: 6893
|
|
142
|
+
Upload URL: memory://employees-export-upload.xlsx
|
|
143
|
+
Uploaded objects: ['employees-export-upload.xlsx']
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Full captured outputs:
|
|
147
|
+
|
|
148
|
+
- [employee-import-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/employee-import-workflow.txt)
|
|
149
|
+
- [create-or-update-import.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/create-or-update-import.txt)
|
|
150
|
+
- [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
|
|
151
|
+
- [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
|
|
152
|
+
- [selection-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/selection-fields.txt)
|
|
153
|
+
- [fastapi-reference.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/fastapi-reference.txt)
|
|
154
|
+
|
|
155
|
+
For a single GitHub page that combines screenshots, representative workflows,
|
|
156
|
+
and captured outputs, see the
|
|
157
|
+
[Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
|
|
158
|
+
|
|
159
|
+
If you want a copyable FastAPI-oriented reference layout rather than a single
|
|
160
|
+
example script, see the
|
|
161
|
+
[FastAPI reference project](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/examples/fastapi_reference/README.md).
|
|
162
|
+
|
|
163
|
+
## Error Feedback
|
|
164
|
+
|
|
165
|
+
ExcelAlchemy keeps workbook-facing validation feedback readable while also
|
|
166
|
+
supporting API-friendly inspection in application code.
|
|
167
|
+
|
|
168
|
+
The stable 2.x result surface includes:
|
|
169
|
+
|
|
170
|
+
- `alchemy.cell_error_map`
|
|
171
|
+
- `alchemy.row_error_map`
|
|
172
|
+
|
|
173
|
+
These objects remain dict-like for compatibility, but also expose helpers such
|
|
174
|
+
as:
|
|
175
|
+
|
|
176
|
+
- `messages_at(...)`
|
|
177
|
+
- `messages_for_row(...)`
|
|
178
|
+
- `flatten()`
|
|
179
|
+
- `to_api_payload()`
|
|
180
|
+
|
|
181
|
+
Common field types now also produce more business-oriented error wording, such
|
|
182
|
+
as expected date formats, sample email/phone/URL formats, and clearer messages
|
|
183
|
+
for configured selection fields.
|
|
184
|
+
|
|
117
185
|
## Why ExcelAlchemy
|
|
118
186
|
|
|
119
187
|
- Pydantic v2-based schema extraction and validation
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ExcelAlchemy
|
|
2
|
+
|
|
3
|
+
Schema-driven Python library for typed Excel import/export workflows with Pydantic and locale-aware workbooks.
|
|
4
|
+
|
|
5
|
+
ExcelAlchemy turns Pydantic models into typed workbook contracts:
|
|
6
|
+
|
|
7
|
+
- generate Excel templates from code
|
|
8
|
+
- validate uploaded workbooks
|
|
9
|
+
- map failures back to rows and cells
|
|
10
|
+
- render workbook-facing output in `zh-CN` or `en`
|
|
11
|
+
- keep storage pluggable through `ExcelStorage`
|
|
12
|
+
|
|
13
|
+
The current stable release is `2.2.6`, which continues the 2.x line with stronger result-object guidance, a copyable FastAPI reference project, more robust smoke verification, and clearer codec fallback diagnostics.
|
|
14
|
+
|
|
15
|
+
[GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
|
|
16
|
+
|
|
17
|
+
## Screenshots
|
|
18
|
+
|
|
19
|
+
### Template
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
### Import Result
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install ExcelAlchemy
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Optional Minio support:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install "ExcelAlchemy[minio]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Minimal Example
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pydantic import BaseModel
|
|
43
|
+
|
|
44
|
+
from excelalchemy import ExcelAlchemy, FieldMeta, ImporterConfig, Number, String
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Importer(BaseModel):
|
|
48
|
+
age: Number = FieldMeta(label='Age', order=1)
|
|
49
|
+
name: String = FieldMeta(label='Name', order=2)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
alchemy = ExcelAlchemy(ImporterConfig(Importer, locale='en'))
|
|
53
|
+
template = alchemy.download_template_artifact(filename='people-template.xlsx')
|
|
54
|
+
|
|
55
|
+
excel_bytes = template.as_bytes()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Modern Annotated Example
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from typing import Annotated
|
|
62
|
+
|
|
63
|
+
from pydantic import BaseModel, Field
|
|
64
|
+
|
|
65
|
+
from excelalchemy import Email, ExcelAlchemy, ExcelMeta, ImporterConfig
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Importer(BaseModel):
|
|
69
|
+
email: Annotated[
|
|
70
|
+
Email,
|
|
71
|
+
Field(min_length=10),
|
|
72
|
+
ExcelMeta(label='Email', order=1, hint='Use your work email'),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
alchemy = ExcelAlchemy(ImporterConfig(Importer, locale='en'))
|
|
77
|
+
template = alchemy.download_template_artifact(filename='people-template.xlsx')
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Example Outputs
|
|
81
|
+
|
|
82
|
+
These fixed outputs are generated from the repository examples by
|
|
83
|
+
[`scripts/generate_example_output_assets.py`](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/scripts/generate_example_output_assets.py).
|
|
84
|
+
|
|
85
|
+
Import workflow:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
Employee import workflow completed
|
|
89
|
+
Result: SUCCESS
|
|
90
|
+
Success rows: 1
|
|
91
|
+
Failed rows: 0
|
|
92
|
+
Result workbook URL: None
|
|
93
|
+
Created rows: 1
|
|
94
|
+
Uploaded artifacts: []
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Export workflow:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
Export workflow completed
|
|
101
|
+
Artifact filename: employees-export.xlsx
|
|
102
|
+
Artifact bytes: 6893
|
|
103
|
+
Upload URL: memory://employees-export-upload.xlsx
|
|
104
|
+
Uploaded objects: ['employees-export-upload.xlsx']
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Full captured outputs:
|
|
108
|
+
|
|
109
|
+
- [employee-import-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/employee-import-workflow.txt)
|
|
110
|
+
- [create-or-update-import.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/create-or-update-import.txt)
|
|
111
|
+
- [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
|
|
112
|
+
- [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
|
|
113
|
+
- [selection-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/selection-fields.txt)
|
|
114
|
+
- [fastapi-reference.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/fastapi-reference.txt)
|
|
115
|
+
|
|
116
|
+
For a single GitHub page that combines screenshots, representative workflows,
|
|
117
|
+
and captured outputs, see the
|
|
118
|
+
[Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
|
|
119
|
+
|
|
120
|
+
If you want a copyable FastAPI-oriented reference layout rather than a single
|
|
121
|
+
example script, see the
|
|
122
|
+
[FastAPI reference project](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/examples/fastapi_reference/README.md).
|
|
123
|
+
|
|
124
|
+
## Error Feedback
|
|
125
|
+
|
|
126
|
+
ExcelAlchemy keeps workbook-facing validation feedback readable while also
|
|
127
|
+
supporting API-friendly inspection in application code.
|
|
128
|
+
|
|
129
|
+
The stable 2.x result surface includes:
|
|
130
|
+
|
|
131
|
+
- `alchemy.cell_error_map`
|
|
132
|
+
- `alchemy.row_error_map`
|
|
133
|
+
|
|
134
|
+
These objects remain dict-like for compatibility, but also expose helpers such
|
|
135
|
+
as:
|
|
136
|
+
|
|
137
|
+
- `messages_at(...)`
|
|
138
|
+
- `messages_for_row(...)`
|
|
139
|
+
- `flatten()`
|
|
140
|
+
- `to_api_payload()`
|
|
141
|
+
|
|
142
|
+
Common field types now also produce more business-oriented error wording, such
|
|
143
|
+
as expected date formats, sample email/phone/URL formats, and clearer messages
|
|
144
|
+
for configured selection fields.
|
|
145
|
+
|
|
146
|
+
## Why ExcelAlchemy
|
|
147
|
+
|
|
148
|
+
- Pydantic v2-based schema extraction and validation
|
|
149
|
+
- locale-aware workbook comments and result workbooks
|
|
150
|
+
- pluggable storage instead of a hard-coded backend
|
|
151
|
+
- `openpyxl`-based runtime path without pandas
|
|
152
|
+
- contract tests, Ruff, and Pyright in the development workflow
|
|
153
|
+
|
|
154
|
+
## Learn More
|
|
155
|
+
|
|
156
|
+
- [Full project README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md)
|
|
157
|
+
- [Architecture notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md)
|
|
158
|
+
- [Locale policy](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/locale.md)
|
|
159
|
+
- [Migration notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""A Python Library for Reading and Writing Excel Files"""
|
|
2
2
|
|
|
3
|
-
__version__ = '2.2.
|
|
3
|
+
__version__ = '2.2.6'
|
|
4
4
|
from excelalchemy._primitives.constants import CharacterSet, DataRangeOption, DateFormat, Option
|
|
5
5
|
from excelalchemy._primitives.deprecation import ExcelAlchemyDeprecationWarning
|
|
6
6
|
from excelalchemy._primitives.identity import (
|
|
@@ -48,13 +48,24 @@ from excelalchemy.core.storage_protocol import ExcelStorage
|
|
|
48
48
|
from excelalchemy.exceptions import ConfigError, ExcelCellError, ExcelRowError, ProgrammaticError
|
|
49
49
|
from excelalchemy.helper.pydantic import extract_pydantic_model
|
|
50
50
|
from excelalchemy.metadata import ExcelMeta, FieldMeta, PatchFieldMeta
|
|
51
|
-
from excelalchemy.results import
|
|
51
|
+
from excelalchemy.results import (
|
|
52
|
+
CellErrorMap,
|
|
53
|
+
CellIssueRecord,
|
|
54
|
+
ImportResult,
|
|
55
|
+
RowIssueMap,
|
|
56
|
+
RowIssueRecord,
|
|
57
|
+
ValidateHeaderResult,
|
|
58
|
+
ValidateResult,
|
|
59
|
+
ValidateRowResult,
|
|
60
|
+
)
|
|
52
61
|
from excelalchemy.util.file import flatten
|
|
53
62
|
|
|
54
63
|
__all__ = [
|
|
55
64
|
'Base64Str',
|
|
56
65
|
'Boolean',
|
|
57
66
|
'BooleanCodec',
|
|
67
|
+
'CellErrorMap',
|
|
68
|
+
'CellIssueRecord',
|
|
58
69
|
'ColumnIndex',
|
|
59
70
|
'CompositeExcelFieldCodec',
|
|
60
71
|
'ConfigError',
|
|
@@ -104,6 +115,8 @@ __all__ = [
|
|
|
104
115
|
'ProgrammaticError',
|
|
105
116
|
'Radio',
|
|
106
117
|
'RowIndex',
|
|
118
|
+
'RowIssueMap',
|
|
119
|
+
'RowIssueRecord',
|
|
107
120
|
'SingleChoiceCodec',
|
|
108
121
|
'SingleOrganization',
|
|
109
122
|
'SingleOrganizationCodec',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
6
|
|
|
6
7
|
from pydantic import GetCoreSchemaHandler
|
|
7
8
|
from pydantic_core import core_schema
|
|
@@ -19,6 +20,63 @@ type WorkbookDisplayValue = Any
|
|
|
19
20
|
type NormalizedImportValue = Any
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _summarize_exception(exc: Exception) -> str:
|
|
24
|
+
details: list[str] = []
|
|
25
|
+
for arg in exc.args:
|
|
26
|
+
if isinstance(arg, list):
|
|
27
|
+
raw_items = cast(list[object], arg)
|
|
28
|
+
list_items: list[str] = []
|
|
29
|
+
for item in raw_items:
|
|
30
|
+
item_text = item.__name__ if isinstance(item, type) else str(item).strip()
|
|
31
|
+
if item_text:
|
|
32
|
+
list_items.append(item_text)
|
|
33
|
+
if list_items:
|
|
34
|
+
details.append(', '.join(list_items))
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
text = str(arg).strip()
|
|
38
|
+
if text:
|
|
39
|
+
details.append(text)
|
|
40
|
+
|
|
41
|
+
if details:
|
|
42
|
+
return '; '.join(details)
|
|
43
|
+
return exc.__class__.__name__
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def log_codec_parse_fallback(
|
|
47
|
+
codec_name: str,
|
|
48
|
+
value: object,
|
|
49
|
+
*,
|
|
50
|
+
field_label: str | None = None,
|
|
51
|
+
exc: Exception,
|
|
52
|
+
) -> None:
|
|
53
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
54
|
+
logging.warning(
|
|
55
|
+
'Codec %s could not parse workbook input%s; keeping the original value %r. Reason: %s',
|
|
56
|
+
codec_name,
|
|
57
|
+
field_context,
|
|
58
|
+
value,
|
|
59
|
+
_summarize_exception(exc),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def log_codec_render_fallback(
|
|
64
|
+
codec_name: str,
|
|
65
|
+
value: object,
|
|
66
|
+
*,
|
|
67
|
+
field_label: str | None = None,
|
|
68
|
+
exc: Exception,
|
|
69
|
+
) -> None:
|
|
70
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
71
|
+
logging.warning(
|
|
72
|
+
'Codec %s could not format workbook value%s; returning %r as-is. Reason: %s',
|
|
73
|
+
codec_name,
|
|
74
|
+
field_context,
|
|
75
|
+
value,
|
|
76
|
+
_summarize_exception(exc),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
22
80
|
class ExcelFieldCodec(ABC):
|
|
23
81
|
"""Excel-facing field adapter responsible for comments, parsing, formatting, and normalization."""
|
|
24
82
|
|
|
@@ -42,6 +100,11 @@ class ExcelFieldCodec(ABC):
|
|
|
42
100
|
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
43
101
|
"""Validate and normalize parsed input before handing it to the Pydantic layer."""
|
|
44
102
|
|
|
103
|
+
@classmethod
|
|
104
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
105
|
+
"""Return a user-facing input hint for invalid values when one is known."""
|
|
106
|
+
return None
|
|
107
|
+
|
|
45
108
|
@classmethod
|
|
46
109
|
def comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
47
110
|
"""Backward-compatible alias for build_comment()."""
|
|
@@ -11,6 +11,7 @@ from excelalchemy.codecs.base import (
|
|
|
11
11
|
NormalizedImportValue,
|
|
12
12
|
WorkbookDisplayValue,
|
|
13
13
|
WorkbookInputValue,
|
|
14
|
+
log_codec_parse_fallback,
|
|
14
15
|
)
|
|
15
16
|
from excelalchemy.exceptions import ConfigError
|
|
16
17
|
from excelalchemy.i18n.messages import MessageKey
|
|
@@ -21,6 +22,13 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
21
22
|
class Date(ExcelFieldCodec, datetime):
|
|
22
23
|
__name__ = 'Date'
|
|
23
24
|
|
|
25
|
+
@classmethod
|
|
26
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
27
|
+
presentation = field_meta.presentation
|
|
28
|
+
if presentation.date_format is None:
|
|
29
|
+
return None
|
|
30
|
+
return msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[presentation.date_format])
|
|
31
|
+
|
|
24
32
|
@classmethod
|
|
25
33
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
26
34
|
declared = field_meta.declared
|
|
@@ -62,12 +70,7 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
62
70
|
dt: DateTime = cast(DateTime, pendulum.parse(v))
|
|
63
71
|
return dt.replace(tzinfo=presentation.timezone)
|
|
64
72
|
except Exception as exc:
|
|
65
|
-
|
|
66
|
-
'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
|
|
67
|
-
cls.__name__,
|
|
68
|
-
value,
|
|
69
|
-
exc,
|
|
70
|
-
)
|
|
73
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
71
74
|
return value
|
|
72
75
|
|
|
73
76
|
@classmethod
|
|
@@ -9,7 +9,8 @@ from pydantic import BaseModel
|
|
|
9
9
|
|
|
10
10
|
from excelalchemy._primitives.constants import DATE_FORMAT_TO_PYTHON_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
|
|
11
11
|
from excelalchemy._primitives.identity import Key
|
|
12
|
-
from excelalchemy.codecs.base import CompositeExcelFieldCodec
|
|
12
|
+
from excelalchemy.codecs.base import CompositeExcelFieldCodec, log_codec_parse_fallback
|
|
13
|
+
from excelalchemy.exceptions import ConfigError
|
|
13
14
|
from excelalchemy.i18n.messages import MessageKey
|
|
14
15
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
15
16
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -53,7 +54,9 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
53
54
|
declared = field_meta.declared
|
|
54
55
|
presentation = field_meta.presentation
|
|
55
56
|
if presentation.date_format is None:
|
|
56
|
-
raise
|
|
57
|
+
raise ConfigError(
|
|
58
|
+
msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED), message_key=MessageKey.DATE_FORMAT_NOT_CONFIGURED
|
|
59
|
+
)
|
|
57
60
|
|
|
58
61
|
return '\n'.join(
|
|
59
62
|
[
|
|
@@ -63,8 +66,13 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
63
66
|
]
|
|
64
67
|
)
|
|
65
68
|
|
|
69
|
+
@classmethod
|
|
70
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
71
|
+
return msg(MessageKey.ENTER_DATE_RANGE_EXPECTED_FORMAT)
|
|
72
|
+
|
|
66
73
|
@classmethod
|
|
67
74
|
def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> object:
|
|
75
|
+
declared = field_meta.declared
|
|
68
76
|
mapping = cls._coerce_mapping(value)
|
|
69
77
|
if mapping is not None:
|
|
70
78
|
try:
|
|
@@ -73,7 +81,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
73
81
|
'end': cls._parse_optional_datetime(mapping.get('end'), field_meta),
|
|
74
82
|
}
|
|
75
83
|
except Exception as exc:
|
|
76
|
-
|
|
84
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
77
85
|
return value
|
|
78
86
|
|
|
79
87
|
if isinstance(value, datetime):
|
|
@@ -83,7 +91,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
83
91
|
try:
|
|
84
92
|
return cls._parse_datetime_text(value, field_meta)
|
|
85
93
|
except Exception as exc:
|
|
86
|
-
|
|
94
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
87
95
|
return value
|
|
88
96
|
|
|
89
97
|
return value
|
|
@@ -11,6 +11,10 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
11
11
|
class Email(String):
|
|
12
12
|
_validator: ClassVar[TypeAdapter[EmailStr]] = TypeAdapter(EmailStr)
|
|
13
13
|
|
|
14
|
+
@classmethod
|
|
15
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
16
|
+
return msg(MessageKey.VALID_EMAIL_REQUIRED)
|
|
17
|
+
|
|
14
18
|
@classmethod
|
|
15
19
|
def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> str:
|
|
16
20
|
# Try to parse the value as a string
|
|
@@ -14,6 +14,43 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
14
14
|
class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
15
15
|
__name__ = 'MultiChoice'
|
|
16
16
|
|
|
17
|
+
@classmethod
|
|
18
|
+
def selection_entity_plural(cls) -> str | None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def _options_preview(cls, field_meta: FieldMetaInfo, *, limit: int = 5) -> str | None:
|
|
23
|
+
options = field_meta.presentation.options
|
|
24
|
+
if not options:
|
|
25
|
+
return None
|
|
26
|
+
preview = MULTI_CHECKBOX_SEPARATOR.join(option.name for option in options[:limit])
|
|
27
|
+
if len(options) > limit:
|
|
28
|
+
preview = f'{preview}{MULTI_CHECKBOX_SEPARATOR}...'
|
|
29
|
+
return preview
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _compose_selection_message(cls, field_meta: FieldMetaInfo) -> str:
|
|
33
|
+
entity_plural = cls.selection_entity_plural()
|
|
34
|
+
if entity_plural is None:
|
|
35
|
+
base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_OPTIONS)
|
|
36
|
+
else:
|
|
37
|
+
base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_ENTITIES, entity_plural=entity_plural)
|
|
38
|
+
|
|
39
|
+
preview = cls._options_preview(field_meta)
|
|
40
|
+
if preview is None:
|
|
41
|
+
return base_message
|
|
42
|
+
return f'{base_message}. {msg(MessageKey.VALID_VALUES_INCLUDE, options=preview)}'
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
46
|
+
return cls._compose_selection_message(field_meta)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _coerce_items(value: object) -> list[object] | None:
|
|
50
|
+
if not isinstance(value, list):
|
|
51
|
+
return None
|
|
52
|
+
return cast(list[object], value)
|
|
53
|
+
|
|
17
54
|
@classmethod
|
|
18
55
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
19
56
|
declared = field_meta.declared
|
|
@@ -29,8 +66,8 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
29
66
|
|
|
30
67
|
@classmethod
|
|
31
68
|
def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> list[str] | object:
|
|
32
|
-
|
|
33
|
-
|
|
69
|
+
items = cls._coerce_items(value)
|
|
70
|
+
if items is not None:
|
|
34
71
|
return [str(item).strip() for item in items]
|
|
35
72
|
|
|
36
73
|
if isinstance(value, str):
|
|
@@ -45,10 +82,10 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
45
82
|
def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> list[str]: # OptionId
|
|
46
83
|
declared = field_meta.declared
|
|
47
84
|
presentation = field_meta.presentation
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
items = cls._coerce_items(value)
|
|
86
|
+
if items is None:
|
|
87
|
+
raise ValueError(cls._compose_selection_message(field_meta))
|
|
50
88
|
|
|
51
|
-
items = cast(list[object], value)
|
|
52
89
|
parsed = [str(item).strip() for item in items]
|
|
53
90
|
|
|
54
91
|
if presentation.options is None:
|
|
@@ -66,7 +103,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
66
103
|
result, errors = presentation.exchange_names_to_option_ids_with_errors(parsed, field_label=declared.label)
|
|
67
104
|
|
|
68
105
|
if errors:
|
|
69
|
-
raise ValueError(
|
|
106
|
+
raise ValueError(cls._compose_selection_message(field_meta))
|
|
70
107
|
else:
|
|
71
108
|
return result
|
|
72
109
|
|
|
@@ -6,6 +6,7 @@ from excelalchemy.codecs.base import (
|
|
|
6
6
|
NormalizedImportValue,
|
|
7
7
|
WorkbookDisplayValue,
|
|
8
8
|
WorkbookInputValue,
|
|
9
|
+
log_codec_parse_fallback,
|
|
9
10
|
)
|
|
10
11
|
from excelalchemy.i18n.messages import MessageKey
|
|
11
12
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -64,6 +65,7 @@ class Number(Decimal, ExcelFieldCodec):
|
|
|
64
65
|
value: str | int | float | WorkbookInputValue | None,
|
|
65
66
|
field_meta: FieldMetaInfo,
|
|
66
67
|
) -> Decimal | WorkbookInputValue:
|
|
68
|
+
declared = field_meta.declared
|
|
67
69
|
if isinstance(value, str):
|
|
68
70
|
value = value.strip()
|
|
69
71
|
if value is None:
|
|
@@ -71,12 +73,7 @@ class Number(Decimal, ExcelFieldCodec):
|
|
|
71
73
|
try:
|
|
72
74
|
return transform_decimal(Decimal(str(value)))
|
|
73
75
|
except Exception as exc:
|
|
74
|
-
|
|
75
|
-
'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
|
|
76
|
-
cls.__name__,
|
|
77
|
-
value,
|
|
78
|
-
exc,
|
|
79
|
-
)
|
|
76
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
80
77
|
return str(value)
|
|
81
78
|
|
|
82
79
|
@classmethod
|
|
@@ -90,13 +87,7 @@ class Number(Decimal, ExcelFieldCodec):
|
|
|
90
87
|
|
|
91
88
|
try:
|
|
92
89
|
return str(transform_decimal(Decimal(value)))
|
|
93
|
-
except Exception
|
|
94
|
-
logging.warning(
|
|
95
|
-
'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
|
|
96
|
-
cls.__name__,
|
|
97
|
-
value,
|
|
98
|
-
exc,
|
|
99
|
-
)
|
|
90
|
+
except Exception:
|
|
100
91
|
return str(value)
|
|
101
92
|
|
|
102
93
|
@classmethod
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from collections.abc import Mapping
|
|
3
2
|
from decimal import Decimal
|
|
4
3
|
from typing import cast
|
|
5
4
|
|
|
6
5
|
from excelalchemy._primitives.identity import Key
|
|
7
|
-
from excelalchemy.codecs.base import CompositeExcelFieldCodec
|
|
6
|
+
from excelalchemy.codecs.base import CompositeExcelFieldCodec, log_codec_parse_fallback
|
|
8
7
|
from excelalchemy.codecs.number import Number, canonicalize_decimal, transform_decimal
|
|
9
8
|
from excelalchemy.i18n.messages import MessageKey
|
|
10
9
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -35,8 +34,13 @@ class NumberRange(CompositeExcelFieldCodec):
|
|
|
35
34
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
36
35
|
return Number.build_comment(field_meta)
|
|
37
36
|
|
|
37
|
+
@classmethod
|
|
38
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
39
|
+
return msg(MessageKey.ENTER_NUMBER_RANGE_EXPECTED_FORMAT)
|
|
40
|
+
|
|
38
41
|
@classmethod
|
|
39
42
|
def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> object:
|
|
43
|
+
declared = field_meta.declared
|
|
40
44
|
if isinstance(value, str):
|
|
41
45
|
value = value.strip()
|
|
42
46
|
|
|
@@ -50,12 +54,7 @@ class NumberRange(CompositeExcelFieldCodec):
|
|
|
50
54
|
end = cls._parse_decimal_boundary(mapping['end'])
|
|
51
55
|
return NumberRange(start, end)
|
|
52
56
|
except (KeyError, TypeError, ValueError) as exc:
|
|
53
|
-
|
|
54
|
-
'%s could not parse Excel input %s; returning the original value. Reason: %s',
|
|
55
|
-
cls.__name__,
|
|
56
|
-
value,
|
|
57
|
-
exc,
|
|
58
|
-
)
|
|
57
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
59
58
|
return value
|
|
60
59
|
|
|
61
60
|
@classmethod
|
|
@@ -68,13 +67,7 @@ class NumberRange(CompositeExcelFieldCodec):
|
|
|
68
67
|
if parsed is None:
|
|
69
68
|
return ''
|
|
70
69
|
return str(transform_decimal(canonicalize_decimal(parsed, presentation.fraction_digits)))
|
|
71
|
-
except Exception
|
|
72
|
-
logging.warning(
|
|
73
|
-
'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
|
|
74
|
-
cls.__name__,
|
|
75
|
-
value,
|
|
76
|
-
exc,
|
|
77
|
-
)
|
|
70
|
+
except Exception:
|
|
78
71
|
return str(value)
|
|
79
72
|
|
|
80
73
|
@classmethod
|