yera 0.1.1__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.
- infra_mvp/base_client.py +29 -0
- infra_mvp/base_server.py +68 -0
- infra_mvp/monitoring/__init__.py +15 -0
- infra_mvp/monitoring/metrics.py +185 -0
- infra_mvp/stream/README.md +56 -0
- infra_mvp/stream/__init__.py +14 -0
- infra_mvp/stream/__main__.py +101 -0
- infra_mvp/stream/agents/demos/financial/chart_additions_plan.md +170 -0
- infra_mvp/stream/agents/demos/financial/portfolio_assistant_stream.json +1571 -0
- infra_mvp/stream/agents/reference/blocks/action.json +170 -0
- infra_mvp/stream/agents/reference/blocks/button.json +66 -0
- infra_mvp/stream/agents/reference/blocks/date.json +65 -0
- infra_mvp/stream/agents/reference/blocks/input_prompt.json +94 -0
- infra_mvp/stream/agents/reference/blocks/layout.json +288 -0
- infra_mvp/stream/agents/reference/blocks/markdown.json +344 -0
- infra_mvp/stream/agents/reference/blocks/slider.json +67 -0
- infra_mvp/stream/agents/reference/blocks/spinner.json +110 -0
- infra_mvp/stream/agents/reference/blocks/table.json +56 -0
- infra_mvp/stream/agents/reference/chat_dynamics/branching_test_stream.json +145 -0
- infra_mvp/stream/app.py +49 -0
- infra_mvp/stream/container.py +112 -0
- infra_mvp/stream/schemas/__init__.py +16 -0
- infra_mvp/stream/schemas/agent.py +24 -0
- infra_mvp/stream/schemas/interaction.py +28 -0
- infra_mvp/stream/schemas/session.py +30 -0
- infra_mvp/stream/server.py +321 -0
- infra_mvp/stream/services/__init__.py +12 -0
- infra_mvp/stream/services/agent_service.py +40 -0
- infra_mvp/stream/services/event_converter.py +83 -0
- infra_mvp/stream/services/session_service.py +247 -0
- yera/__init__.py +50 -1
- yera/agents/__init__.py +2 -0
- yera/agents/context.py +41 -0
- yera/agents/dataclasses.py +69 -0
- yera/agents/decorator.py +207 -0
- yera/agents/discovery.py +124 -0
- yera/agents/typing/__init__.py +0 -0
- yera/agents/typing/coerce.py +408 -0
- yera/agents/typing/utils.py +19 -0
- yera/agents/typing/validate.py +206 -0
- yera/cli.py +377 -0
- yera/config/__init__.py +1 -0
- yera/config/config_utils.py +164 -0
- yera/config/function_config.py +55 -0
- yera/config/logging.py +18 -0
- yera/config/tool_config.py +8 -0
- yera/config2/__init__.py +8 -0
- yera/config2/dataclasses.py +534 -0
- yera/config2/keyring.py +270 -0
- yera/config2/paths.py +28 -0
- yera/config2/read.py +113 -0
- yera/config2/setup.py +109 -0
- yera/config2/setup_handlers/__init__.py +1 -0
- yera/config2/setup_handlers/anthropic.py +126 -0
- yera/config2/setup_handlers/azure.py +236 -0
- yera/config2/setup_handlers/base.py +125 -0
- yera/config2/setup_handlers/llama_cpp.py +205 -0
- yera/config2/setup_handlers/ollama.py +157 -0
- yera/config2/setup_handlers/openai.py +137 -0
- yera/config2/write.py +87 -0
- yera/dsl/__init__.py +0 -0
- yera/dsl/functions.py +94 -0
- yera/dsl/struct.py +20 -0
- yera/dsl/workspace.py +79 -0
- yera/events/__init__.py +57 -0
- yera/events/blocks/__init__.py +68 -0
- yera/events/blocks/action.py +57 -0
- yera/events/blocks/bar_chart.py +92 -0
- yera/events/blocks/base/__init__.py +20 -0
- yera/events/blocks/base/base.py +166 -0
- yera/events/blocks/base/chart.py +288 -0
- yera/events/blocks/base/layout.py +111 -0
- yera/events/blocks/buttons.py +37 -0
- yera/events/blocks/columns.py +26 -0
- yera/events/blocks/container.py +24 -0
- yera/events/blocks/date_picker.py +50 -0
- yera/events/blocks/exit.py +39 -0
- yera/events/blocks/form.py +24 -0
- yera/events/blocks/input_echo.py +22 -0
- yera/events/blocks/input_request.py +31 -0
- yera/events/blocks/line_chart.py +97 -0
- yera/events/blocks/markdown.py +67 -0
- yera/events/blocks/slider.py +54 -0
- yera/events/blocks/spinner.py +55 -0
- yera/events/blocks/system_prompt.py +22 -0
- yera/events/blocks/table.py +291 -0
- yera/events/models/__init__.py +39 -0
- yera/events/models/block_data.py +112 -0
- yera/events/models/in_event.py +7 -0
- yera/events/models/out_event.py +75 -0
- yera/events/runtime.py +187 -0
- yera/events/stream.py +91 -0
- yera/models/__init__.py +0 -0
- yera/models/data_classes.py +20 -0
- yera/models/llm_atlas_proxy.py +44 -0
- yera/models/llm_context.py +99 -0
- yera/models/llm_interfaces/__init__.py +0 -0
- yera/models/llm_interfaces/anthropic.py +153 -0
- yera/models/llm_interfaces/aws_bedrock.py +14 -0
- yera/models/llm_interfaces/azure_openai.py +143 -0
- yera/models/llm_interfaces/base.py +26 -0
- yera/models/llm_interfaces/interface_registry.py +74 -0
- yera/models/llm_interfaces/llama_cpp.py +136 -0
- yera/models/llm_interfaces/mock.py +29 -0
- yera/models/llm_interfaces/ollama_interface.py +118 -0
- yera/models/llm_interfaces/open_ai.py +150 -0
- yera/models/llm_workspace.py +19 -0
- yera/models/model_atlas.py +139 -0
- yera/models/model_definition.py +38 -0
- yera/models/model_factory.py +33 -0
- yera/opaque/__init__.py +9 -0
- yera/opaque/base.py +20 -0
- yera/opaque/decorator.py +8 -0
- yera/opaque/markdown.py +57 -0
- yera/opaque/opaque_function.py +25 -0
- yera/tools/__init__.py +29 -0
- yera/tools/atlas_tool.py +20 -0
- yera/tools/base.py +24 -0
- yera/tools/decorated_tool.py +18 -0
- yera/tools/decorator.py +35 -0
- yera/tools/tool_atlas.py +51 -0
- yera/tools/tool_utils.py +361 -0
- yera/ui/dist/404.html +1 -0
- yera/ui/dist/__next.__PAGE__.txt +10 -0
- yera/ui/dist/__next._full.txt +23 -0
- yera/ui/dist/__next._head.txt +6 -0
- yera/ui/dist/__next._index.txt +5 -0
- yera/ui/dist/__next._tree.txt +7 -0
- yera/ui/dist/_next/static/chunks/4c4688e1ff21ad98.js +1 -0
- yera/ui/dist/_next/static/chunks/652cd53c27924d50.js +4 -0
- yera/ui/dist/_next/static/chunks/786d2107b51e8499.css +1 -0
- yera/ui/dist/_next/static/chunks/7de9141b1af425c3.js +1 -0
- yera/ui/dist/_next/static/chunks/87ef65064d3524c1.js +2 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- yera/ui/dist/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- yera/ui/dist/_next/static/chunks/c4c79d5d0b280aeb.js +1 -0
- yera/ui/dist/_next/static/chunks/dc2d2a247505d66f.css +5 -0
- yera/ui/dist/_next/static/chunks/f773f714b55ec620.js +37 -0
- yera/ui/dist/_next/static/chunks/turbopack-98b3031e1b1dbc33.js +4 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_buildManifest.js +11 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_clientMiddlewareManifest.json +1 -0
- yera/ui/dist/_next/static/lnhYLzJ1-a5EfNbW1uFF6/_ssgManifest.js +1 -0
- yera/ui/dist/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 +0 -0
- yera/ui/dist/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 +0 -0
- yera/ui/dist/_next/static/media/2b2eb4836d2dad95-s.f36de3af.woff2 +0 -0
- yera/ui/dist/_next/static/media/31183d9fd602dc89-s.c4ff9b73.woff2 +0 -0
- yera/ui/dist/_next/static/media/3fcb63a1ac6a562e-s.2f77a576.woff2 +0 -0
- yera/ui/dist/_next/static/media/45ec8de98929b0f6-s.81056204.woff2 +0 -0
- yera/ui/dist/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- yera/ui/dist/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 +0 -0
- yera/ui/dist/_next/static/media/67add6cc0f54b8cf-s.8ce53448.woff2 +0 -0
- yera/ui/dist/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- yera/ui/dist/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- yera/ui/dist/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- yera/ui/dist/_next/static/media/a8ff2d5d0ccb0d12-s.fc5b72a7.woff2 +0 -0
- yera/ui/dist/_next/static/media/aae5f0be330e13db-s.p.853e26d6.woff2 +0 -0
- yera/ui/dist/_next/static/media/b11a6ccf4a3edec7-s.2113d282.woff2 +0 -0
- yera/ui/dist/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 +0 -0
- yera/ui/dist/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- yera/ui/dist/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- yera/ui/dist/_next/static/media/favicon.0b3bf435.ico +0 -0
- yera/ui/dist/_not-found/__next._full.txt +14 -0
- yera/ui/dist/_not-found/__next._head.txt +6 -0
- yera/ui/dist/_not-found/__next._index.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
- yera/ui/dist/_not-found/__next._not-found.txt +4 -0
- yera/ui/dist/_not-found/__next._tree.txt +2 -0
- yera/ui/dist/_not-found.html +1 -0
- yera/ui/dist/_not-found.txt +14 -0
- yera/ui/dist/agent-icon.svg +3 -0
- yera/ui/dist/favicon.ico +0 -0
- yera/ui/dist/file.svg +1 -0
- yera/ui/dist/globe.svg +1 -0
- yera/ui/dist/index.html +1 -0
- yera/ui/dist/index.txt +23 -0
- yera/ui/dist/logo/full_logo.png +0 -0
- yera/ui/dist/logo/rune_logo.png +0 -0
- yera/ui/dist/logo/rune_logo_borderless.png +0 -0
- yera/ui/dist/logo/text_logo.png +0 -0
- yera/ui/dist/next.svg +1 -0
- yera/ui/dist/send.png +0 -0
- yera/ui/dist/send_single.png +0 -0
- yera/ui/dist/vercel.svg +1 -0
- yera/ui/dist/window.svg +1 -0
- yera/utils/__init__.py +1 -0
- yera/utils/path_utils.py +38 -0
- yera-0.2.0.dist-info/METADATA +65 -0
- yera-0.2.0.dist-info/RECORD +190 -0
- {yera-0.1.1.dist-info → yera-0.2.0.dist-info}/WHEEL +1 -1
- yera-0.2.0.dist-info/entry_points.txt +2 -0
- yera-0.1.1.dist-info/METADATA +0 -11
- yera-0.1.1.dist-info/RECORD +0 -4
yera/config2/keyring.py
ADDED
|
@@ -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
|
+
)
|