yera 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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 (192) hide show
  1. infra_mvp/base_client.py +29 -0
  2. infra_mvp/base_server.py +68 -0
  3. infra_mvp/monitoring/__init__.py +15 -0
  4. infra_mvp/monitoring/metrics.py +185 -0
  5. infra_mvp/stream/README.md +56 -0
  6. infra_mvp/stream/__init__.py +14 -0
  7. infra_mvp/stream/__main__.py +101 -0
  8. infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
  9. infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
  10. infra_mvp/stream/agents/reference/blocks/action.json +170 -0
  11. infra_mvp/stream/agents/reference/blocks/button.json +66 -0
  12. infra_mvp/stream/agents/reference/blocks/date.json +65 -0
  13. infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
  14. infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
  15. infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
  16. infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
  17. infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
  18. infra_mvp/stream/agents/reference/blocks/table.json +56 -0
  19. infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
  20. infra_mvp/stream/app.py +49 -0
  21. infra_mvp/stream/container.py +112 -0
  22. infra_mvp/stream/schemas/__init__.py +16 -0
  23. infra_mvp/stream/schemas/agent.py +24 -0
  24. infra_mvp/stream/schemas/interaction.py +28 -0
  25. infra_mvp/stream/schemas/session.py +30 -0
  26. infra_mvp/stream/server.py +321 -0
  27. infra_mvp/stream/services/__init__.py +12 -0
  28. infra_mvp/stream/services/agent_service.py +40 -0
  29. infra_mvp/stream/services/event_converter.py +83 -0
  30. infra_mvp/stream/services/session_service.py +247 -0
  31. yera/__init__.py +50 -1
  32. yera/agents/__init__.py +2 -0
  33. yera/agents/context.py +41 -0
  34. yera/agents/dataclasses.py +69 -0
  35. yera/agents/decorator.py +207 -0
  36. yera/agents/discovery.py +124 -0
  37. yera/agents/typing/__init__.py +0 -0
  38. yera/agents/typing/coerce.py +408 -0
  39. yera/agents/typing/utils.py +19 -0
  40. yera/agents/typing/validate.py +206 -0
  41. yera/cli.py +377 -0
  42. yera/config/__init__.py +1 -0
  43. yera/config/config_utils.py +164 -0
  44. yera/config/function_config.py +55 -0
  45. yera/config/logging.py +18 -0
  46. yera/config/tool_config.py +8 -0
  47. yera/config2/__init__.py +8 -0
  48. yera/config2/dataclasses.py +534 -0
  49. yera/config2/keyring.py +270 -0
  50. yera/config2/paths.py +28 -0
  51. yera/config2/read.py +113 -0
  52. yera/config2/setup.py +109 -0
  53. yera/config2/setup_handlers/__init__.py +1 -0
  54. yera/config2/setup_handlers/anthropic.py +126 -0
  55. yera/config2/setup_handlers/azure.py +236 -0
  56. yera/config2/setup_handlers/base.py +125 -0
  57. yera/config2/setup_handlers/llama_cpp.py +205 -0
  58. yera/config2/setup_handlers/ollama.py +157 -0
  59. yera/config2/setup_handlers/openai.py +137 -0
  60. yera/config2/write.py +87 -0
  61. yera/dsl/__init__.py +0 -0
  62. yera/dsl/functions.py +94 -0
  63. yera/dsl/struct.py +20 -0
  64. yera/dsl/workspace.py +79 -0
  65. yera/events/__init__.py +57 -0
  66. yera/events/blocks/__init__.py +68 -0
  67. yera/events/blocks/action.py +57 -0
  68. yera/events/blocks/bar_chart.py +92 -0
  69. yera/events/blocks/base/__init__.py +20 -0
  70. yera/events/blocks/base/base.py +166 -0
  71. yera/events/blocks/base/chart.py +288 -0
  72. yera/events/blocks/base/layout.py +111 -0
  73. yera/events/blocks/buttons.py +37 -0
  74. yera/events/blocks/columns.py +26 -0
  75. yera/events/blocks/container.py +24 -0
  76. yera/events/blocks/date_picker.py +50 -0
  77. yera/events/blocks/exit.py +39 -0
  78. yera/events/blocks/form.py +24 -0
  79. yera/events/blocks/input_echo.py +22 -0
  80. yera/events/blocks/input_request.py +31 -0
  81. yera/events/blocks/line_chart.py +97 -0
  82. yera/events/blocks/markdown.py +67 -0
  83. yera/events/blocks/slider.py +54 -0
  84. yera/events/blocks/spinner.py +55 -0
  85. yera/events/blocks/system_prompt.py +22 -0
  86. yera/events/blocks/table.py +291 -0
  87. yera/events/models/__init__.py +39 -0
  88. yera/events/models/block_data.py +112 -0
  89. yera/events/models/in_event.py +7 -0
  90. yera/events/models/out_event.py +75 -0
  91. yera/events/runtime.py +187 -0
  92. yera/events/stream.py +91 -0
  93. yera/models/__init__.py +0 -0
  94. yera/models/data_classes.py +20 -0
  95. yera/models/llm_atlas_proxy.py +44 -0
  96. yera/models/llm_context.py +99 -0
  97. yera/models/llm_interfaces/__init__.py +0 -0
  98. yera/models/llm_interfaces/anthropic.py +153 -0
  99. yera/models/llm_interfaces/aws_bedrock.py +14 -0
  100. yera/models/llm_interfaces/azure_openai.py +143 -0
  101. yera/models/llm_interfaces/base.py +26 -0
  102. yera/models/llm_interfaces/interface_registry.py +74 -0
  103. yera/models/llm_interfaces/llama_cpp.py +136 -0
  104. yera/models/llm_interfaces/mock.py +29 -0
  105. yera/models/llm_interfaces/ollama_interface.py +118 -0
  106. yera/models/llm_interfaces/open_ai.py +150 -0
  107. yera/models/llm_workspace.py +19 -0
  108. yera/models/model_atlas.py +139 -0
  109. yera/models/model_definition.py +38 -0
  110. yera/models/model_factory.py +33 -0
  111. yera/opaque/__init__.py +9 -0
  112. yera/opaque/base.py +20 -0
  113. yera/opaque/decorator.py +8 -0
  114. yera/opaque/markdown.py +57 -0
  115. yera/opaque/opaque_function.py +25 -0
  116. yera/tools/__init__.py +29 -0
  117. yera/tools/atlas_tool.py +20 -0
  118. yera/tools/base.py +24 -0
  119. yera/tools/decorated_tool.py +18 -0
  120. yera/tools/decorator.py +35 -0
  121. yera/tools/tool_atlas.py +51 -0
  122. yera/tools/tool_utils.py +361 -0
  123. yera/ui/dist/404.html +1 -0
  124. yera/ui/dist/__next.__PAGE__.txt +10 -0
  125. yera/ui/dist/__next._full.txt +23 -0
  126. yera/ui/dist/__next._head.txt +6 -0
  127. yera/ui/dist/__next._index.txt +5 -0
  128. yera/ui/dist/__next._tree.txt +7 -0
  129. yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
  130. yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
  131. yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
  132. yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
  133. yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
  134. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  135. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  136. yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
  137. yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
  138. yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
  139. yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
  140. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_buildManifest.js +11 -0
  141. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_clientMiddlewareManifest.json +1 -0
  142. yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_ssgManifest.js +1 -0
  143. yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
  144. yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
  145. yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
  146. yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
  147. yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
  148. yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
  149. yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  150. yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
  151. yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
  152. yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  153. yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  154. yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  155. yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
  156. yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
  157. yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
  158. yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
  159. yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  160. yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  161. yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
  162. yera/ui/dist/_not-found/__next._full.txt +14 -0
  163. yera/ui/dist/_not-found/__next._head.txt +6 -0
  164. yera/ui/dist/_not-found/__next._index.txt +5 -0
  165. yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
  166. yera/ui/dist/_not-found/__next._not-found.txt +4 -0
  167. yera/ui/dist/_not-found/__next._tree.txt +2 -0
  168. yera/ui/dist/_not-found.html +1 -0
  169. yera/ui/dist/_not-found.txt +14 -0
  170. yera/ui/dist/agent-icon.svg +3 -0
  171. yera/ui/dist/favicon.ico +0 -0
  172. yera/ui/dist/file.svg +1 -0
  173. yera/ui/dist/globe.svg +1 -0
  174. yera/ui/dist/index.html +1 -0
  175. yera/ui/dist/index.txt +23 -0
  176. yera/ui/dist/logo/full_logo.png +0 -0
  177. yera/ui/dist/logo/rune_logo.png +0 -0
  178. yera/ui/dist/logo/rune_logo_borderless.png +0 -0
  179. yera/ui/dist/logo/text_logo.png +0 -0
  180. yera/ui/dist/next.svg +1 -0
  181. yera/ui/dist/send.png +0 -0
  182. yera/ui/dist/send_single.png +0 -0
  183. yera/ui/dist/vercel.svg +1 -0
  184. yera/ui/dist/window.svg +1 -0
  185. yera/utils/__init__.py +1 -0
  186. yera/utils/path_utils.py +38 -0
  187. yera-0.2.0.dist-info/METADATA +65 -0
  188. yera-0.2.0.dist-info/RECORD +190 -0
  189. {yera-0.1.0.dist-info → yera-0.2.0.dist-info}/WHEEL +1 -1
  190. yera-0.2.0.dist-info/entry_points.txt +2 -0
  191. yera-0.1.0.dist-info/METADATA +0 -11
  192. yera-0.1.0.dist-info/RECORD +0 -4
@@ -0,0 +1,124 @@
1
+ """Utilities for loading agents from files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from yera.agents.decorator import AgentFunctionWrapper
10
+ from yera.utils.path_utils import path_to_module_name
11
+
12
+
13
+ def load_agent(
14
+ file_path: Path | str, agent_identifier: str | None = None
15
+ ) -> AgentFunctionWrapper:
16
+ """Load a Python file and extract the AgentFunctionWrapper instance with the given identifier.
17
+
18
+ Args:
19
+ file_path: Path to the Python file containing one or more agents.
20
+ Can be a Path object or string.
21
+ agent_identifier: Identifier of the agent to load. If None, the first agent is loaded.
22
+
23
+ Returns:
24
+ AgentFunctionWrapper instance.
25
+
26
+ Raises:
27
+ ValueError: If no agents are found in the file.
28
+ ValueError: If multiple agents are found in the file and no identifier is provided.
29
+ ValueError: If the specified agent identifier is not found in the file.
30
+ ValueError: If the specified agent identifier does not match the identifier of the loaded agent.
31
+ """
32
+ agents = load_agents(file_path)
33
+
34
+ if not agents:
35
+ raise ValueError(f"No agents found in {file_path}")
36
+
37
+ if agent_identifier is None:
38
+ if len(agents) > 1:
39
+ raise ValueError(
40
+ f"Multiple agents found in {file_path}, please specify an agent identifier"
41
+ )
42
+ return agents[0]
43
+
44
+ matching_agents = [
45
+ agent for agent in agents if agent.metadata.identifier == agent_identifier
46
+ ]
47
+ if not matching_agents:
48
+ raise ValueError(f"Agent with ID {agent_identifier} not found in {file_path}")
49
+ if len(matching_agents) > 1:
50
+ raise ValueError(
51
+ f"Multiple agents with ID {agent_identifier} found in {file_path}"
52
+ )
53
+ return matching_agents[0]
54
+
55
+
56
+ def load_agents(file_path: Path | str) -> list[AgentFunctionWrapper]:
57
+ """Load a Python file and extract all AgentFunctionWrapper instances.
58
+
59
+ This helper looks for objects in the module that are instances of
60
+ AgentFunctionWrapper (created by the @yr.agent decorator).
61
+
62
+ Args:
63
+ file_path: Path to the Python file containing one or more agents.
64
+ Can be a Path object or string.
65
+
66
+ Returns:
67
+ Dictionary mapping agent identifiers to AgentFunctionWrapper instances.
68
+
69
+ Raises:
70
+ FileNotFoundError: If the file does not exist.
71
+ """
72
+ path = Path(file_path) if isinstance(file_path, str) else file_path
73
+ if not path.exists():
74
+ raise FileNotFoundError(f"Python file not found: {path}")
75
+
76
+ # Load the module from the given file path
77
+ module_name = path_to_module_name(path)
78
+ spec = importlib.util.spec_from_file_location(module_name, path)
79
+ if spec is None or spec.loader is None:
80
+ raise FileNotFoundError(f"Could not load Python module from: {path}")
81
+
82
+ module = importlib.util.module_from_spec(spec)
83
+ # Exec here is fine because we are only allowing users to run via the CLI locally,
84
+ # so there is no risk of arbitrary remote code execution.
85
+ # Deployment will require tracing, which will NOT exec the python directly.
86
+ spec.loader.exec_module(module)
87
+
88
+ # Collect all AgentFunctionWrapper instances defined in the module
89
+ return [
90
+ value
91
+ for value in vars(module).values()
92
+ if isinstance(value, AgentFunctionWrapper)
93
+ ]
94
+
95
+
96
+ def discover_agents(directory: Path | str) -> list[AgentFunctionWrapper]:
97
+ """Discover all agents in a directory.
98
+
99
+ Scans the directory for .py files and loads all agents from each file.
100
+
101
+ Args:
102
+ directory: Directory to scan for agent files.
103
+
104
+ Returns:
105
+ List of AgentFunctionWrapper instances.
106
+
107
+ Raises:
108
+ FileNotFoundError: If the directory does not exist.
109
+ """
110
+ dir_path = Path(directory) if isinstance(directory, str) else directory
111
+ if not dir_path.exists():
112
+ raise FileNotFoundError(f"Directory not found: {dir_path}")
113
+ if not dir_path.is_dir():
114
+ raise ValueError(f"Path is not a directory: {dir_path}")
115
+
116
+ all_agents = []
117
+ for py_file in dir_path.rglob("*.py"):
118
+ try:
119
+ all_agents.extend(load_agents(py_file))
120
+ # Ignore PERF203 because I/O file loading is the real bottleneck.
121
+ except FileNotFoundError: # noqa: PERF203
122
+ logging.warning(f"Failed to load agents from {py_file}")
123
+ continue
124
+ return all_agents
File without changes
@@ -0,0 +1,408 @@
1
+ import io
2
+ import json
3
+ from datetime import date, datetime, time, timedelta
4
+ from decimal import Decimal, InvalidOperation
5
+ from enum import Enum
6
+ from types import UnionType
7
+ from typing import TYPE_CHECKING, Any, Literal, Union, get_args, get_origin
8
+
9
+ if TYPE_CHECKING:
10
+ import pandas as pd
11
+
12
+ from yera.agents.typing.utils import get_type_name
13
+ from yera.agents.typing.validate import is_enum_type, is_struct_type
14
+ from yera.dsl.struct import Struct
15
+
16
+ from .validate import is_dataframe_type
17
+
18
+
19
+ def coerce_set(value: Any, expected_type: Any) -> set:
20
+ if isinstance(value, str):
21
+ try:
22
+ value = json.loads(value)
23
+ except json.JSONDecodeError as e:
24
+ raise TypeError(f"Cannot parse set from JSON string: {e}") from None
25
+
26
+ if isinstance(value, set | list):
27
+ items = value
28
+ else:
29
+ raise TypeError(f"Expected set or list, got {type(value).__name__}")
30
+
31
+ type_args = get_args(expected_type)
32
+ if not type_args:
33
+ raise TypeError(
34
+ "Set type must specify element type (e.g., set[int], not plain set)"
35
+ )
36
+
37
+ element_type = type_args[0]
38
+ return {coerce_value(item, element_type) for i, item in enumerate(items)}
39
+
40
+
41
+ def coerce_datetime(value: Any) -> datetime:
42
+ """Coerce to datetime."""
43
+ if isinstance(value, datetime):
44
+ return value
45
+ if isinstance(value, str):
46
+ # Try ISO format first
47
+ try:
48
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
49
+ except ValueError:
50
+ # Fallback to common formats
51
+ for fmt in [
52
+ "%Y-%m-%d %H:%M:%S",
53
+ "%Y-%m-%dT%H:%M:%S",
54
+ "%Y-%m-%d %H:%M:%S.%f",
55
+ "%Y-%m-%dT%H:%M:%S.%f",
56
+ ]:
57
+ try:
58
+ return datetime.strptime(value, fmt)
59
+ except ValueError: # noqa: PERF203
60
+ continue
61
+ raise TypeError(f"Cannot parse datetime from string: {value!r}") from None
62
+ elif isinstance(value, int | float):
63
+ # Unix timestamp
64
+ try:
65
+ return datetime.fromtimestamp(value)
66
+ except (ValueError, OSError) as e:
67
+ raise TypeError(
68
+ f"Cannot convert timestamp {value} to datetime: {e}"
69
+ ) from None
70
+ else:
71
+ raise TypeError(f"Cannot convert {type(value).__name__} to datetime")
72
+
73
+
74
+ def coerce_date(value: Any) -> date:
75
+ """Coerce to date."""
76
+ if isinstance(value, date) and not isinstance(value, datetime):
77
+ return value
78
+ if isinstance(value, datetime):
79
+ return value.date()
80
+ if isinstance(value, str):
81
+ try:
82
+ return date.fromisoformat(value)
83
+ except ValueError:
84
+ # Try parsing as datetime and extract date
85
+ try:
86
+ dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
87
+ return dt.date()
88
+ except ValueError:
89
+ raise TypeError(f"Cannot parse date from string: {value!r}") from None
90
+ else:
91
+ raise TypeError(f"Cannot convert {type(value).__name__} to date")
92
+
93
+
94
+ def coerce_time(value: Any) -> time:
95
+ """Coerce to time."""
96
+ if isinstance(value, time):
97
+ return value
98
+ if isinstance(value, str):
99
+ try:
100
+ return time.fromisoformat(value)
101
+ except ValueError:
102
+ raise TypeError(
103
+ f"Cannot convert {type(value).__name__} to time. "
104
+ f"Expected ISO format time."
105
+ ) from None
106
+ else:
107
+ raise TypeError(f"Cannot convert {type(value).__name__} to time")
108
+
109
+
110
+ def coerce_timedelta(value: Any) -> timedelta:
111
+ """Coerce to timedelta."""
112
+ if isinstance(value, timedelta):
113
+ return value
114
+ if isinstance(value, str):
115
+ try:
116
+ return timedelta(seconds=float(value))
117
+ except ValueError:
118
+ raise TypeError(
119
+ f"Cannot convert timedelta from invalid string: {value!r}. "
120
+ f"Expected a float for number of seconds."
121
+ ) from None
122
+ else:
123
+ raise TypeError(f"Cannot convert {type(value).__name__} to timedelta")
124
+
125
+
126
+ def coerce_decimal(value: Any) -> Decimal:
127
+ """Coerce to Decimal."""
128
+ if isinstance(value, Decimal):
129
+ return value
130
+ if isinstance(value, int | float | str):
131
+ try:
132
+ return Decimal(str(value))
133
+ except (ValueError, InvalidOperation) as e:
134
+ raise TypeError(f"Cannot convert {value!r} to Decimal: {e}") from None
135
+ else:
136
+ raise TypeError(f"Cannot convert {type(value).__name__} to Decimal")
137
+
138
+
139
+ def coerce_enum(value: Any, expected_type: type[Enum]) -> Enum:
140
+ """Coerce to Enum type."""
141
+ if isinstance(value, expected_type):
142
+ return value
143
+ if isinstance(value, str):
144
+ # Try by name first
145
+ try:
146
+ return expected_type[value]
147
+ except KeyError:
148
+ valid_names = [e.name for e in expected_type]
149
+ raise TypeError(
150
+ f"'{value}' string value is not a valid {expected_type.__name__}. "
151
+ f"Valid names: {valid_names}."
152
+ ) from None
153
+ elif isinstance(value, int):
154
+ try:
155
+ return expected_type(value)
156
+ except ValueError:
157
+ valid_values = [e.value for e in expected_type]
158
+ raise TypeError(
159
+ f"{value} is not a valid {expected_type.__name__} value. "
160
+ f"Valid values: {valid_values}"
161
+ ) from None
162
+ else:
163
+ raise TypeError(
164
+ f"Cannot convert {type(value).__name__} to {expected_type.__name__}. "
165
+ f"Expected str or int."
166
+ )
167
+
168
+
169
+ def serialise_dataframe(df: "pd.DataFrame") -> bytes:
170
+ """Serialize DataFrame to Parquet bytes."""
171
+ buffer = io.BytesIO()
172
+ df.to_parquet(buffer, engine="pyarrow")
173
+ return buffer.getvalue()
174
+
175
+
176
+ def coerce_dataframe(value: Any) -> "pd.DataFrame":
177
+ """Coerce to pandas DataFrame."""
178
+ import pandas as pd
179
+
180
+ if isinstance(value, pd.DataFrame):
181
+ return value
182
+ if isinstance(value, bytes):
183
+ return pd.read_parquet(io.BytesIO(value), engine="pyarrow")
184
+ raise TypeError(
185
+ f"Cannot convert {type(value).__name__} to DataFrame. "
186
+ f"Must be a pandas DataFrame or parquet bytes."
187
+ )
188
+
189
+
190
+ def coerce_none(value: Any) -> None:
191
+ """Coerce to None type."""
192
+ if value is None or value == "None":
193
+ return
194
+ raise TypeError(f"Expected None, got {type(value).__name__}")
195
+
196
+
197
+ def coerce_bool(value: Any) -> bool:
198
+ """Coerce to bool with proper string handling."""
199
+ if isinstance(value, bool):
200
+ return value
201
+ if isinstance(value, str):
202
+ if value.lower() == "true":
203
+ return True
204
+ if value.lower() == "false":
205
+ return False
206
+ raise TypeError(
207
+ f"Cannot convert {value!r} to bool. "
208
+ f"Expected one of: true, false, 1, 0, yes, no, y, n, on, off (case-insensitive)"
209
+ )
210
+ if isinstance(value, int):
211
+ return bool(value)
212
+ raise TypeError(f"Cannot convert {type(value).__name__} to bool")
213
+
214
+
215
+ def coerce_str(value: Any) -> str:
216
+ """Coerce to str with proper string handling."""
217
+ try:
218
+ return str(value)
219
+ except (ValueError, TypeError) as e:
220
+ raise TypeError(f"Cannot convert {value!r} to str. ") from None
221
+
222
+
223
+ def coerce_int(value: Any) -> int:
224
+ """Coerce to int with proper string handling."""
225
+ try:
226
+ return int(value)
227
+ except (ValueError, TypeError) as e:
228
+ raise TypeError(f"Cannot convert {value!r} to int") from None
229
+
230
+
231
+ def coerce_float(value: Any) -> float:
232
+ """Coerce to float with proper string handling."""
233
+ try:
234
+ return float(value)
235
+ except (ValueError, TypeError) as e:
236
+ raise TypeError(f"Cannot convert {value!r} to float") from None
237
+
238
+
239
+ def coerce_literal(value: Any, expected_type: Any) -> Any:
240
+ """Coerce to Literal type."""
241
+ allowed_values = get_args(expected_type)
242
+
243
+ # First try exact match
244
+ if value in allowed_values:
245
+ return value
246
+
247
+ # Try coercing to match one of the literal values
248
+ for literal_value in allowed_values:
249
+ literal_type = type(literal_value)
250
+
251
+ # Skip None in literals for coercion attempts
252
+ if literal_value is None:
253
+ continue
254
+
255
+ try:
256
+ # Try to coerce the input value to the type of this literal
257
+ coerced = coerce_value(value, literal_type)
258
+ if coerced == literal_value:
259
+ return literal_value
260
+ except (TypeError, ValueError):
261
+ continue
262
+
263
+ # If no match found, raise error with helpful message
264
+ raise TypeError(
265
+ f"Value {value!r} is not one of the allowed literal values: {allowed_values}"
266
+ )
267
+
268
+
269
+ def coerce_struct(value: Any, expected_type: type[Struct]) -> Any:
270
+ """Coerce to Struct (Pydantic model) type."""
271
+ if isinstance(value, expected_type):
272
+ return value
273
+ if isinstance(value, dict):
274
+ return expected_type.model_validate(value)
275
+ if isinstance(value, str):
276
+ return expected_type.model_validate_json(value)
277
+ raise TypeError(
278
+ f"Cannot convert {type(value).__name__} to {expected_type.__name__}. "
279
+ f"Expected dict or JSON string."
280
+ )
281
+
282
+
283
+ def coerce_union(value: Any, expected_type: Any) -> Any:
284
+ """Coerce to Union type by trying each type in order."""
285
+ type_args = get_args(expected_type)
286
+ last_error = None
287
+
288
+ for arg_type in type_args:
289
+ try:
290
+ return coerce_value(value, arg_type)
291
+ except (TypeError, ValueError) as e: # noqa: PERF203
292
+ last_error = e
293
+ continue
294
+
295
+ raise TypeError(
296
+ f"Cannot coerce value to any of {type_args}. Last error: {last_error}"
297
+ )
298
+
299
+
300
+ def coerce_list(value: Any, expected_type: Any) -> list:
301
+ """Coerce to list[T] type."""
302
+ if isinstance(value, str):
303
+ try:
304
+ value = json.loads(value)
305
+ except json.JSONDecodeError as e:
306
+ raise TypeError(f"Cannot parse list from JSON string: {e}") from None
307
+
308
+ if not isinstance(value, list):
309
+ raise TypeError(f"Expected list, got {type(value).__name__}")
310
+
311
+ type_args = get_args(expected_type)
312
+ if not type_args:
313
+ raise TypeError(
314
+ "List type must specify element type (e.g., list[int], not plain list)"
315
+ )
316
+
317
+ element_type = type_args[0]
318
+ return [coerce_value(item, element_type) for i, item in enumerate(value)]
319
+
320
+
321
+ def coerce_dict(value: Any, expected_type: Any) -> dict:
322
+ """Coerce to dict[K, V] type."""
323
+ # Handle string input (JSON)
324
+ if isinstance(value, str):
325
+ try:
326
+ value = json.loads(value)
327
+ except json.JSONDecodeError as e:
328
+ raise TypeError(f"Cannot parse dict from JSON string: {e}") from None
329
+
330
+ if not isinstance(value, dict):
331
+ raise TypeError(f"Expected dict, got {type(value).__name__}")
332
+
333
+ type_args = get_args(expected_type)
334
+ if not type_args:
335
+ raise TypeError(
336
+ "Dict type must specify key and value types (e.g., dict[str, int], "
337
+ "not plain dict)"
338
+ )
339
+
340
+ key_type, value_type = type_args[0], type_args[1]
341
+ return {
342
+ coerce_value(k, key_type): coerce_value(v, value_type) for k, v in value.items()
343
+ }
344
+
345
+
346
+ def coerce_fallback(value: Any, expected_type: Any) -> Any:
347
+ """Fallback coercion - just check isinstance."""
348
+ raise TypeError(
349
+ f"Unsupported type {get_type_name(expected_type)} for coercion of value "
350
+ f"{value}. Type must be validated as allowed before coercion."
351
+ )
352
+
353
+
354
+ PARAMETERISED_TYPES = (list, set, dict, Literal, Union, UnionType)
355
+
356
+
357
+ def is_generic(t: type) -> bool:
358
+ return t in PARAMETERISED_TYPES
359
+
360
+
361
+ TYPE_COERCERS = {
362
+ bool: coerce_bool,
363
+ int: coerce_int,
364
+ float: coerce_float,
365
+ str: coerce_str,
366
+ type(None): coerce_none,
367
+ datetime: coerce_datetime,
368
+ date: coerce_date,
369
+ time: coerce_time,
370
+ timedelta: coerce_timedelta,
371
+ Decimal: coerce_decimal,
372
+ list: coerce_list,
373
+ dict: coerce_dict,
374
+ set: coerce_set,
375
+ Literal: coerce_literal,
376
+ Union: coerce_union,
377
+ UnionType: coerce_union,
378
+ }
379
+
380
+
381
+ def coerce_value(value: Any, expected_type: Any) -> Any:
382
+ """Coerce a value to the expected type.
383
+
384
+ Delegates to specialized handlers based on type structure.
385
+ """
386
+ if expected_type is None:
387
+ return coerce_none(value)
388
+
389
+ origin = get_origin(expected_type)
390
+
391
+ if is_struct_type(expected_type):
392
+ return coerce_struct(value, expected_type)
393
+
394
+ if is_enum_type(expected_type):
395
+ return coerce_enum(value, expected_type)
396
+
397
+ if is_dataframe_type(expected_type):
398
+ return coerce_dataframe(value)
399
+
400
+ handler = TYPE_COERCERS.get(origin or expected_type)
401
+ if handler is None:
402
+ return coerce_fallback(value, expected_type)
403
+
404
+ if is_generic(origin or expected_type):
405
+ # noinspection PyArgumentList
406
+ return handler(value, expected_type)
407
+
408
+ return handler(value)
@@ -0,0 +1,19 @@
1
+ from typing import get_args, get_origin
2
+
3
+
4
+ def get_type_name(type_hint):
5
+ if type_hint is None or type_hint is type(None):
6
+ return "None"
7
+
8
+ origin = get_origin(type_hint)
9
+ if origin is not None:
10
+ args = get_args(type_hint)
11
+ if args:
12
+ arg_names = ", ".join(get_type_name(arg) for arg in args)
13
+ return f"{origin.__name__}[{arg_names}]"
14
+ return origin.__name__
15
+
16
+ if hasattr(type_hint, "__name__"):
17
+ return type_hint.__name__
18
+
19
+ return str(type_hint)