tollgate-sdk 0.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,115 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ /lib/
14
+ /lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+ env/
28
+
29
+ # Environment variables
30
+ .env
31
+ .env.local
32
+ .env.*.local
33
+
34
+ # IDE
35
+ .idea/
36
+ .vscode/
37
+ *.swp
38
+ *.swo
39
+ *~
40
+ .project
41
+ .pydevproject
42
+ .settings/
43
+
44
+ # Testing
45
+ .pytest_cache/
46
+ .coverage
47
+ htmlcov/
48
+ .tox/
49
+ .nox/
50
+
51
+ # mypy
52
+ .mypy_cache/
53
+ .dmypy.json
54
+ dmypy.json
55
+
56
+ # Alembic
57
+ *.db
58
+
59
+ # OS
60
+ .DS_Store
61
+ Thumbs.db
62
+
63
+ # Logs
64
+ *.log
65
+ logs/
66
+ # Node / TypeScript
67
+ node_modules/
68
+ dist/
69
+ build/
70
+ *.tsbuildinfo
71
+ .turbo/
72
+ .next/
73
+ .vercel/
74
+ out/
75
+
76
+ # Package manager files
77
+ npm-debug.log*
78
+ yarn-debug.log*
79
+ yarn-error.log*
80
+ pnpm-debug.log*
81
+
82
+ # Environment files (already partially covered — extending)
83
+ .env
84
+ .env.local
85
+ .env.*.local
86
+ !.env.example
87
+
88
+ # Editor / IDE
89
+ .vscode/
90
+ .idea/
91
+ *.swp
92
+ *.swo
93
+ .DS_Store
94
+
95
+ # Logs
96
+ *.log
97
+ logs/
98
+
99
+ # Test outputs
100
+ coverage/
101
+ .nyc_output/
102
+
103
+ # Python tooling
104
+ .mypy_cache/
105
+ .pytest_cache/
106
+ .ruff_cache/
107
+ *.egg-info/
108
+
109
+ # Build / distribution artifacts
110
+ *.pyc
111
+ *.pyo
112
+
113
+ # Local scratch files
114
+ /tmp/
115
+ *.local
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: tollgate-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Tollgate agent control plane
5
+ Project-URL: Homepage, https://tollgate.dev
6
+ Project-URL: Repository, https://github.com/tollgate/tollgate
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: pydantic>=2.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # tollgate-sdk
13
+
14
+ Python SDK for [Tollgate](https://tollgate.dev) — the policy and approval layer for AI agents.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install tollgate-sdk
20
+ ```
21
+
22
+ ## 5-minute quickstart
23
+
24
+ ```python
25
+ from tollgate import Tollgate
26
+
27
+ tg = Tollgate(
28
+ api_key="tg_live_...",
29
+ base_url="http://localhost:8000", # your Tollgate API
30
+ )
31
+
32
+ @tg.guard("issue_refund")
33
+ def issue_refund(amount: float, customer_id: str) -> dict:
34
+ # Your actual refund logic here
35
+ return {"refunded": amount, "customer": customer_id}
36
+
37
+ # Safe to call — Tollgate checks policy before executing
38
+ result = issue_refund(amount=50.0, customer_id="c_123")
39
+ ```
40
+
41
+ When the policy says `require_approval`, the decorator blocks until a human approves or rejects in Slack.
42
+
43
+ ## Usage patterns
44
+
45
+ ### Decorator (recommended)
46
+
47
+ ```python
48
+ @tg.guard("issue_refund")
49
+ def issue_refund(amount: float, customer_id: str) -> dict:
50
+ return stripe.refund(amount=amount, customer=customer_id)
51
+ ```
52
+
53
+ ### Context manager
54
+
55
+ ```python
56
+ with tg.check("issue_refund", {"amount": 500, "customer_id": "c_123"}):
57
+ stripe.refund(amount=500, customer="c_123")
58
+ ```
59
+
60
+ ### Explicit
61
+
62
+ ```python
63
+ decision = tg.check_action("issue_refund", {"amount": 500})
64
+ if decision.allowed:
65
+ stripe.refund(...)
66
+ ```
67
+
68
+ ## Error handling
69
+
70
+ | Exception | When | How to handle |
71
+ |-----------|------|---------------|
72
+ | `ActionDenied` | Policy denied or human rejected | Abort the action, inform the user |
73
+ | `ActionPending` | Approval timed out | Retry later or escalate |
74
+ | `TollgateAuthError` | Invalid API key | Check your `api_key` |
75
+ | `TollgateConnectionError` | Can't reach Tollgate API | Check network; consider `fail_open=True` |
76
+
77
+ ```python
78
+ from tollgate import ActionDenied, ActionPending, TollgateConnectionError
79
+
80
+ try:
81
+ result = issue_refund(amount=500, customer_id="c_123")
82
+ except ActionDenied as e:
83
+ print(f"Refund not allowed: {e.reason}")
84
+ except ActionPending as e:
85
+ print(f"Timed out waiting for approval: {e.action_id}")
86
+ except TollgateConnectionError:
87
+ print("Tollgate unreachable")
88
+ ```
89
+
90
+ ## fail_open
91
+
92
+ By default, if Tollgate is unreachable the SDK raises `TollgateConnectionError` (safe — nothing proceeds). Set `fail_open=True` to allow actions through when Tollgate is down:
93
+
94
+ ```python
95
+ tg = Tollgate(api_key="...", fail_open=True)
96
+ ```
97
+
98
+ **Risk:** If Tollgate is down, all actions proceed without policy checks. Only use `fail_open=True` when availability matters more than safety for your use case.
99
+
100
+ ## Async usage
101
+
102
+ ```python
103
+ from tollgate import AsyncTollgate
104
+
105
+ tg = AsyncTollgate(api_key="tg_live_...")
106
+
107
+ @tg.aguard("issue_refund")
108
+ async def issue_refund(amount: float, customer_id: str) -> dict:
109
+ return await stripe_async.refund(amount=amount)
110
+
111
+ # In an async context:
112
+ result = await issue_refund(amount=50.0, customer_id="c_123")
113
+ ```
114
+
115
+ ## Configuration
116
+
117
+ ```python
118
+ tg = Tollgate(
119
+ api_key="tg_live_...",
120
+ base_url="https://api.tollgate.dev", # override for local dev
121
+ poll_interval=2.0, # seconds between approval polls
122
+ max_wait=300.0, # max seconds to wait for approval
123
+ on_pending=lambda action_id: print(f"Waiting for approval: {action_id}"),
124
+ fail_open=False, # raise on network errors (default)
125
+ timeout=10.0, # HTTP request timeout
126
+ )
127
+ ```
128
+
129
+ ## Links
130
+
131
+ - [Dashboard](https://tollgate.dev)
132
+ - [Docs](https://docs.tollgate.dev)
133
+ - [API Reference](https://api.tollgate.dev/v1/docs)
@@ -0,0 +1,122 @@
1
+ # tollgate-sdk
2
+
3
+ Python SDK for [Tollgate](https://tollgate.dev) — the policy and approval layer for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install tollgate-sdk
9
+ ```
10
+
11
+ ## 5-minute quickstart
12
+
13
+ ```python
14
+ from tollgate import Tollgate
15
+
16
+ tg = Tollgate(
17
+ api_key="tg_live_...",
18
+ base_url="http://localhost:8000", # your Tollgate API
19
+ )
20
+
21
+ @tg.guard("issue_refund")
22
+ def issue_refund(amount: float, customer_id: str) -> dict:
23
+ # Your actual refund logic here
24
+ return {"refunded": amount, "customer": customer_id}
25
+
26
+ # Safe to call — Tollgate checks policy before executing
27
+ result = issue_refund(amount=50.0, customer_id="c_123")
28
+ ```
29
+
30
+ When the policy says `require_approval`, the decorator blocks until a human approves or rejects in Slack.
31
+
32
+ ## Usage patterns
33
+
34
+ ### Decorator (recommended)
35
+
36
+ ```python
37
+ @tg.guard("issue_refund")
38
+ def issue_refund(amount: float, customer_id: str) -> dict:
39
+ return stripe.refund(amount=amount, customer=customer_id)
40
+ ```
41
+
42
+ ### Context manager
43
+
44
+ ```python
45
+ with tg.check("issue_refund", {"amount": 500, "customer_id": "c_123"}):
46
+ stripe.refund(amount=500, customer="c_123")
47
+ ```
48
+
49
+ ### Explicit
50
+
51
+ ```python
52
+ decision = tg.check_action("issue_refund", {"amount": 500})
53
+ if decision.allowed:
54
+ stripe.refund(...)
55
+ ```
56
+
57
+ ## Error handling
58
+
59
+ | Exception | When | How to handle |
60
+ |-----------|------|---------------|
61
+ | `ActionDenied` | Policy denied or human rejected | Abort the action, inform the user |
62
+ | `ActionPending` | Approval timed out | Retry later or escalate |
63
+ | `TollgateAuthError` | Invalid API key | Check your `api_key` |
64
+ | `TollgateConnectionError` | Can't reach Tollgate API | Check network; consider `fail_open=True` |
65
+
66
+ ```python
67
+ from tollgate import ActionDenied, ActionPending, TollgateConnectionError
68
+
69
+ try:
70
+ result = issue_refund(amount=500, customer_id="c_123")
71
+ except ActionDenied as e:
72
+ print(f"Refund not allowed: {e.reason}")
73
+ except ActionPending as e:
74
+ print(f"Timed out waiting for approval: {e.action_id}")
75
+ except TollgateConnectionError:
76
+ print("Tollgate unreachable")
77
+ ```
78
+
79
+ ## fail_open
80
+
81
+ By default, if Tollgate is unreachable the SDK raises `TollgateConnectionError` (safe — nothing proceeds). Set `fail_open=True` to allow actions through when Tollgate is down:
82
+
83
+ ```python
84
+ tg = Tollgate(api_key="...", fail_open=True)
85
+ ```
86
+
87
+ **Risk:** If Tollgate is down, all actions proceed without policy checks. Only use `fail_open=True` when availability matters more than safety for your use case.
88
+
89
+ ## Async usage
90
+
91
+ ```python
92
+ from tollgate import AsyncTollgate
93
+
94
+ tg = AsyncTollgate(api_key="tg_live_...")
95
+
96
+ @tg.aguard("issue_refund")
97
+ async def issue_refund(amount: float, customer_id: str) -> dict:
98
+ return await stripe_async.refund(amount=amount)
99
+
100
+ # In an async context:
101
+ result = await issue_refund(amount=50.0, customer_id="c_123")
102
+ ```
103
+
104
+ ## Configuration
105
+
106
+ ```python
107
+ tg = Tollgate(
108
+ api_key="tg_live_...",
109
+ base_url="https://api.tollgate.dev", # override for local dev
110
+ poll_interval=2.0, # seconds between approval polls
111
+ max_wait=300.0, # max seconds to wait for approval
112
+ on_pending=lambda action_id: print(f"Waiting for approval: {action_id}"),
113
+ fail_open=False, # raise on network errors (default)
114
+ timeout=10.0, # HTTP request timeout
115
+ )
116
+ ```
117
+
118
+ ## Links
119
+
120
+ - [Dashboard](https://tollgate.dev)
121
+ - [Docs](https://docs.tollgate.dev)
122
+ - [API Reference](https://api.tollgate.dev/v1/docs)
@@ -0,0 +1,75 @@
1
+ # Support Agent Example
2
+
3
+ A customer support agent powered by Claude that uses Tollgate to gate risky actions.
4
+
5
+ ## Prerequisites
6
+
7
+ - Tollgate API running locally (`uvicorn tollgate.main:app --reload --port 8000` from `apps/api/`)
8
+ - Docker running (Postgres + Redis via `docker-compose up -d` from repo root)
9
+ - Slack workspace connected (see `apps/api/README.md` → Slack setup)
10
+ - ngrok tunnel active (`ngrok http 8000`)
11
+
12
+ ## Step 1: Create an agent
13
+
14
+ ```bash
15
+ # Sign up and get a JWT
16
+ JWT=$(curl -s -X POST http://localhost:8000/auth/signup \
17
+ -H "Content-Type: application/json" \
18
+ -d '{"email":"demo@example.com","password":"demo-pw-123","org_name":"Demo Org"}' \
19
+ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
20
+
21
+ # Create the agent
22
+ curl -s -X POST http://localhost:8000/agents \
23
+ -H "Authorization: Bearer $JWT" \
24
+ -H "Content-Type: application/json" \
25
+ -d '{"name":"support-agent-demo"}'
26
+ # Save the api_key from the response — you won't see it again
27
+ ```
28
+
29
+ ## Step 2: Upload the sample policy
30
+
31
+ ```bash
32
+ AGENT_ID="<your-agent-id>"
33
+ POLICY=$(cat sample_policy.yaml)
34
+
35
+ curl -s -X POST "http://localhost:8000/agents/$AGENT_ID/policies" \
36
+ -H "Authorization: Bearer $JWT" \
37
+ -H "Content-Type: application/json" \
38
+ -d "{\"source_yaml\": $(python3 -c "import sys,json; print(json.dumps(open('sample_policy.yaml').read()))")}"
39
+ ```
40
+
41
+ The policy allows refunds ≤ $100 automatically, requires Slack approval for refunds > $100, denies `delete_account`, and allows everything else.
42
+
43
+ ## Step 3: Set environment variables
44
+
45
+ ```bash
46
+ export TOLLGATE_API_KEY="tg_live_..." # from Step 1
47
+ export TOLLGATE_BASE_URL="http://localhost:8000"
48
+ export ANTHROPIC_API_KEY="sk-ant-..." # your Anthropic API key
49
+ ```
50
+
51
+ ## Step 4: Run the agent
52
+
53
+ ```bash
54
+ pip install -r requirements.txt
55
+ python support_agent.py
56
+ ```
57
+
58
+ ## Step 5: Try it
59
+
60
+ Ask the agent to perform actions:
61
+
62
+ ```
63
+ You: Issue a $50 refund for customer c_001
64
+ ```
65
+ → Allowed immediately (under $100 threshold)
66
+
67
+ ```
68
+ You: Issue a $500 refund for customer c_002
69
+ ```
70
+ → Pauses and posts to your Slack `#approvals` channel. Check Slack, click Approve or Reject.
71
+
72
+ ```
73
+ You: Delete account for customer c_003
74
+ ```
75
+ → Denied immediately by policy.
@@ -0,0 +1,2 @@
1
+ tollgate-sdk
2
+ anthropic
@@ -0,0 +1,15 @@
1
+ version: 1
2
+ rules:
3
+ - action: issue_refund
4
+ when:
5
+ amount: { lte: 100 }
6
+ decide: allow
7
+ - action: issue_refund
8
+ when:
9
+ amount: { gt: 100 }
10
+ decide: require_approval
11
+ approvers: ["#approvals"]
12
+ - action: delete_account
13
+ decide: deny
14
+ reason: "Account deletion is not permitted via agent"
15
+ default: allow
@@ -0,0 +1,193 @@
1
+ """
2
+ Tollgate Example: AI Support Agent
3
+
4
+ A support agent backed by Claude that uses Tollgate to gate risky actions.
5
+ Three tools are protected: issue_refund, update_account, escalate_to_human.
6
+
7
+ Run: python support_agent.py
8
+ """
9
+
10
+ import os
11
+ import sys
12
+
13
+ import anthropic
14
+
15
+ from tollgate import ActionDenied, ActionPending, Tollgate
16
+
17
+ TOLLGATE_API_KEY = os.environ.get("TOLLGATE_API_KEY", "")
18
+ TOLLGATE_BASE_URL = os.environ.get("TOLLGATE_BASE_URL", "http://localhost:8000")
19
+ ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
20
+
21
+ if not TOLLGATE_API_KEY:
22
+ print("Error: TOLLGATE_API_KEY is not set.")
23
+ sys.exit(1)
24
+ if not ANTHROPIC_API_KEY:
25
+ print("Error: ANTHROPIC_API_KEY is not set.")
26
+ sys.exit(1)
27
+
28
+ tg = Tollgate(
29
+ api_key=TOLLGATE_API_KEY,
30
+ base_url=TOLLGATE_BASE_URL,
31
+ on_pending=lambda action_id: print(f"\n[Tollgate] Waiting for approval in Slack... (action_id={action_id})"),
32
+ poll_interval=3.0,
33
+ max_wait=300.0,
34
+ )
35
+
36
+ client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
37
+
38
+ TOOLS = [
39
+ {
40
+ "name": "issue_refund",
41
+ "description": "Issue a refund to a customer for a specified amount.",
42
+ "input_schema": {
43
+ "type": "object",
44
+ "properties": {
45
+ "amount": {"type": "number", "description": "Refund amount in USD"},
46
+ "customer_id": {"type": "string", "description": "Customer identifier"},
47
+ "reason": {"type": "string", "description": "Reason for the refund"},
48
+ },
49
+ "required": ["amount", "customer_id"],
50
+ },
51
+ },
52
+ {
53
+ "name": "update_account",
54
+ "description": "Update a customer's account information.",
55
+ "input_schema": {
56
+ "type": "object",
57
+ "properties": {
58
+ "customer_id": {"type": "string", "description": "Customer identifier"},
59
+ "field": {"type": "string", "description": "Field to update (email, plan, etc.)"},
60
+ "value": {"type": "string", "description": "New value for the field"},
61
+ },
62
+ "required": ["customer_id", "field", "value"],
63
+ },
64
+ },
65
+ {
66
+ "name": "escalate_to_human",
67
+ "description": "Escalate this support case to a human agent.",
68
+ "input_schema": {
69
+ "type": "object",
70
+ "properties": {
71
+ "customer_id": {"type": "string", "description": "Customer identifier"},
72
+ "reason": {"type": "string", "description": "Reason for escalation"},
73
+ "priority": {"type": "string", "enum": ["low", "medium", "high"], "description": "Priority level"},
74
+ },
75
+ "required": ["customer_id", "reason"],
76
+ },
77
+ },
78
+ ]
79
+
80
+
81
+ @tg.guard("issue_refund")
82
+ def issue_refund(amount: float, customer_id: str, reason: str = "") -> dict: # type: ignore[return]
83
+ """Execute the refund (would call Stripe in production)."""
84
+ print(f"[Tool] Issuing refund: ${amount} to {customer_id}")
85
+ return {"status": "refunded", "amount": amount, "customer_id": customer_id}
86
+
87
+
88
+ @tg.guard("update_account")
89
+ def update_account(customer_id: str, field: str, value: str) -> dict: # type: ignore[return]
90
+ """Execute the account update (would call your DB in production)."""
91
+ print(f"[Tool] Updating {field}={value} for {customer_id}")
92
+ return {"status": "updated", "customer_id": customer_id, "field": field, "value": value}
93
+
94
+
95
+ @tg.guard("escalate_to_human")
96
+ def escalate_to_human(customer_id: str, reason: str, priority: str = "medium") -> dict: # type: ignore[return]
97
+ """Escalate to a human agent."""
98
+ print(f"[Tool] Escalating {customer_id} — {reason} (priority={priority})")
99
+ return {"status": "escalated", "customer_id": customer_id, "ticket_id": "TKT-001"}
100
+
101
+
102
+ def run_tool(name: str, inputs: dict) -> str: # type: ignore[type-arg]
103
+ """Dispatch a tool call, catching Tollgate exceptions."""
104
+ try:
105
+ if name == "issue_refund":
106
+ result = issue_refund(**inputs)
107
+ elif name == "update_account":
108
+ result = update_account(**inputs)
109
+ elif name == "escalate_to_human":
110
+ result = escalate_to_human(**inputs)
111
+ else:
112
+ return f"Unknown tool: {name}"
113
+ return str(result)
114
+ except ActionDenied as e:
115
+ return f"[Tollgate] Action denied: {e.reason}"
116
+ except ActionPending as e:
117
+ return f"[Tollgate] Approval timed out after {e.timeout_seconds}s for action {e.action_id}"
118
+
119
+
120
+ def chat_loop() -> None:
121
+ """Run the support agent loop."""
122
+ print("Tollgate Support Agent — type 'quit' to exit")
123
+ print(f"Connected to Tollgate at {TOLLGATE_BASE_URL}\n")
124
+ messages: list[dict] = [] # type: ignore[type-arg]
125
+
126
+ while True:
127
+ try:
128
+ user_input = input("You: ").strip()
129
+ except (EOFError, KeyboardInterrupt):
130
+ print("\nGoodbye.")
131
+ break
132
+
133
+ if user_input.lower() in ("quit", "exit", "q"):
134
+ print("Goodbye.")
135
+ break
136
+ if not user_input:
137
+ continue
138
+
139
+ messages.append({"role": "user", "content": user_input})
140
+
141
+ response = client.messages.create(
142
+ model="claude-haiku-4-5-20251001",
143
+ max_tokens=1024,
144
+ system=(
145
+ "You are a helpful customer support agent. "
146
+ "Use the available tools to help customers with refunds, account updates, and escalations. "
147
+ "Always confirm the customer_id before taking actions."
148
+ ),
149
+ tools=TOOLS, # type: ignore[arg-type]
150
+ messages=messages, # type: ignore[arg-type]
151
+ )
152
+
153
+ while response.stop_reason == "tool_use":
154
+ tool_results = []
155
+ assistant_content = []
156
+
157
+ for block in response.content:
158
+ assistant_content.append(block)
159
+ if block.type == "tool_use":
160
+ print(f"\n[Agent] Calling {block.name}({block.input})")
161
+ result = run_tool(block.name, block.input) # type: ignore[arg-type]
162
+ tool_results.append({
163
+ "type": "tool_result",
164
+ "tool_use_id": block.id,
165
+ "content": result,
166
+ })
167
+
168
+ messages.append({"role": "assistant", "content": assistant_content})
169
+ messages.append({"role": "user", "content": tool_results})
170
+
171
+ response = client.messages.create(
172
+ model="claude-haiku-4-5-20251001",
173
+ max_tokens=1024,
174
+ system=(
175
+ "You are a helpful customer support agent. "
176
+ "Use the available tools to help customers with refunds, account updates, and escalations."
177
+ ),
178
+ tools=TOOLS, # type: ignore[arg-type]
179
+ messages=messages, # type: ignore[arg-type]
180
+ )
181
+
182
+ # Extract final text response
183
+ reply = ""
184
+ for block in response.content:
185
+ if hasattr(block, "text"):
186
+ reply += block.text
187
+
188
+ messages.append({"role": "assistant", "content": response.content})
189
+ print(f"\nAgent: {reply}\n")
190
+
191
+
192
+ if __name__ == "__main__":
193
+ chat_loop()
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tollgate-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the Tollgate agent control plane"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "httpx>=0.27",
13
+ "pydantic>=2.0",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://tollgate.dev"
18
+ Repository = "https://github.com/tollgate/tollgate"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/tollgate"]
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "mypy>=1.8",
26
+ "pytest>=8.0",
27
+ "pytest-asyncio>=0.23",
28
+ "respx>=0.21",
29
+ "anyio>=4.0",
30
+ ]
31
+
32
+ [tool.pytest.ini_options]
33
+ asyncio_mode = "auto"
34
+ testpaths = ["tests"]
35
+
36
+ [tool.mypy]
37
+ strict = true
38
+ python_version = "3.10"
39
+ mypy_path = "src"
40
+
41
+ [tool.ruff.lint]
42
+ select = ["E", "F", "I"]