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,68 @@
1
+ """Chat-specific block construction helpers for the Yera library.
2
+
3
+ This package provides block construction functions that can be used either via
4
+ the events namespace:
5
+
6
+ import yera
7
+
8
+ yera.events.markdown(...)
9
+ yera.events.buttons(...)
10
+ ...
11
+
12
+ or via the top-level re-exports:
13
+
14
+ import yera as yr
15
+
16
+ yr.markdown(...)
17
+ yr.buttons(...)
18
+ ...
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from yera.events.blocks.action import action
24
+ from yera.events.blocks.bar_chart import bar_chart
25
+ from yera.events.blocks.base.base import _BlockFactory
26
+ from yera.events.blocks.buttons import request_input_buttons as request_input_buttons
27
+ from yera.events.blocks.columns import columns
28
+ from yera.events.blocks.container import container
29
+ from yera.events.blocks.date_picker import (
30
+ request_input_date_picker as request_input_date_picker,
31
+ )
32
+ from yera.events.blocks.exit import exit_event, quit_event
33
+ from yera.events.blocks.form import form
34
+ from yera.events.blocks.input_echo import input_echo as input_echo
35
+ from yera.events.blocks.input_request import request_input_text as request_input_text
36
+ from yera.events.blocks.line_chart import line_chart
37
+ from yera.events.blocks.markdown import markdown
38
+ from yera.events.blocks.slider import request_input_slider as request_input_slider
39
+ from yera.events.blocks.spinner import spinner
40
+ from yera.events.blocks.system_prompt import system_prompt as system_prompt
41
+ from yera.events.blocks.table import table
42
+
43
+
44
+ def reset_all_factories() -> None:
45
+ """Reset all block factory counters to 0."""
46
+ _BlockFactory.reset_all()
47
+
48
+
49
+ __all__ = [
50
+ "action",
51
+ "bar_chart",
52
+ "columns",
53
+ "container",
54
+ "exit_event",
55
+ "form",
56
+ "input_echo",
57
+ "line_chart",
58
+ "markdown",
59
+ "quit_event",
60
+ "request_input_buttons",
61
+ "request_input_date_picker",
62
+ "request_input_slider",
63
+ "request_input_text",
64
+ "reset_all_factories",
65
+ "spinner",
66
+ "system_prompt",
67
+ "table",
68
+ ]
@@ -0,0 +1,57 @@
1
+ """Action block implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from yera.events.blocks.base import _BlockFactory, _StreamHandle
6
+ from yera.events.models import ActionData
7
+ from yera.events.stream import push_output
8
+
9
+
10
+ class ActionStream(_StreamHandle):
11
+ """Stream handle for creating multiple chunks of the same action block."""
12
+
13
+ def __init__(self, block_id: str, initial_message: str):
14
+ super().__init__(block_id, "action")
15
+ self.initial_message = initial_message
16
+
17
+ def __enter__(self):
18
+ event = self._create_chunk_event(
19
+ data=ActionData(status="active", message=self.initial_message),
20
+ )
21
+ push_output(event)
22
+ return self
23
+
24
+ def __exit__(self, exc_type, exc_value, traceback):
25
+ event = self._create_chunk_event(
26
+ data=ActionData(status="complete"),
27
+ )
28
+ push_output(event)
29
+ return False
30
+
31
+ def update(self, message: str) -> None:
32
+ """Create an update chunk with status 'active'."""
33
+ event = self._create_chunk_event(
34
+ data=ActionData(status="active", message=message),
35
+ )
36
+ push_output(event)
37
+
38
+
39
+ class _ActionBlockFactory(_BlockFactory):
40
+ """Factory for action blocks. Returns a context manager when called with message."""
41
+
42
+ def __init__(self):
43
+ super().__init__("action")
44
+
45
+ def __call__(
46
+ self,
47
+ message: str,
48
+ ) -> ActionStream:
49
+ """Return a context manager for creating multiple action chunks."""
50
+ block_id = self._generate_block_id()
51
+ return ActionStream(block_id, message)
52
+
53
+
54
+ # Create the action callable/context manager
55
+ action = _ActionBlockFactory()
56
+
57
+ __all__ = ["action"]
@@ -0,0 +1,92 @@
1
+ """Bar chart block implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ import pandas as pd
10
+
11
+ from yera.events.blocks.base import _ChartBlockFactory
12
+ from yera.events.blocks.base.chart import _ChartConversionData
13
+ from yera.events.models import BarChartData
14
+ from yera.events.stream import push_output
15
+
16
+
17
+ class _BarChartBlockFactory(_ChartBlockFactory):
18
+ """Factory for creating bar chart blocks, matching Streamlit's st.bar_chart API."""
19
+
20
+ def __init__(self):
21
+ super().__init__("bar_chart")
22
+
23
+ def _create_chart_data_from_conversion(
24
+ self,
25
+ conversion_data: _ChartConversionData,
26
+ chart_specific_params: dict,
27
+ ) -> BarChartData:
28
+ """Convert intermediate data to BarChartData."""
29
+ return BarChartData(
30
+ x_label=conversion_data.x_label,
31
+ x_categories=conversion_data.x_categories or [],
32
+ y_label=conversion_data.y_label,
33
+ y_values=conversion_data.y_values,
34
+ colour_label=conversion_data.colour_label,
35
+ colour_categories=conversion_data.colour_categories,
36
+ colour_values=conversion_data.colour_values,
37
+ horizontal=chart_specific_params.get("horizontal", False),
38
+ stack=chart_specific_params.get("stack", True),
39
+ )
40
+
41
+ def __call__(
42
+ self,
43
+ data: pd.DataFrame,
44
+ *,
45
+ x: str | None = None,
46
+ y: str | Sequence[str] | None = None,
47
+ colour: str | Sequence[str] | None = None,
48
+ horizontal: bool = False,
49
+ stack: bool = True,
50
+ ) -> None:
51
+ """Create a bar chart block, matching Streamlit's st.bar_chart() signature.
52
+
53
+ Args:
54
+ data: DataFrame to plot
55
+ x: Column name for x-axis (None = use index)
56
+ y: Column name(s) for y-axis (None = use all columns except x)
57
+ colour: Column name for colour grouping, or list of colors
58
+ horizontal: Whether bars are horizontal
59
+ stack: Whether bars are stacked
60
+
61
+ Returns:
62
+ None (informational block)
63
+
64
+ """
65
+ chart_specific_params = {"horizontal": horizontal, "stack": stack}
66
+
67
+ if x is None and y is None:
68
+ conversion_data = self._convert_default_format(data)
69
+ elif x is not None and isinstance(y, str):
70
+ conversion_data = self._convert_long_format(data, x, y, colour)
71
+ elif x is not None and isinstance(y, Sequence) and not isinstance(y, str):
72
+ conversion_data = self._convert_wide_format(data, x, y, colour)
73
+ else:
74
+ raise ValueError(
75
+ f"Unsupported bar_chart parameter combination: x={x}, y={y}"
76
+ )
77
+
78
+ chart_data = self._create_chart_data_from_conversion(
79
+ conversion_data, chart_specific_params
80
+ )
81
+
82
+ event = self._create_block_output_event(
83
+ chart_data, message_type="informational"
84
+ )
85
+ push_output(event)
86
+ return
87
+
88
+
89
+ # Create the bar_chart callable
90
+ bar_chart = _BarChartBlockFactory()
91
+
92
+ __all__ = ["bar_chart"]
@@ -0,0 +1,20 @@
1
+ """Base classes for block construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from yera.events.blocks.base.base import _BlockFactory, _StreamHandle
6
+ from yera.events.blocks.base.chart import _ChartBlockFactory
7
+ from yera.events.blocks.base.layout import (
8
+ LayoutContext,
9
+ LayoutHandle,
10
+ _current_layout,
11
+ )
12
+
13
+ __all__ = [
14
+ "LayoutContext",
15
+ "LayoutHandle",
16
+ "_BlockFactory",
17
+ "_ChartBlockFactory",
18
+ "_StreamHandle",
19
+ "_current_layout",
20
+ ]
@@ -0,0 +1,166 @@
1
+ """Base classes for block construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar
6
+
7
+ from yera.agents import get_agent_context
8
+ from yera.events.models import (
9
+ ActionData,
10
+ BarChartData,
11
+ ButtonsData,
12
+ ColumnsData,
13
+ ContainerData,
14
+ DatePickerData,
15
+ ExitEventData,
16
+ InputRequestData,
17
+ LayoutEndData,
18
+ LineChartData,
19
+ MarkdownData,
20
+ OutputEvent,
21
+ SliderData,
22
+ SpinnerData,
23
+ TableData,
24
+ )
25
+ from yera.events.models.in_event import InputEvent
26
+
27
+
28
+ def get_parent_block_id(exclude_block_id: str | None = None) -> str | None:
29
+ """Get parent block ID from layout context if available."""
30
+ # Import here to avoid circular import
31
+ from yera.events.blocks.base.layout import _current_layout
32
+
33
+ layout_context = _current_layout.get()
34
+ if layout_context and layout_context.block_id != exclude_block_id:
35
+ return layout_context.block_id
36
+ return None
37
+
38
+
39
+ def create_event(
40
+ block_type: str,
41
+ block_id: str,
42
+ data: (
43
+ MarkdownData
44
+ | ButtonsData
45
+ | ActionData
46
+ | SpinnerData
47
+ | DatePickerData
48
+ | SliderData
49
+ | TableData
50
+ | ContainerData
51
+ | ColumnsData
52
+ | BarChartData
53
+ | LineChartData
54
+ | LayoutEndData
55
+ ),
56
+ message_type: str,
57
+ chunk_id: int,
58
+ parent_block_id: str | None = None,
59
+ ) -> OutputEvent:
60
+ """Create an event with automatic layout context detection."""
61
+ agent_ctx = get_agent_context()
62
+
63
+ if parent_block_id is None:
64
+ parent_block_id = get_parent_block_id(exclude_block_id=block_id)
65
+ return OutputEvent(
66
+ message_type=message_type,
67
+ block_type=block_type,
68
+ block_id=block_id,
69
+ data=data,
70
+ chunk_id=chunk_id,
71
+ agent_instance=agent_ctx.metadata.make_instance_id(0),
72
+ parent_block_id=parent_block_id,
73
+ )
74
+
75
+
76
+ class _StreamHandle:
77
+ """Base class for streamable block handles."""
78
+
79
+ def __init__(self, block_id: str, block_type: str):
80
+ self.block_id = block_id
81
+ self.block_type = block_type
82
+ self.chunk_counter = 0
83
+
84
+ def _create_chunk_event(
85
+ self,
86
+ data: (
87
+ MarkdownData
88
+ | ActionData
89
+ | SpinnerData
90
+ | TableData
91
+ | ContainerData
92
+ | ColumnsData
93
+ ),
94
+ message_type: str = "informational",
95
+ parent_block_id: str | None = None,
96
+ ) -> OutputEvent:
97
+ """Create a chunk event (for stream handles)."""
98
+ self.chunk_counter += 1
99
+ return create_event(
100
+ block_type=self.block_type,
101
+ block_id=self.block_id,
102
+ data=data,
103
+ message_type=message_type,
104
+ chunk_id=self.chunk_counter,
105
+ parent_block_id=parent_block_id,
106
+ )
107
+
108
+
109
+ class _BlockFactory:
110
+ """Base class for block construction factories."""
111
+
112
+ _registry: ClassVar[list[_BlockFactory]] = [] # Track all factory instances
113
+
114
+ def __init__(self, block_type: str):
115
+ self.block_type = block_type
116
+ self._block_counter = 0
117
+ _BlockFactory._registry.append(self) # Auto-register instance
118
+
119
+ def reset(self) -> None:
120
+ """Reset the block counter to 0."""
121
+ self._block_counter = 0
122
+
123
+ @classmethod
124
+ def reset_all(cls) -> None:
125
+ """Reset all registered factory instances."""
126
+ for factory in cls._registry:
127
+ factory.reset()
128
+
129
+ def _generate_block_id(self) -> str:
130
+ """Generate a block_id."""
131
+ self._block_counter += 1
132
+ return f"{self.block_type}-{self._block_counter}"
133
+
134
+ def _create_block_input_event(self) -> InputEvent:
135
+ pass
136
+
137
+ def _create_block_output_event(
138
+ self,
139
+ data: (
140
+ MarkdownData
141
+ | ButtonsData
142
+ | ActionData
143
+ | SpinnerData
144
+ | DatePickerData
145
+ | SliderData
146
+ | TableData
147
+ | ContainerData
148
+ | ColumnsData
149
+ | BarChartData
150
+ | LineChartData
151
+ | InputRequestData
152
+ | ExitEventData
153
+ ),
154
+ message_type: str,
155
+ ) -> OutputEvent:
156
+ """Create a block event (for factory methods)."""
157
+ return create_event(
158
+ block_type=self.block_type,
159
+ block_id=self._generate_block_id(),
160
+ data=data,
161
+ message_type=message_type,
162
+ chunk_id=1, # Block events are always single chunks
163
+ )
164
+
165
+
166
+ __all__ = ["_BlockFactory", "_StreamHandle"]
@@ -0,0 +1,288 @@
1
+ """Base chart block factory for shared chart functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ import pandas as pd
12
+
13
+ from yera.events.blocks.base.base import _BlockFactory
14
+ from yera.events.models import BarChartData, LineChartData
15
+
16
+
17
+ def _is_hex_color(value: str) -> bool:
18
+ """Check if a string is a valid hex color (starts with # and has valid hex digits).
19
+
20
+ Raises ValueError if the value is not a valid hex color format.
21
+ """
22
+ if not value.startswith("#"):
23
+ raise ValueError(f"Hex color must start with '#', got: {value!r}")
24
+ if len(value) not in (4, 7): # #rgb or #rrggbb format
25
+ raise ValueError(
26
+ f"Hex color must be 4 or 7 characters (#rgb or #rrggbb), got {len(value)} characters: {value!r}"
27
+ )
28
+ # Check that all characters after # are valid hex digits (0-9, a-f, A-F)
29
+ hex_digits = set("0123456789abcdefABCDEF")
30
+ invalid_chars = [c for c in value[1:] if c not in hex_digits]
31
+ if invalid_chars:
32
+ raise ValueError(
33
+ f"Hex color contains invalid characters: {invalid_chars}. "
34
+ f"Only 0-9, a-f, A-F are allowed after '#'. Got: {value!r}"
35
+ )
36
+ return True
37
+
38
+
39
+ def _get_default_colors(count: int) -> list[str]:
40
+ """Get default colors for the given number of series."""
41
+ default_palette = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF"]
42
+ return [default_palette[i % len(default_palette)] for i in range(count)]
43
+
44
+
45
+ def _check_if_colour_column_contains_hex_colors(colour_values: pd.Series) -> bool:
46
+ """Check if all values in a colour column are valid hex colors."""
47
+ try:
48
+ all(_is_hex_color(str(val)) for val in colour_values)
49
+ return True
50
+ except ValueError:
51
+ return False
52
+
53
+
54
+ def _build_x_colour_to_y_value_map(
55
+ df: pd.DataFrame, x_column: str, colour_column: str, y_column: str
56
+ ) -> dict[tuple[str, str], float | None]:
57
+ """Build a mapping from (x_category, colour) to y_value."""
58
+ value_map = {}
59
+ for _, row in df.iterrows():
60
+ x_val = str(row[x_column])
61
+ colour_val = str(row[colour_column])
62
+ y_val = row[y_column]
63
+ value_map[(x_val, colour_val)] = float(y_val) if y_val is not None else None
64
+ return value_map
65
+
66
+
67
+ def _extract_y_values_for_x_and_colour_categories(
68
+ x_categories: list[str],
69
+ colour_categories: list[str],
70
+ value_map: dict[tuple[str, str], float | None],
71
+ ) -> list[list[float | None]]:
72
+ """Extract y_values matrix from value map for given x and colour categories."""
73
+ y_values = []
74
+ for x_cat in x_categories:
75
+ row_values = [
76
+ value_map.get((x_cat, colour_cat)) for colour_cat in colour_categories
77
+ ]
78
+ y_values.append(row_values)
79
+ return y_values
80
+
81
+
82
+ def _simplify_to_single_value_per_x_category(
83
+ y_values: list[list[float | None]],
84
+ ) -> list[list[float | None]]:
85
+ """Simplify y_values when each x-category has only one non-None value."""
86
+ has_single_value_per_x = all(
87
+ sum(1 for v in row if v is not None) == 1 for row in y_values
88
+ )
89
+ if has_single_value_per_x:
90
+ return [[next(v for v in row if v is not None)] for row in y_values]
91
+ return y_values
92
+
93
+
94
+ def _get_y_value_for_x_category(
95
+ df: pd.DataFrame, x_column: str, x_category: str, y_column: str
96
+ ) -> float | None:
97
+ """Get the y value for a specific x category (assumes single match)."""
98
+ mask = df[x_column] == x_category
99
+ matching_rows = df[mask]
100
+ if len(matching_rows) > 0:
101
+ val = matching_rows[y_column].iloc[0]
102
+ return float(val) if val is not None else None
103
+ return None
104
+
105
+
106
+ def _get_y_values_for_x_categories_with_colour_grouping(
107
+ df: pd.DataFrame,
108
+ x_column: str,
109
+ y_column: str,
110
+ colour_column: str,
111
+ x_categories: list[str],
112
+ colour_categories: list[str],
113
+ ) -> list[list[float | None]]:
114
+ """Get y_values matrix grouped by x and colour categories."""
115
+ y_values = []
116
+ for x_cat in x_categories:
117
+ row_values = []
118
+ for colour_cat in colour_categories:
119
+ mask = (df[x_column] == x_cat) & (df[colour_column] == colour_cat)
120
+ matching_rows = df[mask]
121
+ if len(matching_rows) > 0:
122
+ val = matching_rows[y_column].iloc[0]
123
+ row_values.append(float(val) if val is not None else None)
124
+ else:
125
+ row_values.append(None)
126
+ y_values.append(row_values)
127
+ return y_values
128
+
129
+
130
+ @dataclass
131
+ class _ChartConversionData:
132
+ """Intermediate data structure for chart conversion."""
133
+
134
+ x_label: str | None
135
+ x_categories: list[str] | None
136
+ y_label: str | None
137
+ y_values: list[list[float | None]]
138
+ colour_label: str | None
139
+ colour_categories: list[str]
140
+ colour_values: list[str]
141
+
142
+
143
+ class _ChartBlockFactory(_BlockFactory, ABC):
144
+ """Base factory for chart blocks that process DataFrames."""
145
+
146
+ def _convert_default_format(
147
+ self,
148
+ df: pd.DataFrame,
149
+ ) -> _ChartConversionData:
150
+ """Convert DataFrame using default format: index as x, all columns as y series."""
151
+ x_categories = [str(idx) for idx in df.index]
152
+ y_columns = list(df.columns)
153
+ y_values = [
154
+ [
155
+ float(df.iloc[i][col]) if df.iloc[i][col] is not None else None
156
+ for col in y_columns
157
+ ]
158
+ for i in range(len(df))
159
+ ]
160
+ return _ChartConversionData(
161
+ x_label=None,
162
+ x_categories=x_categories,
163
+ y_label=None,
164
+ y_values=y_values,
165
+ colour_label=None,
166
+ colour_categories=y_columns,
167
+ colour_values=_get_default_colors(len(y_columns)),
168
+ )
169
+
170
+ def _convert_long_format(
171
+ self,
172
+ df: pd.DataFrame,
173
+ x_column: str,
174
+ y_column: str,
175
+ colour: str | Sequence[str] | None,
176
+ ) -> _ChartConversionData:
177
+ """Convert DataFrame in long format: x and y columns specified, optional colour grouping."""
178
+ x_categories = [str(val) for val in df[x_column].unique()]
179
+
180
+ if isinstance(colour, str):
181
+ colour_label = colour
182
+ unique_colour_values = df[colour].unique()
183
+
184
+ if _check_if_colour_column_contains_hex_colors(unique_colour_values):
185
+ colour_categories = [str(val) for val in unique_colour_values]
186
+ colour_values = colour_categories.copy()
187
+
188
+ value_map = _build_x_colour_to_y_value_map(
189
+ df, x_column, colour, y_column
190
+ )
191
+ y_values = _extract_y_values_for_x_and_colour_categories(
192
+ x_categories, colour_categories, value_map
193
+ )
194
+ y_values = _simplify_to_single_value_per_x_category(y_values)
195
+ else:
196
+ colour_categories = sorted([str(val) for val in unique_colour_values])
197
+ colour_values = _get_default_colors(len(colour_categories))
198
+ y_values = _get_y_values_for_x_categories_with_colour_grouping(
199
+ df, x_column, y_column, colour, x_categories, colour_categories
200
+ )
201
+ else:
202
+ colour_label = None
203
+ colour_categories = [y_column]
204
+ colour_values = _get_default_colors(1)
205
+ y_values = [
206
+ [_get_y_value_for_x_category(df, x_column, x_cat, y_column)]
207
+ for x_cat in x_categories
208
+ ]
209
+
210
+ return _ChartConversionData(
211
+ x_label=x_column,
212
+ x_categories=x_categories,
213
+ y_label=y_column,
214
+ y_values=y_values,
215
+ colour_label=colour_label,
216
+ colour_categories=colour_categories,
217
+ colour_values=colour_values,
218
+ )
219
+
220
+ def _convert_wide_format(
221
+ self,
222
+ df: pd.DataFrame,
223
+ x_column: str | None,
224
+ y_columns: Sequence[str],
225
+ colour: str | Sequence[str] | None,
226
+ ) -> _ChartConversionData:
227
+ """Convert DataFrame in wide format: x column specified (or None), multiple y columns."""
228
+ if x_column is not None:
229
+ x_categories = [str(val) for val in df[x_column].unique()]
230
+ x_label = x_column
231
+ else:
232
+ # When x is None but y is specified, use index
233
+ x_categories = [str(idx) for idx in df.index]
234
+ x_label = None
235
+
236
+ if isinstance(colour, Sequence) and not isinstance(colour, str):
237
+ colour_values = list(colour)
238
+ colour_categories = list(y_columns)
239
+ else:
240
+ colour_values = _get_default_colors(len(y_columns))
241
+ colour_categories = list(y_columns)
242
+
243
+ y_values = []
244
+ if x_column is not None:
245
+ for x_cat in x_categories:
246
+ mask = df[x_column] == x_cat
247
+ matching_rows = df[mask]
248
+ if len(matching_rows) > 0:
249
+ row = matching_rows.iloc[0]
250
+ y_values.append(
251
+ [
252
+ float(row[col]) if row[col] is not None else None
253
+ for col in y_columns
254
+ ]
255
+ )
256
+ else:
257
+ y_values.append([None] * len(y_columns))
258
+ else:
259
+ # Use index for x
260
+ for i in range(len(df)):
261
+ row = df.iloc[i]
262
+ y_values.append(
263
+ [
264
+ float(row[col]) if row[col] is not None else None
265
+ for col in y_columns
266
+ ]
267
+ )
268
+
269
+ return _ChartConversionData(
270
+ x_label=x_label,
271
+ x_categories=x_categories,
272
+ y_label=None,
273
+ y_values=y_values,
274
+ colour_label=None,
275
+ colour_categories=colour_categories,
276
+ colour_values=colour_values,
277
+ )
278
+
279
+ @abstractmethod
280
+ def _create_chart_data_from_conversion(
281
+ self,
282
+ conversion_data: _ChartConversionData,
283
+ chart_specific_params: dict,
284
+ ) -> BarChartData | LineChartData:
285
+ """Convert intermediate data to specific chart data type. Override in subclasses."""
286
+
287
+
288
+ __all__ = ["_ChartBlockFactory"]