ExcelAlchemy 2.2.5__tar.gz → 2.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/PKG-INFO +30 -3
  2. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/README-pypi.md +29 -2
  3. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/__init__.py +11 -1
  4. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/base.py +104 -1
  5. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/boolean.py +15 -8
  6. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/date.py +11 -8
  7. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/date_range.py +14 -5
  8. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/email.py +4 -0
  9. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/multi_checkbox.py +40 -9
  10. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/number.py +6 -15
  11. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/number_range.py +8 -15
  12. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/organization.py +16 -3
  13. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/phone_number.py +4 -0
  14. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/radio.py +43 -16
  15. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/staff.py +16 -8
  16. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/tree.py +14 -4
  17. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/url.py +4 -0
  18. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/exceptions.py +17 -0
  19. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/helper/pydantic.py +110 -33
  20. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/i18n/messages.py +31 -3
  21. excelalchemy-2.2.7/src/excelalchemy/results.py +457 -0
  22. excelalchemy-2.2.5/src/excelalchemy/results.py +0 -173
  23. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/LICENSE +0 -0
  24. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/pyproject.toml +0 -0
  25. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/__init__.py +0 -0
  26. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/constants.py +0 -0
  27. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/deprecation.py +0 -0
  28. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/header_models.py +0 -0
  29. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/identity.py +0 -0
  30. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/_primitives/payloads.py +0 -0
  31. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/artifacts.py +0 -0
  32. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/__init__.py +0 -0
  33. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/money.py +0 -0
  34. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/codecs/string.py +0 -0
  35. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/config.py +0 -0
  36. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/const.py +0 -0
  37. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/__init__.py +0 -0
  38. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/abstract.py +0 -0
  39. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/alchemy.py +0 -0
  40. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/executor.py +0 -0
  41. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/headers.py +0 -0
  42. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/import_session.py +0 -0
  43. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/rendering.py +0 -0
  44. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/rows.py +0 -0
  45. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/schema.py +0 -0
  46. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/storage.py +0 -0
  47. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/storage_minio.py +0 -0
  48. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/storage_protocol.py +0 -0
  49. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/table.py +0 -0
  50. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/core/writer.py +0 -0
  51. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/exc.py +0 -0
  52. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/header_models.py +0 -0
  53. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/helper/__init__.py +0 -0
  54. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/i18n/__init__.py +0 -0
  55. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/identity.py +0 -0
  56. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/metadata.py +0 -0
  57. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/py.typed +0 -0
  58. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/__init__.py +0 -0
  59. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/abstract.py +0 -0
  60. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/alchemy.py +0 -0
  61. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/field.py +0 -0
  62. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/header.py +0 -0
  63. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/identity.py +0 -0
  64. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/result.py +0 -0
  65. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/__init__.py +0 -0
  66. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/boolean.py +0 -0
  67. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/date.py +0 -0
  68. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/date_range.py +0 -0
  69. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/email.py +0 -0
  70. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/money.py +0 -0
  71. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
  72. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/number.py +0 -0
  73. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/number_range.py +0 -0
  74. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/organization.py +0 -0
  75. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/phone_number.py +0 -0
  76. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/radio.py +0 -0
  77. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/staff.py +0 -0
  78. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/string.py +0 -0
  79. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/tree.py +0 -0
  80. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/types/value/url.py +0 -0
  81. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/__init__.py +0 -0
  82. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/converter.py +0 -0
  83. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/convertor.py +0 -0
  84. {excelalchemy-2.2.5 → excelalchemy-2.2.7}/src/excelalchemy/util/file.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ExcelAlchemy
3
- Version: 2.2.5
3
+ Version: 2.2.7
4
4
  Summary: Schema-driven Python library for typed Excel import/export workflows with Pydantic and locale-aware workbooks.
5
5
  Keywords: excel,openpyxl,pydantic,minio,schema
6
6
  Author: Ray
@@ -49,9 +49,9 @@ ExcelAlchemy turns Pydantic models into typed workbook contracts:
49
49
  - render workbook-facing output in `zh-CN` or `en`
50
50
  - keep storage pluggable through `ExcelStorage`
51
51
 
52
- The current stable release is `2.2.5`, which continues the 2.x line with richer import-failure feedback, clearer documentation entry points, stronger examples, and stronger smoke coverage.
52
+ The current stable release is `2.2.7`, which continues the 2.x line with stronger API-facing result payloads, a more complete FastAPI reference app, harder install-time smoke verification, and more consistent codec diagnostics.
53
53
 
54
- [GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
54
+ [GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
55
55
 
56
56
  ## Screenshots
57
57
 
@@ -150,11 +150,38 @@ Full captured outputs:
150
150
  - [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
151
151
  - [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
152
152
  - [selection-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/selection-fields.txt)
153
+ - [fastapi-reference.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/fastapi-reference.txt)
153
154
 
154
155
  For a single GitHub page that combines screenshots, representative workflows,
155
156
  and captured outputs, see the
156
157
  [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
157
158
 
159
+ If you want a copyable FastAPI-oriented reference layout rather than a single
160
+ example script, see the
161
+ [FastAPI reference project](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/examples/fastapi_reference/README.md).
162
+
163
+ ## Error Feedback
164
+
165
+ ExcelAlchemy keeps workbook-facing validation feedback readable while also
166
+ supporting API-friendly inspection in application code.
167
+
168
+ The stable 2.x result surface includes:
169
+
170
+ - `alchemy.cell_error_map`
171
+ - `alchemy.row_error_map`
172
+
173
+ These objects remain dict-like for compatibility, but also expose helpers such
174
+ as:
175
+
176
+ - `messages_at(...)`
177
+ - `messages_for_row(...)`
178
+ - `flatten()`
179
+ - `to_api_payload()`
180
+
181
+ Common field types now also produce more business-oriented error wording, such
182
+ as expected date formats, sample email/phone/URL formats, and clearer messages
183
+ for configured selection fields.
184
+
158
185
  ## Why ExcelAlchemy
159
186
 
160
187
  - Pydantic v2-based schema extraction and validation
@@ -10,9 +10,9 @@ ExcelAlchemy turns Pydantic models into typed workbook contracts:
10
10
  - render workbook-facing output in `zh-CN` or `en`
11
11
  - keep storage pluggable through `ExcelStorage`
12
12
 
13
- The current stable release is `2.2.5`, which continues the 2.x line with richer import-failure feedback, clearer documentation entry points, stronger examples, and stronger smoke coverage.
13
+ The current stable release is `2.2.7`, which continues the 2.x line with stronger API-facing result payloads, a more complete FastAPI reference app, harder install-time smoke verification, and more consistent codec diagnostics.
14
14
 
15
- [GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
15
+ [GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
16
16
 
17
17
  ## Screenshots
18
18
 
@@ -111,11 +111,38 @@ Full captured outputs:
111
111
  - [export-workflow.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/export-workflow.txt)
112
112
  - [date-and-range-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/date-and-range-fields.txt)
113
113
  - [selection-fields.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/selection-fields.txt)
114
+ - [fastapi-reference.txt](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/files/example-outputs/fastapi-reference.txt)
114
115
 
115
116
  For a single GitHub page that combines screenshots, representative workflows,
116
117
  and captured outputs, see the
117
118
  [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md).
118
119
 
120
+ If you want a copyable FastAPI-oriented reference layout rather than a single
121
+ example script, see the
122
+ [FastAPI reference project](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/examples/fastapi_reference/README.md).
123
+
124
+ ## Error Feedback
125
+
126
+ ExcelAlchemy keeps workbook-facing validation feedback readable while also
127
+ supporting API-friendly inspection in application code.
128
+
129
+ The stable 2.x result surface includes:
130
+
131
+ - `alchemy.cell_error_map`
132
+ - `alchemy.row_error_map`
133
+
134
+ These objects remain dict-like for compatibility, but also expose helpers such
135
+ as:
136
+
137
+ - `messages_at(...)`
138
+ - `messages_for_row(...)`
139
+ - `flatten()`
140
+ - `to_api_payload()`
141
+
142
+ Common field types now also produce more business-oriented error wording, such
143
+ as expected date formats, sample email/phone/URL formats, and clearer messages
144
+ for configured selection fields.
145
+
119
146
  ## Why ExcelAlchemy
120
147
 
121
148
  - Pydantic v2-based schema extraction and validation
@@ -1,6 +1,6 @@
1
1
  """A Python Library for Reading and Writing Excel Files"""
2
2
 
3
- __version__ = '2.2.5'
3
+ __version__ = '2.2.7'
4
4
  from excelalchemy._primitives.constants import CharacterSet, DataRangeOption, DateFormat, Option
5
5
  from excelalchemy._primitives.deprecation import ExcelAlchemyDeprecationWarning
6
6
  from excelalchemy._primitives.identity import (
@@ -50,8 +50,13 @@ from excelalchemy.helper.pydantic import extract_pydantic_model
50
50
  from excelalchemy.metadata import ExcelMeta, FieldMeta, PatchFieldMeta
51
51
  from excelalchemy.results import (
52
52
  CellErrorMap,
53
+ CellIssueRecord,
54
+ CodeIssueSummary,
55
+ FieldIssueSummary,
53
56
  ImportResult,
54
57
  RowIssueMap,
58
+ RowIssueRecord,
59
+ RowIssueSummary,
55
60
  ValidateHeaderResult,
56
61
  ValidateResult,
57
62
  ValidateRowResult,
@@ -63,6 +68,8 @@ __all__ = [
63
68
  'Boolean',
64
69
  'BooleanCodec',
65
70
  'CellErrorMap',
71
+ 'CellIssueRecord',
72
+ 'CodeIssueSummary',
66
73
  'ColumnIndex',
67
74
  'CompositeExcelFieldCodec',
68
75
  'ConfigError',
@@ -84,6 +91,7 @@ __all__ = [
84
91
  'ExcelRowError',
85
92
  'ExcelStorage',
86
93
  'ExporterConfig',
94
+ 'FieldIssueSummary',
87
95
  'FieldMeta',
88
96
  'ImportMode',
89
97
  'ImportResult',
@@ -113,6 +121,8 @@ __all__ = [
113
121
  'Radio',
114
122
  'RowIndex',
115
123
  'RowIssueMap',
124
+ 'RowIssueRecord',
125
+ 'RowIssueSummary',
116
126
  'SingleChoiceCodec',
117
127
  'SingleOrganization',
118
128
  'SingleOrganizationCodec',
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, cast
5
6
 
6
7
  from pydantic import GetCoreSchemaHandler
7
8
  from pydantic_core import core_schema
@@ -18,6 +19,103 @@ type WorkbookInputValue = Any
18
19
  type WorkbookDisplayValue = Any
19
20
  type NormalizedImportValue = Any
20
21
 
22
+ CODEC_LOGGER_NAME = 'excelalchemy.codecs'
23
+ codec_logger = logging.getLogger(CODEC_LOGGER_NAME)
24
+
25
+
26
+ def _summarize_exception(exc: Exception) -> str:
27
+ details: list[str] = []
28
+ for arg in exc.args:
29
+ if isinstance(arg, list):
30
+ raw_items = cast(list[object], arg)
31
+ list_items: list[str] = []
32
+ for item in raw_items:
33
+ item_text = item.__name__ if isinstance(item, type) else str(item).strip()
34
+ if item_text:
35
+ list_items.append(item_text)
36
+ if list_items:
37
+ details.append(', '.join(list_items))
38
+ continue
39
+
40
+ text = str(arg).strip()
41
+ if text:
42
+ details.append(text)
43
+
44
+ if details:
45
+ return '; '.join(details)
46
+ return exc.__class__.__name__
47
+
48
+
49
+ def _fallback_reason(*, exc: Exception | None = None, reason: str | None = None) -> str:
50
+ if reason:
51
+ return reason
52
+ if exc is not None:
53
+ return _summarize_exception(exc)
54
+ return 'No additional details'
55
+
56
+
57
+ def log_codec_parse_fallback(
58
+ codec_name: str,
59
+ value: object,
60
+ *,
61
+ field_label: str | None = None,
62
+ exc: Exception | None = None,
63
+ reason: str | None = None,
64
+ ) -> None:
65
+ field_context = f' for field "{field_label}"' if field_label else ''
66
+ codec_logger.warning(
67
+ 'Codec %s could not parse workbook input%s; keeping the original value %r. Reason: %s',
68
+ codec_name,
69
+ field_context,
70
+ value,
71
+ _fallback_reason(exc=exc, reason=reason),
72
+ )
73
+
74
+
75
+ def log_codec_render_fallback(
76
+ codec_name: str,
77
+ value: object,
78
+ *,
79
+ field_label: str | None = None,
80
+ exc: Exception | None = None,
81
+ reason: str | None = None,
82
+ ) -> None:
83
+ field_context = f' for field "{field_label}"' if field_label else ''
84
+ codec_logger.warning(
85
+ 'Codec %s could not format workbook value%s; returning %r as-is. Reason: %s',
86
+ codec_name,
87
+ field_context,
88
+ value,
89
+ _fallback_reason(exc=exc, reason=reason),
90
+ )
91
+
92
+
93
+ def log_codec_option_resolution_fallback(
94
+ codec_name: str,
95
+ value: object,
96
+ *,
97
+ field_label: str | None = None,
98
+ exc: Exception | None = None,
99
+ reason: str | None = None,
100
+ ) -> None:
101
+ field_context = f' for field "{field_label}"' if field_label else ''
102
+ codec_logger.warning(
103
+ 'Codec %s could not resolve a configured option%s; returning %r as-is. Reason: %s',
104
+ codec_name,
105
+ field_context,
106
+ value,
107
+ _fallback_reason(exc=exc, reason=reason),
108
+ )
109
+
110
+
111
+ def log_codec_missing_options(codec_name: str, *, field_label: str | None = None) -> None:
112
+ field_context = f' for field "{field_label}"' if field_label else ''
113
+ codec_logger.warning(
114
+ 'Codec %s is missing configured options%s; workbook comments and validation may be incomplete.',
115
+ codec_name,
116
+ field_context,
117
+ )
118
+
21
119
 
22
120
  class ExcelFieldCodec(ABC):
23
121
  """Excel-facing field adapter responsible for comments, parsing, formatting, and normalization."""
@@ -42,6 +140,11 @@ class ExcelFieldCodec(ABC):
42
140
  def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
43
141
  """Validate and normalize parsed input before handing it to the Pydantic layer."""
44
142
 
143
+ @classmethod
144
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
145
+ """Return a user-facing input hint for invalid values when one is known."""
146
+ return None
147
+
45
148
  @classmethod
46
149
  def comment(cls, field_meta: FieldMetaInfo) -> str:
47
150
  """Backward-compatible alias for build_comment()."""
@@ -1,7 +1,10 @@
1
- import logging
2
-
3
1
  from excelalchemy.codecs import excel_choice_codec
4
- from excelalchemy.codecs.base import ExcelFieldCodec, WorkbookDisplayValue, WorkbookInputValue
2
+ from excelalchemy.codecs.base import (
3
+ ExcelFieldCodec,
4
+ WorkbookDisplayValue,
5
+ WorkbookInputValue,
6
+ log_codec_render_fallback,
7
+ )
5
8
  from excelalchemy.i18n.messages import MessageKey
6
9
  from excelalchemy.i18n.messages import display_message as dmsg
7
10
  from excelalchemy.i18n.messages import message as msg
@@ -63,15 +66,19 @@ class Boolean(ExcelFieldCodec):
63
66
  if value in cls._false_values():
64
67
  return cls._false_display()
65
68
  if value not in cls._true_values() | cls._false_values():
66
- logging.warning('Could not recognize boolean value %s; returning the original value', value)
69
+ log_codec_render_fallback(
70
+ cls.__name__,
71
+ value,
72
+ field_label=declared.label,
73
+ reason=f'Expected {cls._true_display()!r} or {cls._false_display()!r}',
74
+ )
67
75
  return value
68
76
  else:
69
- logging.warning(
70
- 'Type %s could not deserialize %s for field %s; returning the default value %s',
77
+ log_codec_render_fallback(
71
78
  cls.__name__,
72
79
  value,
73
- declared.label,
74
- cls._false_display(),
80
+ field_label=declared.label,
81
+ reason=f'Expected a boolean or one of {cls._true_display()!r}/{cls._false_display()!r}',
75
82
  )
76
83
 
77
84
  return cls._true_display() if str(value) in cls._true_values() else cls._false_display()
@@ -1,4 +1,3 @@
1
- import logging
2
1
  from datetime import datetime
3
2
  from typing import cast
4
3
 
@@ -11,6 +10,8 @@ from excelalchemy.codecs.base import (
11
10
  NormalizedImportValue,
12
11
  WorkbookDisplayValue,
13
12
  WorkbookInputValue,
13
+ codec_logger,
14
+ log_codec_parse_fallback,
14
15
  )
15
16
  from excelalchemy.exceptions import ConfigError
16
17
  from excelalchemy.i18n.messages import MessageKey
@@ -21,6 +22,13 @@ from excelalchemy.metadata import FieldMetaInfo
21
22
  class Date(ExcelFieldCodec, datetime):
22
23
  __name__ = 'Date'
23
24
 
25
+ @classmethod
26
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
27
+ presentation = field_meta.presentation
28
+ if presentation.date_format is None:
29
+ return None
30
+ return msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[presentation.date_format])
31
+
24
32
  @classmethod
25
33
  def build_comment(cls, field_meta: FieldMetaInfo) -> str:
26
34
  declared = field_meta.declared
@@ -45,7 +53,7 @@ class Date(ExcelFieldCodec, datetime):
45
53
  declared = field_meta.declared
46
54
  presentation = field_meta.presentation
47
55
  if isinstance(value, DateTime):
48
- logging.info(
56
+ codec_logger.info(
49
57
  'Codec %s received a parsed datetime for %s; returning it unchanged: %s',
50
58
  cls.__name__,
51
59
  declared.label,
@@ -62,12 +70,7 @@ class Date(ExcelFieldCodec, datetime):
62
70
  dt: DateTime = cast(DateTime, pendulum.parse(v))
63
71
  return dt.replace(tzinfo=presentation.timezone)
64
72
  except Exception as exc:
65
- 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
@@ -1,4 +1,3 @@
1
- import logging
2
1
  from collections.abc import Mapping
3
2
  from datetime import datetime
4
3
  from typing import cast
@@ -9,7 +8,7 @@ from pydantic import BaseModel
9
8
 
10
9
  from excelalchemy._primitives.constants import DATE_FORMAT_TO_PYTHON_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
11
10
  from excelalchemy._primitives.identity import Key
12
- from excelalchemy.codecs.base import CompositeExcelFieldCodec
11
+ from excelalchemy.codecs.base import CompositeExcelFieldCodec, log_codec_parse_fallback, log_codec_render_fallback
13
12
  from excelalchemy.exceptions import ConfigError
14
13
  from excelalchemy.i18n.messages import MessageKey
15
14
  from excelalchemy.i18n.messages import display_message as dmsg
@@ -66,8 +65,13 @@ class DateRange(CompositeExcelFieldCodec):
66
65
  ]
67
66
  )
68
67
 
68
+ @classmethod
69
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
70
+ return msg(MessageKey.ENTER_DATE_RANGE_EXPECTED_FORMAT)
71
+
69
72
  @classmethod
70
73
  def parse_input(cls, value: object, field_meta: FieldMetaInfo) -> object:
74
+ declared = field_meta.declared
71
75
  mapping = cls._coerce_mapping(value)
72
76
  if mapping is not None:
73
77
  try:
@@ -76,7 +80,7 @@ class DateRange(CompositeExcelFieldCodec):
76
80
  'end': cls._parse_optional_datetime(mapping.get('end'), field_meta),
77
81
  }
78
82
  except Exception as exc:
79
- logging.warning('Could not parse value %s for field %s. Reason: %s', value, cls.__name__, exc)
83
+ log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
80
84
  return value
81
85
 
82
86
  if isinstance(value, datetime):
@@ -86,7 +90,7 @@ class DateRange(CompositeExcelFieldCodec):
86
90
  try:
87
91
  return cls._parse_datetime_text(value, field_meta)
88
92
  except Exception as exc:
89
- logging.warning('Could not parse value %s for field %s. Reason: %s', value, cls.__name__, exc)
93
+ log_codec_parse_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
90
94
  return value
91
95
 
92
96
  return value
@@ -144,7 +148,12 @@ class DateRange(CompositeExcelFieldCodec):
144
148
  if mapping is not None:
145
149
  return cls.__deserialize__dict(py_date_format, mapping)
146
150
 
147
- logging.warning('%s could not be deserialized; returning the original value', cls.__name__)
151
+ log_codec_render_fallback(
152
+ cls.__name__,
153
+ value,
154
+ field_label=field_meta.declared.label,
155
+ reason='The workbook value is not a string, datetime, or start/end mapping',
156
+ )
148
157
  return str(value)
149
158
 
150
159
  @classmethod
@@ -11,6 +11,10 @@ from excelalchemy.metadata import FieldMetaInfo
11
11
  class Email(String):
12
12
  _validator: ClassVar[TypeAdapter[EmailStr]] = TypeAdapter(EmailStr)
13
13
 
14
+ @classmethod
15
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
16
+ return msg(MessageKey.VALID_EMAIL_REQUIRED)
17
+
14
18
  @classmethod
15
19
  def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> str:
16
20
  # Try to parse the value as a string
@@ -1,9 +1,8 @@
1
- import logging
2
1
  from typing import cast
3
2
 
4
3
  from excelalchemy._primitives.constants import MULTI_CHECKBOX_SEPARATOR
5
4
  from excelalchemy._primitives.identity import OptionId
6
- from excelalchemy.codecs.base import ExcelFieldCodec
5
+ from excelalchemy.codecs.base import ExcelFieldCodec, log_codec_missing_options, log_codec_parse_fallback
7
6
  from excelalchemy.exceptions import ProgrammaticError
8
7
  from excelalchemy.i18n.messages import MessageKey
9
8
  from excelalchemy.i18n.messages import display_message as dmsg
@@ -14,6 +13,37 @@ from excelalchemy.metadata import FieldMetaInfo
14
13
  class MultiCheckbox(ExcelFieldCodec, list[str]):
15
14
  __name__ = 'MultiChoice'
16
15
 
16
+ @classmethod
17
+ def selection_entity_plural(cls) -> str | None:
18
+ return None
19
+
20
+ @classmethod
21
+ def _options_preview(cls, field_meta: FieldMetaInfo, *, limit: int = 5) -> str | None:
22
+ options = field_meta.presentation.options
23
+ if not options:
24
+ return None
25
+ preview = MULTI_CHECKBOX_SEPARATOR.join(option.name for option in options[:limit])
26
+ if len(options) > limit:
27
+ preview = f'{preview}{MULTI_CHECKBOX_SEPARATOR}...'
28
+ return preview
29
+
30
+ @classmethod
31
+ def _compose_selection_message(cls, field_meta: FieldMetaInfo) -> str:
32
+ entity_plural = cls.selection_entity_plural()
33
+ if entity_plural is None:
34
+ base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_OPTIONS)
35
+ else:
36
+ base_message = msg(MessageKey.SELECT_ONLY_CONFIGURED_ENTITIES, entity_plural=entity_plural)
37
+
38
+ preview = cls._options_preview(field_meta)
39
+ if preview is None:
40
+ return base_message
41
+ return f'{base_message}. {msg(MessageKey.VALID_VALUES_INCLUDE, options=preview)}'
42
+
43
+ @classmethod
44
+ def expected_input_message(cls, field_meta: FieldMetaInfo) -> str | None:
45
+ return cls._compose_selection_message(field_meta)
46
+
17
47
  @staticmethod
18
48
  def _coerce_items(value: object) -> list[object] | None:
19
49
  if not isinstance(value, list):
@@ -42,8 +72,11 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
42
72
  if isinstance(value, str):
43
73
  return [item.strip() for item in value.split(MULTI_CHECKBOX_SEPARATOR)]
44
74
 
45
- logging.warning(
46
- 'ValueType <%s> could not parse Excel input %s; returning the original value', cls.__name__, value
75
+ log_codec_parse_fallback(
76
+ cls.__name__,
77
+ value,
78
+ field_label=field_meta.declared.label,
79
+ reason='Expected a delimited string or a list of selected values',
47
80
  )
48
81
  return value
49
82
 
@@ -53,7 +86,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
53
86
  presentation = field_meta.presentation
54
87
  items = cls._coerce_items(value)
55
88
  if items is None:
56
- raise ValueError(msg(MessageKey.OPTION_NOT_FOUND_HEADER_COMMENT))
89
+ raise ValueError(cls._compose_selection_message(field_meta))
57
90
 
58
91
  parsed = [str(item).strip() for item in items]
59
92
 
@@ -61,9 +94,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
61
94
  raise ProgrammaticError(msg(MessageKey.OPTIONS_CANNOT_BE_NONE_FOR_VALUE_TYPE, value_type=cls.__name__))
62
95
 
63
96
  if not presentation.options: # empty
64
- logging.warning(
65
- 'Field %s of type %s has no options; returning the original value', declared.label, cls.__name__
66
- )
97
+ log_codec_missing_options(cls.__name__, field_label=declared.label)
67
98
  return parsed
68
99
 
69
100
  if len(parsed) != len(set(parsed)):
@@ -72,7 +103,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
72
103
  result, errors = presentation.exchange_names_to_option_ids_with_errors(parsed, field_label=declared.label)
73
104
 
74
105
  if errors:
75
- raise ValueError(*errors)
106
+ raise ValueError(cls._compose_selection_message(field_meta))
76
107
  else:
77
108
  return result
78
109
 
@@ -1,4 +1,3 @@
1
- import logging
2
1
  from decimal import ROUND_DOWN, Context, Decimal, InvalidOperation
3
2
 
4
3
  from excelalchemy.codecs.base import (
@@ -6,6 +5,8 @@ from excelalchemy.codecs.base import (
6
5
  NormalizedImportValue,
7
6
  WorkbookDisplayValue,
8
7
  WorkbookInputValue,
8
+ codec_logger,
9
+ log_codec_parse_fallback,
9
10
  )
10
11
  from excelalchemy.i18n.messages import MessageKey
11
12
  from excelalchemy.i18n.messages import display_message as dmsg
@@ -23,7 +24,7 @@ def canonicalize_decimal(value: Decimal, digits_limit: int | None) -> Decimal:
23
24
  context=Context(rounding=ROUND_DOWN),
24
25
  )
25
26
  except InvalidOperation as e:
26
- logging.warning('fraction_digits is too small and causes precision loss: %s', e)
27
+ codec_logger.warning('Codec Number detected precision loss while quantizing fraction_digits: %s', e)
27
28
  return value
28
29
 
29
30
 
@@ -64,6 +65,7 @@ class Number(Decimal, ExcelFieldCodec):
64
65
  value: str | int | float | WorkbookInputValue | None,
65
66
  field_meta: FieldMetaInfo,
66
67
  ) -> Decimal | WorkbookInputValue:
68
+ declared = field_meta.declared
67
69
  if isinstance(value, str):
68
70
  value = value.strip()
69
71
  if value is None:
@@ -71,12 +73,7 @@ class Number(Decimal, ExcelFieldCodec):
71
73
  try:
72
74
  return transform_decimal(Decimal(str(value)))
73
75
  except Exception as exc:
74
- 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