ExcelAlchemy 2.2.4__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.4 → excelalchemy-2.2.5}/PKG-INFO +43 -2
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/README-pypi.md +42 -1
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/__init__.py +11 -2
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/date_range.py +4 -1
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/multi_checkbox.py +10 -4
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/organization.py +2 -1
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/staff.py +2 -1
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/alchemy.py +9 -9
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/headers.py +2 -3
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/import_session.py +16 -10
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/rows.py +10 -14
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/schema.py +8 -2
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/writer.py +9 -3
- excelalchemy-2.2.5/src/excelalchemy/exceptions.py +145 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/helper/pydantic.py +50 -5
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/i18n/messages.py +15 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/metadata.py +9 -3
- excelalchemy-2.2.5/src/excelalchemy/results.py +173 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/util/converter.py +2 -3
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/util/file.py +21 -5
- excelalchemy-2.2.4/src/excelalchemy/exceptions.py +0 -80
- excelalchemy-2.2.4/src/excelalchemy/results.py +0 -91
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/LICENSE +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/pyproject.toml +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/base.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/boolean.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/date.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/email.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/number.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/number_range.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/phone_number.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/radio.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/tree.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/codecs/url.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/storage.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.4 → excelalchemy-2.2.5}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.4 → 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',
|
|
@@ -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
|
|
@@ -53,7 +54,9 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
53
54
|
declared = field_meta.declared
|
|
54
55
|
presentation = field_meta.presentation
|
|
55
56
|
if presentation.date_format is None:
|
|
56
|
-
raise
|
|
57
|
+
raise ConfigError(
|
|
58
|
+
msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED), message_key=MessageKey.DATE_FORMAT_NOT_CONFIGURED
|
|
59
|
+
)
|
|
57
60
|
|
|
58
61
|
return '\n'.join(
|
|
59
62
|
[
|
|
@@ -14,6 +14,12 @@ from excelalchemy.metadata import FieldMetaInfo
|
|
|
14
14
|
class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
15
15
|
__name__ = 'MultiChoice'
|
|
16
16
|
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _coerce_items(value: object) -> list[object] | None:
|
|
19
|
+
if not isinstance(value, list):
|
|
20
|
+
return None
|
|
21
|
+
return cast(list[object], value)
|
|
22
|
+
|
|
17
23
|
@classmethod
|
|
18
24
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
19
25
|
declared = field_meta.declared
|
|
@@ -29,8 +35,8 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
29
35
|
|
|
30
36
|
@classmethod
|
|
31
37
|
def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> list[str] | object:
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
items = cls._coerce_items(value)
|
|
39
|
+
if items is not None:
|
|
34
40
|
return [str(item).strip() for item in items]
|
|
35
41
|
|
|
36
42
|
if isinstance(value, str):
|
|
@@ -45,10 +51,10 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
45
51
|
def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> list[str]: # OptionId
|
|
46
52
|
declared = field_meta.declared
|
|
47
53
|
presentation = field_meta.presentation
|
|
48
|
-
|
|
54
|
+
items = cls._coerce_items(value)
|
|
55
|
+
if items is None:
|
|
49
56
|
raise ValueError(msg(MessageKey.OPTION_NOT_FOUND_HEADER_COMMENT))
|
|
50
57
|
|
|
51
|
-
items = cast(list[object], value)
|
|
52
58
|
parsed = [str(item).strip() for item in items]
|
|
53
59
|
|
|
54
60
|
if presentation.options is None:
|
|
@@ -74,7 +74,8 @@ class MultiOrganization(MultiCheckbox):
|
|
|
74
74
|
return value
|
|
75
75
|
|
|
76
76
|
if isinstance(value, list):
|
|
77
|
-
items = cast(
|
|
77
|
+
items = MultiOrganization._coerce_items(cast(object, value))
|
|
78
|
+
assert items is not None
|
|
78
79
|
option_ids = [OptionId(option_id) for option_id in items]
|
|
79
80
|
option_names = presentation.exchange_option_ids_to_names(option_ids, field_label=declared.label)
|
|
80
81
|
return MULTI_CHECKBOX_SEPARATOR.join(map(str, option_names))
|
|
@@ -80,7 +80,8 @@ class MultiStaff(MultiCheckbox):
|
|
|
80
80
|
return value
|
|
81
81
|
|
|
82
82
|
if isinstance(value, list):
|
|
83
|
-
items = cast(
|
|
83
|
+
items = MultiStaff._coerce_items(cast(object, value))
|
|
84
|
+
assert items is not None
|
|
84
85
|
option_ids = [OptionId(option_id) for option_id in items]
|
|
85
86
|
if len(option_ids) != len(set(option_ids)):
|
|
86
87
|
raise ValueError(msg(MessageKey.OPTIONS_CONTAIN_DUPLICATES))
|
|
@@ -9,7 +9,7 @@ from excelalchemy._primitives.constants import (
|
|
|
9
9
|
RESULT_COLUMN_KEY,
|
|
10
10
|
)
|
|
11
11
|
from excelalchemy._primitives.header_models import ExcelHeader
|
|
12
|
-
from excelalchemy._primitives.identity import
|
|
12
|
+
from excelalchemy._primitives.identity import DataUrlStr, Label, UniqueKey, UniqueLabel, UrlStr
|
|
13
13
|
from excelalchemy._primitives.payloads import DataConverter, ExportRowPayload
|
|
14
14
|
from excelalchemy.artifacts import ExcelArtifact
|
|
15
15
|
from excelalchemy.codecs.base import SystemReserved
|
|
@@ -22,13 +22,13 @@ from excelalchemy.core.schema import ExcelSchemaLayout
|
|
|
22
22
|
from excelalchemy.core.storage import build_storage_gateway
|
|
23
23
|
from excelalchemy.core.storage_protocol import ExcelStorage
|
|
24
24
|
from excelalchemy.core.table import WorksheetTable
|
|
25
|
-
from excelalchemy.exceptions import ConfigError
|
|
25
|
+
from excelalchemy.exceptions import ConfigError
|
|
26
26
|
from excelalchemy.helper.pydantic import get_model_field_names
|
|
27
27
|
from excelalchemy.i18n.messages import MessageKey, use_display_locale
|
|
28
28
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
29
29
|
from excelalchemy.i18n.messages import message as msg
|
|
30
30
|
from excelalchemy.metadata import FieldMetaInfo
|
|
31
|
-
from excelalchemy.results import ImportResult
|
|
31
|
+
from excelalchemy.results import CellErrorMap, ImportResult, RowIssueMap
|
|
32
32
|
from excelalchemy.util.file import flatten
|
|
33
33
|
|
|
34
34
|
HEADER_HINT_LINE_COUNT = 1
|
|
@@ -210,24 +210,24 @@ class ExcelAlchemy[
|
|
|
210
210
|
return self._last_import_session.header_table
|
|
211
211
|
|
|
212
212
|
@property
|
|
213
|
-
def cell_error_map(self) ->
|
|
213
|
+
def cell_error_map(self) -> CellErrorMap:
|
|
214
214
|
if self._last_import_session is None:
|
|
215
|
-
return
|
|
215
|
+
return CellErrorMap()
|
|
216
216
|
return self._last_import_session.cell_error_map
|
|
217
217
|
|
|
218
218
|
@property
|
|
219
|
-
def row_error_map(self) ->
|
|
219
|
+
def row_error_map(self) -> RowIssueMap:
|
|
220
220
|
if self._last_import_session is None:
|
|
221
|
-
return
|
|
221
|
+
return RowIssueMap()
|
|
222
222
|
return self._last_import_session.row_error_map
|
|
223
223
|
|
|
224
224
|
@property
|
|
225
|
-
def cell_errors(self) ->
|
|
225
|
+
def cell_errors(self) -> CellErrorMap:
|
|
226
226
|
"""Backward-compatible alias for cell_error_map."""
|
|
227
227
|
return self.cell_error_map
|
|
228
228
|
|
|
229
229
|
@property
|
|
230
|
-
def row_errors(self) ->
|
|
230
|
+
def row_errors(self) -> RowIssueMap:
|
|
231
231
|
"""Backward-compatible alias for row_error_map."""
|
|
232
232
|
return self.row_error_map
|
|
233
233
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Header parsing and validation helpers for import workbooks."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Container, Sequence
|
|
4
|
-
from typing import cast
|
|
5
4
|
|
|
6
5
|
from excelalchemy._primitives.header_models import ExcelHeader
|
|
7
6
|
from excelalchemy._primitives.identity import Label, UniqueLabel
|
|
@@ -129,12 +128,12 @@ class ExcelHeaderValidator:
|
|
|
129
128
|
|
|
130
129
|
@staticmethod
|
|
131
130
|
def _ordered_difference[T](values: Sequence[T], allowed: Container[T]) -> list[T]:
|
|
132
|
-
seen: set[
|
|
131
|
+
seen: set[T] = set()
|
|
133
132
|
result: list[T] = []
|
|
134
133
|
for value in values:
|
|
135
134
|
if value in allowed or value in seen:
|
|
136
135
|
continue
|
|
137
|
-
seen.add(
|
|
136
|
+
seen.add(value)
|
|
138
137
|
result.append(value)
|
|
139
138
|
return result
|
|
140
139
|
|
|
@@ -5,13 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
from dataclasses import dataclass, replace
|
|
6
6
|
from enum import StrEnum
|
|
7
7
|
from functools import cached_property
|
|
8
|
-
from typing import cast
|
|
9
8
|
|
|
10
9
|
from pydantic import BaseModel
|
|
11
10
|
|
|
12
11
|
from excelalchemy._primitives.constants import REASON_COLUMN_KEY, RESULT_COLUMN_KEY
|
|
13
12
|
from excelalchemy._primitives.header_models import ExcelHeader
|
|
14
|
-
from excelalchemy._primitives.identity import
|
|
13
|
+
from excelalchemy._primitives.identity import DataUrlStr, RowIndex, UniqueLabel, UrlStr
|
|
15
14
|
from excelalchemy._primitives.payloads import FlatRowPayload, ModelRowPayload
|
|
16
15
|
from excelalchemy.codecs.base import SystemReserved
|
|
17
16
|
from excelalchemy.config import ImporterConfig
|
|
@@ -21,13 +20,13 @@ from excelalchemy.core.rendering import ExcelRenderer
|
|
|
21
20
|
from excelalchemy.core.rows import ImportIssueTracker, RowAggregator
|
|
22
21
|
from excelalchemy.core.schema import ExcelSchemaLayout
|
|
23
22
|
from excelalchemy.core.storage_protocol import ExcelStorage
|
|
24
|
-
from excelalchemy.core.table import WorksheetTable
|
|
25
|
-
from excelalchemy.exceptions import ConfigError
|
|
23
|
+
from excelalchemy.core.table import WorksheetRow, WorksheetTable
|
|
24
|
+
from excelalchemy.exceptions import ConfigError
|
|
26
25
|
from excelalchemy.i18n.messages import MessageKey, use_display_locale
|
|
27
26
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
28
27
|
from excelalchemy.i18n.messages import message as msg
|
|
29
28
|
from excelalchemy.metadata import FieldMetaInfo
|
|
30
|
-
from excelalchemy.results import ImportResult, ValidateHeaderResult, ValidateResult
|
|
29
|
+
from excelalchemy.results import CellErrorMap, ImportResult, RowIssueMap, ValidateHeaderResult, ValidateResult
|
|
31
30
|
|
|
32
31
|
HEADER_HINT_LINE_COUNT = 1
|
|
33
32
|
|
|
@@ -104,20 +103,20 @@ class ImportSession[
|
|
|
104
103
|
self._snapshot = ImportSessionSnapshot()
|
|
105
104
|
|
|
106
105
|
@property
|
|
107
|
-
def cell_error_map(self) ->
|
|
106
|
+
def cell_error_map(self) -> CellErrorMap:
|
|
108
107
|
return self.issue_tracker.cell_errors
|
|
109
108
|
|
|
110
109
|
@property
|
|
111
|
-
def row_error_map(self) ->
|
|
110
|
+
def row_error_map(self) -> RowIssueMap:
|
|
112
111
|
return self.issue_tracker.row_errors
|
|
113
112
|
|
|
114
113
|
@property
|
|
115
|
-
def cell_errors(self) ->
|
|
114
|
+
def cell_errors(self) -> CellErrorMap:
|
|
116
115
|
"""Backward-compatible alias for cell_error_map."""
|
|
117
116
|
return self.cell_error_map
|
|
118
117
|
|
|
119
118
|
@property
|
|
120
|
-
def row_errors(self) ->
|
|
119
|
+
def row_errors(self) -> RowIssueMap:
|
|
121
120
|
"""Backward-compatible alias for row_error_map."""
|
|
122
121
|
return self.row_error_map
|
|
123
122
|
|
|
@@ -240,7 +239,7 @@ class ImportSession[
|
|
|
240
239
|
processed_row_count = 0
|
|
241
240
|
for table_row_index in range(self.extra_header_count_on_import, len(self.worksheet_table)):
|
|
242
241
|
row = self.worksheet_table.row_at(table_row_index)
|
|
243
|
-
aggregate_data = self._aggregate_data(
|
|
242
|
+
aggregate_data = self._aggregate_data(self._row_payload(row))
|
|
244
243
|
success = await self.executor.execute(RowIndex(table_row_index), aggregate_data, self.worksheet_table)
|
|
245
244
|
processed_row_count += 1
|
|
246
245
|
all_success = all_success and success
|
|
@@ -265,6 +264,13 @@ class ImportSession[
|
|
|
265
264
|
def _aggregate_data(self, row_data: FlatRowPayload) -> ModelRowPayload:
|
|
266
265
|
return self.row_aggregator.aggregate(row_data)
|
|
267
266
|
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _row_payload(row: WorksheetRow) -> FlatRowPayload:
|
|
269
|
+
payload: FlatRowPayload = {}
|
|
270
|
+
for key, value in row.items():
|
|
271
|
+
payload[str(key)] = value
|
|
272
|
+
return payload
|
|
273
|
+
|
|
268
274
|
def _render_import_result_excel(self) -> DataUrlStr:
|
|
269
275
|
return self.renderer.render_data(
|
|
270
276
|
self.worksheet_table,
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Row aggregation and import issue tracking helpers."""
|
|
2
2
|
|
|
3
|
-
from collections import defaultdict
|
|
4
3
|
from collections.abc import Iterator
|
|
5
|
-
from typing import cast
|
|
6
4
|
|
|
7
5
|
from excelalchemy._primitives.identity import ColumnIndex, Key, RowIndex, UniqueLabel
|
|
8
6
|
from excelalchemy._primitives.payloads import AggregatedRowPayload, ModelRowPayload, RowPayloadLike
|
|
@@ -11,7 +9,7 @@ from excelalchemy.exceptions import ConfigError, ExcelCellError, ExcelRowError
|
|
|
11
9
|
from excelalchemy.i18n.messages import MessageKey
|
|
12
10
|
from excelalchemy.i18n.messages import message as msg
|
|
13
11
|
from excelalchemy.metadata import FieldMetaInfo
|
|
14
|
-
from excelalchemy.results import ValidateRowResult
|
|
12
|
+
from excelalchemy.results import CellErrorMap, RowIssueMap, ValidateRowResult
|
|
15
13
|
from excelalchemy.util.file import value_is_nan
|
|
16
14
|
|
|
17
15
|
from .schema import ExcelSchemaLayout
|
|
@@ -76,8 +74,8 @@ class ImportIssueTracker:
|
|
|
76
74
|
def __init__(self, layout: ExcelSchemaLayout, import_result_field_meta: list[FieldMetaInfo]):
|
|
77
75
|
self.layout = layout
|
|
78
76
|
self.import_result_field_meta = import_result_field_meta
|
|
79
|
-
self.cell_errors
|
|
80
|
-
self.row_errors
|
|
77
|
+
self.cell_errors = CellErrorMap()
|
|
78
|
+
self.row_errors = RowIssueMap()
|
|
81
79
|
|
|
82
80
|
def register_row_error(
|
|
83
81
|
self,
|
|
@@ -86,9 +84,9 @@ class ImportIssueTracker:
|
|
|
86
84
|
) -> None:
|
|
87
85
|
"""Record one row-level issue or a batch of issues for the same row."""
|
|
88
86
|
if isinstance(error, list):
|
|
89
|
-
self.row_errors
|
|
87
|
+
self.row_errors.add_many(row_index, error)
|
|
90
88
|
else:
|
|
91
|
-
self.row_errors
|
|
89
|
+
self.row_errors.add(row_index, error)
|
|
92
90
|
|
|
93
91
|
def register_cell_errors(
|
|
94
92
|
self,
|
|
@@ -99,8 +97,8 @@ class ImportIssueTracker:
|
|
|
99
97
|
"""Map cell errors from schema labels to rendered workbook coordinates."""
|
|
100
98
|
for error in errors:
|
|
101
99
|
for index in self._column_indices(worksheet_table, error.unique_label):
|
|
102
|
-
column_index =
|
|
103
|
-
self.cell_errors.
|
|
100
|
+
column_index = ColumnIndex(index + len(self.import_result_field_meta))
|
|
101
|
+
self.cell_errors.add(row_index, column_index, error)
|
|
104
102
|
|
|
105
103
|
def add_result_columns(
|
|
106
104
|
self,
|
|
@@ -115,17 +113,15 @@ class ImportIssueTracker:
|
|
|
115
113
|
reason: list[str] = []
|
|
116
114
|
|
|
117
115
|
for index in worksheet_table.index[extra_header_count_on_import:]:
|
|
118
|
-
row_errors = self.row_errors.
|
|
116
|
+
row_errors = self.row_errors.at(RowIndex(index))
|
|
119
117
|
if not row_errors:
|
|
120
118
|
result.append(str(ValidateRowResult.SUCCESS))
|
|
121
119
|
reason.append('')
|
|
122
120
|
continue
|
|
123
121
|
|
|
124
122
|
result.append(str(ValidateRowResult.FAIL))
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
]
|
|
128
|
-
reason.append('\n'.join(numbered_reasons))
|
|
123
|
+
ordered_errors = list(self.layout.order_errors(list(row_errors)))
|
|
124
|
+
reason.append('\n'.join(self.row_errors.numbered_messages(ordered_errors)))
|
|
129
125
|
|
|
130
126
|
if extra_header_count_on_import == 1:
|
|
131
127
|
result = [str(result_unique_label), *result]
|
|
@@ -5,7 +5,6 @@ from collections import defaultdict
|
|
|
5
5
|
from collections.abc import Iterable, Sequence
|
|
6
6
|
from decimal import Decimal
|
|
7
7
|
from itertools import chain
|
|
8
|
-
from typing import cast
|
|
9
8
|
|
|
10
9
|
from pydantic import BaseModel
|
|
11
10
|
|
|
@@ -88,11 +87,18 @@ class ExcelSchemaLayout:
|
|
|
88
87
|
return sorted(
|
|
89
88
|
field_metas,
|
|
90
89
|
key=lambda x: (
|
|
91
|
-
|
|
90
|
+
cls._parent_order(orders, x),
|
|
92
91
|
x.runtime.offset,
|
|
93
92
|
),
|
|
94
93
|
)
|
|
95
94
|
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _parent_order(orders: dict[Label, int], field_meta: FieldMetaInfo) -> int | Decimal:
|
|
97
|
+
parent_label = field_meta.runtime.parent_label
|
|
98
|
+
if parent_label is None:
|
|
99
|
+
return Decimal('Infinity')
|
|
100
|
+
return orders.get(parent_label, Decimal('Infinity'))
|
|
101
|
+
|
|
96
102
|
def has_merged_header(self, selected_keys: list[UniqueKey]) -> bool:
|
|
97
103
|
"""Return whether the selected keys need a two-row merged header."""
|
|
98
104
|
return any(
|
|
@@ -22,7 +22,7 @@ from excelalchemy._primitives.constants import (
|
|
|
22
22
|
)
|
|
23
23
|
from excelalchemy._primitives.identity import ColumnIndex, DataUrlStr, Label, RowIndex, UniqueLabel
|
|
24
24
|
from excelalchemy.core.table import WorksheetTable, WorksheetValue
|
|
25
|
-
from excelalchemy.exceptions import ExcelCellError
|
|
25
|
+
from excelalchemy.exceptions import ExcelCellError, ProgrammaticError
|
|
26
26
|
from excelalchemy.i18n.messages import MessageKey
|
|
27
27
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
28
28
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -167,7 +167,10 @@ def _write_horizontally_merged_header(
|
|
|
167
167
|
declared = field_meta.declared
|
|
168
168
|
runtime = field_meta.runtime
|
|
169
169
|
if runtime.parent_label is None:
|
|
170
|
-
raise
|
|
170
|
+
raise ProgrammaticError(
|
|
171
|
+
msg(MessageKey.PARENT_LABEL_EMPTY_RUNTIME),
|
|
172
|
+
message_key=MessageKey.PARENT_LABEL_EMPTY_RUNTIME,
|
|
173
|
+
)
|
|
171
174
|
counter[runtime.parent_label] += 1
|
|
172
175
|
|
|
173
176
|
for openpyxl_col_index, column in enumerate(
|
|
@@ -178,7 +181,10 @@ def _write_horizontally_merged_header(
|
|
|
178
181
|
declared = field_meta.declared
|
|
179
182
|
runtime = field_meta.runtime
|
|
180
183
|
if runtime.parent_label is None:
|
|
181
|
-
raise
|
|
184
|
+
raise ProgrammaticError(
|
|
185
|
+
msg(MessageKey.PARENT_LABEL_EMPTY_RUNTIME),
|
|
186
|
+
message_key=MessageKey.PARENT_LABEL_EMPTY_RUNTIME,
|
|
187
|
+
)
|
|
182
188
|
if declared.label != runtime.parent_label and runtime.offset == 0:
|
|
183
189
|
cell = _worksheet_cell(worksheet, row=start_row, column=openpyxl_col_index)
|
|
184
190
|
cell.value = str(runtime.parent_label)
|