datacules-agent-identity 0.3.3__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.
@@ -0,0 +1,213 @@
1
+ """
2
+ agent_identity — Python SDK for the agent-identity credential routing sidecar
3
+
4
+ Installation:
5
+ pip install datacules-agent-identity
6
+ # or directly:
7
+ pip install git+https://github.com/hvrcharon1/agent-identity.git#subdirectory=packages/python-sdk
8
+
9
+ Usage:
10
+ from agent_identity import AgentIdentityClient
11
+ from datetime import datetime, timezone
12
+
13
+ client = AgentIdentityClient(base_url="http://localhost:3001")
14
+
15
+ resolved = client.resolve({
16
+ "userId": "user-abc",
17
+ "resourceId": "crm-db",
18
+ "resourceKind": "shared",
19
+ "provider": "anthropic",
20
+ "model": "claude-sonnet-4-20250514",
21
+ "action": "read",
22
+ "traceId": "trace-xyz",
23
+ "requestedAt": datetime.now(timezone.utc).isoformat(),
24
+ })
25
+
26
+ pair = client.resolve_migration({
27
+ "migrationId": "migration-2026-q2",
28
+ "phase": "load",
29
+ "sourceResourceId": "crm-postgres-prod",
30
+ "targetResourceId": "crm-postgres-v2",
31
+ "userId": "svc-migration-bot",
32
+ "provider": "anthropic",
33
+ "model": "claude-sonnet-4-20250514",
34
+ "traceId": "trace-abc123",
35
+ "dryRun": False,
36
+ })
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ from datetime import datetime, timezone
43
+ from typing import Any, Dict, Literal, Optional, TypedDict
44
+
45
+ try:
46
+ import urllib.request as _urllib
47
+ except ImportError: # pragma: no cover
48
+ raise RuntimeError("agent-identity requires Python 3.8+")
49
+
50
+
51
+ # ─── Type aliases ────────────────────────────────────────────────────────────────────
52
+
53
+ ResourceKind = Literal["shared", "personal"]
54
+ SupportedProvider = Literal["openai", "anthropic", "gemini", "mistral", "local"]
55
+ MigrationPhase = Literal["dry-run", "extract", "transform", "load", "verify", "rollback"]
56
+
57
+
58
+ class AgentRequestContext(TypedDict, total=False):
59
+ userId: str # required
60
+ resourceId: str # required
61
+ resourceKind: ResourceKind # required
62
+ provider: SupportedProvider # required
63
+ model: str # required
64
+ action: str # required
65
+ traceId: str # required
66
+ requestedAt: str # required (ISO 8601)
67
+ sessionId: str # optional
68
+ parentTraceId: str # optional
69
+
70
+
71
+ class MigrateResolveRequest(TypedDict, total=False):
72
+ migrationId: str # required
73
+ phase: MigrationPhase # required
74
+ sourceResourceId: str # required
75
+ targetResourceId: str # required
76
+ userId: str # required
77
+ provider: SupportedProvider # required
78
+ model: str # required
79
+ traceId: str # required
80
+ dryRun: bool # optional, default False
81
+ batchIndex: int # optional
82
+ totalBatches: int # optional
83
+
84
+
85
+ class ResolveResponse(TypedDict):
86
+ ok: bool
87
+ resolvedFor: str
88
+ expiresAt: Optional[str]
89
+
90
+
91
+ class MigrateResolveResponse(TypedDict):
92
+ migrationId: str
93
+ phase: MigrationPhase
94
+ sourceResolvedFor: str
95
+ targetResolvedFor: str
96
+ dryRun: bool
97
+ expiresAt: Optional[str]
98
+
99
+
100
+ # ─── Exceptions ───────────────────────────────────────────────────────────────────
101
+
102
+ class AgentIdentityError(Exception):
103
+ """Base exception for all agent-identity SDK errors."""
104
+ def __init__(self, message: str, status_code: Optional[int] = None) -> None:
105
+ super().__init__(message)
106
+ self.status_code = status_code
107
+
108
+
109
+ class NoCredentialError(AgentIdentityError):
110
+ """Raised when the server returns 403 — no routing rule matched."""
111
+
112
+
113
+ class ValidationError(AgentIdentityError):
114
+ """Raised when the server returns 400 — invalid request body."""
115
+
116
+
117
+ # ─── Client ────────────────────────────────────────────────────────────────────────
118
+
119
+ class AgentIdentityClient:
120
+ """
121
+ Thin HTTP client for the agent-identity sidecar.
122
+ No Node.js required — pure Python 3.8+ stdlib.
123
+
124
+ Args:
125
+ base_url: URL of the running sidecar, e.g. 'http://localhost:3001'
126
+ timeout: Request timeout in seconds (default 10)
127
+ """
128
+
129
+ def __init__(self, base_url: str = "http://localhost:3001", timeout: int = 10) -> None:
130
+ self.base_url = base_url.rstrip("/")
131
+ self.timeout = timeout
132
+
133
+ # ─── Public API ────────────────────────────────────────────────────────────────────
134
+
135
+ def resolve(self, ctx: AgentRequestContext) -> ResolveResponse:
136
+ """
137
+ Resolve the best credential for a single agent request.
138
+
139
+ Args:
140
+ ctx: AgentRequestContext dict — all required fields must be present.
141
+ If 'requestedAt' is omitted, the current UTC time is used.
142
+
143
+ Returns:
144
+ ResolveResponse with resolvedFor and optional expiresAt.
145
+
146
+ Raises:
147
+ ValidationError: Server returned 400 (missing/invalid field).
148
+ NoCredentialError: Server returned 403 (no rule matched).
149
+ AgentIdentityError: Any other HTTP error.
150
+ """
151
+ if "requestedAt" not in ctx:
152
+ ctx = {**ctx, "requestedAt": datetime.now(timezone.utc).isoformat()} # type: ignore[assignment]
153
+ return self._post("/api/resolve", ctx) # type: ignore[return-value]
154
+
155
+ def resolve_migration(
156
+ self, request: MigrateResolveRequest
157
+ ) -> MigrateResolveResponse:
158
+ """
159
+ Resolve source + target credentials for a migration phase.
160
+
161
+ Call once at the start of each phase. The returned expiresAt lets
162
+ the agent decide when to re-call before the batch loop ends.
163
+
164
+ Args:
165
+ request: MigrateResolveRequest dict.
166
+
167
+ Returns:
168
+ MigrateResolveResponse with sourceResolvedFor, targetResolvedFor,
169
+ and optional expiresAt (earliest of the two credentials).
170
+ """
171
+ if "dryRun" not in request:
172
+ request = {**request, "dryRun": False} # type: ignore[assignment]
173
+ return self._post("/api/migrate/resolve", request) # type: ignore[return-value]
174
+
175
+ def health(self) -> bool:
176
+ """Returns True if the sidecar is reachable and healthy."""
177
+ try:
178
+ self._post("/api/health", {}, method="GET")
179
+ return True
180
+ except AgentIdentityError:
181
+ return False
182
+ except Exception: # noqa: BLE001
183
+ return False
184
+
185
+ # ─── Internal ──────────────────────────────────────────────────────────────────────
186
+
187
+ def _post(self, path: str, body: Dict[str, Any], method: str = "POST") -> Any:
188
+ url = f"{self.base_url}{path}"
189
+ data = json.dumps(body).encode("utf-8")
190
+ req = _urllib.Request(
191
+ url,
192
+ data=data if method == "POST" else None,
193
+ headers={"Content-Type": "application/json", "Accept": "application/json"},
194
+ method=method,
195
+ )
196
+ try:
197
+ with _urllib.urlopen(req, timeout=self.timeout) as resp:
198
+ return json.loads(resp.read().decode("utf-8"))
199
+ except _urllib.HTTPError as e:
200
+ status = e.code
201
+ try:
202
+ error_body = json.loads(e.read().decode("utf-8"))
203
+ message = error_body.get("error", str(e))
204
+ except Exception: # noqa: BLE001
205
+ message = str(e)
206
+
207
+ if status == 400:
208
+ raise ValidationError(message, status_code=status) from e
209
+ if status == 403:
210
+ raise NoCredentialError(message, status_code=status) from e
211
+ raise AgentIdentityError(message, status_code=status) from e
212
+ except Exception as e:
213
+ raise AgentIdentityError(f"Request failed: {e}") from e
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: datacules-agent-identity
3
+ Version: 0.3.3
4
+ Summary: Python SDK for the agent-identity credential routing sidecar — by Datacules LLC
5
+ Author-email: Datacules LLC <harshalrasal792@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hvrcharon1/agent-identity
8
+ Project-URL: Repository, https://github.com/hvrcharon1/agent-identity
9
+ Project-URL: Documentation, https://github.com/hvrcharon1/agent-identity/tree/main/packages/python-sdk
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: pytest-cov; extra == "dev"
15
+
16
+ # agent-identity Python SDK
17
+
18
+ Pure-Python client for the `agent-identity` sidecar. No Node.js required.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install agent-identity
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ from agent_identity import AgentIdentityClient
30
+ from datetime import datetime, timezone
31
+
32
+ client = AgentIdentityClient(base_url="http://localhost:3001")
33
+
34
+ # Resolve a credential for a single agent request
35
+ resolved = client.resolve({
36
+ "userId": "user-abc",
37
+ "resourceId": "crm-db",
38
+ "resourceKind": "shared",
39
+ "provider": "anthropic",
40
+ "model": "claude-sonnet-4-20250514",
41
+ "action": "read",
42
+ "traceId": "trace-xyz",
43
+ "requestedAt": datetime.now(timezone.utc).isoformat(),
44
+ })
45
+ print(resolved["resolvedFor"]) # 'service' or userId
46
+
47
+ # Resolve source + target credentials for a migration phase
48
+ pair = client.resolve_migration({
49
+ "migrationId": "migration-2026-q2",
50
+ "phase": "load",
51
+ "sourceResourceId": "crm-postgres-prod",
52
+ "targetResourceId": "crm-postgres-v2",
53
+ "userId": "svc-migration-bot",
54
+ "provider": "anthropic",
55
+ "model": "claude-sonnet-4-20250514",
56
+ "traceId": "trace-abc123",
57
+ "dryRun": False,
58
+ })
59
+ print(pair["expiresAt"]) # ISO 8601 or None
60
+ ```
61
+
62
+ ## LangChain integration
63
+
64
+ ```python
65
+ from langchain_anthropic import ChatAnthropic
66
+ from agent_identity import AgentIdentityClient
67
+
68
+ client = AgentIdentityClient()
69
+ resolved = client.resolve({...})
70
+
71
+ # resolved["resolvedFor"] is safe to log; the raw API key stays on the server
72
+ llm = ChatAnthropic(model="claude-sonnet-4-20250514")
73
+ # The sidecar injects the API key server-side when you call /api/resolve
74
+ ```
75
+
76
+ ## Error handling
77
+
78
+ ```python
79
+ from agent_identity import AgentIdentityClient, NoCredentialError, ValidationError
80
+
81
+ try:
82
+ result = client.resolve(ctx)
83
+ except NoCredentialError:
84
+ # 403 — no routing rule matched this context
85
+ ...
86
+ except ValidationError as e:
87
+ # 400 — bad request body
88
+ print(e)
89
+ ```
@@ -0,0 +1,5 @@
1
+ agent_identity/__init__.py,sha256=dLLVEhHdTP2GG_fMZRcE1sMFzm-k_3n9h6DdxbT7moY,8178
2
+ datacules_agent_identity-0.3.3.dist-info/METADATA,sha256=p9e9yFx65vGiRmkZzZksRPfJri4KgiU5fVsCDcIsRYY,2531
3
+ datacules_agent_identity-0.3.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
+ datacules_agent_identity-0.3.3.dist-info/top_level.txt,sha256=KzARZw8H4t-44rbjTSmhOC70sSualwA5fjSPZK0Jbyo,15
5
+ datacules_agent_identity-0.3.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ agent_identity