ExcelAlchemy 2.2.5__tar.gz → 2.2.6__tar.gz

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