flowwatch-client 2.1.0__tar.gz
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,76 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
**/node_modules/
|
|
4
|
+
|
|
5
|
+
# Build output
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
out/
|
|
9
|
+
coverage/
|
|
10
|
+
*.tsbuildinfo
|
|
11
|
+
|
|
12
|
+
# Package manager caches
|
|
13
|
+
.npm/
|
|
14
|
+
.pnpm-store/
|
|
15
|
+
.yarn/
|
|
16
|
+
!.yarn/releases/
|
|
17
|
+
!.yarn/plugins/
|
|
18
|
+
!.yarn/sdks/
|
|
19
|
+
!.yarn/versions/
|
|
20
|
+
|
|
21
|
+
# Logs
|
|
22
|
+
logs/
|
|
23
|
+
*.log
|
|
24
|
+
npm-debug.log*
|
|
25
|
+
yarn-debug.log*
|
|
26
|
+
yarn-error.log*
|
|
27
|
+
pnpm-debug.log*
|
|
28
|
+
|
|
29
|
+
# Environment and local secrets
|
|
30
|
+
.env
|
|
31
|
+
.env.*
|
|
32
|
+
!.env.example
|
|
33
|
+
!.env*.example
|
|
34
|
+
.fw.env
|
|
35
|
+
|
|
36
|
+
# Test app (internal development only)
|
|
37
|
+
apps/test-app/
|
|
38
|
+
apps/
|
|
39
|
+
|
|
40
|
+
# Local infra data
|
|
41
|
+
infra/data/
|
|
42
|
+
infra/.data/
|
|
43
|
+
docker-data/
|
|
44
|
+
postgres-data/
|
|
45
|
+
redis-data/
|
|
46
|
+
elasticsearch-data/
|
|
47
|
+
flowwatch_npm_project_details.md
|
|
48
|
+
PROJECT_HANDOFF.md
|
|
49
|
+
docs/
|
|
50
|
+
|
|
51
|
+
# Runtime temp files
|
|
52
|
+
tmp/
|
|
53
|
+
temp/
|
|
54
|
+
.cache/
|
|
55
|
+
|
|
56
|
+
# Test and tooling caches
|
|
57
|
+
.nyc_output/
|
|
58
|
+
.vitest/
|
|
59
|
+
.jest/
|
|
60
|
+
.eslintcache
|
|
61
|
+
|
|
62
|
+
# Framework/tool output
|
|
63
|
+
.vite/
|
|
64
|
+
.turbo/
|
|
65
|
+
.next/
|
|
66
|
+
|
|
67
|
+
# IDE/editor
|
|
68
|
+
.vscode/
|
|
69
|
+
.idea/
|
|
70
|
+
*.swp
|
|
71
|
+
*.swo
|
|
72
|
+
|
|
73
|
+
# OS files
|
|
74
|
+
.DS_Store
|
|
75
|
+
Thumbs.db
|
|
76
|
+
desktop.ini
|
|
@@ -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,51 @@
|
|
|
1
|
+
# flowwatch (Python)
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install flowwatch-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from flowwatch import FlowwatchClient
|
|
15
|
+
|
|
16
|
+
client = FlowwatchClient("http://localhost:9400", token="your-sidecar-token")
|
|
17
|
+
|
|
18
|
+
# Feature flag
|
|
19
|
+
if client.evaluate_flag("new-billing-flow", {"userId": "u123", "plan": "pro"}):
|
|
20
|
+
use_new_flow()
|
|
21
|
+
|
|
22
|
+
# Durable workflow
|
|
23
|
+
result = client.trigger_workflow("checkout", {"cartId": "cart_1"})
|
|
24
|
+
print(result["executionId"])
|
|
25
|
+
|
|
26
|
+
# Auto-timed trace span
|
|
27
|
+
with client.trace_span("process-order", "function"):
|
|
28
|
+
process_order()
|
|
29
|
+
|
|
30
|
+
# Error capture
|
|
31
|
+
try:
|
|
32
|
+
risky()
|
|
33
|
+
except Exception as e:
|
|
34
|
+
import traceback
|
|
35
|
+
client.capture_error(str(e), stack=traceback.format_exc(), source="my-service")
|
|
36
|
+
|
|
37
|
+
# Health check
|
|
38
|
+
print(client.health())
|
|
39
|
+
|
|
40
|
+
client.close()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Async
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from flowwatch import AsyncFlowwatchClient
|
|
47
|
+
|
|
48
|
+
async with AsyncFlowwatchClient("http://localhost:9400", token="your-token") as client:
|
|
49
|
+
enabled = await client.evaluate_flag("dark-mode", {"userId": "u1"})
|
|
50
|
+
result = await client.trigger_workflow("send-email", {"to": "user@example.com"})
|
|
51
|
+
```
|
|
@@ -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,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flowwatch-client"
|
|
3
|
+
version = "2.1.0"
|
|
4
|
+
description = "Python client SDK for FlowWatch — feature flags, durable workflows, tracing, and error capture"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
license = { text = "ISC" }
|
|
8
|
+
dependencies = ["httpx>=0.27"]
|
|
9
|
+
|
|
10
|
+
[project.urls]
|
|
11
|
+
Homepage = "https://github.com/PranshulSoni/flowwatch"
|
|
12
|
+
Issues = "https://github.com/PranshulSoni/flowwatch/issues"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["flowwatch"]
|