evolvingmachines-evolve 0.0.55.dev1355__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.
evolve/integrations.py ADDED
@@ -0,0 +1,173 @@
1
+ """Managed integrations async helpers."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List, Optional
5
+
6
+ from .bridge import BridgeManager
7
+
8
+
9
+ @dataclass
10
+ class IntegrationAuthResult:
11
+ """Result from an integration auth request."""
12
+ url: str
13
+ account_id: Optional[str] = None
14
+
15
+
16
+ @dataclass
17
+ class IntegrationAccount:
18
+ """Connected account for a managed app integration."""
19
+ user_id: str
20
+ app: str
21
+ status: str
22
+ app_name: Optional[str] = None
23
+ app_icon: Optional[str] = None
24
+ account_label: Optional[str] = None
25
+ account_id: Optional[str] = None
26
+
27
+
28
+ @dataclass
29
+ class IntegrationAccountUpdateResult:
30
+ """Result from updating a connected account."""
31
+ success: bool
32
+ account_id: str
33
+ account_label: Optional[str] = None
34
+
35
+
36
+ @dataclass
37
+ class IntegrationAccountDeleteResult:
38
+ """Result from deleting a connected account."""
39
+ success: bool
40
+ account_id: str
41
+
42
+
43
+ async def auth(
44
+ *,
45
+ user_id: str,
46
+ app: str,
47
+ account_label: Optional[str] = None,
48
+ api_key: Optional[str] = None,
49
+ dashboard_url: Optional[str] = None,
50
+ ) -> IntegrationAuthResult:
51
+ """Create an auth URL for one app account."""
52
+ bridge = BridgeManager()
53
+ try:
54
+ await bridge.start()
55
+ params: Dict[str, object] = {'user_id': user_id, 'app': app}
56
+ if account_label is not None:
57
+ params['account_label'] = account_label
58
+ if api_key:
59
+ params['api_key'] = api_key
60
+ if dashboard_url:
61
+ params['dashboard_url'] = dashboard_url
62
+ response = await bridge.call('integrations_auth', params)
63
+ return IntegrationAuthResult(
64
+ url=response['url'],
65
+ account_id=response.get('account_id'),
66
+ )
67
+ finally:
68
+ await bridge.stop()
69
+
70
+
71
+ class _Accounts:
72
+ async def list(
73
+ self,
74
+ *,
75
+ user_ids: List[str],
76
+ app: Optional[str] = None,
77
+ statuses: Optional[List[str]] = None,
78
+ api_key: Optional[str] = None,
79
+ dashboard_url: Optional[str] = None,
80
+ ) -> List[IntegrationAccount]:
81
+ """List connected accounts for one or more integration users."""
82
+ bridge = BridgeManager()
83
+ try:
84
+ await bridge.start()
85
+ params: Dict[str, object] = {'user_ids': user_ids}
86
+ if app:
87
+ params['app'] = app
88
+ if statuses:
89
+ params['statuses'] = statuses
90
+ if api_key:
91
+ params['api_key'] = api_key
92
+ if dashboard_url:
93
+ params['dashboard_url'] = dashboard_url
94
+ response = await bridge.call('integrations_accounts_list', params)
95
+ return [
96
+ IntegrationAccount(
97
+ user_id=account['user_id'],
98
+ app=account['app'],
99
+ status=account['status'],
100
+ app_name=account.get('app_name'),
101
+ app_icon=account.get('app_icon'),
102
+ account_label=account.get('account_label'),
103
+ account_id=account.get('account_id'),
104
+ )
105
+ for account in response['accounts']
106
+ ]
107
+ finally:
108
+ await bridge.stop()
109
+
110
+ async def update(
111
+ self,
112
+ *,
113
+ account_id: str,
114
+ account_label: Optional[str] = None,
115
+ api_key: Optional[str] = None,
116
+ dashboard_url: Optional[str] = None,
117
+ ) -> IntegrationAccountUpdateResult:
118
+ """Set, update, or clear a connected account label."""
119
+ bridge = BridgeManager()
120
+ try:
121
+ await bridge.start()
122
+ params: Dict[str, object] = {'account_id': account_id}
123
+ if account_label is not None:
124
+ params['account_label'] = account_label
125
+ if api_key:
126
+ params['api_key'] = api_key
127
+ if dashboard_url:
128
+ params['dashboard_url'] = dashboard_url
129
+ response = await bridge.call('integrations_account_update', params)
130
+ return IntegrationAccountUpdateResult(
131
+ success=response['success'],
132
+ account_id=response['account_id'],
133
+ account_label=response.get('account_label'),
134
+ )
135
+ finally:
136
+ await bridge.stop()
137
+
138
+ async def delete(
139
+ self,
140
+ *,
141
+ account_id: str,
142
+ api_key: Optional[str] = None,
143
+ dashboard_url: Optional[str] = None,
144
+ ) -> IntegrationAccountDeleteResult:
145
+ """Delete a connected account."""
146
+ bridge = BridgeManager()
147
+ try:
148
+ await bridge.start()
149
+ params: Dict[str, object] = {'account_id': account_id}
150
+ if api_key:
151
+ params['api_key'] = api_key
152
+ if dashboard_url:
153
+ params['dashboard_url'] = dashboard_url
154
+ response = await bridge.call('integrations_account_delete', params)
155
+ return IntegrationAccountDeleteResult(
156
+ success=response['success'],
157
+ account_id=response['account_id'],
158
+ )
159
+ finally:
160
+ await bridge.stop()
161
+
162
+
163
+ accounts = _Accounts()
164
+
165
+
166
+ __all__ = [
167
+ 'auth',
168
+ 'accounts',
169
+ 'IntegrationAuthResult',
170
+ 'IntegrationAccount',
171
+ 'IntegrationAccountUpdateResult',
172
+ 'IntegrationAccountDeleteResult',
173
+ ]
@@ -0,0 +1,175 @@
1
+ """Standalone managed secrets client."""
2
+
3
+ import asyncio
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from . import _http
9
+ from .config import ManagedSecretsClientConfig
10
+
11
+
12
+ DEFAULT_DASHBOARD_URL = 'https://dashboard.evolvingmachines.ai'
13
+
14
+
15
+ @dataclass
16
+ class ManagedSecretMetadata:
17
+ id: str
18
+ name: str
19
+ allowed_hosts: List[str]
20
+ allowed_path_prefixes: List[str]
21
+ allowed_methods: List[str]
22
+ created_at: str
23
+ updated_at: str
24
+ last_used_at: Optional[str]
25
+ #: The row's label — secrets are unique by (name, label), 'default' when
26
+ #: none was chosen at store time. None: servers older than the label
27
+ #: lane omit it.
28
+ label: Optional[str] = None
29
+ #: The row's delivery mode: 'brokered' (the value never enters any
30
+ #: sandbox — placeholder env + egress-proxy swap toward allowed_hosts)
31
+ #: or 'direct' (the raw value is placed in the sandbox environment —
32
+ #: URL-parameter keys, gRPC, websockets). None: servers older than the
33
+ #: delivery lane omit it and always broker.
34
+ delivery: Optional[str] = None
35
+
36
+
37
+ def _metadata_from_dict(data: Dict[str, Any]) -> ManagedSecretMetadata:
38
+ return ManagedSecretMetadata(
39
+ id=data['id'],
40
+ name=data['name'],
41
+ allowed_hosts=list(data.get('allowedHosts') or data.get('allowed_hosts') or []),
42
+ allowed_path_prefixes=list(data.get('allowedPathPrefixes') or data.get('allowed_path_prefixes') or []),
43
+ allowed_methods=list(data.get('allowedMethods') or data.get('allowed_methods') or []),
44
+ created_at=data.get('createdAt') or data.get('created_at') or '',
45
+ updated_at=data.get('updatedAt') or data.get('updated_at') or '',
46
+ last_used_at=data.get('lastUsedAt') or data.get('last_used_at'),
47
+ label=data.get('label'),
48
+ delivery=data.get('delivery'),
49
+ )
50
+
51
+
52
+ @dataclass
53
+ class ManagedSecretWriteResult:
54
+ #: 'created' for a fresh (name, label) identity; 'updated' when the
55
+ #: request restated the stored value byte-for-byte and the row converged
56
+ #: (the one path where delivery and scoping are editable). A DIFFERENT
57
+ #: value under an existing identity never overwrites — the server
58
+ #: refuses typed (HTTP 409, code ``secret_exists``).
59
+ status: str
60
+ secret: ManagedSecretMetadata
61
+
62
+
63
+ @dataclass
64
+ class ManagedSecretDeleteResult:
65
+ ok: bool
66
+ name: str
67
+ #: The label of the row that was deleted (resolution may have defaulted it).
68
+ label: str
69
+
70
+
71
+ class ManagedSecretsClient:
72
+ """Dashboard-stored managed secrets: list metadata, set and delete.
73
+
74
+ ``set`` speaks the API-key write door (``POST /api/managed-secrets``):
75
+ the VALUE rides the HTTPS body and is sealed server-side with the vault
76
+ cipher — the same posture the inline job-secrets door on job create
77
+ established; no read ever returns it. LLM provider keys (BYOK) are not
78
+ writable here — they gate billing and stay behind the signed-in
79
+ dashboard session.
80
+ """
81
+
82
+ def __init__(self, config: Optional[ManagedSecretsClientConfig] = None):
83
+ self.config = config or ManagedSecretsClientConfig()
84
+
85
+ async def __aenter__(self):
86
+ return self
87
+
88
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
89
+ await self.close()
90
+
91
+ async def close(self):
92
+ return None
93
+
94
+ async def list(self) -> List[ManagedSecretMetadata]:
95
+ result = await self._request_json('/api/managed-secrets')
96
+ return [_metadata_from_dict(item) for item in result.get('secrets', [])]
97
+
98
+ async def set(
99
+ self,
100
+ *,
101
+ name: str,
102
+ value: str,
103
+ delivery: str,
104
+ label: Optional[str] = None,
105
+ allowed_hosts: Optional[List[str]] = None,
106
+ allowed_path_prefixes: Optional[List[str]] = None,
107
+ allowed_methods: Optional[List[str]] = None,
108
+ ) -> ManagedSecretWriteResult:
109
+ """Create or update an env secret (identity = (name, label)).
110
+
111
+ ``delivery`` is required: 'brokered' needs the allowed_hosts /
112
+ allowed_path_prefixes / allowed_methods scoping triple, 'direct'
113
+ refuses it. An existing identity converges only on a byte-equal
114
+ restatement of the stored value; a different value refuses typed —
115
+ rotate by delete + set, or use another label.
116
+ """
117
+ body: Dict[str, Any] = {'name': name, 'value': value, 'delivery': delivery}
118
+ if label is not None:
119
+ body['label'] = label
120
+ if allowed_hosts is not None:
121
+ body['allowed_hosts'] = list(allowed_hosts)
122
+ if allowed_path_prefixes is not None:
123
+ body['allowed_path_prefixes'] = list(allowed_path_prefixes)
124
+ if allowed_methods is not None:
125
+ body['allowed_methods'] = list(allowed_methods)
126
+ result = await self._request_json('/api/managed-secrets', method='POST', body=body)
127
+ return ManagedSecretWriteResult(
128
+ status=str(result.get('status') or ''),
129
+ secret=_metadata_from_dict(result.get('secret') or {}),
130
+ )
131
+
132
+ async def delete(
133
+ self, *, name: str, label: Optional[str] = None
134
+ ) -> ManagedSecretDeleteResult:
135
+ """Delete an env secret by (name, label).
136
+
137
+ An omitted label resolves by the shared law: the 'default' row, else
138
+ the single row, else a typed ambiguity refusal naming every label.
139
+ """
140
+ body: Dict[str, Any] = {'name': name}
141
+ if label is not None:
142
+ body['label'] = label
143
+ result = await self._request_json('/api/managed-secrets', method='DELETE', body=body)
144
+ return ManagedSecretDeleteResult(
145
+ ok=bool(result.get('ok')),
146
+ name=str(result.get('name') or name),
147
+ label=str(result.get('label') or ''),
148
+ )
149
+
150
+ async def _request_json(
151
+ self,
152
+ path: str,
153
+ *,
154
+ method: str = 'GET',
155
+ body: Optional[Dict[str, Any]] = None,
156
+ ) -> Dict[str, Any]:
157
+ return await asyncio.to_thread(
158
+ _http.request_json,
159
+ f'{_dashboard_base_url(self.config)}{path}',
160
+ api_key=_resolve_api_key(self.config),
161
+ error_prefix='Managed secrets',
162
+ method=method,
163
+ body=body,
164
+ )
165
+
166
+
167
+ def _dashboard_base_url(config: ManagedSecretsClientConfig) -> str:
168
+ return (config.dashboard_url or os.environ.get('EVOLVE_DASHBOARD_URL') or DEFAULT_DASHBOARD_URL).rstrip('/')
169
+
170
+
171
+ def _resolve_api_key(config: ManagedSecretsClientConfig) -> str:
172
+ api_key = config.api_key or os.environ.get('EVOLVE_API_KEY')
173
+ if not api_key:
174
+ raise ValueError('Managed secrets require EVOLVE_API_KEY or ManagedSecretsClientConfig(api_key=...)')
175
+ return api_key
@@ -0,0 +1,59 @@
1
+ """Pipeline - Fluent API for chaining Swarm operations."""
2
+
3
+ from .pipeline import Pipeline, TerminalPipeline
4
+ from .types import (
5
+ # Step configs
6
+ MapConfig,
7
+ FilterConfig,
8
+ ReduceConfig,
9
+ # Results
10
+ StepResult,
11
+ PipelineResult,
12
+ # Events
13
+ PipelineEvents,
14
+ PipelineEventMap,
15
+ StepEvent,
16
+ StepStartEvent,
17
+ StepCompleteEvent,
18
+ StepErrorEvent,
19
+ ItemRetryEvent,
20
+ WorkerCompleteEvent,
21
+ VerifierCompleteEvent,
22
+ CandidateCompleteEvent,
23
+ JudgeCompleteEvent,
24
+ # Types
25
+ EmitOption,
26
+ EventName,
27
+ Step,
28
+ StepType,
29
+ )
30
+
31
+ __all__ = [
32
+ # Main classes
33
+ 'Pipeline',
34
+ 'TerminalPipeline',
35
+ # Step configs
36
+ 'MapConfig',
37
+ 'FilterConfig',
38
+ 'ReduceConfig',
39
+ # Results
40
+ 'StepResult',
41
+ 'PipelineResult',
42
+ # Events
43
+ 'PipelineEvents',
44
+ 'PipelineEventMap',
45
+ 'StepEvent',
46
+ 'StepStartEvent',
47
+ 'StepCompleteEvent',
48
+ 'StepErrorEvent',
49
+ 'ItemRetryEvent',
50
+ 'WorkerCompleteEvent',
51
+ 'VerifierCompleteEvent',
52
+ 'CandidateCompleteEvent',
53
+ 'JudgeCompleteEvent',
54
+ # Types
55
+ 'EmitOption',
56
+ 'EventName',
57
+ 'Step',
58
+ 'StepType',
59
+ ]