dominus-sdk-python 2.0.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.
@@ -0,0 +1,357 @@
1
+ """
2
+ Redis Namespace - Whisperer caching operations.
3
+
4
+ Provides Redis caching operations via Upstash REST API.
5
+ """
6
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..start import Dominus
10
+
11
+
12
+ class RedisNamespace:
13
+ """
14
+ Redis caching namespace.
15
+
16
+ All caching operations go through /api/whisperer/* endpoints.
17
+ TTL enforced: min 60 seconds, max 86400 seconds (24 hours).
18
+
19
+ Usage:
20
+ # Set/get values
21
+ await dominus.redis.set("user:123", {"name": "John"}, ttl=3600)
22
+ value = await dominus.redis.get("user:123")
23
+
24
+ # Get with TTL refresh
25
+ value = await dominus.redis.get("user:123", nudge=True, ttl=3600)
26
+
27
+ # Distributed locks
28
+ acquired = await dominus.redis.setnx("lock:resource", "owner_id", ttl=30)
29
+
30
+ # Counters
31
+ count = await dominus.redis.incr("page:views", delta=1)
32
+
33
+ # Hash operations
34
+ await dominus.redis.hset("user:123", "email", "john@example.com")
35
+ email = await dominus.redis.hget("user:123", "email")
36
+ """
37
+
38
+ def __init__(self, client: "Dominus"):
39
+ self._client = client
40
+
41
+ async def set(
42
+ self,
43
+ key: str,
44
+ value: Any,
45
+ ttl: int = 3600,
46
+ category: Optional[str] = None
47
+ ) -> Dict[str, Any]:
48
+ """
49
+ Set a value with TTL.
50
+
51
+ Args:
52
+ key: Key name (logical_path)
53
+ value: Any JSON-serializable value
54
+ ttl: Time-to-live in seconds (60-86400, default: 3600)
55
+ category: Optional namespace category
56
+
57
+ Returns:
58
+ Dict with "key", "stored", "ttl_seconds"
59
+ """
60
+ body = {
61
+ "logical_path": key,
62
+ "value": value,
63
+ "ttl_seconds": ttl
64
+ }
65
+ if category:
66
+ body["category"] = category
67
+
68
+ return await self._client._request(
69
+ endpoint="/api/whisperer/set",
70
+ body=body
71
+ )
72
+
73
+ async def get(
74
+ self,
75
+ key: str,
76
+ category: Optional[str] = None,
77
+ nudge: bool = False,
78
+ ttl: Optional[int] = None
79
+ ) -> Dict[str, Any]:
80
+ """
81
+ Get a value, optionally refreshing TTL.
82
+
83
+ Args:
84
+ key: Key name (logical_path)
85
+ category: Optional namespace category
86
+ nudge: If True, refresh TTL on read
87
+ ttl: New TTL if nudge=True (uses default if not provided)
88
+
89
+ Returns:
90
+ Dict with "found" (bool) and "value" (if found)
91
+ """
92
+ body = {
93
+ "logical_path": key,
94
+ "nudge": nudge
95
+ }
96
+ if category:
97
+ body["category"] = category
98
+ if ttl and nudge:
99
+ body["ttl_seconds"] = ttl
100
+
101
+ return await self._client._request(
102
+ endpoint="/api/whisperer/get",
103
+ body=body
104
+ )
105
+
106
+ async def delete(
107
+ self,
108
+ key: str,
109
+ category: Optional[str] = None
110
+ ) -> Dict[str, Any]:
111
+ """
112
+ Delete a key.
113
+
114
+ Args:
115
+ key: Key name (logical_path)
116
+ category: Optional namespace category
117
+
118
+ Returns:
119
+ Dict with "deleted" (bool)
120
+ """
121
+ body = {"logical_path": key}
122
+ if category:
123
+ body["category"] = category
124
+
125
+ return await self._client._request(
126
+ endpoint="/api/whisperer/delete",
127
+ body=body
128
+ )
129
+
130
+ async def list(
131
+ self,
132
+ prefix: Optional[str] = None,
133
+ category: Optional[str] = None,
134
+ limit: int = 100,
135
+ cursor: Optional[str] = None
136
+ ) -> Dict[str, Any]:
137
+ """
138
+ List keys by prefix with pagination.
139
+
140
+ Args:
141
+ prefix: Key prefix filter
142
+ category: Optional namespace category
143
+ limit: Maximum keys to return (max 500, default: 100)
144
+ cursor: Pagination cursor from previous response
145
+
146
+ Returns:
147
+ Dict with "keys" list, "cursor" (for next page), "count"
148
+ """
149
+ body = {"limit": min(limit, 500)}
150
+ if prefix:
151
+ body["logical_prefix"] = prefix
152
+ if category:
153
+ body["category"] = category
154
+ if cursor:
155
+ body["cursor"] = cursor
156
+
157
+ return await self._client._request(
158
+ endpoint="/api/whisperer/list",
159
+ body=body
160
+ )
161
+
162
+ async def mget(self, keys: List[Dict[str, str]]) -> Dict[str, Any]:
163
+ """
164
+ Get multiple keys at once.
165
+
166
+ Args:
167
+ keys: List of key specs [{"logical_path": "...", "category": "..."}]
168
+ Maximum 100 keys per request.
169
+
170
+ Returns:
171
+ Dict with "results" list of {"found": bool, "value": ...}
172
+ """
173
+ return await self._client._request(
174
+ endpoint="/api/whisperer/mget",
175
+ body={"keys": keys[:100]}
176
+ )
177
+
178
+ async def setnx(
179
+ self,
180
+ key: str,
181
+ value: Any,
182
+ ttl: int = 60,
183
+ category: Optional[str] = None
184
+ ) -> Dict[str, Any]:
185
+ """
186
+ Set if not exists (for distributed locks/idempotency).
187
+
188
+ Args:
189
+ key: Key name (logical_path)
190
+ value: Any JSON-serializable value
191
+ ttl: Time-to-live in seconds (60-86400, default: 60)
192
+ category: Optional namespace category
193
+
194
+ Returns:
195
+ Dict with "acquired" (bool), and "existing" value if not acquired
196
+ """
197
+ body = {
198
+ "logical_path": key,
199
+ "value": value,
200
+ "ttl_seconds": ttl
201
+ }
202
+ if category:
203
+ body["category"] = category
204
+
205
+ return await self._client._request(
206
+ endpoint="/api/whisperer/setnx",
207
+ body=body
208
+ )
209
+
210
+ async def incr(
211
+ self,
212
+ key: str,
213
+ delta: int = 1,
214
+ ttl: int = 3600,
215
+ category: Optional[str] = None
216
+ ) -> Dict[str, Any]:
217
+ """
218
+ Increment counter (creates if not exists).
219
+
220
+ Args:
221
+ key: Key name (logical_path)
222
+ delta: Increment amount (can be negative, default: 1)
223
+ ttl: Time-to-live in seconds (60-86400, default: 3600)
224
+ category: Optional namespace category
225
+
226
+ Returns:
227
+ Dict with "value" (new counter value)
228
+ """
229
+ body = {
230
+ "logical_path": key,
231
+ "delta": delta,
232
+ "ttl_seconds": ttl
233
+ }
234
+ if category:
235
+ body["category"] = category
236
+
237
+ return await self._client._request(
238
+ endpoint="/api/whisperer/incr",
239
+ body=body
240
+ )
241
+
242
+ async def hset(
243
+ self,
244
+ key: str,
245
+ field: str,
246
+ value: Any,
247
+ ttl: int = 3600,
248
+ category: Optional[str] = None
249
+ ) -> Dict[str, Any]:
250
+ """
251
+ Set a hash field.
252
+
253
+ Args:
254
+ key: Hash key name (logical_path)
255
+ field: Field name within the hash
256
+ value: Any JSON-serializable value
257
+ ttl: Time-to-live in seconds (60-86400, default: 3600)
258
+ category: Optional namespace category
259
+
260
+ Returns:
261
+ Dict with "set" (bool - True if new field, False if updated)
262
+ """
263
+ body = {
264
+ "logical_path": key,
265
+ "field": field,
266
+ "value": value,
267
+ "ttl_seconds": ttl
268
+ }
269
+ if category:
270
+ body["category"] = category
271
+
272
+ return await self._client._request(
273
+ endpoint="/api/whisperer/hset",
274
+ body=body
275
+ )
276
+
277
+ async def hget(
278
+ self,
279
+ key: str,
280
+ field: str,
281
+ category: Optional[str] = None
282
+ ) -> Dict[str, Any]:
283
+ """
284
+ Get a hash field.
285
+
286
+ Args:
287
+ key: Hash key name (logical_path)
288
+ field: Field name within the hash
289
+ category: Optional namespace category
290
+
291
+ Returns:
292
+ Dict with "found" (bool) and "value" (if found)
293
+ """
294
+ body = {
295
+ "logical_path": key,
296
+ "field": field
297
+ }
298
+ if category:
299
+ body["category"] = category
300
+
301
+ return await self._client._request(
302
+ endpoint="/api/whisperer/hget",
303
+ body=body
304
+ )
305
+
306
+ async def hgetall(
307
+ self,
308
+ key: str,
309
+ category: Optional[str] = None
310
+ ) -> Dict[str, Any]:
311
+ """
312
+ Get all fields from a hash.
313
+
314
+ Args:
315
+ key: Hash key name (logical_path)
316
+ category: Optional namespace category
317
+
318
+ Returns:
319
+ Dict with "found" (bool) and "fields" dict (if found)
320
+ """
321
+ body = {"logical_path": key}
322
+ if category:
323
+ body["category"] = category
324
+
325
+ return await self._client._request(
326
+ endpoint="/api/whisperer/hgetall",
327
+ body=body
328
+ )
329
+
330
+ async def hdel(
331
+ self,
332
+ key: str,
333
+ field: str,
334
+ category: Optional[str] = None
335
+ ) -> Dict[str, Any]:
336
+ """
337
+ Delete a hash field.
338
+
339
+ Args:
340
+ key: Hash key name (logical_path)
341
+ field: Field name to delete
342
+ category: Optional namespace category
343
+
344
+ Returns:
345
+ Dict with "deleted" (bool)
346
+ """
347
+ body = {
348
+ "logical_path": key,
349
+ "field": field
350
+ }
351
+ if category:
352
+ body["category"] = category
353
+
354
+ return await self._client._request(
355
+ endpoint="/api/whisperer/hdel",
356
+ body=body
357
+ )
@@ -0,0 +1,126 @@
1
+ """
2
+ Secrets Namespace - Warden secrets management.
3
+
4
+ Provides CRUD operations for secrets stored in the Warden service.
5
+ """
6
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..start import Dominus
10
+
11
+
12
+ class SecretsNamespace:
13
+ """
14
+ Secrets management namespace.
15
+
16
+ All secrets operations go through /api/warden/secrets endpoint.
17
+
18
+ Usage:
19
+ value = await dominus.secrets.get("API_KEY")
20
+ await dominus.secrets.upsert("API_KEY", "secret_value")
21
+ secrets = await dominus.secrets.list(prefix="DB_")
22
+ await dominus.secrets.delete("OLD_KEY")
23
+ """
24
+
25
+ def __init__(self, client: "Dominus"):
26
+ self._client = client
27
+
28
+ async def get(self, key: str) -> Any:
29
+ """
30
+ Get a secret value.
31
+
32
+ Args:
33
+ key: Secret key name
34
+
35
+ Returns:
36
+ Secret value (usually string, but can be any JSON-serializable value)
37
+
38
+ Raises:
39
+ NotFoundError: If secret doesn't exist
40
+ """
41
+ result = await self._client._request(
42
+ endpoint="/api/warden/secrets",
43
+ body={"action": "get", "key": key}
44
+ )
45
+ # Response format: {"secret": {"key": "...", "value": "..."}}
46
+ secret = result.get("secret", {})
47
+ return secret.get("value")
48
+
49
+ async def upsert(
50
+ self,
51
+ key: str,
52
+ value: str,
53
+ comment: Optional[str] = None
54
+ ) -> Dict[str, Any]:
55
+ """
56
+ Create or update a secret.
57
+
58
+ Tries update first, falls back to create if secret doesn't exist.
59
+
60
+ Args:
61
+ key: Secret key name
62
+ value: Secret value
63
+ comment: Optional comment/description
64
+
65
+ Returns:
66
+ Operation result with created/updated status
67
+ """
68
+ from ..errors import NotFoundError
69
+
70
+ body = {"action": "update", "key": key, "value": value}
71
+ if comment:
72
+ body["comment"] = comment
73
+
74
+ try:
75
+ result = await self._client._request(
76
+ endpoint="/api/warden/secrets",
77
+ body=body
78
+ )
79
+ result["operation"] = "updated"
80
+ return result
81
+ except Exception as e:
82
+ # If update fails (secret doesn't exist), try create
83
+ if hasattr(e, 'status_code') and e.status_code == 500:
84
+ body["action"] = "create"
85
+ result = await self._client._request(
86
+ endpoint="/api/warden/secrets",
87
+ body=body
88
+ )
89
+ result["operation"] = "created"
90
+ return result
91
+ raise
92
+
93
+ async def list(self, prefix: Optional[str] = None) -> List[Dict[str, Any]]:
94
+ """
95
+ List secrets, optionally filtered by prefix.
96
+
97
+ Args:
98
+ prefix: Optional key prefix filter (e.g., "DB_" for all DB secrets)
99
+
100
+ Returns:
101
+ List of secret metadata (keys only, not values)
102
+ """
103
+ body = {"action": "list"}
104
+ if prefix:
105
+ body["prefix"] = prefix
106
+
107
+ result = await self._client._request(
108
+ endpoint="/api/warden/secrets",
109
+ body=body
110
+ )
111
+ return result.get("secrets", [])
112
+
113
+ async def delete(self, key: str) -> Dict[str, Any]:
114
+ """
115
+ Delete a secret.
116
+
117
+ Args:
118
+ key: Secret key to delete
119
+
120
+ Returns:
121
+ Operation result with deleted status
122
+ """
123
+ return await self._client._request(
124
+ endpoint="/api/warden/secrets",
125
+ body={"action": "delete", "key": key}
126
+ )
@@ -0,0 +1,2 @@
1
+ """Service handlers for CB Dominus SDK"""
2
+