ExcelAlchemy 2.2.3__tar.gz → 2.2.4__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.3 → excelalchemy-2.2.4}/PKG-INFO +1 -1
  2. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/__init__.py +1 -1
  3. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/constants.py +0 -3
  4. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/identity.py +2 -4
  5. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/base.py +36 -13
  6. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/boolean.py +14 -8
  7. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/date.py +42 -21
  8. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/date_range.py +18 -13
  9. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/money.py +17 -4
  10. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/multi_checkbox.py +14 -8
  11. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/number.py +51 -29
  12. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/number_range.py +4 -2
  13. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/organization.py +14 -6
  14. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/phone_number.py +2 -2
  15. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/radio.py +24 -17
  16. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/staff.py +15 -7
  17. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/string.py +17 -13
  18. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/tree.py +20 -10
  19. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/url.py +2 -3
  20. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/alchemy.py +24 -10
  21. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/import_session.py +20 -10
  22. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/rows.py +6 -5
  23. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/schema.py +21 -15
  24. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/storage.py +2 -2
  25. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/writer.py +17 -11
  26. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/exceptions.py +3 -5
  27. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/helper/pydantic.py +19 -12
  28. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/metadata.py +204 -91
  29. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/file.py +2 -2
  30. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/LICENSE +0 -0
  31. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/README-pypi.md +0 -0
  32. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/pyproject.toml +0 -0
  33. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/__init__.py +0 -0
  34. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/deprecation.py +0 -0
  35. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/header_models.py +0 -0
  36. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/payloads.py +0 -0
  37. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/artifacts.py +0 -0
  38. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/__init__.py +0 -0
  39. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/email.py +0 -0
  40. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/config.py +0 -0
  41. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/const.py +0 -0
  42. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/__init__.py +0 -0
  43. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/abstract.py +0 -0
  44. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/executor.py +0 -0
  45. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/headers.py +0 -0
  46. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/rendering.py +0 -0
  47. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/storage_minio.py +0 -0
  48. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/storage_protocol.py +0 -0
  49. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/table.py +0 -0
  50. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/exc.py +0 -0
  51. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/header_models.py +0 -0
  52. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/helper/__init__.py +0 -0
  53. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/i18n/__init__.py +0 -0
  54. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/i18n/messages.py +0 -0
  55. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/identity.py +0 -0
  56. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/py.typed +0 -0
  57. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/results.py +0 -0
  58. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/__init__.py +0 -0
  59. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/abstract.py +0 -0
  60. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/alchemy.py +0 -0
  61. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/field.py +0 -0
  62. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/header.py +0 -0
  63. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/identity.py +0 -0
  64. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/result.py +0 -0
  65. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/__init__.py +0 -0
  66. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/boolean.py +0 -0
  67. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/date.py +0 -0
  68. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/date_range.py +0 -0
  69. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/email.py +0 -0
  70. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/money.py +0 -0
  71. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
  72. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/number.py +0 -0
  73. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/number_range.py +0 -0
  74. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/organization.py +0 -0
  75. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/phone_number.py +0 -0
  76. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/radio.py +0 -0
  77. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/staff.py +0 -0
  78. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/string.py +0 -0
  79. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/tree.py +0 -0
  80. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/url.py +0 -0
  81. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/__init__.py +0 -0
  82. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/converter.py +0 -0
  83. {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/convertor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ExcelAlchemy
3
- Version: 2.2.3
3
+ Version: 2.2.4
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
@@ -1,6 +1,6 @@
1
1
  """A Python Library for Reading and Writing Excel Files"""
2
2
 
3
- __version__ = '2.2.3'
3
+ __version__ = '2.2.4'
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 (
@@ -1,6 +1,5 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import StrEnum
3
- from typing import Any
4
3
 
5
4
  from excelalchemy._primitives.identity import Key, Label, OptionId
6
5
  from excelalchemy.i18n.messages import MessageKey
@@ -38,8 +37,6 @@ MILLISECOND_TO_SECOND = 1000
38
37
  MAX_OPTIONS_COUNT = 100
39
38
 
40
39
  DEFAULT_FIELD_META_ORDER = -1
41
- type DictStrAny = dict[str, Any]
42
- type DictAny = dict[Any, Any]
43
40
  type SetStr = set[str]
44
41
  type ListStr = list[str]
45
42
  type IntStr = int | str
@@ -1,7 +1,5 @@
1
1
  """Internal typed primitives used across the ExcelAlchemy core layer."""
2
2
 
3
- from typing import Any
4
-
5
3
  from pydantic import GetCoreSchemaHandler
6
4
  from pydantic_core import core_schema
7
5
 
@@ -10,7 +8,7 @@ class _StringIdentity(str):
10
8
  @classmethod
11
9
  def __get_pydantic_core_schema__(
12
10
  cls,
13
- source_type: Any,
11
+ source_type: object,
14
12
  handler: GetCoreSchemaHandler,
15
13
  ) -> core_schema.CoreSchema:
16
14
  return core_schema.no_info_after_validator_function(cls, core_schema.str_schema())
@@ -20,7 +18,7 @@ class _IntegerIdentity(int):
20
18
  @classmethod
21
19
  def __get_pydantic_core_schema__(
22
20
  cls,
23
- source_type: Any,
21
+ source_type: object,
24
22
  handler: GetCoreSchemaHandler,
25
23
  ) -> core_schema.CoreSchema:
26
24
  return core_schema.no_info_after_validator_function(cls, core_schema.int_schema())
@@ -11,6 +11,13 @@ from excelalchemy._primitives.identity import Key
11
11
  if TYPE_CHECKING:
12
12
  from excelalchemy.metadata import FieldMetaInfo
13
13
 
14
+ # These aliases remain `Any` intentionally because codec subclasses narrow their
15
+ # accepted workbook values heavily. Using `object` here makes every override
16
+ # incompatible under pyright's method override rules.
17
+ type WorkbookInputValue = Any
18
+ type WorkbookDisplayValue = Any
19
+ type NormalizedImportValue = Any
20
+
14
21
 
15
22
  class ExcelFieldCodec(ABC):
16
23
  """Excel-facing field adapter responsible for comments, parsing, formatting, and normalization."""
@@ -22,17 +29,17 @@ class ExcelFieldCodec(ABC):
22
29
 
23
30
  @classmethod
24
31
  @abstractmethod
25
- def parse_input(cls, value: Any, field_meta: FieldMetaInfo) -> Any: # value is always not None
32
+ def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
26
33
  """Parse workbook input into the intermediate Python value consumed by the import pipeline."""
27
34
 
28
35
  @classmethod
29
36
  @abstractmethod
30
- def format_display_value(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
37
+ def format_display_value(cls, value: WorkbookDisplayValue, field_meta: FieldMetaInfo) -> WorkbookDisplayValue:
31
38
  """Format a raw worksheet value back into a user-recognizable display value."""
32
39
 
33
40
  @classmethod
34
41
  @abstractmethod
35
- def normalize_import_value(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
42
+ def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
36
43
  """Validate and normalize parsed input before handing it to the Pydantic layer."""
37
44
 
38
45
  @classmethod
@@ -41,24 +48,24 @@ class ExcelFieldCodec(ABC):
41
48
  return cls.build_comment(field_meta)
42
49
 
43
50
  @classmethod
44
- def serialize(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
51
+ def serialize(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
45
52
  """Backward-compatible alias for parse_input()."""
46
53
  return cls.parse_input(value, field_meta)
47
54
 
48
55
  @classmethod
49
- def deserialize(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
56
+ def deserialize(cls, value: WorkbookDisplayValue, field_meta: FieldMetaInfo) -> WorkbookDisplayValue:
50
57
  """Backward-compatible alias for format_display_value()."""
51
58
  return cls.format_display_value(value, field_meta)
52
59
 
53
60
  @classmethod
54
- def __validate__(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
61
+ def __validate__(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
55
62
  """Backward-compatible alias for normalize_import_value()."""
56
63
  return cls.normalize_import_value(value, field_meta)
57
64
 
58
65
  @classmethod
59
66
  def __get_pydantic_core_schema__(
60
67
  cls,
61
- source_type: Any,
68
+ source_type: object,
62
69
  handler: GetCoreSchemaHandler,
63
70
  ) -> core_schema.CoreSchema:
64
71
  # ExcelAlchemy runs metadata-aware validation in its adapter layer.
@@ -88,15 +95,23 @@ class SystemReserved(ExcelFieldCodec):
88
95
  return ''
89
96
 
90
97
  @classmethod
91
- def parse_input(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
98
+ def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
92
99
  return value
93
100
 
94
101
  @classmethod
95
- def format_display_value(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
102
+ def format_display_value(
103
+ cls,
104
+ value: WorkbookDisplayValue,
105
+ field_meta: FieldMetaInfo,
106
+ ) -> WorkbookDisplayValue:
96
107
  return value
97
108
 
98
109
  @classmethod
99
- def normalize_import_value(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
110
+ def normalize_import_value(
111
+ cls,
112
+ value: WorkbookInputValue,
113
+ field_meta: FieldMetaInfo,
114
+ ) -> NormalizedImportValue:
100
115
  return value
101
116
 
102
117
 
@@ -108,15 +123,23 @@ class Undefined(ExcelFieldCodec):
108
123
  return ''
109
124
 
110
125
  @classmethod
111
- def parse_input(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
126
+ def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
112
127
  return value
113
128
 
114
129
  @classmethod
115
- def format_display_value(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
130
+ def format_display_value(
131
+ cls,
132
+ value: WorkbookDisplayValue,
133
+ field_meta: FieldMetaInfo,
134
+ ) -> WorkbookDisplayValue:
116
135
  return value
117
136
 
118
137
  @classmethod
119
- def normalize_import_value(cls, value: Any, field_meta: FieldMetaInfo) -> Any:
138
+ def normalize_import_value(
139
+ cls,
140
+ value: WorkbookInputValue,
141
+ field_meta: FieldMetaInfo,
142
+ ) -> NormalizedImportValue:
120
143
  return value
121
144
 
122
145
 
@@ -1,8 +1,7 @@
1
1
  import logging
2
- from typing import Any
3
2
 
4
3
  from excelalchemy.codecs import excel_choice_codec
5
- from excelalchemy.codecs.base import ExcelFieldCodec
4
+ from excelalchemy.codecs.base import ExcelFieldCodec, WorkbookDisplayValue, WorkbookInputValue
6
5
  from excelalchemy.i18n.messages import MessageKey
7
6
  from excelalchemy.i18n.messages import display_message as dmsg
8
7
  from excelalchemy.i18n.messages import message as msg
@@ -31,19 +30,26 @@ class Boolean(ExcelFieldCodec):
31
30
 
32
31
  @classmethod
33
32
  def build_comment(cls, field_meta: FieldMetaInfo) -> str:
33
+ declared = field_meta.declared
34
+ presentation = field_meta.presentation
34
35
  return '\n'.join(
35
36
  [
36
- field_meta.comment_required,
37
- field_meta.comment_hint,
37
+ declared.comment_required,
38
+ presentation.comment_hint,
38
39
  ]
39
40
  )
40
41
 
41
42
  @classmethod
42
- def parse_input(cls, value: Any, field_meta: FieldMetaInfo) -> str:
43
+ def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> str:
43
44
  return str(value).strip()
44
45
 
45
46
  @classmethod
46
- def format_display_value(cls, value: bool | str | None | Any, field_meta: FieldMetaInfo) -> str:
47
+ def format_display_value(
48
+ cls,
49
+ value: bool | str | WorkbookDisplayValue | None,
50
+ field_meta: FieldMetaInfo,
51
+ ) -> str:
52
+ declared = field_meta.declared
47
53
  if value is None or value == '':
48
54
  return cls._false_display()
49
55
 
@@ -64,14 +70,14 @@ class Boolean(ExcelFieldCodec):
64
70
  'Type %s could not deserialize %s for field %s; returning the default value %s',
65
71
  cls.__name__,
66
72
  value,
67
- field_meta.label,
73
+ declared.label,
68
74
  cls._false_display(),
69
75
  )
70
76
 
71
77
  return cls._true_display() if str(value) in cls._true_values() else cls._false_display()
72
78
 
73
79
  @classmethod
74
- def normalize_import_value(cls, value: str | bool | Any, field_meta: FieldMetaInfo) -> bool:
80
+ def normalize_import_value(cls, value: str | bool | WorkbookInputValue, field_meta: FieldMetaInfo) -> bool:
75
81
  if isinstance(value, bool):
76
82
  return value
77
83
 
@@ -1,12 +1,17 @@
1
1
  import logging
2
2
  from datetime import datetime
3
- from typing import Any, cast
3
+ from typing import cast
4
4
 
5
5
  import pendulum
6
6
  from pendulum import DateTime
7
7
 
8
8
  from excelalchemy._primitives.constants import DATE_FORMAT_TO_HINT_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
9
- from excelalchemy.codecs.base import ExcelFieldCodec
9
+ from excelalchemy.codecs.base import (
10
+ ExcelFieldCodec,
11
+ NormalizedImportValue,
12
+ WorkbookDisplayValue,
13
+ WorkbookInputValue,
14
+ )
10
15
  from excelalchemy.exceptions import ConfigError
11
16
  from excelalchemy.i18n.messages import MessageKey
12
17
  from excelalchemy.i18n.messages import message as msg
@@ -18,36 +23,44 @@ class Date(ExcelFieldCodec, datetime):
18
23
 
19
24
  @classmethod
20
25
  def build_comment(cls, field_meta: FieldMetaInfo) -> str:
21
- if not field_meta.date_format:
26
+ declared = field_meta.declared
27
+ presentation = field_meta.presentation
28
+ if not presentation.date_format:
22
29
  raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
23
30
  return '\n'.join(
24
31
  [
25
- field_meta.comment_required,
26
- field_meta.comment_date_format,
27
- field_meta.comment_date_range_option,
28
- field_meta.comment_hint,
32
+ declared.comment_required,
33
+ presentation.comment_date_format,
34
+ presentation.comment_date_range_option,
35
+ presentation.comment_hint,
29
36
  ]
30
37
  )
31
38
 
32
39
  @classmethod
33
- def parse_input(cls, value: str | DateTime | Any, field_meta: FieldMetaInfo) -> datetime | Any:
40
+ def parse_input(
41
+ cls,
42
+ value: str | DateTime | WorkbookInputValue,
43
+ field_meta: FieldMetaInfo,
44
+ ) -> datetime | WorkbookInputValue:
45
+ declared = field_meta.declared
46
+ presentation = field_meta.presentation
34
47
  if isinstance(value, DateTime):
35
48
  logging.info(
36
49
  'Codec %s received a parsed datetime for %s; returning it unchanged: %s',
37
50
  cls.__name__,
38
- field_meta.label,
51
+ declared.label,
39
52
  value,
40
53
  )
41
54
  return value
42
55
 
43
- if not field_meta.date_format:
56
+ if not presentation.date_format:
44
57
  raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
45
58
 
46
59
  value = str(value).strip()
47
60
  try:
48
61
  v = value.replace('/', '-') # pendulum does not accept "/" as a date separator here.
49
62
  dt: DateTime = cast(DateTime, pendulum.parse(v))
50
- return dt.replace(tzinfo=field_meta.timezone)
63
+ return dt.replace(tzinfo=presentation.timezone)
51
64
  except Exception as exc:
52
65
  logging.warning(
53
66
  'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
@@ -58,27 +71,33 @@ class Date(ExcelFieldCodec, datetime):
58
71
  return value
59
72
 
60
73
  @classmethod
61
- def format_display_value(cls, value: str | datetime | None | Any, field_meta: FieldMetaInfo) -> str:
74
+ def format_display_value(
75
+ cls,
76
+ value: str | datetime | WorkbookDisplayValue | None,
77
+ field_meta: FieldMetaInfo,
78
+ ) -> str:
79
+ presentation = field_meta.presentation
62
80
  match value:
63
81
  case None | '':
64
82
  return ''
65
83
  case datetime():
66
- return value.strftime(field_meta.python_date_format)
84
+ return value.strftime(presentation.python_date_format)
67
85
  case int() | float():
68
86
  return datetime.fromtimestamp(int(value) / MILLISECOND_TO_SECOND).strftime(
69
- field_meta.python_date_format
87
+ presentation.python_date_format
70
88
  )
71
89
  case _:
72
90
  return str(value) if value is not None else ''
73
91
 
74
92
  @classmethod
75
- def normalize_import_value(cls, value: Any, field_meta: FieldMetaInfo) -> int:
76
- if field_meta.date_format is None:
93
+ def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
94
+ presentation = field_meta.presentation
95
+ if presentation.date_format is None:
77
96
  raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
78
97
 
79
98
  if not isinstance(value, datetime):
80
99
  raise ValueError(
81
- msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[field_meta.date_format])
100
+ msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[presentation.date_format])
82
101
  )
83
102
 
84
103
  parsed = cls._parse_date(value, field_meta)
@@ -91,17 +110,19 @@ class Date(ExcelFieldCodec, datetime):
91
110
 
92
111
  @staticmethod
93
112
  def _parse_date(v: datetime, field_meta: FieldMetaInfo) -> datetime:
94
- format_ = field_meta.python_date_format
113
+ presentation = field_meta.presentation
114
+ format_ = presentation.python_date_format
95
115
  parsed = datetime.strptime(v.strftime(format_), format_)
96
- parsed = parsed.replace(tzinfo=field_meta.timezone)
116
+ parsed = parsed.replace(tzinfo=presentation.timezone)
97
117
  return parsed
98
118
 
99
119
  @staticmethod
100
120
  def _validate_date_range(parsed: datetime, field_meta: FieldMetaInfo) -> list[str]:
101
- now = datetime.now(tz=field_meta.timezone)
121
+ presentation = field_meta.presentation
122
+ now = datetime.now(tz=presentation.timezone)
102
123
  errors: list[str] = []
103
124
 
104
- match field_meta.date_range_option:
125
+ match presentation.date_range_option:
105
126
  case DataRangeOption.PRE:
106
127
  if parsed > now:
107
128
  errors.append(msg(MessageKey.DATE_MUST_BE_EARLIER_THAN_NOW))
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from collections.abc import Mapping
3
3
  from datetime import datetime
4
- from typing import Any, cast
4
+ from typing import cast
5
5
 
6
6
  import pendulum
7
7
  from pendulum import DateTime
@@ -28,7 +28,7 @@ class DateRange(CompositeExcelFieldCodec):
28
28
  __name__ = 'DateRange'
29
29
 
30
30
  @classmethod
31
- def model_validate(cls, obj: Any) -> 'DateRange':
31
+ def model_validate(cls, obj: object) -> 'DateRange':
32
32
  impl = _DateRangeImpl.model_validate(obj)
33
33
  self = cls(impl.start, impl.end)
34
34
  return self
@@ -50,14 +50,16 @@ class DateRange(CompositeExcelFieldCodec):
50
50
 
51
51
  @classmethod
52
52
  def build_comment(cls, field_meta: FieldMetaInfo) -> str:
53
- if field_meta.date_format is None:
53
+ declared = field_meta.declared
54
+ presentation = field_meta.presentation
55
+ if presentation.date_format is None:
54
56
  raise RuntimeError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
55
57
 
56
58
  return '\n'.join(
57
59
  [
58
- field_meta.comment_required,
59
- field_meta.comment_date_format,
60
- dmsg(MessageKey.COMMENT_DATE_RANGE_START_NOT_AFTER_END, extra_hint=field_meta.hint or ''),
60
+ declared.comment_required,
61
+ presentation.comment_date_format,
62
+ dmsg(MessageKey.COMMENT_DATE_RANGE_START_NOT_AFTER_END, extra_hint=presentation.hint or ''),
61
63
  ]
62
64
  )
63
65
 
@@ -92,20 +94,21 @@ class DateRange(CompositeExcelFieldCodec):
92
94
  value: object,
93
95
  field_meta: FieldMetaInfo,
94
96
  ) -> 'DateRange':
97
+ presentation = field_meta.presentation
95
98
  try:
96
99
  parsed = DateRange.model_validate(value)
97
- parsed.start = pendulum.instance(parsed.start, tz=field_meta.timezone) if parsed.start else None
98
- parsed.end = pendulum.instance(parsed.end, tz=field_meta.timezone) if parsed.end else None
100
+ parsed.start = pendulum.instance(parsed.start, tz=presentation.timezone) if parsed.start else None
101
+ parsed.end = pendulum.instance(parsed.end, tz=presentation.timezone) if parsed.end else None
99
102
  except Exception as exc:
100
103
  raise ValueError(msg(MessageKey.INVALID_INPUT)) from exc
101
104
 
102
105
  errors: list[str] = []
103
- now = datetime.now(tz=field_meta.timezone)
106
+ now = datetime.now(tz=presentation.timezone)
104
107
 
105
108
  if parsed.start and parsed.end and parsed.start > parsed.end:
106
109
  errors.append(msg(MessageKey.DATE_RANGE_START_AFTER_END))
107
110
 
108
- match field_meta.date_range_option:
111
+ match presentation.date_range_option:
109
112
  case DataRangeOption.PRE:
110
113
  if (parsed.start and parsed.start > now) or (parsed.end and parsed.end > now):
111
114
  errors.append(msg(MessageKey.DATE_MUST_BE_EARLIER_THAN_NOW))
@@ -124,7 +127,8 @@ class DateRange(CompositeExcelFieldCodec):
124
127
  def format_display_value(cls, value: object | None, field_meta: FieldMetaInfo) -> str:
125
128
  if value is None or value == '':
126
129
  return ''
127
- date_format = field_meta.must_date_format
130
+ presentation = field_meta.presentation
131
+ date_format = presentation.must_date_format
128
132
  py_date_format = DATE_FORMAT_TO_PYTHON_MAPPING[date_format]
129
133
 
130
134
  if isinstance(value, str):
@@ -178,11 +182,12 @@ class DateRange(CompositeExcelFieldCodec):
178
182
 
179
183
  @staticmethod
180
184
  def _parse_datetime_text(value: str, field_meta: FieldMetaInfo) -> DateTime:
185
+ presentation = field_meta.presentation
181
186
  parsed = pendulum.parse(value)
182
187
  if isinstance(parsed, DateTime):
183
- return parsed.replace(tzinfo=field_meta.timezone)
188
+ return parsed.replace(tzinfo=presentation.timezone)
184
189
  if isinstance(parsed, datetime):
185
- return pendulum.instance(parsed).replace(tzinfo=field_meta.timezone)
190
+ return pendulum.instance(parsed).replace(tzinfo=presentation.timezone)
186
191
  raise ValueError(msg(MessageKey.INVALID_INPUT))
187
192
 
188
193
 
@@ -1,5 +1,7 @@
1
- from typing import Any, ClassVar
1
+ from dataclasses import replace
2
+ from typing import ClassVar
2
3
 
4
+ from excelalchemy.codecs.base import NormalizedImportValue, WorkbookDisplayValue, WorkbookInputValue
3
5
  from excelalchemy.codecs.number import Number
4
6
  from excelalchemy.metadata import FieldMetaInfo
5
7
 
@@ -10,7 +12,10 @@ class Money(Number):
10
12
  @classmethod
11
13
  def _money_field_meta(cls, field_meta: FieldMetaInfo) -> FieldMetaInfo:
12
14
  money_field_meta = field_meta.clone()
13
- money_field_meta.fraction_digits = cls.MONEY_FRACTION_DIGITS
15
+ money_field_meta.presentation_meta = replace(
16
+ money_field_meta.presentation_meta,
17
+ fraction_digits=cls.MONEY_FRACTION_DIGITS,
18
+ )
14
19
  return money_field_meta
15
20
 
16
21
  @classmethod
@@ -18,11 +23,19 @@ class Money(Number):
18
23
  return super().build_comment(cls._money_field_meta(field_meta))
19
24
 
20
25
  @classmethod
21
- def format_display_value(cls, value: str | None | Any, field_meta: FieldMetaInfo) -> str:
26
+ def format_display_value(
27
+ cls,
28
+ value: str | WorkbookDisplayValue | None,
29
+ field_meta: FieldMetaInfo,
30
+ ) -> str:
22
31
  return super().format_display_value(value, cls._money_field_meta(field_meta))
23
32
 
24
33
  @classmethod
25
- def normalize_import_value(cls, value: Any, field_meta: FieldMetaInfo) -> float | int:
34
+ def normalize_import_value(
35
+ cls,
36
+ value: WorkbookInputValue,
37
+ field_meta: FieldMetaInfo,
38
+ ) -> NormalizedImportValue:
26
39
  return super().normalize_import_value(value, cls._money_field_meta(field_meta))
27
40
 
28
41
 
@@ -16,12 +16,14 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
16
16
 
17
17
  @classmethod
18
18
  def build_comment(cls, field_meta: FieldMetaInfo) -> str:
19
+ declared = field_meta.declared
20
+ presentation = field_meta.presentation
19
21
  return '\n'.join(
20
22
  [
21
- field_meta.comment_required,
22
- field_meta.comment_options,
23
+ declared.comment_required,
24
+ presentation.comment_options,
23
25
  dmsg(MessageKey.COMMENT_SELECTION_MODE, value=dmsg(MessageKey.COMMENT_SELECTION_VALUE_MULTI)),
24
- field_meta.comment_hint,
26
+ presentation.comment_hint,
25
27
  ]
26
28
  )
27
29
 
@@ -41,25 +43,27 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
41
43
 
42
44
  @classmethod
43
45
  def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> list[str]: # OptionId
46
+ declared = field_meta.declared
47
+ presentation = field_meta.presentation
44
48
  if not isinstance(value, list):
45
49
  raise ValueError(msg(MessageKey.OPTION_NOT_FOUND_HEADER_COMMENT))
46
50
 
47
51
  items = cast(list[object], value)
48
52
  parsed = [str(item).strip() for item in items]
49
53
 
50
- if field_meta.options is None:
54
+ if presentation.options is None:
51
55
  raise ProgrammaticError(msg(MessageKey.OPTIONS_CANNOT_BE_NONE_FOR_VALUE_TYPE, value_type=cls.__name__))
52
56
 
53
- if not field_meta.options: # empty
57
+ if not presentation.options: # empty
54
58
  logging.warning(
55
- 'Field %s of type %s has no options; returning the original value', field_meta.label, cls.__name__
59
+ 'Field %s of type %s has no options; returning the original value', declared.label, cls.__name__
56
60
  )
57
61
  return parsed
58
62
 
59
63
  if len(parsed) != len(set(parsed)):
60
64
  raise ValueError(msg(MessageKey.OPTIONS_CONTAIN_DUPLICATES))
61
65
 
62
- result, errors = field_meta.exchange_names_to_option_ids_with_errors(parsed)
66
+ result, errors = presentation.exchange_names_to_option_ids_with_errors(parsed, field_label=declared.label)
63
67
 
64
68
  if errors:
65
69
  raise ValueError(*errors)
@@ -68,6 +72,8 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
68
72
 
69
73
  @classmethod
70
74
  def format_display_value(cls, value: str | list[OptionId] | None, field_meta: FieldMetaInfo) -> str:
75
+ declared = field_meta.declared
76
+ presentation = field_meta.presentation
71
77
  match value:
72
78
  case None | '':
73
79
  return ''
@@ -75,7 +81,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
75
81
  return value
76
82
  case list():
77
83
  option_ids = [OptionId(option_id) for option_id in value]
78
- option_names = field_meta.exchange_option_ids_to_names(option_ids)
84
+ option_names = presentation.exchange_option_ids_to_names(option_ids, field_label=declared.label)
79
85
  return f'{MULTI_CHECKBOX_SEPARATOR}'.join(option_names)
80
86
 
81
87