dominus-sdk-python-dev 2.1.6__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,367 @@
1
+ """
2
+ Logs Namespace - Herald structured logging operations.
3
+
4
+ Provides zero-friction logging to BetterStack via Herald service.
5
+ SDK automatically captures callsite (file, function) - users just call:
6
+ await dominus.logs.info("message", {"key": "value"})
7
+ """
8
+ import inspect
9
+ import json
10
+ import sys
11
+ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from ..start import Dominus
15
+
16
+
17
+ class LogsNamespace:
18
+ """
19
+ Logging namespace for Herald service.
20
+
21
+ All methods automatically capture callsite information (file, function)
22
+ and send to the Herald backend at /api/herald/*.
23
+
24
+ Log levels: debug, info, notice, warn, error, critical
25
+
26
+ Usage:
27
+ # Basic logging
28
+ await dominus.logs.info("User logged in", {"user_id": "123"})
29
+ await dominus.logs.error("Payment failed", {"order_id": "456"})
30
+
31
+ # With category
32
+ await dominus.logs.info("Cache hit", {"key": "user:123"}, category="cache")
33
+
34
+ # With exception
35
+ try:
36
+ do_something()
37
+ except Exception as e:
38
+ await dominus.logs.error("Operation failed", exception=e)
39
+
40
+ # Query logs (admin or scoped to your project)
41
+ logs = await dominus.logs.query(level="error", limit=50)
42
+ """
43
+
44
+ def __init__(self, client: "Dominus"):
45
+ self._client = client
46
+
47
+ def _capture_callsite(self, depth: int = 3) -> Tuple[Optional[str], Optional[str]]:
48
+ """Capture file and function from call stack.
49
+
50
+ Args:
51
+ depth: Stack frame depth to inspect (default: 3)
52
+
53
+ Returns:
54
+ Tuple of (filepath, function_name)
55
+ """
56
+ frame = inspect.currentframe()
57
+ try:
58
+ for _ in range(depth):
59
+ if frame is not None:
60
+ frame = frame.f_back
61
+ if frame is None:
62
+ return None, None
63
+
64
+ filepath = frame.f_code.co_filename
65
+ function = frame.f_code.co_name
66
+
67
+ # Normalize filepath - strip common prefixes
68
+ for prefix in ['/app/', '/home/', 'C:\\', '/Users/']:
69
+ if filepath.startswith(prefix):
70
+ filepath = filepath[len(prefix):]
71
+ break
72
+
73
+ # Also strip any remaining absolute path prefix
74
+ if '/' in filepath:
75
+ # Keep last 3 path components max
76
+ parts = filepath.split('/')
77
+ if len(parts) > 3:
78
+ filepath = '/'.join(parts[-3:])
79
+ elif '\\' in filepath:
80
+ parts = filepath.split('\\')
81
+ if len(parts) > 3:
82
+ filepath = '/'.join(parts[-3:])
83
+
84
+ return filepath, function
85
+ finally:
86
+ del frame
87
+
88
+ def _fallback_log(
89
+ self,
90
+ level: str,
91
+ message: str,
92
+ context: Optional[Dict[str, Any]] = None
93
+ ) -> None:
94
+ """Print to stderr as fallback when backend unavailable."""
95
+ try:
96
+ from datetime import datetime, timezone
97
+ entry = {
98
+ "dt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
99
+ "level": level.upper(),
100
+ "message": message,
101
+ "_fallback": True
102
+ }
103
+ if context:
104
+ entry["context"] = context
105
+ print(json.dumps(entry, separators=(",", ":")), file=sys.stderr)
106
+ except Exception:
107
+ pass
108
+
109
+ async def _log(
110
+ self,
111
+ level: str,
112
+ message: str,
113
+ context: Optional[Dict[str, Any]] = None,
114
+ category: Optional[str] = None,
115
+ exception: Optional[Exception] = None
116
+ ) -> bool:
117
+ """Internal log method with callsite capture.
118
+
119
+ Args:
120
+ level: Log level
121
+ message: Log message
122
+ context: Optional context dict
123
+ category: Optional category (default: "general")
124
+ exception: Optional exception to include
125
+
126
+ Returns:
127
+ True if successfully sent, False otherwise
128
+ """
129
+ file, function = self._capture_callsite(depth=3)
130
+
131
+ body: Dict[str, Any] = {
132
+ "level": level,
133
+ "message": message,
134
+ }
135
+ if context:
136
+ body["context"] = context
137
+ if category:
138
+ body["category"] = category
139
+ if file:
140
+ body["file"] = file
141
+ if function:
142
+ body["function"] = function
143
+ if exception:
144
+ body["context"] = body.get("context", {})
145
+ body["context"]["exception"] = {
146
+ "type": type(exception).__name__,
147
+ "message": str(exception)
148
+ }
149
+
150
+ try:
151
+ result = await self._client._request(
152
+ endpoint="/api/herald/log",
153
+ body=body
154
+ )
155
+ return result.get("stored", False)
156
+ except Exception:
157
+ # Never raise - fallback to local
158
+ self._fallback_log(level, message, context)
159
+ return False
160
+
161
+ async def debug(
162
+ self,
163
+ message: str,
164
+ context: Optional[Dict[str, Any]] = None,
165
+ category: Optional[str] = None
166
+ ) -> bool:
167
+ """Log debug message.
168
+
169
+ Args:
170
+ message: Log message
171
+ context: Optional structured context
172
+ category: Optional category (default: "general")
173
+
174
+ Returns:
175
+ True if successfully sent to Herald
176
+ """
177
+ return await self._log("debug", message, context, category)
178
+
179
+ async def info(
180
+ self,
181
+ message: str,
182
+ context: Optional[Dict[str, Any]] = None,
183
+ category: Optional[str] = None
184
+ ) -> bool:
185
+ """Log info message.
186
+
187
+ Args:
188
+ message: Log message
189
+ context: Optional structured context
190
+ category: Optional category (default: "general")
191
+
192
+ Returns:
193
+ True if successfully sent to Herald
194
+ """
195
+ return await self._log("info", message, context, category)
196
+
197
+ async def notice(
198
+ self,
199
+ message: str,
200
+ context: Optional[Dict[str, Any]] = None,
201
+ category: Optional[str] = None
202
+ ) -> bool:
203
+ """Log notice message.
204
+
205
+ Args:
206
+ message: Log message
207
+ context: Optional structured context
208
+ category: Optional category (default: "general")
209
+
210
+ Returns:
211
+ True if successfully sent to Herald
212
+ """
213
+ return await self._log("notice", message, context, category)
214
+
215
+ async def warn(
216
+ self,
217
+ message: str,
218
+ context: Optional[Dict[str, Any]] = None,
219
+ category: Optional[str] = None
220
+ ) -> bool:
221
+ """Log warning message.
222
+
223
+ Args:
224
+ message: Log message
225
+ context: Optional structured context
226
+ category: Optional category (default: "general")
227
+
228
+ Returns:
229
+ True if successfully sent to Herald
230
+ """
231
+ return await self._log("warn", message, context, category)
232
+
233
+ async def error(
234
+ self,
235
+ message: str,
236
+ context: Optional[Dict[str, Any]] = None,
237
+ category: Optional[str] = None,
238
+ exception: Optional[Exception] = None
239
+ ) -> bool:
240
+ """Log error message.
241
+
242
+ Args:
243
+ message: Log message
244
+ context: Optional structured context
245
+ category: Optional category (default: "general")
246
+ exception: Optional exception to include
247
+
248
+ Returns:
249
+ True if successfully sent to Herald
250
+ """
251
+ return await self._log("error", message, context, category, exception)
252
+
253
+ async def critical(
254
+ self,
255
+ message: str,
256
+ context: Optional[Dict[str, Any]] = None,
257
+ category: Optional[str] = None,
258
+ exception: Optional[Exception] = None
259
+ ) -> bool:
260
+ """Log critical message.
261
+
262
+ Args:
263
+ message: Log message
264
+ context: Optional structured context
265
+ category: Optional category (default: "general")
266
+ exception: Optional exception to include
267
+
268
+ Returns:
269
+ True if successfully sent to Herald
270
+ """
271
+ return await self._log("critical", message, context, category, exception)
272
+
273
+ async def query(
274
+ self,
275
+ level: Optional[str] = None,
276
+ category: Optional[str] = None,
277
+ start_time: Optional[str] = None,
278
+ end_time: Optional[str] = None,
279
+ file: Optional[str] = None,
280
+ function: Optional[str] = None,
281
+ search: Optional[str] = None,
282
+ limit: int = 100,
283
+ # Admin only:
284
+ project: Optional[str] = None,
285
+ environment: Optional[str] = None
286
+ ) -> List[Dict[str, Any]]:
287
+ """Query logs from Herald.
288
+
289
+ Args:
290
+ level: Filter by log level
291
+ category: Filter by category
292
+ start_time: Start of time range (ISO 8601)
293
+ end_time: End of time range (ISO 8601)
294
+ file: Filter by file (partial match)
295
+ function: Filter by function (partial match)
296
+ search: Message text search
297
+ limit: Maximum results (1-1000, default: 100)
298
+ project: Admin only - query specific project
299
+ environment: Admin only - query specific environment
300
+
301
+ Returns:
302
+ List of log entries
303
+ """
304
+ body: Dict[str, Any] = {"limit": limit}
305
+ if level:
306
+ body["level"] = level
307
+ if category:
308
+ body["category"] = category
309
+ if start_time:
310
+ body["start_time"] = start_time
311
+ if end_time:
312
+ body["end_time"] = end_time
313
+ if file:
314
+ body["file"] = file
315
+ if function:
316
+ body["function"] = function
317
+ if search:
318
+ body["search"] = search
319
+ if project:
320
+ body["project"] = project
321
+ if environment:
322
+ body["environment"] = environment
323
+
324
+ try:
325
+ result = await self._client._request(
326
+ endpoint="/api/herald/query",
327
+ body=body
328
+ )
329
+ return result.get("logs", [])
330
+ except Exception:
331
+ return []
332
+
333
+ async def batch(
334
+ self,
335
+ events: List[Dict[str, Any]]
336
+ ) -> Dict[str, Any]:
337
+ """Send multiple log events in one request.
338
+
339
+ Args:
340
+ events: List of log event dicts, each with:
341
+ - level (required): debug, info, notice, warn, error, critical
342
+ - message (required): Log message
343
+ - context (optional): Structured context
344
+ - category (optional): Log category
345
+
346
+ Returns:
347
+ Dict with total, stored, failed counts
348
+ """
349
+ try:
350
+ result = await self._client._request(
351
+ endpoint="/api/herald/batch",
352
+ body={"events": events[:100]}
353
+ )
354
+ return result
355
+ except Exception:
356
+ # Fallback - log each locally
357
+ for event in events:
358
+ self._fallback_log(
359
+ event.get("level", "info"),
360
+ event.get("message", ""),
361
+ event.get("context")
362
+ )
363
+ return {
364
+ "total": len(events),
365
+ "stored": 0,
366
+ "failed": len(events)
367
+ }
@@ -0,0 +1,72 @@
1
+ """
2
+ Open Namespace - Direct database access operations.
3
+
4
+ Provides DSN retrieval and raw SQL execution for advanced use cases.
5
+ """
6
+ from typing import Any, Dict, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..start import Dominus
10
+
11
+
12
+ class OpenNamespace:
13
+ """
14
+ Open database access namespace.
15
+
16
+ Provides direct database connection info and raw SQL execution.
17
+ Use with caution - bypasses most safety checks.
18
+
19
+ Usage:
20
+ # Get connection DSN
21
+ dsn = await dominus.open.dsn()
22
+
23
+ # Execute raw SQL
24
+ result = await dominus.open.execute("SELECT * FROM public.users WHERE id = $1", {"1": user_id})
25
+ """
26
+
27
+ def __init__(self, client: "Dominus"):
28
+ self._client = client
29
+
30
+ async def dsn(self) -> str:
31
+ """
32
+ Get the PostgreSQL connection DSN.
33
+
34
+ Returns the complete PostgreSQL connection URI that can be
35
+ used directly by clients to connect to the database.
36
+
37
+ Returns:
38
+ PostgreSQL connection URI string
39
+ """
40
+ result = await self._client._request(
41
+ endpoint="/api/scribe/open/dsn",
42
+ method="GET"
43
+ )
44
+ if isinstance(result, dict):
45
+ return result.get("dsn", result.get("connection_string", ""))
46
+ return result
47
+
48
+ async def execute(
49
+ self,
50
+ sql: str,
51
+ params: Optional[Dict[str, Any]] = None
52
+ ) -> Dict[str, Any]:
53
+ """
54
+ Execute raw SQL query.
55
+
56
+ Use with caution - this bypasses most safety checks.
57
+
58
+ Args:
59
+ sql: SQL query string
60
+ params: Optional parameter dictionary
61
+
62
+ Returns:
63
+ Query result
64
+ """
65
+ body = {"sql": sql}
66
+ if params:
67
+ body["params"] = params
68
+
69
+ return await self._client._request(
70
+ endpoint="/api/scribe/open/execute",
71
+ body=body
72
+ )