flowwatch-client 2.1.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.
flowwatch/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .client import AsyncFlowwatchClient, FlowwatchClient
2
+
3
+ __all__ = ["FlowwatchClient", "AsyncFlowwatchClient"]
flowwatch/client.py ADDED
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from contextlib import contextmanager
5
+ from typing import Any, Generator
6
+
7
+ import httpx
8
+
9
+
10
+ class FlowwatchClient:
11
+ """Synchronous FlowWatch sidecar client. Use as a context manager for connection cleanup."""
12
+
13
+ def __init__(self, base_url: str = "http://localhost:9400", token: str | None = None) -> None:
14
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
15
+ self._http = httpx.Client(base_url=base_url, headers=headers)
16
+
17
+ def evaluate_flag(self, key: str, context: dict[str, Any] | None = None) -> bool:
18
+ r = self._http.post("/api/flag", json={"key": key, "context": context or {}})
19
+ r.raise_for_status()
20
+ return bool(r.json().get("enabled", False))
21
+
22
+ def trigger_workflow(self, name: str, input: Any = None) -> dict[str, Any]:
23
+ r = self._http.post("/api/trigger", json={"name": name, "input": input})
24
+ r.raise_for_status()
25
+ return r.json()
26
+
27
+ def log_trace_span(
28
+ self,
29
+ name: str,
30
+ type: str,
31
+ duration_ms: float,
32
+ status: str = "ok",
33
+ metadata: dict[str, Any] | None = None,
34
+ ) -> None:
35
+ r = self._http.post(
36
+ "/api/trace-span",
37
+ json={"name": name, "type": type, "durationMs": duration_ms, "status": status, "metadata": metadata},
38
+ )
39
+ r.raise_for_status()
40
+
41
+ def capture_error(
42
+ self,
43
+ message: str,
44
+ name: str = "Error",
45
+ stack: str | None = None,
46
+ source: str | None = None,
47
+ **options: Any,
48
+ ) -> None:
49
+ r = self._http.post(
50
+ "/api/capture-error",
51
+ json={
52
+ "error": {"message": message, "name": name, "stack": stack},
53
+ "options": {"source": source, **options},
54
+ },
55
+ )
56
+ r.raise_for_status()
57
+
58
+ def health(self) -> dict[str, Any]:
59
+ r = self._http.get("/api/health")
60
+ r.raise_for_status()
61
+ return r.json()
62
+
63
+ @contextmanager
64
+ def trace_span(
65
+ self, name: str, type: str, metadata: dict[str, Any] | None = None
66
+ ) -> Generator[None, None, None]:
67
+ """Auto-times the wrapped block and submits a trace span on exit."""
68
+ start = time.monotonic()
69
+ status = "ok"
70
+ try:
71
+ yield
72
+ except Exception:
73
+ status = "error"
74
+ raise
75
+ finally:
76
+ self.log_trace_span(name, type, (time.monotonic() - start) * 1000, status, metadata)
77
+
78
+ def close(self) -> None:
79
+ self._http.close()
80
+
81
+ def __enter__(self) -> FlowwatchClient:
82
+ return self
83
+
84
+ def __exit__(self, *_: Any) -> None:
85
+ self.close()
86
+
87
+
88
+ class AsyncFlowwatchClient:
89
+ """Async FlowWatch sidecar client. Use as an async context manager for connection cleanup."""
90
+
91
+ def __init__(self, base_url: str = "http://localhost:9400", token: str | None = None) -> None:
92
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
93
+ self._http = httpx.AsyncClient(base_url=base_url, headers=headers)
94
+
95
+ async def evaluate_flag(self, key: str, context: dict[str, Any] | None = None) -> bool:
96
+ r = await self._http.post("/api/flag", json={"key": key, "context": context or {}})
97
+ r.raise_for_status()
98
+ return bool(r.json().get("enabled", False))
99
+
100
+ async def trigger_workflow(self, name: str, input: Any = None) -> dict[str, Any]:
101
+ r = await self._http.post("/api/trigger", json={"name": name, "input": input})
102
+ r.raise_for_status()
103
+ return r.json()
104
+
105
+ async def log_trace_span(
106
+ self,
107
+ name: str,
108
+ type: str,
109
+ duration_ms: float,
110
+ status: str = "ok",
111
+ metadata: dict[str, Any] | None = None,
112
+ ) -> None:
113
+ r = await self._http.post(
114
+ "/api/trace-span",
115
+ json={"name": name, "type": type, "durationMs": duration_ms, "status": status, "metadata": metadata},
116
+ )
117
+ r.raise_for_status()
118
+
119
+ async def capture_error(
120
+ self,
121
+ message: str,
122
+ name: str = "Error",
123
+ stack: str | None = None,
124
+ source: str | None = None,
125
+ **options: Any,
126
+ ) -> None:
127
+ r = await self._http.post(
128
+ "/api/capture-error",
129
+ json={
130
+ "error": {"message": message, "name": name, "stack": stack},
131
+ "options": {"source": source, **options},
132
+ },
133
+ )
134
+ r.raise_for_status()
135
+
136
+ async def health(self) -> dict[str, Any]:
137
+ r = await self._http.get("/api/health")
138
+ r.raise_for_status()
139
+ return r.json()
140
+
141
+ async def aclose(self) -> None:
142
+ await self._http.aclose()
143
+
144
+ async def __aenter__(self) -> AsyncFlowwatchClient:
145
+ return self
146
+
147
+ async def __aexit__(self, *_: Any) -> None:
148
+ await self.aclose()
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowwatch-client
3
+ Version: 2.1.0
4
+ Summary: Python client SDK for FlowWatch — feature flags, durable workflows, tracing, and error capture
5
+ Project-URL: Homepage, https://github.com/PranshulSoni/flowwatch
6
+ Project-URL: Issues, https://github.com/PranshulSoni/flowwatch/issues
7
+ License: ISC
8
+ Requires-Python: >=3.8
9
+ Requires-Dist: httpx>=0.27
10
+ Description-Content-Type: text/markdown
11
+
12
+ # flowwatch (Python)
13
+
14
+ Python client for [FlowWatch](https://github.com/PranshulSoni/flowwatch). Wraps the FlowWatch sidecar REST API — requires the [sidecar server](https://github.com/PranshulSoni/flowwatch#multi-language-support-sidecar-server) to be running.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install flowwatch-client
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```python
25
+ from flowwatch import FlowwatchClient
26
+
27
+ client = FlowwatchClient("http://localhost:9400", token="your-sidecar-token")
28
+
29
+ # Feature flag
30
+ if client.evaluate_flag("new-billing-flow", {"userId": "u123", "plan": "pro"}):
31
+ use_new_flow()
32
+
33
+ # Durable workflow
34
+ result = client.trigger_workflow("checkout", {"cartId": "cart_1"})
35
+ print(result["executionId"])
36
+
37
+ # Auto-timed trace span
38
+ with client.trace_span("process-order", "function"):
39
+ process_order()
40
+
41
+ # Error capture
42
+ try:
43
+ risky()
44
+ except Exception as e:
45
+ import traceback
46
+ client.capture_error(str(e), stack=traceback.format_exc(), source="my-service")
47
+
48
+ # Health check
49
+ print(client.health())
50
+
51
+ client.close()
52
+ ```
53
+
54
+ ### Async
55
+
56
+ ```python
57
+ from flowwatch import AsyncFlowwatchClient
58
+
59
+ async with AsyncFlowwatchClient("http://localhost:9400", token="your-token") as client:
60
+ enabled = await client.evaluate_flag("dark-mode", {"userId": "u1"})
61
+ result = await client.trigger_workflow("send-email", {"to": "user@example.com"})
62
+ ```
@@ -0,0 +1,5 @@
1
+ flowwatch/__init__.py,sha256=G0IrIF7Qn5hnx9ZssTLITUMsqoRU1sieEq9Otn4zuHk,113
2
+ flowwatch/client.py,sha256=xPSvXa1pzyPXpANowBHF7ZmEiFCVZuYo55VGL1b7zbw,4811
3
+ flowwatch_client-2.1.0.dist-info/METADATA,sha256=H6IXA_AI-4zonnGANM9Ti1O4eVtYSzlhArxJoM4dhao,1725
4
+ flowwatch_client-2.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ flowwatch_client-2.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any