ExcelAlchemy 2.2.3__tar.gz → 2.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/PKG-INFO +43 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/README-pypi.md +42 -1
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/__init__.py +11 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/constants.py +0 -3
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/identity.py +2 -4
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/base.py +36 -13
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/boolean.py +14 -8
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/date.py +42 -21
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/date_range.py +22 -14
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/money.py +17 -4
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/multi_checkbox.py +24 -12
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/number.py +51 -29
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/number_range.py +4 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/organization.py +16 -7
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/phone_number.py +2 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/radio.py +24 -17
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/staff.py +17 -8
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/string.py +17 -13
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/tree.py +20 -10
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/url.py +2 -3
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/alchemy.py +29 -15
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/headers.py +2 -3
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/import_session.py +28 -12
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/rows.py +16 -19
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/schema.py +28 -16
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/storage.py +2 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/writer.py +26 -14
- excelalchemy-2.2.5/src/excelalchemy/exceptions.py +145 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/helper/pydantic.py +69 -17
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/i18n/messages.py +15 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/metadata.py +210 -91
- excelalchemy-2.2.5/src/excelalchemy/results.py +173 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/util/converter.py +2 -3
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/util/file.py +23 -7
- excelalchemy-2.2.3/src/excelalchemy/exceptions.py +0 -82
- excelalchemy-2.2.3/src/excelalchemy/results.py +0 -91
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/LICENSE +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/pyproject.toml +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/codecs/email.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.5}/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.5
|
|
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.5`, which continues the 2.x line with richer import-failure feedback, clearer documentation entry points, stronger examples, and stronger smoke coverage.
|
|
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)
|
|
53
55
|
|
|
54
56
|
## Screenshots
|
|
55
57
|
|
|
@@ -114,6 +116,45 @@ 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
|
+
|
|
154
|
+
For a single GitHub page that combines screenshots, representative workflows,
|
|
155
|
+
and captured outputs, see the
|
|
156
|
+
[Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
|
|
157
|
+
|
|
117
158
|
## Why ExcelAlchemy
|
|
118
159
|
|
|
119
160
|
- Pydantic v2-based schema extraction and validation
|
|
@@ -10,7 +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
|
-
|
|
13
|
+
The current stable release is `2.2.5`, which continues the 2.x line with richer import-failure feedback, clearer documentation entry points, stronger examples, and stronger smoke coverage.
|
|
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)
|
|
14
16
|
|
|
15
17
|
## Screenshots
|
|
16
18
|
|
|
@@ -75,6 +77,45 @@ alchemy = ExcelAlchemy(ImporterConfig(Importer, locale='en'))
|
|
|
75
77
|
template = alchemy.download_template_artifact(filename='people-template.xlsx')
|
|
76
78
|
```
|
|
77
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
|
+
|
|
115
|
+
For a single GitHub page that combines screenshots, representative workflows,
|
|
116
|
+
and captured outputs, see the
|
|
117
|
+
[Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
|
|
118
|
+
|
|
78
119
|
## Why ExcelAlchemy
|
|
79
120
|
|
|
80
121
|
- 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.5'
|
|
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,21 @@ 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
|
+
ImportResult,
|
|
54
|
+
RowIssueMap,
|
|
55
|
+
ValidateHeaderResult,
|
|
56
|
+
ValidateResult,
|
|
57
|
+
ValidateRowResult,
|
|
58
|
+
)
|
|
52
59
|
from excelalchemy.util.file import flatten
|
|
53
60
|
|
|
54
61
|
__all__ = [
|
|
55
62
|
'Base64Str',
|
|
56
63
|
'Boolean',
|
|
57
64
|
'BooleanCodec',
|
|
65
|
+
'CellErrorMap',
|
|
58
66
|
'ColumnIndex',
|
|
59
67
|
'CompositeExcelFieldCodec',
|
|
60
68
|
'ConfigError',
|
|
@@ -104,6 +112,7 @@ __all__ = [
|
|
|
104
112
|
'ProgrammaticError',
|
|
105
113
|
'Radio',
|
|
106
114
|
'RowIndex',
|
|
115
|
+
'RowIssueMap',
|
|
107
116
|
'SingleChoiceCodec',
|
|
108
117
|
'SingleOrganization',
|
|
109
118
|
'SingleOrganizationCodec',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import StrEnum
|
|
3
|
-
from typing import Any
|
|
4
3
|
|
|
5
4
|
from excelalchemy._primitives.identity import Key, Label, OptionId
|
|
6
5
|
from excelalchemy.i18n.messages import MessageKey
|
|
@@ -38,8 +37,6 @@ MILLISECOND_TO_SECOND = 1000
|
|
|
38
37
|
MAX_OPTIONS_COUNT = 100
|
|
39
38
|
|
|
40
39
|
DEFAULT_FIELD_META_ORDER = -1
|
|
41
|
-
type DictStrAny = dict[str, Any]
|
|
42
|
-
type DictAny = dict[Any, Any]
|
|
43
40
|
type SetStr = set[str]
|
|
44
41
|
type ListStr = list[str]
|
|
45
42
|
type IntStr = int | str
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Internal typed primitives used across the ExcelAlchemy core layer."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
3
|
from pydantic import GetCoreSchemaHandler
|
|
6
4
|
from pydantic_core import core_schema
|
|
7
5
|
|
|
@@ -10,7 +8,7 @@ class _StringIdentity(str):
|
|
|
10
8
|
@classmethod
|
|
11
9
|
def __get_pydantic_core_schema__(
|
|
12
10
|
cls,
|
|
13
|
-
source_type:
|
|
11
|
+
source_type: object,
|
|
14
12
|
handler: GetCoreSchemaHandler,
|
|
15
13
|
) -> core_schema.CoreSchema:
|
|
16
14
|
return core_schema.no_info_after_validator_function(cls, core_schema.str_schema())
|
|
@@ -20,7 +18,7 @@ class _IntegerIdentity(int):
|
|
|
20
18
|
@classmethod
|
|
21
19
|
def __get_pydantic_core_schema__(
|
|
22
20
|
cls,
|
|
23
|
-
source_type:
|
|
21
|
+
source_type: object,
|
|
24
22
|
handler: GetCoreSchemaHandler,
|
|
25
23
|
) -> core_schema.CoreSchema:
|
|
26
24
|
return core_schema.no_info_after_validator_function(cls, core_schema.int_schema())
|
|
@@ -11,6 +11,13 @@ from excelalchemy._primitives.identity import Key
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from excelalchemy.metadata import FieldMetaInfo
|
|
13
13
|
|
|
14
|
+
# These aliases remain `Any` intentionally because codec subclasses narrow their
|
|
15
|
+
# accepted workbook values heavily. Using `object` here makes every override
|
|
16
|
+
# incompatible under pyright's method override rules.
|
|
17
|
+
type WorkbookInputValue = Any
|
|
18
|
+
type WorkbookDisplayValue = Any
|
|
19
|
+
type NormalizedImportValue = Any
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
class ExcelFieldCodec(ABC):
|
|
16
23
|
"""Excel-facing field adapter responsible for comments, parsing, formatting, and normalization."""
|
|
@@ -22,17 +29,17 @@ class ExcelFieldCodec(ABC):
|
|
|
22
29
|
|
|
23
30
|
@classmethod
|
|
24
31
|
@abstractmethod
|
|
25
|
-
def parse_input(cls, value:
|
|
32
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
26
33
|
"""Parse workbook input into the intermediate Python value consumed by the import pipeline."""
|
|
27
34
|
|
|
28
35
|
@classmethod
|
|
29
36
|
@abstractmethod
|
|
30
|
-
def format_display_value(cls, value:
|
|
37
|
+
def format_display_value(cls, value: WorkbookDisplayValue, field_meta: FieldMetaInfo) -> WorkbookDisplayValue:
|
|
31
38
|
"""Format a raw worksheet value back into a user-recognizable display value."""
|
|
32
39
|
|
|
33
40
|
@classmethod
|
|
34
41
|
@abstractmethod
|
|
35
|
-
def normalize_import_value(cls, value:
|
|
42
|
+
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
36
43
|
"""Validate and normalize parsed input before handing it to the Pydantic layer."""
|
|
37
44
|
|
|
38
45
|
@classmethod
|
|
@@ -41,24 +48,24 @@ class ExcelFieldCodec(ABC):
|
|
|
41
48
|
return cls.build_comment(field_meta)
|
|
42
49
|
|
|
43
50
|
@classmethod
|
|
44
|
-
def serialize(cls, value:
|
|
51
|
+
def serialize(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
45
52
|
"""Backward-compatible alias for parse_input()."""
|
|
46
53
|
return cls.parse_input(value, field_meta)
|
|
47
54
|
|
|
48
55
|
@classmethod
|
|
49
|
-
def deserialize(cls, value:
|
|
56
|
+
def deserialize(cls, value: WorkbookDisplayValue, field_meta: FieldMetaInfo) -> WorkbookDisplayValue:
|
|
50
57
|
"""Backward-compatible alias for format_display_value()."""
|
|
51
58
|
return cls.format_display_value(value, field_meta)
|
|
52
59
|
|
|
53
60
|
@classmethod
|
|
54
|
-
def __validate__(cls, value:
|
|
61
|
+
def __validate__(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
55
62
|
"""Backward-compatible alias for normalize_import_value()."""
|
|
56
63
|
return cls.normalize_import_value(value, field_meta)
|
|
57
64
|
|
|
58
65
|
@classmethod
|
|
59
66
|
def __get_pydantic_core_schema__(
|
|
60
67
|
cls,
|
|
61
|
-
source_type:
|
|
68
|
+
source_type: object,
|
|
62
69
|
handler: GetCoreSchemaHandler,
|
|
63
70
|
) -> core_schema.CoreSchema:
|
|
64
71
|
# ExcelAlchemy runs metadata-aware validation in its adapter layer.
|
|
@@ -88,15 +95,23 @@ class SystemReserved(ExcelFieldCodec):
|
|
|
88
95
|
return ''
|
|
89
96
|
|
|
90
97
|
@classmethod
|
|
91
|
-
def parse_input(cls, value:
|
|
98
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
92
99
|
return value
|
|
93
100
|
|
|
94
101
|
@classmethod
|
|
95
|
-
def format_display_value(
|
|
102
|
+
def format_display_value(
|
|
103
|
+
cls,
|
|
104
|
+
value: WorkbookDisplayValue,
|
|
105
|
+
field_meta: FieldMetaInfo,
|
|
106
|
+
) -> WorkbookDisplayValue:
|
|
96
107
|
return value
|
|
97
108
|
|
|
98
109
|
@classmethod
|
|
99
|
-
def normalize_import_value(
|
|
110
|
+
def normalize_import_value(
|
|
111
|
+
cls,
|
|
112
|
+
value: WorkbookInputValue,
|
|
113
|
+
field_meta: FieldMetaInfo,
|
|
114
|
+
) -> NormalizedImportValue:
|
|
100
115
|
return value
|
|
101
116
|
|
|
102
117
|
|
|
@@ -108,15 +123,23 @@ class Undefined(ExcelFieldCodec):
|
|
|
108
123
|
return ''
|
|
109
124
|
|
|
110
125
|
@classmethod
|
|
111
|
-
def parse_input(cls, value:
|
|
126
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
112
127
|
return value
|
|
113
128
|
|
|
114
129
|
@classmethod
|
|
115
|
-
def format_display_value(
|
|
130
|
+
def format_display_value(
|
|
131
|
+
cls,
|
|
132
|
+
value: WorkbookDisplayValue,
|
|
133
|
+
field_meta: FieldMetaInfo,
|
|
134
|
+
) -> WorkbookDisplayValue:
|
|
116
135
|
return value
|
|
117
136
|
|
|
118
137
|
@classmethod
|
|
119
|
-
def normalize_import_value(
|
|
138
|
+
def normalize_import_value(
|
|
139
|
+
cls,
|
|
140
|
+
value: WorkbookInputValue,
|
|
141
|
+
field_meta: FieldMetaInfo,
|
|
142
|
+
) -> NormalizedImportValue:
|
|
120
143
|
return value
|
|
121
144
|
|
|
122
145
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Any
|
|
3
2
|
|
|
4
3
|
from excelalchemy.codecs import excel_choice_codec
|
|
5
|
-
from excelalchemy.codecs.base import ExcelFieldCodec
|
|
4
|
+
from excelalchemy.codecs.base import ExcelFieldCodec, WorkbookDisplayValue, WorkbookInputValue
|
|
6
5
|
from excelalchemy.i18n.messages import MessageKey
|
|
7
6
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
8
7
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -31,19 +30,26 @@ class Boolean(ExcelFieldCodec):
|
|
|
31
30
|
|
|
32
31
|
@classmethod
|
|
33
32
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
33
|
+
declared = field_meta.declared
|
|
34
|
+
presentation = field_meta.presentation
|
|
34
35
|
return '\n'.join(
|
|
35
36
|
[
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
declared.comment_required,
|
|
38
|
+
presentation.comment_hint,
|
|
38
39
|
]
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
@classmethod
|
|
42
|
-
def parse_input(cls, value:
|
|
43
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> str:
|
|
43
44
|
return str(value).strip()
|
|
44
45
|
|
|
45
46
|
@classmethod
|
|
46
|
-
def format_display_value(
|
|
47
|
+
def format_display_value(
|
|
48
|
+
cls,
|
|
49
|
+
value: bool | str | WorkbookDisplayValue | None,
|
|
50
|
+
field_meta: FieldMetaInfo,
|
|
51
|
+
) -> str:
|
|
52
|
+
declared = field_meta.declared
|
|
47
53
|
if value is None or value == '':
|
|
48
54
|
return cls._false_display()
|
|
49
55
|
|
|
@@ -64,14 +70,14 @@ class Boolean(ExcelFieldCodec):
|
|
|
64
70
|
'Type %s could not deserialize %s for field %s; returning the default value %s',
|
|
65
71
|
cls.__name__,
|
|
66
72
|
value,
|
|
67
|
-
|
|
73
|
+
declared.label,
|
|
68
74
|
cls._false_display(),
|
|
69
75
|
)
|
|
70
76
|
|
|
71
77
|
return cls._true_display() if str(value) in cls._true_values() else cls._false_display()
|
|
72
78
|
|
|
73
79
|
@classmethod
|
|
74
|
-
def normalize_import_value(cls, value: str | bool |
|
|
80
|
+
def normalize_import_value(cls, value: str | bool | WorkbookInputValue, field_meta: FieldMetaInfo) -> bool:
|
|
75
81
|
if isinstance(value, bool):
|
|
76
82
|
return value
|
|
77
83
|
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import cast
|
|
4
4
|
|
|
5
5
|
import pendulum
|
|
6
6
|
from pendulum import DateTime
|
|
7
7
|
|
|
8
8
|
from excelalchemy._primitives.constants import DATE_FORMAT_TO_HINT_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
|
|
9
|
-
from excelalchemy.codecs.base import
|
|
9
|
+
from excelalchemy.codecs.base import (
|
|
10
|
+
ExcelFieldCodec,
|
|
11
|
+
NormalizedImportValue,
|
|
12
|
+
WorkbookDisplayValue,
|
|
13
|
+
WorkbookInputValue,
|
|
14
|
+
)
|
|
10
15
|
from excelalchemy.exceptions import ConfigError
|
|
11
16
|
from excelalchemy.i18n.messages import MessageKey
|
|
12
17
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -18,36 +23,44 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
18
23
|
|
|
19
24
|
@classmethod
|
|
20
25
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
21
|
-
|
|
26
|
+
declared = field_meta.declared
|
|
27
|
+
presentation = field_meta.presentation
|
|
28
|
+
if not presentation.date_format:
|
|
22
29
|
raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
|
|
23
30
|
return '\n'.join(
|
|
24
31
|
[
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
declared.comment_required,
|
|
33
|
+
presentation.comment_date_format,
|
|
34
|
+
presentation.comment_date_range_option,
|
|
35
|
+
presentation.comment_hint,
|
|
29
36
|
]
|
|
30
37
|
)
|
|
31
38
|
|
|
32
39
|
@classmethod
|
|
33
|
-
def parse_input(
|
|
40
|
+
def parse_input(
|
|
41
|
+
cls,
|
|
42
|
+
value: str | DateTime | WorkbookInputValue,
|
|
43
|
+
field_meta: FieldMetaInfo,
|
|
44
|
+
) -> datetime | WorkbookInputValue:
|
|
45
|
+
declared = field_meta.declared
|
|
46
|
+
presentation = field_meta.presentation
|
|
34
47
|
if isinstance(value, DateTime):
|
|
35
48
|
logging.info(
|
|
36
49
|
'Codec %s received a parsed datetime for %s; returning it unchanged: %s',
|
|
37
50
|
cls.__name__,
|
|
38
|
-
|
|
51
|
+
declared.label,
|
|
39
52
|
value,
|
|
40
53
|
)
|
|
41
54
|
return value
|
|
42
55
|
|
|
43
|
-
if not
|
|
56
|
+
if not presentation.date_format:
|
|
44
57
|
raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
|
|
45
58
|
|
|
46
59
|
value = str(value).strip()
|
|
47
60
|
try:
|
|
48
61
|
v = value.replace('/', '-') # pendulum does not accept "/" as a date separator here.
|
|
49
62
|
dt: DateTime = cast(DateTime, pendulum.parse(v))
|
|
50
|
-
return dt.replace(tzinfo=
|
|
63
|
+
return dt.replace(tzinfo=presentation.timezone)
|
|
51
64
|
except Exception as exc:
|
|
52
65
|
logging.warning(
|
|
53
66
|
'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
|
|
@@ -58,27 +71,33 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
58
71
|
return value
|
|
59
72
|
|
|
60
73
|
@classmethod
|
|
61
|
-
def format_display_value(
|
|
74
|
+
def format_display_value(
|
|
75
|
+
cls,
|
|
76
|
+
value: str | datetime | WorkbookDisplayValue | None,
|
|
77
|
+
field_meta: FieldMetaInfo,
|
|
78
|
+
) -> str:
|
|
79
|
+
presentation = field_meta.presentation
|
|
62
80
|
match value:
|
|
63
81
|
case None | '':
|
|
64
82
|
return ''
|
|
65
83
|
case datetime():
|
|
66
|
-
return value.strftime(
|
|
84
|
+
return value.strftime(presentation.python_date_format)
|
|
67
85
|
case int() | float():
|
|
68
86
|
return datetime.fromtimestamp(int(value) / MILLISECOND_TO_SECOND).strftime(
|
|
69
|
-
|
|
87
|
+
presentation.python_date_format
|
|
70
88
|
)
|
|
71
89
|
case _:
|
|
72
90
|
return str(value) if value is not None else ''
|
|
73
91
|
|
|
74
92
|
@classmethod
|
|
75
|
-
def normalize_import_value(cls, value:
|
|
76
|
-
|
|
93
|
+
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
94
|
+
presentation = field_meta.presentation
|
|
95
|
+
if presentation.date_format is None:
|
|
77
96
|
raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
|
|
78
97
|
|
|
79
98
|
if not isinstance(value, datetime):
|
|
80
99
|
raise ValueError(
|
|
81
|
-
msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[
|
|
100
|
+
msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[presentation.date_format])
|
|
82
101
|
)
|
|
83
102
|
|
|
84
103
|
parsed = cls._parse_date(value, field_meta)
|
|
@@ -91,17 +110,19 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
91
110
|
|
|
92
111
|
@staticmethod
|
|
93
112
|
def _parse_date(v: datetime, field_meta: FieldMetaInfo) -> datetime:
|
|
94
|
-
|
|
113
|
+
presentation = field_meta.presentation
|
|
114
|
+
format_ = presentation.python_date_format
|
|
95
115
|
parsed = datetime.strptime(v.strftime(format_), format_)
|
|
96
|
-
parsed = parsed.replace(tzinfo=
|
|
116
|
+
parsed = parsed.replace(tzinfo=presentation.timezone)
|
|
97
117
|
return parsed
|
|
98
118
|
|
|
99
119
|
@staticmethod
|
|
100
120
|
def _validate_date_range(parsed: datetime, field_meta: FieldMetaInfo) -> list[str]:
|
|
101
|
-
|
|
121
|
+
presentation = field_meta.presentation
|
|
122
|
+
now = datetime.now(tz=presentation.timezone)
|
|
102
123
|
errors: list[str] = []
|
|
103
124
|
|
|
104
|
-
match
|
|
125
|
+
match presentation.date_range_option:
|
|
105
126
|
case DataRangeOption.PRE:
|
|
106
127
|
if parsed > now:
|
|
107
128
|
errors.append(msg(MessageKey.DATE_MUST_BE_EARLIER_THAN_NOW))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from collections.abc import Mapping
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import cast
|
|
5
5
|
|
|
6
6
|
import pendulum
|
|
7
7
|
from pendulum import DateTime
|
|
@@ -10,6 +10,7 @@ from pydantic import BaseModel
|
|
|
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
12
|
from excelalchemy.codecs.base import CompositeExcelFieldCodec
|
|
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
|
|
@@ -28,7 +29,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
28
29
|
__name__ = 'DateRange'
|
|
29
30
|
|
|
30
31
|
@classmethod
|
|
31
|
-
def model_validate(cls, obj:
|
|
32
|
+
def model_validate(cls, obj: object) -> 'DateRange':
|
|
32
33
|
impl = _DateRangeImpl.model_validate(obj)
|
|
33
34
|
self = cls(impl.start, impl.end)
|
|
34
35
|
return self
|
|
@@ -50,14 +51,18 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
50
51
|
|
|
51
52
|
@classmethod
|
|
52
53
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
declared = field_meta.declared
|
|
55
|
+
presentation = field_meta.presentation
|
|
56
|
+
if presentation.date_format is None:
|
|
57
|
+
raise ConfigError(
|
|
58
|
+
msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED), message_key=MessageKey.DATE_FORMAT_NOT_CONFIGURED
|
|
59
|
+
)
|
|
55
60
|
|
|
56
61
|
return '\n'.join(
|
|
57
62
|
[
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
dmsg(MessageKey.COMMENT_DATE_RANGE_START_NOT_AFTER_END, extra_hint=
|
|
63
|
+
declared.comment_required,
|
|
64
|
+
presentation.comment_date_format,
|
|
65
|
+
dmsg(MessageKey.COMMENT_DATE_RANGE_START_NOT_AFTER_END, extra_hint=presentation.hint or ''),
|
|
61
66
|
]
|
|
62
67
|
)
|
|
63
68
|
|
|
@@ -92,20 +97,21 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
92
97
|
value: object,
|
|
93
98
|
field_meta: FieldMetaInfo,
|
|
94
99
|
) -> 'DateRange':
|
|
100
|
+
presentation = field_meta.presentation
|
|
95
101
|
try:
|
|
96
102
|
parsed = DateRange.model_validate(value)
|
|
97
|
-
parsed.start = pendulum.instance(parsed.start, tz=
|
|
98
|
-
parsed.end = pendulum.instance(parsed.end, tz=
|
|
103
|
+
parsed.start = pendulum.instance(parsed.start, tz=presentation.timezone) if parsed.start else None
|
|
104
|
+
parsed.end = pendulum.instance(parsed.end, tz=presentation.timezone) if parsed.end else None
|
|
99
105
|
except Exception as exc:
|
|
100
106
|
raise ValueError(msg(MessageKey.INVALID_INPUT)) from exc
|
|
101
107
|
|
|
102
108
|
errors: list[str] = []
|
|
103
|
-
now = datetime.now(tz=
|
|
109
|
+
now = datetime.now(tz=presentation.timezone)
|
|
104
110
|
|
|
105
111
|
if parsed.start and parsed.end and parsed.start > parsed.end:
|
|
106
112
|
errors.append(msg(MessageKey.DATE_RANGE_START_AFTER_END))
|
|
107
113
|
|
|
108
|
-
match
|
|
114
|
+
match presentation.date_range_option:
|
|
109
115
|
case DataRangeOption.PRE:
|
|
110
116
|
if (parsed.start and parsed.start > now) or (parsed.end and parsed.end > now):
|
|
111
117
|
errors.append(msg(MessageKey.DATE_MUST_BE_EARLIER_THAN_NOW))
|
|
@@ -124,7 +130,8 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
124
130
|
def format_display_value(cls, value: object | None, field_meta: FieldMetaInfo) -> str:
|
|
125
131
|
if value is None or value == '':
|
|
126
132
|
return ''
|
|
127
|
-
|
|
133
|
+
presentation = field_meta.presentation
|
|
134
|
+
date_format = presentation.must_date_format
|
|
128
135
|
py_date_format = DATE_FORMAT_TO_PYTHON_MAPPING[date_format]
|
|
129
136
|
|
|
130
137
|
if isinstance(value, str):
|
|
@@ -178,11 +185,12 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
178
185
|
|
|
179
186
|
@staticmethod
|
|
180
187
|
def _parse_datetime_text(value: str, field_meta: FieldMetaInfo) -> DateTime:
|
|
188
|
+
presentation = field_meta.presentation
|
|
181
189
|
parsed = pendulum.parse(value)
|
|
182
190
|
if isinstance(parsed, DateTime):
|
|
183
|
-
return parsed.replace(tzinfo=
|
|
191
|
+
return parsed.replace(tzinfo=presentation.timezone)
|
|
184
192
|
if isinstance(parsed, datetime):
|
|
185
|
-
return pendulum.instance(parsed).replace(tzinfo=
|
|
193
|
+
return pendulum.instance(parsed).replace(tzinfo=presentation.timezone)
|
|
186
194
|
raise ValueError(msg(MessageKey.INVALID_INPUT))
|
|
187
195
|
|
|
188
196
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from dataclasses import replace
|
|
2
|
+
from typing import ClassVar
|
|
2
3
|
|
|
4
|
+
from excelalchemy.codecs.base import NormalizedImportValue, WorkbookDisplayValue, WorkbookInputValue
|
|
3
5
|
from excelalchemy.codecs.number import Number
|
|
4
6
|
from excelalchemy.metadata import FieldMetaInfo
|
|
5
7
|
|
|
@@ -10,7 +12,10 @@ class Money(Number):
|
|
|
10
12
|
@classmethod
|
|
11
13
|
def _money_field_meta(cls, field_meta: FieldMetaInfo) -> FieldMetaInfo:
|
|
12
14
|
money_field_meta = field_meta.clone()
|
|
13
|
-
money_field_meta.
|
|
15
|
+
money_field_meta.presentation_meta = replace(
|
|
16
|
+
money_field_meta.presentation_meta,
|
|
17
|
+
fraction_digits=cls.MONEY_FRACTION_DIGITS,
|
|
18
|
+
)
|
|
14
19
|
return money_field_meta
|
|
15
20
|
|
|
16
21
|
@classmethod
|
|
@@ -18,11 +23,19 @@ class Money(Number):
|
|
|
18
23
|
return super().build_comment(cls._money_field_meta(field_meta))
|
|
19
24
|
|
|
20
25
|
@classmethod
|
|
21
|
-
def format_display_value(
|
|
26
|
+
def format_display_value(
|
|
27
|
+
cls,
|
|
28
|
+
value: str | WorkbookDisplayValue | None,
|
|
29
|
+
field_meta: FieldMetaInfo,
|
|
30
|
+
) -> str:
|
|
22
31
|
return super().format_display_value(value, cls._money_field_meta(field_meta))
|
|
23
32
|
|
|
24
33
|
@classmethod
|
|
25
|
-
def normalize_import_value(
|
|
34
|
+
def normalize_import_value(
|
|
35
|
+
cls,
|
|
36
|
+
value: WorkbookInputValue,
|
|
37
|
+
field_meta: FieldMetaInfo,
|
|
38
|
+
) -> NormalizedImportValue:
|
|
26
39
|
return super().normalize_import_value(value, cls._money_field_meta(field_meta))
|
|
27
40
|
|
|
28
41
|
|