ExcelAlchemy 2.2.4__tar.gz → 2.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/PKG-INFO +70 -2
  2. excelalchemy-2.2.6/README-pypi.md +159 -0
  3. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/__init__.py +15 -2
  4. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/base.py +64 -1
  5. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/date.py +9 -6
  6. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/date_range.py +12 -4
  7. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/email.py +4 -0
  8. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/multi_checkbox.py +43 -6
  9. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/number.py +4 -13
  10. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/number_range.py +8 -15
  11. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/organization.py +10 -1
  12. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/phone_number.py +4 -0
  13. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/radio.py +33 -2
  14. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/staff.py +10 -1
  15. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/tree.py +8 -0
  16. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/url.py +4 -0
  17. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/alchemy.py +9 -9
  18. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/headers.py +2 -3
  19. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/import_session.py +16 -10
  20. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/rows.py +10 -14
  21. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/schema.py +8 -2
  22. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/writer.py +9 -3
  23. excelalchemy-2.2.6/src/excelalchemy/exceptions.py +145 -0
  24. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/helper/pydantic.py +144 -22
  25. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/i18n/messages.py +46 -3
  26. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/metadata.py +9 -3
  27. excelalchemy-2.2.6/src/excelalchemy/results.py +231 -0
  28. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/converter.py +2 -3
  29. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/file.py +21 -5
  30. excelalchemy-2.2.4/README-pypi.md +0 -91
  31. excelalchemy-2.2.4/src/excelalchemy/exceptions.py +0 -80
  32. excelalchemy-2.2.4/src/excelalchemy/results.py +0 -91
  33. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/LICENSE +0 -0
  34. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/pyproject.toml +0 -0
  35. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/__init__.py +0 -0
  36. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/constants.py +0 -0
  37. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/deprecation.py +0 -0
  38. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/header_models.py +0 -0
  39. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/identity.py +0 -0
  40. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/_primitives/payloads.py +0 -0
  41. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/artifacts.py +0 -0
  42. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/__init__.py +0 -0
  43. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/boolean.py +0 -0
  44. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/money.py +0 -0
  45. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/codecs/string.py +0 -0
  46. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/config.py +0 -0
  47. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/const.py +0 -0
  48. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/__init__.py +0 -0
  49. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/abstract.py +0 -0
  50. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/executor.py +0 -0
  51. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/rendering.py +0 -0
  52. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/storage.py +0 -0
  53. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/storage_minio.py +0 -0
  54. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/storage_protocol.py +0 -0
  55. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/core/table.py +0 -0
  56. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/exc.py +0 -0
  57. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/header_models.py +0 -0
  58. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/helper/__init__.py +0 -0
  59. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/i18n/__init__.py +0 -0
  60. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/identity.py +0 -0
  61. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/py.typed +0 -0
  62. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/__init__.py +0 -0
  63. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/abstract.py +0 -0
  64. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/alchemy.py +0 -0
  65. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/field.py +0 -0
  66. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/header.py +0 -0
  67. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/identity.py +0 -0
  68. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/result.py +0 -0
  69. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/__init__.py +0 -0
  70. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/boolean.py +0 -0
  71. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/date.py +0 -0
  72. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/date_range.py +0 -0
  73. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/email.py +0 -0
  74. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/money.py +0 -0
  75. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
  76. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/number.py +0 -0
  77. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/number_range.py +0 -0
  78. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/organization.py +0 -0
  79. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/phone_number.py +0 -0
  80. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/radio.py +0 -0
  81. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/staff.py +0 -0
  82. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/string.py +0 -0
  83. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/tree.py +0 -0
  84. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/types/value/url.py +0 -0
  85. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/__init__.py +0 -0
  86. {excelalchemy-2.2.4 → excelalchemy-2.2.6}/src/excelalchemy/util/convertor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ExcelAlchemy
3
- Version: 2.2.4
3
+ Version: 2.2.6
4
4
  Summary: Schema-driven Python library for typed Excel import/export workflows with Pydantic and locale-aware workbooks.
5
5
  Keywords: excel,openpyxl,pydantic,minio,schema
6
6
  Author: Ray
@@ -49,7 +49,9 @@ ExcelAlchemy turns Pydantic models into typed workbook contracts:
49
49
  - render workbook-facing output in `zh-CN` or `en`
50
50
  - keep storage pluggable through `ExcelStorage`
51
51
 
52
- [GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
52
+ The current stable release is `2.2.6`, which continues the 2.x line with stronger result-object guidance, a copyable FastAPI reference project, more robust smoke verification, and clearer codec fallback diagnostics.
53
+
54
+ [GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
53
55
 
54
56
  ## Screenshots
55
57
 
@@ -114,6 +116,72 @@ alchemy = ExcelAlchemy(ImporterConfig(Importer, locale='en'))
114
116
  template = alchemy.download_template_artifact(filename='people-template.xlsx')
115
117
  ```
116
118
 
119
+ ## Example Outputs
120
+
121
+ These fixed outputs are generated from the repository examples by
122
+ [`scripts/generate_example_output_assets.py`](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/scripts/generate_example_output_assets.py).
123
+
124
+ Import workflow:
125
+
126
+ ```text
127
+ Employee import workflow completed
128
+ Result: SUCCESS
129
+ Success rows: 1
130
+ Failed rows: 0
131
+ Result workbook URL: None
132
+ Created rows: 1
133
+ Uploaded artifacts: []
134
+ ```
135
+
136
+ Export workflow:
137
+
138
+ ```text
139
+ Export workflow completed
140
+ Artifact filename: employees-export.xlsx
141
+ Artifact bytes: 6893
142
+ Upload URL: memory://employees-export-upload.xlsx
143
+ Uploaded objects: ['employees-export-upload.xlsx']
144
+ ```
145
+
146
+ Full captured outputs:
147
+
148
+ - [employee-import-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/employee-import-workflow.txt)
149
+ - [create-or-update-import.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/create-or-update-import.txt)
150
+ - [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
151
+ - [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
152
+ - [selection-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/selection-fields.txt)
153
+ - [fastapi-reference.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/fastapi-reference.txt)
154
+
155
+ For a single GitHub page that combines screenshots, representative workflows,
156
+ and captured outputs, see the
157
+ [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
158
+
159
+ If you want a copyable FastAPI-oriented reference layout rather than a single
160
+ example script, see the
161
+ [FastAPI reference project](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/examples/fastapi_reference/README.md).
162
+
163
+ ## Error Feedback
164
+
165
+ ExcelAlchemy keeps workbook-facing validation feedback readable while also
166
+ supporting API-friendly inspection in application code.
167
+
168
+ The stable 2.x result surface includes:
169
+
170
+ - `alchemy.cell_error_map`
171
+ - `alchemy.row_error_map`
172
+
173
+ These objects remain dict-like for compatibility, but also expose helpers such
174
+ as:
175
+
176
+ - `messages_at(...)`
177
+ - `messages_for_row(...)`
178
+ - `flatten()`
179
+ - `to_api_payload()`
180
+
181
+ Common field types now also produce more business-oriented error wording, such
182
+ as expected date formats, sample email/phone/URL formats, and clearer messages
183
+ for configured selection fields.
184
+
117
185
  ## Why ExcelAlchemy
118
186
 
119
187
  - Pydantic v2-based schema extraction and validation
@@ -0,0 +1,159 @@
1
+ # ExcelAlchemy
2
+
3
+ Schema-driven Python library for typed Excel import/export workflows with Pydantic and locale-aware workbooks.
4
+
5
+ ExcelAlchemy turns Pydantic models into typed workbook contracts:
6
+
7
+ - generate Excel templates from code
8
+ - validate uploaded workbooks
9
+ - map failures back to rows and cells
10
+ - render workbook-facing output in `zh-CN` or `en`
11
+ - keep storage pluggable through `ExcelStorage`
12
+
13
+ The current stable release is `2.2.6`, which continues the 2.x line with stronger result-object guidance, a copyable FastAPI reference project, more robust smoke verification, and clearer codec fallback diagnostics.
14
+
15
+ [GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
16
+
17
+ ## Screenshots
18
+
19
+ ### Template
20
+
21
+ ![Excel template screenshot](https://raw.githubusercontent.com/RayCarterLab/ExcelAlchemy/main/images/portfolio-template-en.png)
22
+
23
+ ### Import Result
24
+
25
+ ![Excel import result screenshot](https://raw.githubusercontent.com/RayCarterLab/ExcelAlchemy/main/images/portfolio-import-result-en.png)
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install ExcelAlchemy
31
+ ```
32
+
33
+ Optional Minio support:
34
+
35
+ ```bash
36
+ pip install "ExcelAlchemy[minio]"
37
+ ```
38
+
39
+ ## Minimal Example
40
+
41
+ ```python
42
+ from pydantic import BaseModel
43
+
44
+ from excelalchemy import ExcelAlchemy, FieldMeta, ImporterConfig, Number, String
45
+
46
+
47
+ class Importer(BaseModel):
48
+ age: Number = FieldMeta(label='Age', order=1)
49
+ name: String = FieldMeta(label='Name', order=2)
50
+
51
+
52
+ alchemy = ExcelAlchemy(ImporterConfig(Importer, locale='en'))
53
+ template = alchemy.download_template_artifact(filename='people-template.xlsx')
54
+
55
+ excel_bytes = template.as_bytes()
56
+ ```
57
+
58
+ ## Modern Annotated Example
59
+
60
+ ```python
61
+ from typing import Annotated
62
+
63
+ from pydantic import BaseModel, Field
64
+
65
+ from excelalchemy import Email, ExcelAlchemy, ExcelMeta, ImporterConfig
66
+
67
+
68
+ class Importer(BaseModel):
69
+ email: Annotated[
70
+ Email,
71
+ Field(min_length=10),
72
+ ExcelMeta(label='Email', order=1, hint='Use your work email'),
73
+ ]
74
+
75
+
76
+ alchemy = ExcelAlchemy(ImporterConfig(Importer, locale='en'))
77
+ template = alchemy.download_template_artifact(filename='people-template.xlsx')
78
+ ```
79
+
80
+ ## Example Outputs
81
+
82
+ These fixed outputs are generated from the repository examples by
83
+ [`scripts/generate_example_output_assets.py`](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/scripts/generate_example_output_assets.py).
84
+
85
+ Import workflow:
86
+
87
+ ```text
88
+ Employee import workflow completed
89
+ Result: SUCCESS
90
+ Success rows: 1
91
+ Failed rows: 0
92
+ Result workbook URL: None
93
+ Created rows: 1
94
+ Uploaded artifacts: []
95
+ ```
96
+
97
+ Export workflow:
98
+
99
+ ```text
100
+ Export workflow completed
101
+ Artifact filename: employees-export.xlsx
102
+ Artifact bytes: 6893
103
+ Upload URL: memory://employees-export-upload.xlsx
104
+ Uploaded objects: ['employees-export-upload.xlsx']
105
+ ```
106
+
107
+ Full captured outputs:
108
+
109
+ - [employee-import-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/employee-import-workflow.txt)
110
+ - [create-or-update-import.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/create-or-update-import.txt)
111
+ - [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
112
+ - [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
113
+ - [selection-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/selection-fields.txt)
114
+ - [fastapi-reference.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/fastapi-reference.txt)
115
+
116
+ For a single GitHub page that combines screenshots, representative workflows,
117
+ and captured outputs, see the
118
+ [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
119
+
120
+ If you want a copyable FastAPI-oriented reference layout rather than a single
121
+ example script, see the
122
+ [FastAPI reference project](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/examples/fastapi_reference/README.md).
123
+
124
+ ## Error Feedback
125
+
126
+ ExcelAlchemy keeps workbook-facing validation feedback readable while also
127
+ supporting API-friendly inspection in application code.
128
+
129
+ The stable 2.x result surface includes:
130
+
131
+ - `alchemy.cell_error_map`
132
+ - `alchemy.row_error_map`
133
+
134
+ These objects remain dict-like for compatibility, but also expose helpers such
135
+ as:
136
+
137
+ - `messages_at(...)`
138
+ - `messages_for_row(...)`
139
+ - `flatten()`
140
+ - `to_api_payload()`
141
+
142
+ Common field types now also produce more business-oriented error wording, such
143
+ as expected date formats, sample email/phone/URL formats, and clearer messages
144
+ for configured selection fields.
145
+
146
+ ## Why ExcelAlchemy
147
+
148
+ - Pydantic v2-based schema extraction and validation
149
+ - locale-aware workbook comments and result workbooks
150
+ - pluggable storage instead of a hard-coded backend
151
+ - `openpyxl`-based runtime path without pandas
152
+ - contract tests, Ruff, and Pyright in the development workflow
153
+
154
+ ## Learn More
155
+
156
+ - [Full project README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md)
157
+ - [Architecture notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md)
158
+ - [Locale policy](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/locale.md)
159
+ - [Migration notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
@@ -1,6 +1,6 @@
1
1
  """A Python Library for Reading and Writing Excel Files"""
2
2
 
3
- __version__ = '2.2.4'
3
+ __version__ = '2.2.6'
4
4
  from excelalchemy._primitives.constants import CharacterSet, DataRangeOption, DateFormat, Option
5
5
  from excelalchemy._primitives.deprecation import ExcelAlchemyDeprecationWarning
6
6
  from excelalchemy._primitives.identity import (
@@ -48,13 +48,24 @@ from excelalchemy.core.storage_protocol import ExcelStorage
48
48
  from excelalchemy.exceptions import ConfigError, ExcelCellError, ExcelRowError, ProgrammaticError
49
49
  from excelalchemy.helper.pydantic import extract_pydantic_model
50
50
  from excelalchemy.metadata import ExcelMeta, FieldMeta, PatchFieldMeta
51
- from excelalchemy.results import ImportResult, ValidateHeaderResult, ValidateResult, ValidateRowResult
51
+ from excelalchemy.results import (
52
+ CellErrorMap,
53
+ CellIssueRecord,
54
+ ImportResult,
55
+ RowIssueMap,
56
+ RowIssueRecord,
57
+ ValidateHeaderResult,
58
+ ValidateResult,
59
+ ValidateRowResult,
60
+ )
52
61
  from excelalchemy.util.file import flatten
53
62
 
54
63
  __all__ = [
55
64
  'Base64Str',
56
65
  'Boolean',
57
66
  'BooleanCodec',
67
+ 'CellErrorMap',
68
+ 'CellIssueRecord',
58
69
  'ColumnIndex',
59
70
  'CompositeExcelFieldCodec',
60
71
  'ConfigError',
@@ -104,6 +115,8 @@ __all__ = [
104
115
  'ProgrammaticError',
105
116
  'Radio',
106
117
  'RowIndex',
118
+ 'RowIssueMap',
119
+ 'RowIssueRecord',
107
120
  'SingleChoiceCodec',
108
121
  'SingleOrganization',
109
122
  'SingleOrganizationCodec',
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, cast
5
6
 
6
7
  from pydantic import GetCoreSchemaHandler
7
8
  from pydantic_core import core_schema
@@ -19,6 +20,63 @@ type WorkbookDisplayValue = Any
19
20
  type NormalizedImportValue = Any
20
21
 
21
22
 
23
+ def _summarize_exception(exc: Exception) -> str:
24
+ details: list[str] = []
25
+ for arg in exc.args:
26
+ if isinstance(arg, list):
27
+ raw_items = cast(list[object], arg)
28
+ list_items: list[str] = []
29
+ for item in raw_items:
30
+ item_text = item.__name__ if isinstance(item, type) else str(item).strip()
31
+ if item_text:
32
+ list_items.append(item_text)
33
+ if list_items:
34
+ details.append(', '.join(list_items))
35
+ continue
36
+
37
+ text = str(arg).strip()
38
+ if text:
39
+ details.append(text)
40
+
41
+ if details:
42
+ return '; '.join(details)
43
+ return exc.__class__.__name__
44
+
45
+
46
+ def log_codec_parse_fallback(
47
+ codec_name: str,
48
+ value: object,
49
+ *,
50
+ field_label: str | None = None,
51
+ exc: Exception,
52
+ ) -> None:
53
+ field_context = f' for field "{field_label}"' if field_label else ''
54
+ logging.warning(
55
+ 'Codec %s could not parse workbook input%s; keeping the original value %r. Reason: %s',
56
+ codec_name,
57
+ field_context,
58
+ value,
59
+ _summarize_exception(exc),
60
+ )
61
+
62
+
63
+ def log_codec_render_fallback(
64
+ codec_name: str,
65
+ value: object,
66
+ *,
67
+ field_label: str | None = None,
68
+ exc: Exception,
69
+ ) -> None:
70
+ field_context = f' for field "{field_label}"' if field_label else ''
71
+ logging.warning(
72
+ 'Codec %s could not format workbook value%s; returning %r as-is. Reason: %s',
73
+ codec_name,
74
+ field_context,
75
+ value,
76
+ _summarize_exception(exc),
77
+ )
78
+
79
+
22
80
  class ExcelFieldCodec(ABC):
23
81
  """Excel-facing field adapter responsible for comments, parsing, formatting, and normalization."""
24
82
 
@@ -42,6 +100,11 @@ class ExcelFieldCodec(ABC):
42
100
  def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
43
101
  """Validate and normalize parsed input before handing it to the Pydantic layer."""
44
102
 
103
+ @classmethod
104
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
105
+ """Return a user-facing input hint for invalid values when one is known."""
106
+ return None
107
+
45
108
  @classmethod
46
109
  def comment(cls, field_meta: FieldMetaInfo) -> str:
47
110
  """Backward-compatible alias for build_comment()."""
@@ -11,6 +11,7 @@ from excelalchemy.codecs.base import (
11
11
  NormalizedImportValue,
12
12
  WorkbookDisplayValue,
13
13
  WorkbookInputValue,
14
+ log_codec_parse_fallback,
14
15
  )
15
16
  from excelalchemy.exceptions import ConfigError
16
17
  from excelalchemy.i18n.messages import MessageKey
@@ -21,6 +22,13 @@ from excelalchemy.metadata import FieldMetaInfo
21
22
  class Date(ExcelFieldCodec, datetime):
22
23
  __name__ = 'Date'
23
24
 
25
+ @classmethod
26
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
27
+ presentation = field_meta.presentation
28
+ if presentation.date_format is None:
29
+ return None
30
+ return msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[presentation.date_format])
31
+
24
32
  @classmethod
25
33
  def build_comment(cls, field_meta: FieldMetaInfo) -> str:
26
34
  declared = field_meta.declared
@@ -62,12 +70,7 @@ class Date(ExcelFieldCodec, datetime):
62
70
  dt: DateTime = cast(DateTime, pendulum.parse(v))
63
71
  return dt.replace(tzinfo=presentation.timezone)
64
72
  except Exception as exc:
65
- logging.warning(
66
- 'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
67
- cls.__name__,
68
- value,
69
- exc,
70
- )
73
+ log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
71
74
  return value
72
75
 
73
76
  @classmethod
@@ -9,7 +9,8 @@ from pydantic import BaseModel
9
9
 
10
10
  from excelalchemy._primitives.constants import DATE_FORMAT_TO_PYTHON_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
11
11
  from excelalchemy._primitives.identity import Key
12
- from excelalchemy.codecs.base import CompositeExcelFieldCodec
12
+ from excelalchemy.codecs.base import CompositeExcelFieldCodec, log_codec_parse_fallback
13
+ from excelalchemy.exceptions import ConfigError
13
14
  from excelalchemy.i18n.messages import MessageKey
14
15
  from excelalchemy.i18n.messages import display_message as dmsg
15
16
  from excelalchemy.i18n.messages import message as msg
@@ -53,7 +54,9 @@ class DateRange(CompositeExcelFieldCodec):
53
54
  declared = field_meta.declared
54
55
  presentation = field_meta.presentation
55
56
  if presentation.date_format is None:
56
- raise RuntimeError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
57
+ raise ConfigError(
58
+ msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED), message_key=MessageKey.DATE_FORMAT_NOT_CONFIGURED
59
+ )
57
60
 
58
61
  return '\n'.join(
59
62
  [
@@ -63,8 +66,13 @@ class DateRange(CompositeExcelFieldCodec):
63
66
  ]
64
67
  )
65
68
 
69
+ @classmethod
70
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
71
+ return msg(MessageKey.ENTER_DATE_RANGE_EXPECTED_FORMAT)
72
+
66
73
  @classmethod
67
74
  def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> object:
75
+ declared = field_meta.declared
68
76
  mapping = cls._coerce_mapping(value)
69
77
  if mapping is not None:
70
78
  try:
@@ -73,7 +81,7 @@ class DateRange(CompositeExcelFieldCodec):
73
81
  'end': cls._parse_optional_datetime(mapping.get('end'), field_meta),
74
82
  }
75
83
  except Exception as exc:
76
- logging.warning('Could not parse value %s for field %s. Reason: %s', value, cls.__name__, exc)
84
+ log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
77
85
  return value
78
86
 
79
87
  if isinstance(value, datetime):
@@ -83,7 +91,7 @@ class DateRange(CompositeExcelFieldCodec):
83
91
  try:
84
92
  return cls._parse_datetime_text(value, field_meta)
85
93
  except Exception as exc:
86
- logging.warning('Could not parse value %s for field %s. Reason: %s', value, cls.__name__, exc)
94
+ log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
87
95
  return value
88
96
 
89
97
  return value
@@ -11,6 +11,10 @@ from excelalchemy.metadata import FieldMetaInfo
11
11
  class Email(String):
12
12
  _validator: ClassVar[TypeAdapter[EmailStr]] = TypeAdapter(EmailStr)
13
13
 
14
+ @classmethod
15
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
16
+ return msg(MessageKey.VALID_EMAIL_REQUIRED)
17
+
14
18
  @classmethod
15
19
  def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> str:
16
20
  # Try to parse the value as a string
@@ -14,6 +14,43 @@ from excelalchemy.metadata import FieldMetaInfo
14
14
  class MultiCheckbox(ExcelFieldCodec, list[str]):
15
15
  __name__ = 'MultiChoice'
16
16
 
17
+ @classmethod
18
+ def selection_entity_plural(cls) -> str | None:
19
+ return None
20
+
21
+ @classmethod
22
+ def _options_preview(cls, field_meta: FieldMetaInfo, *, limit: int = 5) -> str | None:
23
+ options = field_meta.presentation.options
24
+ if not options:
25
+ return None
26
+ preview = MULTI_CHECKBOX_SEPARATOR.join(option.name for option in options[:limit])
27
+ if len(options) > limit:
28
+ preview = f'{preview}{MULTI_CHECKBOX_SEPARATOR}...'
29
+ return preview
30
+
31
+ @classmethod
32
+ def _compose_selection_message(cls, field_meta: FieldMetaInfo) -> str:
33
+ entity_plural = cls.selection_entity_plural()
34
+ if entity_plural is None:
35
+ base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_OPTIONS)
36
+ else:
37
+ base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_ENTITIES, entity_plural=entity_plural)
38
+
39
+ preview = cls._options_preview(field_meta)
40
+ if preview is None:
41
+ return base_message
42
+ return f'{base_message}. {msg(MessageKey.VALID_VALUES_INCLUDE, options=preview)}'
43
+
44
+ @classmethod
45
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
46
+ return cls._compose_selection_message(field_meta)
47
+
48
+ @staticmethod
49
+ def _coerce_items(value: object) -> list[object] | None:
50
+ if not isinstance(value, list):
51
+ return None
52
+ return cast(list[object], value)
53
+
17
54
  @classmethod
18
55
  def build_comment(cls, field_meta: FieldMetaInfo) -> str:
19
56
  declared = field_meta.declared
@@ -29,8 +66,8 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
29
66
 
30
67
  @classmethod
31
68
  def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> list[str] | object:
32
- if isinstance(value, list):
33
- items = cast(list[object], value)
69
+ items = cls._coerce_items(value)
70
+ if items is not None:
34
71
  return [str(item).strip() for item in items]
35
72
 
36
73
  if isinstance(value, str):
@@ -45,10 +82,10 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
45
82
  def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> list[str]: # OptionId
46
83
  declared = field_meta.declared
47
84
  presentation = field_meta.presentation
48
- if not isinstance(value, list):
49
- raise ValueError(msg(MessageKey.OPTION_NOT_FOUND_HEADER_COMMENT))
85
+ items = cls._coerce_items(value)
86
+ if items is None:
87
+ raise ValueError(cls._compose_selection_message(field_meta))
50
88
 
51
- items = cast(list[object], value)
52
89
  parsed = [str(item).strip() for item in items]
53
90
 
54
91
  if presentation.options is None:
@@ -66,7 +103,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
66
103
  result, errors = presentation.exchange_names_to_option_ids_with_errors(parsed, field_label=declared.label)
67
104
 
68
105
  if errors:
69
- raise ValueError(*errors)
106
+ raise ValueError(cls._compose_selection_message(field_meta))
70
107
  else:
71
108
  return result
72
109
 
@@ -6,6 +6,7 @@ from excelalchemy.codecs.base import (
6
6
  NormalizedImportValue,
7
7
  WorkbookDisplayValue,
8
8
  WorkbookInputValue,
9
+ log_codec_parse_fallback,
9
10
  )
10
11
  from excelalchemy.i18n.messages import MessageKey
11
12
  from excelalchemy.i18n.messages import display_message as dmsg
@@ -64,6 +65,7 @@ class Number(Decimal, ExcelFieldCodec):
64
65
  value: str | int | float | WorkbookInputValue | None,
65
66
  field_meta: FieldMetaInfo,
66
67
  ) -> Decimal | WorkbookInputValue:
68
+ declared = field_meta.declared
67
69
  if isinstance(value, str):
68
70
  value = value.strip()
69
71
  if value is None:
@@ -71,12 +73,7 @@ class Number(Decimal, ExcelFieldCodec):
71
73
  try:
72
74
  return transform_decimal(Decimal(str(value)))
73
75
  except Exception as exc:
74
- logging.warning(
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 as exc:
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
- logging.warning(
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 as exc:
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