ExcelAlchemy 2.2.5__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.5 → excelalchemy-2.2.6}/PKG-INFO +30 -3
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/README-pypi.md +29 -2
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/__init__.py +5 -1
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/base.py +64 -1
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/date.py +9 -6
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/date_range.py +8 -3
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/email.py +4 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/multi_checkbox.py +33 -2
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/number.py +4 -13
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/number_range.py +8 -15
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/organization.py +8 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/phone_number.py +4 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/radio.py +33 -2
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/staff.py +8 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/tree.py +8 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/url.py +4 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/helper/pydantic.py +110 -33
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/i18n/messages.py +31 -3
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/results.py +58 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/LICENSE +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/pyproject.toml +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/boolean.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/alchemy.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/headers.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/import_session.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/rows.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/schema.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/storage.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/core/writer.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/exceptions.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/metadata.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/util/converter.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/src/excelalchemy/util/convertor.py +0 -0
- {excelalchemy-2.2.5 → excelalchemy-2.2.6}/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.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,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.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
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) · [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.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
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) · [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.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 (
|
|
@@ -50,8 +50,10 @@ 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,
|
|
53
54
|
ImportResult,
|
|
54
55
|
RowIssueMap,
|
|
56
|
+
RowIssueRecord,
|
|
55
57
|
ValidateHeaderResult,
|
|
56
58
|
ValidateResult,
|
|
57
59
|
ValidateRowResult,
|
|
@@ -63,6 +65,7 @@ __all__ = [
|
|
|
63
65
|
'Boolean',
|
|
64
66
|
'BooleanCodec',
|
|
65
67
|
'CellErrorMap',
|
|
68
|
+
'CellIssueRecord',
|
|
66
69
|
'ColumnIndex',
|
|
67
70
|
'CompositeExcelFieldCodec',
|
|
68
71
|
'ConfigError',
|
|
@@ -113,6 +116,7 @@ __all__ = [
|
|
|
113
116
|
'Radio',
|
|
114
117
|
'RowIndex',
|
|
115
118
|
'RowIssueMap',
|
|
119
|
+
'RowIssueRecord',
|
|
116
120
|
'SingleChoiceCodec',
|
|
117
121
|
'SingleOrganization',
|
|
118
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,7 @@ 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
13
|
from excelalchemy.exceptions import ConfigError
|
|
14
14
|
from excelalchemy.i18n.messages import MessageKey
|
|
15
15
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -66,8 +66,13 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
66
66
|
]
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
+
@classmethod
|
|
70
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
71
|
+
return msg(MessageKey.ENTER_DATE_RANGE_EXPECTED_FORMAT)
|
|
72
|
+
|
|
69
73
|
@classmethod
|
|
70
74
|
def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> object:
|
|
75
|
+
declared = field_meta.declared
|
|
71
76
|
mapping = cls._coerce_mapping(value)
|
|
72
77
|
if mapping is not None:
|
|
73
78
|
try:
|
|
@@ -76,7 +81,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
76
81
|
'end': cls._parse_optional_datetime(mapping.get('end'), field_meta),
|
|
77
82
|
}
|
|
78
83
|
except Exception as exc:
|
|
79
|
-
|
|
84
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
80
85
|
return value
|
|
81
86
|
|
|
82
87
|
if isinstance(value, datetime):
|
|
@@ -86,7 +91,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
86
91
|
try:
|
|
87
92
|
return cls._parse_datetime_text(value, field_meta)
|
|
88
93
|
except Exception as exc:
|
|
89
|
-
|
|
94
|
+
log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
90
95
|
return value
|
|
91
96
|
|
|
92
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,37 @@ 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
|
+
|
|
17
48
|
@staticmethod
|
|
18
49
|
def _coerce_items(value: object) -> list[object] | None:
|
|
19
50
|
if not isinstance(value, list):
|
|
@@ -53,7 +84,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
53
84
|
presentation = field_meta.presentation
|
|
54
85
|
items = cls._coerce_items(value)
|
|
55
86
|
if items is None:
|
|
56
|
-
raise ValueError(
|
|
87
|
+
raise ValueError(cls._compose_selection_message(field_meta))
|
|
57
88
|
|
|
58
89
|
parsed = [str(item).strip() for item in items]
|
|
59
90
|
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -13,6 +13,10 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
13
13
|
class SingleOrganization(Radio):
|
|
14
14
|
__name__ = 'SingleOrganization'
|
|
15
15
|
|
|
16
|
+
@classmethod
|
|
17
|
+
def selection_entity_singular(cls) -> str | None:
|
|
18
|
+
return 'organization'
|
|
19
|
+
|
|
16
20
|
@classmethod
|
|
17
21
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
18
22
|
declared = field_meta.declared
|
|
@@ -48,6 +52,10 @@ class SingleOrganization(Radio):
|
|
|
48
52
|
class MultiOrganization(MultiCheckbox):
|
|
49
53
|
__name__ = 'MultiOrganization'
|
|
50
54
|
|
|
55
|
+
@classmethod
|
|
56
|
+
def selection_entity_plural(cls) -> str | None:
|
|
57
|
+
return 'organizations'
|
|
58
|
+
|
|
51
59
|
@classmethod
|
|
52
60
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
53
61
|
declared = field_meta.declared
|
|
@@ -10,6 +10,10 @@ PHONE_NUMBER_PATTERN = re.compile(r'^((0\d{2,3}-\d{7,8})|(1[3456789]\d{9}))$')
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class PhoneNumber(String):
|
|
13
|
+
@classmethod
|
|
14
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
15
|
+
return msg(MessageKey.VALID_PHONE_NUMBER_REQUIRED)
|
|
16
|
+
|
|
13
17
|
@classmethod
|
|
14
18
|
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> str:
|
|
15
19
|
parsed = str(value)
|
|
@@ -13,6 +13,37 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
13
13
|
class Radio(ExcelFieldCodec, str):
|
|
14
14
|
__name__ = 'SingleChoice'
|
|
15
15
|
|
|
16
|
+
@classmethod
|
|
17
|
+
def selection_entity_singular(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 = cls.selection_entity_singular()
|
|
33
|
+
if entity is None:
|
|
34
|
+
base_message = msg(MessageKey.SELECT_ONE_CONFIGURED_OPTION)
|
|
35
|
+
else:
|
|
36
|
+
base_message = msg(MessageKey.SELECT_ONE_CONFIGURED_ENTITY, entity=entity)
|
|
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
|
+
|
|
16
47
|
@classmethod
|
|
17
48
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
18
49
|
declared = field_meta.declared
|
|
@@ -57,7 +88,7 @@ class Radio(ExcelFieldCodec, str):
|
|
|
57
88
|
declared = field_meta.declared
|
|
58
89
|
presentation = field_meta.presentation
|
|
59
90
|
if MULTI_CHECKBOX_SEPARATOR in value:
|
|
60
|
-
raise ValueError(
|
|
91
|
+
raise ValueError(cls._compose_selection_message(field_meta))
|
|
61
92
|
|
|
62
93
|
parsed = value.strip()
|
|
63
94
|
|
|
@@ -76,7 +107,7 @@ class Radio(ExcelFieldCodec, str):
|
|
|
76
107
|
|
|
77
108
|
options_name_map = presentation.options_name_map(field_label=declared.label)
|
|
78
109
|
if parsed not in options_name_map:
|
|
79
|
-
raise ValueError(
|
|
110
|
+
raise ValueError(cls._compose_selection_message(field_meta))
|
|
80
111
|
|
|
81
112
|
return options_name_map[parsed].id
|
|
82
113
|
|
|
@@ -14,6 +14,10 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
14
14
|
class SingleStaff(Radio):
|
|
15
15
|
__name__ = 'SingleStaff'
|
|
16
16
|
|
|
17
|
+
@classmethod
|
|
18
|
+
def selection_entity_singular(cls) -> str | None:
|
|
19
|
+
return 'staff member'
|
|
20
|
+
|
|
17
21
|
@classmethod
|
|
18
22
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
19
23
|
declared = field_meta.declared
|
|
@@ -53,6 +57,10 @@ class SingleStaff(Radio):
|
|
|
53
57
|
class MultiStaff(MultiCheckbox):
|
|
54
58
|
__name__ = 'MultiStaff'
|
|
55
59
|
|
|
60
|
+
@classmethod
|
|
61
|
+
def selection_entity_plural(cls) -> str | None:
|
|
62
|
+
return 'staff members'
|
|
63
|
+
|
|
56
64
|
@classmethod
|
|
57
65
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
58
66
|
declared = field_meta.declared
|
|
@@ -11,6 +11,10 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
11
11
|
class SingleTreeNode(Radio):
|
|
12
12
|
__name__ = 'SingleTreeNode'
|
|
13
13
|
|
|
14
|
+
@classmethod
|
|
15
|
+
def selection_entity_singular(cls) -> str | None:
|
|
16
|
+
return 'tree node'
|
|
17
|
+
|
|
14
18
|
@classmethod
|
|
15
19
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
16
20
|
declared = field_meta.declared
|
|
@@ -47,6 +51,10 @@ class SingleTreeNode(Radio):
|
|
|
47
51
|
class MultiTreeNode(MultiCheckbox):
|
|
48
52
|
__name__ = 'MultiTreeNode'
|
|
49
53
|
|
|
54
|
+
@classmethod
|
|
55
|
+
def selection_entity_plural(cls) -> str | None:
|
|
56
|
+
return 'tree nodes'
|
|
57
|
+
|
|
50
58
|
@classmethod
|
|
51
59
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
52
60
|
declared = field_meta.declared
|
|
@@ -10,6 +10,10 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
10
10
|
class Url(String):
|
|
11
11
|
_validator = TypeAdapter(HttpUrl)
|
|
12
12
|
|
|
13
|
+
@classmethod
|
|
14
|
+
def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
|
|
15
|
+
return msg(MessageKey.VALID_URL_REQUIRED)
|
|
16
|
+
|
|
13
17
|
@classmethod
|
|
14
18
|
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> str:
|
|
15
19
|
parsed = str(value)
|