yera 0.1.1__py3-none-any.whl → 0.2.1__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/T8WGYqDMoHDKKoHj0O3HK/_buildManifest.js +11 -0
  130. yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_clientMiddlewareManifest.json +1 -0
  131. yera/ui/dist/_next/static/T8WGYqDMoHDKKoHj0O3HK/_ssgManifest.js +1 -0
  132. yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
  133. yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
  134. yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
  135. yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
  136. yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
  137. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  138. yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  139. yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
  140. yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
  141. yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
  142. yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -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.1.dist-info/METADATA +65 -0
  188. yera-0.2.1.dist-info/RECORD +190 -0
  189. {yera-0.1.1.dist-info → yera-0.2.1.dist-info}/WHEEL +1 -1
  190. yera-0.2.1.dist-info/entry_points.txt +2 -0
  191. yera-0.1.1.dist-info/METADATA +0 -11
  192. yera-0.1.1.dist-info/RECORD +0 -4
@@ -0,0 +1,270 @@
1
+ """Secure credential management for Yera using system keyring.
2
+
3
+ This module provides a wrapper around the system keyring for storing and
4
+ retrieving credentials securely. It maintains a manifest of stored keys
5
+ and validates key formats to ensure consistency across the application.
6
+
7
+ """
8
+
9
+ import contextlib
10
+ import json
11
+ import re
12
+
13
+ import keyring
14
+ from keyring.backends.fail import Keyring as FailKeyring
15
+ from keyring.errors import KeyringError, NoKeyringError, PasswordDeleteError
16
+
17
+
18
+ class DevKeyring:
19
+ """Manages secure credential storage using the system keyring.
20
+
21
+ DevKeyring provides a high-level interface for storing, retrieving, and
22
+ managing credentials in the system keyring. It maintains a manifest of
23
+ all stored keys to enable listing and bulk operations, and enforces a
24
+ consistent key naming scheme.
25
+
26
+ All credentials are stored under the service name "yera" and use a
27
+ dot-separated hierarchical key format (e.g., 'providers.anthropic.api_key').
28
+
29
+ Attributes:
30
+ SERVICE_NAME: The service identifier used for all keyring operations.
31
+ MANIFEST_KEY: The special key used to store the list of credential keys.
32
+ KEY_PATTERN: Regex pattern for validating credential key formats.
33
+ """
34
+
35
+ SERVICE_NAME = "yera"
36
+ MANIFEST_KEY = "_yera_manifest"
37
+ KEY_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$")
38
+
39
+ @classmethod
40
+ def is_available(cls) -> bool:
41
+ """Check if a valid keyring backend is available.
42
+
43
+ Determines whether the system has a functional keyring backend
44
+ that can be used for credential storage. Returns False if only
45
+ the fail backend is available or if there are keyring errors.
46
+
47
+ Returns:
48
+ bool: True if a valid keyring backend is available, False otherwise.
49
+
50
+ """
51
+ try:
52
+ backend = keyring.get_keyring()
53
+ return not isinstance(backend, FailKeyring)
54
+ except (NoKeyringError, KeyringError):
55
+ return False
56
+
57
+ @classmethod
58
+ def _validate_key(cls, key: str) -> None:
59
+ if not key or not key.strip():
60
+ raise ValueError("Credential key cannot be empty")
61
+ if not cls.KEY_PATTERN.match(key):
62
+ raise ValueError(
63
+ f"Invalid key format: '{key}'. "
64
+ "Key must contain only letters, numbers, underscores, and hyphens, "
65
+ "with parts separated by dots (e.g., 'providers.anthropic.api_key')"
66
+ )
67
+
68
+ @classmethod
69
+ def _get_manifest(cls) -> set[str]:
70
+ with contextlib.suppress(KeyError, json.JSONDecodeError):
71
+ manifest_json = keyring.get_password(cls.SERVICE_NAME, cls.MANIFEST_KEY)
72
+ if manifest_json:
73
+ return set(json.loads(manifest_json))
74
+ return set()
75
+
76
+ @classmethod
77
+ def _save_manifest(cls, keys: set[str]) -> None:
78
+ with contextlib.suppress(KeyringError):
79
+ keyring.set_password(
80
+ cls.SERVICE_NAME, cls.MANIFEST_KEY, json.dumps(list(keys))
81
+ )
82
+
83
+ @classmethod
84
+ def _add_to_manifest(cls, key: str) -> None:
85
+ manifest = cls._get_manifest()
86
+ manifest.add(key)
87
+ cls._save_manifest(manifest)
88
+
89
+ @classmethod
90
+ def _remove_from_manifest(cls, key: str) -> None:
91
+ manifest = cls._get_manifest()
92
+ manifest.discard(key)
93
+ cls._save_manifest(manifest)
94
+
95
+ @classmethod
96
+ def get_backend_info(cls) -> str:
97
+ """Get information about the current keyring backend.
98
+
99
+ Returns a string describing the active keyring backend, including
100
+ its name and priority level. Useful for debugging credential storage
101
+ issues.
102
+
103
+ Returns:
104
+ str: Backend name and priority (e.g., "macOS Keychain (5)"),
105
+ or "No backend available" if no valid backend exists.
106
+
107
+ Example:
108
+ >>> print(DevKeyring.get_backend_info())
109
+ macOS Keychain (5)
110
+ """
111
+ if cls.is_available():
112
+ backend = keyring.get_keyring()
113
+ return f"{backend.name} ({backend.priority})"
114
+ return "No backend available"
115
+
116
+ @classmethod
117
+ def set(cls, key: str, value: str) -> None:
118
+ """Store a credential in the keyring.
119
+
120
+ Saves a credential value associated with the given key. The key is
121
+ automatically added to the manifest for tracking purposes.
122
+
123
+ Args:
124
+ key: Credential identifier following the format requirements
125
+ (alphanumeric, underscores, hyphens, dot-separated).
126
+ value: The credential value to store (e.g., API key, password).
127
+
128
+ Raises:
129
+ ValueError: If the key format is invalid or if key/value is empty.
130
+ KeyringError: If the keyring backend fails to store the credential.
131
+
132
+ Example:
133
+ >>> DevKeyring.set('providers.anthropic.api_key', 'sk-ant-...')
134
+ >>> DevKeyring.set('databases.my_db.password', 'secret123')
135
+ """
136
+ cls._validate_key(key)
137
+ if not value or not value.strip():
138
+ raise ValueError("Credential value cannot be empty")
139
+ keyring.set_password(cls.SERVICE_NAME, key.strip(), value.strip())
140
+ cls._add_to_manifest(key)
141
+
142
+ @classmethod
143
+ def get(cls, key: str) -> str:
144
+ """Retrieve a credential from the keyring.
145
+
146
+ Fetches the credential value associated with the given key.
147
+
148
+ Args:
149
+ key: Credential identifier to retrieve.
150
+
151
+ Returns:
152
+ str: The credential value associated with the key.
153
+
154
+ Raises:
155
+ ValueError: If the key format is invalid or if no credential
156
+ exists for the given key.
157
+
158
+ Example:
159
+ >>> api_key = DevKeyring.get('providers.anthropic.api_key')
160
+ >>> print(api_key)
161
+ sk-ant-...
162
+ """
163
+ cls._validate_key(key)
164
+ try:
165
+ cred = keyring.get_password(cls.SERVICE_NAME, key)
166
+ except KeyringError:
167
+ cred = None
168
+
169
+ if cred:
170
+ return cred
171
+ raise ValueError(f"No credentials found for {key}") from None
172
+
173
+ @classmethod
174
+ def delete(cls, key: str) -> None:
175
+ """Delete a credential from the keyring.
176
+
177
+ Removes the credential associated with the given key and updates
178
+ the manifest accordingly. Safe to call even if the key doesn't exist.
179
+
180
+ Args:
181
+ key: Credential identifier to delete.
182
+
183
+ Raises:
184
+ ValueError: If the key format is invalid.
185
+ RuntimeError: If the keyring backend fails to delete the credential.
186
+
187
+ Example:
188
+ >>> DevKeyring.delete('providers.anthropic.api_key')
189
+ """
190
+ cls._validate_key(key)
191
+ try:
192
+ keyring.delete_password(cls.SERVICE_NAME, key)
193
+ cls._remove_from_manifest(key)
194
+ except PasswordDeleteError:
195
+ # Key doesn't exist
196
+ cls._remove_from_manifest(key) # Clean up manifest anyway
197
+ except KeyringError as e:
198
+ raise RuntimeError(f"Failed to delete secret: {e}") from None
199
+
200
+ @classmethod
201
+ def exists(cls, key: str) -> bool:
202
+ """Check if a credential exists in the keyring.
203
+
204
+ Verifies whether a credential is stored for the given key without
205
+ retrieving its value.
206
+
207
+ Args:
208
+ key: Credential identifier to check.
209
+
210
+ Returns:
211
+ bool: True if the credential exists, False otherwise.
212
+
213
+ Raises:
214
+ ValueError: If the key format is invalid.
215
+
216
+ Example:
217
+ >>> if DevKeyring.exists('api.key'):
218
+ ... print("Credential found")
219
+ ... else:
220
+ ... print("Credential not found")
221
+ """
222
+ cls._validate_key(key)
223
+ try:
224
+ cls.get(key)
225
+ return True
226
+ except ValueError:
227
+ return False
228
+
229
+ @classmethod
230
+ def list(cls) -> list[str]:
231
+ """List all stored credential keys.
232
+
233
+ Returns a sorted list of all credential keys currently stored in
234
+ the keyring, as tracked by the manifest.
235
+
236
+ Returns:
237
+ list[str]: Sorted list of credential keys. Returns an empty
238
+ list if no credentials are stored.
239
+
240
+ Example:
241
+ >>> keys = DevKeyring.list()
242
+ >>> print(keys)
243
+ ['database.password', 'providers.anthropic.api_key']
244
+ """
245
+ return sorted(cls._get_manifest())
246
+
247
+ @classmethod
248
+ def delete_all(cls) -> None:
249
+ """Delete all credentials managed by DevKeyring.
250
+
251
+ Removes all credentials tracked in the manifest and deletes the
252
+ manifest itself. This operation is irreversible and will clear
253
+ all Yera-related credentials from the system keyring.
254
+
255
+ Note:
256
+ This is a best-effort operation that continues even if individual
257
+ deletions fail.
258
+
259
+ Example:
260
+ >>> DevKeyring.delete_all() # Removes all stored credentials
261
+ """
262
+ if not cls.exists(cls.MANIFEST_KEY):
263
+ # Nothing to delete
264
+ return
265
+
266
+ for key in cls.list():
267
+ with contextlib.suppress(PasswordDeleteError, KeyringError):
268
+ cls.delete(key)
269
+
270
+ keyring.delete_password(cls.SERVICE_NAME, cls.MANIFEST_KEY)
yera/config2/paths.py ADDED
@@ -0,0 +1,28 @@
1
+ """Helper functions for paths."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def has_local_yera_toml() -> bool:
7
+ """Check whether there is a local yera toml file.
8
+
9
+ Local tomls are under current working directory
10
+
11
+ Returns:
12
+ true if present otherwise false
13
+ """
14
+ toml_path = Path.cwd() / "yera.toml"
15
+ return toml_path.exists()
16
+
17
+
18
+ def has_global_yera_toml() -> bool:
19
+ """Check whether there is a global yera toml file.
20
+
21
+ global tomls are under the user home/.config/yera directory
22
+
23
+ Returns:
24
+ true if present otherwise false
25
+
26
+ """
27
+ toml_path = Path.home() / ".config" / "yera" / "yera.toml"
28
+ return toml_path.exists()
yera/config2/read.py ADDED
@@ -0,0 +1,113 @@
1
+ """Module for reading yera config toml."""
2
+
3
+ from pathlib import Path
4
+
5
+ import tomllib
6
+
7
+ from yera.config2.dataclasses import YeraConfig
8
+
9
+
10
+ def _read_toml(use_global: bool = False) -> dict:
11
+ """Read yera.toml configuration file.
12
+
13
+ First this function tries in CWD, then HOME/.config/yera and then errors if neither
14
+ are present.
15
+
16
+ Args:
17
+ use_global: whether to use the global config, if local is present (default:
18
+ False).
19
+
20
+ Returns:
21
+ dict: yera.toml content loaded into a dictionary
22
+
23
+ """
24
+ toml_path = Path.cwd() / "yera.toml"
25
+ if not toml_path.exists() or use_global:
26
+ toml_path = Path.home() / ".config" / "yera" / "yera.toml"
27
+
28
+ if not toml_path.exists():
29
+ raise FileNotFoundError(
30
+ "No yera.toml found in current working directory or home/.config/yera"
31
+ )
32
+
33
+ with toml_path.open("rb") as f:
34
+ return tomllib.load(f)
35
+
36
+
37
+ def _fill_config_object(config_dict: dict) -> YeraConfig:
38
+ """Create YeraConfig from parsed TOML dictionary.
39
+
40
+ This method flattens the nested TOML structure (models.llm.provider.model_name)
41
+ into the flat dictionary structure (models.llm["provider.model_name"]) and
42
+ adds the `id` field to each model.
43
+
44
+ Args:
45
+ config_dict: Dictionary from tomllib.load()
46
+
47
+ Returns:
48
+ Validated YeraConfig instance
49
+
50
+ """
51
+
52
+ def _flatten_nested_models(
53
+ nested_dict: dict, path: list[str] | None = None
54
+ ) -> dict:
55
+ if path is None:
56
+ path = []
57
+
58
+ flattened = {}
59
+
60
+ for k, v in nested_dict.items():
61
+ current_path = [*path, k]
62
+
63
+ is_leaf = isinstance(v, dict) and any(
64
+ not isinstance(sv, dict) for sv in v.values()
65
+ )
66
+
67
+ if is_leaf:
68
+ full_id = ".".join(current_path)
69
+ model_cfg = {**v, "id": full_id}
70
+ flattened[full_id] = model_cfg
71
+ elif isinstance(v, dict):
72
+ flattened.update(_flatten_nested_models(v, current_path))
73
+ else:
74
+ raise TypeError(
75
+ f"Expected nested dicts only in models section. Encountered {type(v)}"
76
+ )
77
+ return flattened
78
+
79
+ kws = {**config_dict, "models": {}}
80
+
81
+ for model_type in config_dict["models"]:
82
+ kws["models"][model_type] = _flatten_nested_models(
83
+ config_dict["models"][model_type]
84
+ )
85
+
86
+ return YeraConfig(**kws)
87
+
88
+
89
+ def read_config(use_global: bool = False) -> YeraConfig:
90
+ """Read and parse yera.toml configuration into a YeraConfig object.
91
+
92
+ Searches for yera.toml in the current working directory first, then falls
93
+ back to ~/.config/yera/yera.toml. The TOML structure is parsed and validated
94
+ into a YeraConfig dataclass instance.
95
+
96
+ Args:
97
+ use_global: If True, skip the local directory and use the global config
98
+ at ~/.config/yera/yera.toml even if a local config exists (default: False).
99
+
100
+ Returns:
101
+ Validated YeraConfig instance with flattened model configurations.
102
+
103
+ Raises:
104
+ FileNotFoundError: If no yera.toml is found in either location.
105
+ ValidationError: If the TOML structure doesn't match YeraConfig schema.
106
+
107
+ Example:
108
+ >>> config = read_config() # Uses local config if available
109
+ >>> config = read_config(use_global=True) # Forces global config
110
+
111
+ """
112
+ toml_dict = _read_toml(use_global=use_global)
113
+ return _fill_config_object(toml_dict)
yera/config2/setup.py ADDED
@@ -0,0 +1,109 @@
1
+ # ruff: noqa: T201
2
+ """Module for setting up Yera provider configuration and credentials."""
3
+
4
+ from typing import Literal
5
+
6
+ from yera.config2.dataclasses import YeraConfig
7
+ from yera.config2.read import read_config
8
+ from yera.config2.setup_handlers.anthropic import AnthropicSetup
9
+ from yera.config2.setup_handlers.azure import AzureSetup
10
+ from yera.config2.setup_handlers.llama_cpp import LlamaCppSetup
11
+ from yera.config2.setup_handlers.ollama import OllamaSetup
12
+ from yera.config2.setup_handlers.openai import OpenAISetup
13
+ from yera.config2.write import write_config
14
+
15
+ handlers = {
16
+ "anthropic": AnthropicSetup(),
17
+ "openai": OpenAISetup(),
18
+ "ollama": OllamaSetup(),
19
+ "azure": AzureSetup(),
20
+ "llama-cpp": LlamaCppSetup(),
21
+ }
22
+
23
+
24
+ def setup(
25
+ location: Literal["global", "local"],
26
+ auto_import: bool,
27
+ ) -> None:
28
+ """Set up Yera provider configuration and credentials.
29
+
30
+ Configures model provider credentials by either automatically importing from
31
+ environment variables or prompting the user interactively. The configuration is
32
+ saved to the specified location and credentials are saved to the secure developer
33
+ keyring.
34
+
35
+ Args:
36
+ location: Where to save the configuration - "global" for user-wide
37
+ settings (~/.config/yera/config.toml) or "local" for project-specific
38
+ settings (./yera.toml)
39
+ auto_import: If True, automatically import credentials from environment
40
+ variables and only prompt for providers that fail auto-import. If False,
41
+ prompt interactively for all providers.
42
+
43
+ The function will:
44
+ 1. Load existing config from the specified location (or create new)
45
+ 2. Attempt auto-import if enabled, falling back to interactive prompt
46
+ 3. Allow user to configure additional providers from a menu
47
+ 4. Save the merged configuration to the specified location
48
+ """
49
+ try:
50
+ config = read_config(use_global=location == "global")
51
+ except FileNotFoundError:
52
+ config = YeraConfig()
53
+
54
+ to_ask = []
55
+
56
+ if auto_import:
57
+ for k, handler in handlers.items():
58
+ new_config = handler.automatic_setup()
59
+ if new_config:
60
+ config = config.merge(new_config)
61
+ else:
62
+ print(f"Could not find credentials for {k}")
63
+ to_ask.append(k)
64
+ else:
65
+ to_ask = list(handlers.keys())
66
+
67
+ user_has_quit = False
68
+ while not user_has_quit:
69
+ if len(to_ask) == 0:
70
+ break
71
+
72
+ print(f"There are {len(to_ask)} other providers you can set up")
73
+ sep = "\n * "
74
+ print(sep + sep.join(f"{i + 1}: {name}" for i, name in enumerate(to_ask)))
75
+
76
+ print("\nOr you can enter 0 to finish.")
77
+ ix = input(" > ")
78
+ if ix == "0":
79
+ break
80
+ handler = handlers[to_ask[int(ix) - 1]]
81
+ new_config = handler.interactive_setup()
82
+ config = config.merge(new_config)
83
+
84
+ del to_ask[int(ix) - 1]
85
+
86
+ write_config(config, location, True)
87
+
88
+
89
+ def set_default_llm(location: Literal["global", "local"]) -> None:
90
+ """Interactive user flow to choose a default LLM for your configuration.
91
+
92
+ Args:
93
+ location: which config to read and update: global or local.
94
+
95
+ """
96
+ config = read_config(use_global=location == "global")
97
+
98
+ labels = [v.id for v in config.models.llm.values()]
99
+ print(f"You have {len(labels)} models to choose from:\n * ")
100
+ print("\n * ".join(labels))
101
+ print("Please enter your choice of default llm:")
102
+ choice = input(" > ")
103
+ while choice not in labels:
104
+ print(f"{choice} not in list. Please try again")
105
+ choice = input(" > ")
106
+
107
+ config.defaults.models.llm = choice
108
+ write_config(config, location, True)
109
+ print("Config updated.")
@@ -0,0 +1 @@
1
+ """Module containing handler classes for finding and importing model providers."""
@@ -0,0 +1,126 @@
1
+ # ruff: noqa: T201
2
+ """Configuration and credentials import handler for Anthropic."""
3
+
4
+ import os
5
+ import re
6
+
7
+ from anthropic import Anthropic
8
+ from anthropic.types import ModelInfo
9
+
10
+ from yera.config2.dataclasses import CredentialsMap, LLMConfig, ModelRegistry
11
+ from yera.config2.keyring import DevKeyring
12
+ from yera.config2.setup_handlers.base import BaseProviderSetup
13
+
14
+
15
+ class AnthropicSetup(BaseProviderSetup):
16
+ """Setup handler for Anthropic provider credentials and models.
17
+
18
+ Manages credential detection, validation, and model fetching for the
19
+ Anthropic API. Supports both automatic import from environment variables
20
+ and interactive credential entry.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialise the Anthropic setup handler."""
25
+ super().__init__("anthropic")
26
+
27
+ def detect_creds(self) -> dict[str, str] | None:
28
+ """Detect Anthropic credentials from environment variables.
29
+
30
+ Returns:
31
+ Dictionary with "api_key" if ANTHROPIC_API_KEY environment variable
32
+ is set, otherwise None.
33
+ """
34
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
35
+ if api_key:
36
+ return {"api_key": api_key}
37
+ return None
38
+
39
+ def ask_for_creds(self) -> dict[str, str] | None:
40
+ """Prompt user to enter Anthropic API credentials interactively.
41
+
42
+ Returns:
43
+ Dictionary with "api_key" if user provides credentials, otherwise
44
+ None if user declines to enter credentials.
45
+ """
46
+ ask = self._confirm_ask_for_creds()
47
+ if ask:
48
+ print(" Please enter your Anthropic API Key:")
49
+ api_key = input(" > ")
50
+ return {"api_key": api_key}
51
+ return None
52
+
53
+ def validate_creds(self, creds: dict[str, str]) -> None:
54
+ """Validate Anthropic API key format.
55
+
56
+ Args:
57
+ creds: Dictionary containing "api_key" to validate.
58
+
59
+ Raises:
60
+ ValueError: If the API key format is invalid (wrong prefix, invalid
61
+ characters, or appears incomplete).
62
+ """
63
+ api_key = creds["api_key"]
64
+
65
+ pattern = r"^sk-ant-[A-Za-z0-9_-]+$"
66
+
67
+ if not api_key.startswith("sk-ant-"):
68
+ raise ValueError(
69
+ "Invalid API key format. Anthropic API keys should start with 'sk-ant-'\n"
70
+ "Please ensure you've copied the complete API key from the Anthropic Console."
71
+ )
72
+
73
+ if not re.match(pattern, api_key):
74
+ raise ValueError(
75
+ "Invalid API key format. Key contains invalid characters.\n"
76
+ "Please ensure you've copied the complete API key from the Anthropic Console."
77
+ )
78
+
79
+ # Basic length check - Anthropic keys are typically quite long
80
+ if len(api_key) < 40:
81
+ raise ValueError(
82
+ "API key appears too short. Please ensure you've copied the complete key."
83
+ )
84
+
85
+ def fetch_models(self, creds_map: CredentialsMap) -> ModelRegistry:
86
+ """Fetch available models from Anthropic API.
87
+
88
+ Retrieves the list of available Claude models and creates LLMConfig
89
+ entries for each. Model IDs are normalised to lowercase with hyphens.
90
+
91
+ Args:
92
+ creds_map: Credentials map containing Anthropic provider credential labels.
93
+
94
+ Returns:
95
+ ModelRegistry containing LLMConfig entries for all available
96
+ Anthropic models.
97
+ """
98
+ creds = {
99
+ k: DevKeyring.get(f"providers.{v}")
100
+ for k, v in creds_map.providers["anthropic"].items()
101
+ }
102
+
103
+ client = Anthropic(**creds)
104
+
105
+ def _make_model_cfg(model_info: ModelInfo) -> LLMConfig:
106
+ model_id = (
107
+ model_info.display_name.lower().replace(" ", "-").replace(".", "-")
108
+ )
109
+ return LLMConfig(
110
+ id=f"anthropic.{model_id}".lower(),
111
+ display_name=model_info.display_name,
112
+ credentials="anthropic",
113
+ provider="anthropic",
114
+ interface="anthropic-sdk",
115
+ model_id=model_info.id,
116
+ )
117
+
118
+ model_list_result = client.models.list()
119
+ model_configs = [_make_model_cfg(info) for info in model_list_result.data]
120
+
121
+ names = " \n * ".join(m.display_name for m in model_configs)
122
+ print(f" Adding {len(model_configs)} LLMs from Anthropic:\n * {names}")
123
+
124
+ return ModelRegistry(
125
+ llm={m.id: m for m in model_configs},
126
+ )