ExcelAlchemy 2.2.5__tar.gz → 2.2.7__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.5 → excelalchemy-2.2.7}/PKG-INFO +30 -3
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/README-pypi.md +29 -2
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/__init__.py +11 -1
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/base.py +104 -1
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/boolean.py +15 -8
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/date.py +11 -8
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/date_range.py +14 -5
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/email.py +4 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/multi_checkbox.py +40 -9
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/number.py +6 -15
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/number_range.py +8 -15
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/organization.py +16 -3
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/phone_number.py +4 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/radio.py +43 -16
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/staff.py +16 -8
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/tree.py +14 -4
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/url.py +4 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/exceptions.py +17 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/helper/pydantic.py +110 -33
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/i18n/messages.py +31 -3
- excelalchemy-2.2.7/src/excelalchemy/results.py +457 -0
- excelalchemy-2.2.5/src/excelalchemy/results.py +0 -173
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/LICENSE +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/pyproject.toml +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/alchemy.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/headers.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/import_session.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/rows.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/schema.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/storage.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/writer.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/metadata.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/converter.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/convertor.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/file.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.7
|
|
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,9 +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
|
-
The current stable release is `2.2.
|
|
52
|
+
The current stable release is `2.2.7`, which continues the 2.x line with stronger API-facing result payloads, a more complete FastAPI reference app, harder install-time smoke verification, and more consistent codec diagnostics.
|
|
53
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) · [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)
|
|
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) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.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)
|
|
55
55
|
|
|
56
56
|
## Screenshots
|
|
57
57
|
|
|
@@ -150,11 +150,38 @@ Full captured outputs:
|
|
|
150
150
|
- [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
|
|
151
151
|
- [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
|
|
152
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)
|
|
153
154
|
|
|
154
155
|
For a single GitHub page that combines screenshots, representative workflows,
|
|
155
156
|
and captured outputs, see the
|
|
156
157
|
[Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
|
|
157
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
|
+
|
|
158
185
|
## Why ExcelAlchemy
|
|
159
186
|
|
|
160
187
|
- Pydantic v2-based schema extraction and validation
|
|
@@ -10,9 +10,9 @@ ExcelAlchemy turns Pydantic models into typed workbook contracts:
|
|
|
10
10
|
- render workbook-facing output in `zh-CN` or `en`
|
|
11
11
|
- keep storage pluggable through `ExcelStorage`
|
|
12
12
|
|
|
13
|
-
The current stable release is `2.2.
|
|
13
|
+
The current stable release is `2.2.7`, which continues the 2.x line with stronger API-facing result payloads, a more complete FastAPI reference app, harder install-time smoke verification, and more consistent codec diagnostics.
|
|
14
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) · [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)
|
|
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) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.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
16
|
|
|
17
17
|
## Screenshots
|
|
18
18
|
|
|
@@ -111,11 +111,38 @@ Full captured outputs:
|
|
|
111
111
|
- [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
|
|
112
112
|
- [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
|
|
113
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)
|
|
114
115
|
|
|
115
116
|
For a single GitHub page that combines screenshots, representative workflows,
|
|
116
117
|
and captured outputs, see the
|
|
117
118
|
[Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
|
|
118
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
|
+
|
|
119
146
|
## Why ExcelAlchemy
|
|
120
147
|
|
|
121
148
|
- Pydantic v2-based schema extraction and validation
|
|
@@ -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.7'
|
|
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 (
|
|
@@ -50,8 +50,13 @@ from excelalchemy.helper.pydantic import extract_pydantic_model
|
|
|
50
50
|
from excelalchemy.metadata import ExcelMeta, FieldMeta, PatchFieldMeta
|
|
51
51
|
from excelalchemy.results import (
|
|
52
52
|
CellErrorMap,
|
|
53
|
+
CellIssueRecord,
|
|
54
|
+
CodeIssueSummary,
|
|
55
|
+
FieldIssueSummary,
|
|
53
56
|
ImportResult,
|
|
54
57
|
RowIssueMap,
|
|
58
|
+
RowIssueRecord,
|
|
59
|
+
RowIssueSummary,
|
|
55
60
|
ValidateHeaderResult,
|
|
56
61
|
ValidateResult,
|
|
57
62
|
ValidateRowResult,
|
|
@@ -63,6 +68,8 @@ __all__ = [
|
|
|
63
68
|
'Boolean',
|
|
64
69
|
'BooleanCodec',
|
|
65
70
|
'CellErrorMap',
|
|
71
|
+
'CellIssueRecord',
|
|
72
|
+
'CodeIssueSummary',
|
|
66
73
|
'ColumnIndex',
|
|
67
74
|
'CompositeExcelFieldCodec',
|
|
68
75
|
'ConfigError',
|
|
@@ -84,6 +91,7 @@ __all__ = [
|
|
|
84
91
|
'ExcelRowError',
|
|
85
92
|
'ExcelStorage',
|
|
86
93
|
'ExporterConfig',
|
|
94
|
+
'FieldIssueSummary',
|
|
87
95
|
'FieldMeta',
|
|
88
96
|
'ImportMode',
|
|
89
97
|
'ImportResult',
|
|
@@ -113,6 +121,8 @@ __all__ = [
|
|
|
113
121
|
'Radio',
|
|
114
122
|
'RowIndex',
|
|
115
123
|
'RowIssueMap',
|
|
124
|
+
'RowIssueRecord',
|
|
125
|
+
'RowIssueSummary',
|
|
116
126
|
'SingleChoiceCodec',
|
|
117
127
|
'SingleOrganization',
|
|
118
128
|
'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
|
|
@@ -18,6 +19,103 @@ type WorkbookInputValue = Any
|
|
|
18
19
|
type WorkbookDisplayValue = Any
|
|
19
20
|
type NormalizedImportValue = Any
|
|
20
21
|
|
|
22
|
+
CODEC_LOGGER_NAME = 'excelalchemy.codecs'
|
|
23
|
+
codec_logger = logging.getLogger(CODEC_LOGGER_NAME)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _summarize_exception(exc: Exception) -> str:
|
|
27
|
+
details: list[str] = []
|
|
28
|
+
for arg in exc.args:
|
|
29
|
+
if isinstance(arg, list):
|
|
30
|
+
raw_items = cast(list[object], arg)
|
|
31
|
+
list_items: list[str] = []
|
|
32
|
+
for item in raw_items:
|
|
33
|
+
item_text = item.__name__ if isinstance(item, type) else str(item).strip()
|
|
34
|
+
if item_text:
|
|
35
|
+
list_items.append(item_text)
|
|
36
|
+
if list_items:
|
|
37
|
+
details.append(', '.join(list_items))
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
text = str(arg).strip()
|
|
41
|
+
if text:
|
|
42
|
+
details.append(text)
|
|
43
|
+
|
|
44
|
+
if details:
|
|
45
|
+
return '; '.join(details)
|
|
46
|
+
return exc.__class__.__name__
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fallback_reason(*, exc: Exception | None = None, reason: str | None = None) -> str:
|
|
50
|
+
if reason:
|
|
51
|
+
return reason
|
|
52
|
+
if exc is not None:
|
|
53
|
+
return _summarize_exception(exc)
|
|
54
|
+
return 'No additional details'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def log_codec_parse_fallback(
|
|
58
|
+
codec_name: str,
|
|
59
|
+
value: object,
|
|
60
|
+
*,
|
|
61
|
+
field_label: str | None = None,
|
|
62
|
+
exc: Exception | None = None,
|
|
63
|
+
reason: str | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
66
|
+
codec_logger.warning(
|
|
67
|
+
'Codec %s could not parse workbook input%s; keeping the original value %r. Reason: %s',
|
|
68
|
+
codec_name,
|
|
69
|
+
field_context,
|
|
70
|
+
value,
|
|
71
|
+
_fallback_reason(exc=exc, reason=reason),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def log_codec_render_fallback(
|
|
76
|
+
codec_name: str,
|
|
77
|
+
value: object,
|
|
78
|
+
*,
|
|
79
|
+
field_label: str | None = None,
|
|
80
|
+
exc: Exception | None = None,
|
|
81
|
+
reason: str | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
84
|
+
codec_logger.warning(
|
|
85
|
+
'Codec %s could not format workbook value%s; returning %r as-is. Reason: %s',
|
|
86
|
+
codec_name,
|
|
87
|
+
field_context,
|
|
88
|
+
value,
|
|
89
|
+
_fallback_reason(exc=exc, reason=reason),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def log_codec_option_resolution_fallback(
|
|
94
|
+
codec_name: str,
|
|
95
|
+
value: object,
|
|
96
|
+
*,
|
|
97
|
+
field_label: str | None = None,
|
|
98
|
+
exc: Exception | None = None,
|
|
99
|
+
reason: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
102
|
+
codec_logger.warning(
|
|
103
|
+
'Codec %s could not resolve a configured option%s; returning %r as-is. Reason: %s',
|
|
104
|
+
codec_name,
|
|
105
|
+
field_context,
|
|
106
|
+
value,
|
|
107
|
+
_fallback_reason(exc=exc, reason=reason),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def log_codec_missing_options(codec_name: str, *, field_label: str | None = None) -> None:
|
|
112
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
113
|
+
codec_logger.warning(
|
|
114
|
+
'Codec %s is missing configured options%s; workbook comments and validation may be incomplete.',
|
|
115
|
+
codec_name,
|
|
116
|
+
field_context,
|
|
117
|
+
)
|
|
118
|
+
|
|
21
119
|
|
|
22
120
|
class ExcelFieldCodec(ABC):
|
|
23
121
|
"""Excel-facing field adapter responsible for comments, parsing, formatting, and normalization."""
|
|
@@ -42,6 +140,11 @@ class ExcelFieldCodec(ABC):
|
|
|
42
140
|
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
43
141
|
"""Validate and normalize parsed input before handing it to the Pydantic layer."""
|
|
44
142
|
|
|
143
|
+
@classmethod
|
|
144
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
145
|
+
"""Return a user-facing input hint for invalid values when one is known."""
|
|
146
|
+
return None
|
|
147
|
+
|
|
45
148
|
@classmethod
|
|
46
149
|
def comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
47
150
|
"""Backward-compatible alias for build_comment()."""
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
1
|
from excelalchemy.codecs import excel_choice_codec
|
|
4
|
-
from excelalchemy.codecs.base import
|
|
2
|
+
from excelalchemy.codecs.base import (
|
|
3
|
+
ExcelFieldCodec,
|
|
4
|
+
WorkbookDisplayValue,
|
|
5
|
+
WorkbookInputValue,
|
|
6
|
+
log_codec_render_fallback,
|
|
7
|
+
)
|
|
5
8
|
from excelalchemy.i18n.messages import MessageKey
|
|
6
9
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
7
10
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -63,15 +66,19 @@ class Boolean(ExcelFieldCodec):
|
|
|
63
66
|
if value in cls._false_values():
|
|
64
67
|
return cls._false_display()
|
|
65
68
|
if value not in cls._true_values() | cls._false_values():
|
|
66
|
-
|
|
69
|
+
log_codec_render_fallback(
|
|
70
|
+
cls.__name__,
|
|
71
|
+
value,
|
|
72
|
+
field_label=declared.label,
|
|
73
|
+
reason=f'Expected {cls._true_display()!r} or {cls._false_display()!r}',
|
|
74
|
+
)
|
|
67
75
|
return value
|
|
68
76
|
else:
|
|
69
|
-
|
|
70
|
-
'Type %s could not deserialize %s for field %s; returning the default value %s',
|
|
77
|
+
log_codec_render_fallback(
|
|
71
78
|
cls.__name__,
|
|
72
79
|
value,
|
|
73
|
-
declared.label,
|
|
74
|
-
cls._false_display(),
|
|
80
|
+
field_label=declared.label,
|
|
81
|
+
reason=f'Expected a boolean or one of {cls._true_display()!r}/{cls._false_display()!r}',
|
|
75
82
|
)
|
|
76
83
|
|
|
77
84
|
return cls._true_display() if str(value) in cls._true_values() else cls._false_display()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from datetime import datetime
|
|
3
2
|
from typing import cast
|
|
4
3
|
|
|
@@ -11,6 +10,8 @@ from excelalchemy.codecs.base import (
|
|
|
11
10
|
NormalizedImportValue,
|
|
12
11
|
WorkbookDisplayValue,
|
|
13
12
|
WorkbookInputValue,
|
|
13
|
+
codec_logger,
|
|
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
|
|
@@ -45,7 +53,7 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
45
53
|
declared = field_meta.declared
|
|
46
54
|
presentation = field_meta.presentation
|
|
47
55
|
if isinstance(value, DateTime):
|
|
48
|
-
|
|
56
|
+
codec_logger.info(
|
|
49
57
|
'Codec %s received a parsed datetime for %s; returning it unchanged: %s',
|
|
50
58
|
cls.__name__,
|
|
51
59
|
declared.label,
|
|
@@ -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
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from collections.abc import Mapping
|
|
3
2
|
from datetime import datetime
|
|
4
3
|
from typing import cast
|
|
@@ -9,7 +8,7 @@ from pydantic import BaseModel
|
|
|
9
8
|
|
|
10
9
|
from excelalchemy._primitives.constants import DATE_FORMAT_TO_PYTHON_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
|
|
11
10
|
from excelalchemy._primitives.identity import Key
|
|
12
|
-
from excelalchemy.codecs.base import CompositeExcelFieldCodec
|
|
11
|
+
from excelalchemy.codecs.base import CompositeExcelFieldCodec, log_codec_parse_fallback, log_codec_render_fallback
|
|
13
12
|
from excelalchemy.exceptions import ConfigError
|
|
14
13
|
from excelalchemy.i18n.messages import MessageKey
|
|
15
14
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -66,8 +65,13 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
66
65
|
]
|
|
67
66
|
)
|
|
68
67
|
|
|
68
|
+
@classmethod
|
|
69
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
70
|
+
return msg(MessageKey.ENTER_DATE_RANGE_EXPECTED_FORMAT)
|
|
71
|
+
|
|
69
72
|
@classmethod
|
|
70
73
|
def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> object:
|
|
74
|
+
declared = field_meta.declared
|
|
71
75
|
mapping = cls._coerce_mapping(value)
|
|
72
76
|
if mapping is not None:
|
|
73
77
|
try:
|
|
@@ -76,7 +80,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
76
80
|
'end': cls._parse_optional_datetime(mapping.get('end'), field_meta),
|
|
77
81
|
}
|
|
78
82
|
except Exception as exc:
|
|
79
|
-
|
|
83
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
80
84
|
return value
|
|
81
85
|
|
|
82
86
|
if isinstance(value, datetime):
|
|
@@ -86,7 +90,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
86
90
|
try:
|
|
87
91
|
return cls._parse_datetime_text(value, field_meta)
|
|
88
92
|
except Exception as exc:
|
|
89
|
-
|
|
93
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
90
94
|
return value
|
|
91
95
|
|
|
92
96
|
return value
|
|
@@ -144,7 +148,12 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
144
148
|
if mapping is not None:
|
|
145
149
|
return cls.__deserialize__dict(py_date_format, mapping)
|
|
146
150
|
|
|
147
|
-
|
|
151
|
+
log_codec_render_fallback(
|
|
152
|
+
cls.__name__,
|
|
153
|
+
value,
|
|
154
|
+
field_label=field_meta.declared.label,
|
|
155
|
+
reason='The workbook value is not a string, datetime, or start/end mapping',
|
|
156
|
+
)
|
|
148
157
|
return str(value)
|
|
149
158
|
|
|
150
159
|
@classmethod
|
|
@@ -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
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from typing import cast
|
|
3
2
|
|
|
4
3
|
from excelalchemy._primitives.constants import MULTI_CHECKBOX_SEPARATOR
|
|
5
4
|
from excelalchemy._primitives.identity import OptionId
|
|
6
|
-
from excelalchemy.codecs.base import ExcelFieldCodec
|
|
5
|
+
from excelalchemy.codecs.base import ExcelFieldCodec, log_codec_missing_options, log_codec_parse_fallback
|
|
7
6
|
from excelalchemy.exceptions import ProgrammaticError
|
|
8
7
|
from excelalchemy.i18n.messages import MessageKey
|
|
9
8
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -14,6 +13,37 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
14
13
|
class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
15
14
|
__name__ = 'MultiChoice'
|
|
16
15
|
|
|
16
|
+
@classmethod
|
|
17
|
+
def selection_entity_plural(cls) -> str | None:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def _options_preview(cls, field_meta: FieldMetaInfo, *, limit: int = 5) -> str | None:
|
|
22
|
+
options = field_meta.presentation.options
|
|
23
|
+
if not options:
|
|
24
|
+
return None
|
|
25
|
+
preview = MULTI_CHECKBOX_SEPARATOR.join(option.name for option in options[:limit])
|
|
26
|
+
if len(options) > limit:
|
|
27
|
+
preview = f'{preview}{MULTI_CHECKBOX_SEPARATOR}...'
|
|
28
|
+
return preview
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def _compose_selection_message(cls, field_meta: FieldMetaInfo) -> str:
|
|
32
|
+
entity_plural = cls.selection_entity_plural()
|
|
33
|
+
if entity_plural is None:
|
|
34
|
+
base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_OPTIONS)
|
|
35
|
+
else:
|
|
36
|
+
base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_ENTITIES, entity_plural=entity_plural)
|
|
37
|
+
|
|
38
|
+
preview = cls._options_preview(field_meta)
|
|
39
|
+
if preview is None:
|
|
40
|
+
return base_message
|
|
41
|
+
return f'{base_message}. {msg(MessageKey.VALID_VALUES_INCLUDE, options=preview)}'
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
45
|
+
return cls._compose_selection_message(field_meta)
|
|
46
|
+
|
|
17
47
|
@staticmethod
|
|
18
48
|
def _coerce_items(value: object) -> list[object] | None:
|
|
19
49
|
if not isinstance(value, list):
|
|
@@ -42,8 +72,11 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
42
72
|
if isinstance(value, str):
|
|
43
73
|
return [item.strip() for item in value.split(MULTI_CHECKBOX_SEPARATOR)]
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
log_codec_parse_fallback(
|
|
76
|
+
cls.__name__,
|
|
77
|
+
value,
|
|
78
|
+
field_label=field_meta.declared.label,
|
|
79
|
+
reason='Expected a delimited string or a list of selected values',
|
|
47
80
|
)
|
|
48
81
|
return value
|
|
49
82
|
|
|
@@ -53,7 +86,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
53
86
|
presentation = field_meta.presentation
|
|
54
87
|
items = cls._coerce_items(value)
|
|
55
88
|
if items is None:
|
|
56
|
-
raise ValueError(
|
|
89
|
+
raise ValueError(cls._compose_selection_message(field_meta))
|
|
57
90
|
|
|
58
91
|
parsed = [str(item).strip() for item in items]
|
|
59
92
|
|
|
@@ -61,9 +94,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
61
94
|
raise ProgrammaticError(msg(MessageKey.OPTIONS_CANNOT_BE_NONE_FOR_VALUE_TYPE, value_type=cls.__name__))
|
|
62
95
|
|
|
63
96
|
if not presentation.options: # empty
|
|
64
|
-
|
|
65
|
-
'Field %s of type %s has no options; returning the original value', declared.label, cls.__name__
|
|
66
|
-
)
|
|
97
|
+
log_codec_missing_options(cls.__name__, field_label=declared.label)
|
|
67
98
|
return parsed
|
|
68
99
|
|
|
69
100
|
if len(parsed) != len(set(parsed)):
|
|
@@ -72,7 +103,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
72
103
|
result, errors = presentation.exchange_names_to_option_ids_with_errors(parsed, field_label=declared.label)
|
|
73
104
|
|
|
74
105
|
if errors:
|
|
75
|
-
raise ValueError(
|
|
106
|
+
raise ValueError(cls._compose_selection_message(field_meta))
|
|
76
107
|
else:
|
|
77
108
|
return result
|
|
78
109
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from decimal import ROUND_DOWN, Context, Decimal, InvalidOperation
|
|
3
2
|
|
|
4
3
|
from excelalchemy.codecs.base import (
|
|
@@ -6,6 +5,8 @@ from excelalchemy.codecs.base import (
|
|
|
6
5
|
NormalizedImportValue,
|
|
7
6
|
WorkbookDisplayValue,
|
|
8
7
|
WorkbookInputValue,
|
|
8
|
+
codec_logger,
|
|
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
|
|
@@ -23,7 +24,7 @@ def canonicalize_decimal(value: Decimal, digits_limit: int | None) -> Decimal:
|
|
|
23
24
|
context=Context(rounding=ROUND_DOWN),
|
|
24
25
|
)
|
|
25
26
|
except InvalidOperation as e:
|
|
26
|
-
|
|
27
|
+
codec_logger.warning('Codec Number detected precision loss while quantizing fraction_digits: %s', e)
|
|
27
28
|
return value
|
|
28
29
|
|
|
29
30
|
|
|
@@ -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
|