provision-stack 0.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.
@@ -0,0 +1,38 @@
1
+ """
2
+ Provision Stack — Python client and tool wrappers.
3
+
4
+ This package provides a thin Python client for the Provision Stack
5
+ Execution API, plus framework-specific integrations (LangChain,
6
+ CrewAI, Composio).
7
+
8
+ Branding
9
+ --------
10
+ All user-visible strings (product name, package name, URLs) are
11
+ defined in ``config.py``. To rebrand:
12
+
13
+ 1. Edit ``config.py`` — change ``PRODUCT_NAME``, ``PYPI_PACKAGE``,
14
+ ``API_BASE_URL_DEFAULT``, and the description strings.
15
+ 2. Rename this directory and update ``pyproject.toml`` name/description.
16
+ 3. Search-replace the old name in README files.
17
+
18
+ That's it. All runtime strings flow from ``config.py``.
19
+ """
20
+
21
+ from provision_stack.config import (
22
+ PRODUCT_NAME,
23
+ API_BASE_URL_DEFAULT,
24
+ SANDBOX_URL,
25
+ )
26
+ from provision_stack.client import ProvisionStackClient
27
+ from provision_stack.tools import BaseTool, ToolResult
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ __all__ = [
32
+ "PRODUCT_NAME",
33
+ "API_BASE_URL_DEFAULT",
34
+ "SANDBOX_URL",
35
+ "ProvisionStackClient",
36
+ "BaseTool",
37
+ "ToolResult",
38
+ ]
@@ -0,0 +1,435 @@
1
+ """
2
+ Thin Python client for the Provision Stack Execution API.
3
+
4
+ All API methods return parsed dicts. No third-party dependencies
5
+ beyond ``requests`` (or ``httpx`` if available).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from typing import Any, Optional
12
+
13
+ from provision_stack.config import (
14
+ API_BASE_URL_DEFAULT,
15
+ SANDBOX_URL,
16
+ DEFAULT_REGION,
17
+ DEFAULT_TIER,
18
+ )
19
+
20
+
21
+ def _http_post(
22
+ url: str,
23
+ json: dict[str, Any] | None = None,
24
+ headers: dict[str, str] | None = None,
25
+ ) -> dict[str, Any]:
26
+ """POST with requests, falling back to httpx, then urllib."""
27
+ try:
28
+ import requests
29
+
30
+ resp = requests.post(url, json=json, headers=headers or {}, timeout=30)
31
+ resp.raise_for_status()
32
+ return resp.json()
33
+ except ImportError:
34
+ pass
35
+
36
+ try:
37
+ import httpx
38
+
39
+ with httpx.Client(timeout=30) as client:
40
+ resp = client.post(url, json=json, headers=headers or {})
41
+ resp.raise_for_status()
42
+ return resp.json()
43
+ except ImportError:
44
+ pass
45
+
46
+ import urllib.request
47
+ import json as _json
48
+
49
+ req = urllib.request.Request(
50
+ url,
51
+ data=_json.dumps(json or {}).encode(),
52
+ headers={"Content-Type": "application/json", **(headers or {})},
53
+ method="POST",
54
+ )
55
+ with urllib.request.urlopen(req, timeout=30) as resp:
56
+ return _json.loads(resp.read())
57
+
58
+
59
+ def _http_get(
60
+ url: str,
61
+ headers: dict[str, str] | None = None,
62
+ ) -> dict[str, Any]:
63
+ """GET with requests, falling back to httpx, then urllib."""
64
+ try:
65
+ import requests
66
+
67
+ resp = requests.get(url, headers=headers or {}, timeout=30)
68
+ resp.raise_for_status()
69
+ return resp.json()
70
+ except ImportError:
71
+ pass
72
+
73
+ try:
74
+ import httpx
75
+
76
+ with httpx.Client(timeout=30) as client:
77
+ resp = client.get(url, headers=headers or {})
78
+ resp.raise_for_status()
79
+ return resp.json()
80
+ except ImportError:
81
+ pass
82
+
83
+ import urllib.request
84
+ import json as _json
85
+
86
+ req = urllib.request.Request(
87
+ url,
88
+ headers=headers or {},
89
+ method="GET",
90
+ )
91
+ with urllib.request.urlopen(req, timeout=30) as resp:
92
+ return _json.loads(resp.read())
93
+
94
+
95
+ def _http_delete(
96
+ url: str,
97
+ headers: dict[str, str] | None = None,
98
+ ) -> dict[str, Any]:
99
+ """DELETE with requests, falling back to httpx, then urllib."""
100
+ try:
101
+ import requests
102
+
103
+ resp = requests.delete(url, headers=headers or {}, timeout=30)
104
+ resp.raise_for_status()
105
+ return resp.json()
106
+ except ImportError:
107
+ pass
108
+
109
+ try:
110
+ import httpx
111
+
112
+ with httpx.Client(timeout=30) as client:
113
+ resp = client.delete(url, headers=headers or {})
114
+ resp.raise_for_status()
115
+ return resp.json()
116
+ except ImportError:
117
+ pass
118
+
119
+ import urllib.request
120
+ import json as _json
121
+
122
+ req = urllib.request.Request(
123
+ url,
124
+ headers=headers or {},
125
+ method="DELETE",
126
+ )
127
+ with urllib.request.urlopen(req, timeout=30) as resp:
128
+ return _json.loads(resp.read())
129
+
130
+
131
+ class ProvisionStackClient:
132
+ """
133
+ Python client for the Provision Stack Execution API.
134
+
135
+ Usage::
136
+
137
+ from provision_stack import ProvisionStackClient
138
+
139
+ client = ProvisionStackClient(api_key="your-token")
140
+
141
+ # Get suggestions
142
+ suggestions = client.suggest("deploy a production web app")
143
+
144
+ # Deploy
145
+ deploy = client.deploy(
146
+ outcome="deploy a production web app",
147
+ resources=suggestions["options"][0]["resources"],
148
+ tier="MVP",
149
+ )
150
+
151
+ # Check status
152
+ status = client.status(deploy["deploymentId"])
153
+
154
+ # Check balance
155
+ balance = client.balance()
156
+
157
+ Parameters
158
+ ----------
159
+ api_key : str, optional
160
+ Bearer token for authentication. Falls back to
161
+ ``PROVISION_STACK_API_KEY`` env var, then ``PS_API_KEY``.
162
+ base_url : str, optional
163
+ API base URL. Falls back to ``PROVISION_STACK_API_URL`` env var,
164
+ then the default production URL.
165
+ sandbox : bool, optional
166
+ If True, use the sandbox URL (mock Terraform, no real deploys).
167
+ Defaults to False.
168
+ """
169
+
170
+ def __init__(
171
+ self,
172
+ api_key: str | None = None,
173
+ base_url: str | None = None,
174
+ sandbox: bool = False,
175
+ ) -> None:
176
+ self.api_key = (
177
+ api_key
178
+ or os.environ.get("PROVISION_STACK_API_KEY")
179
+ or os.environ.get("PS_API_KEY")
180
+ or ""
181
+ )
182
+ if sandbox:
183
+ self.base_url = SANDBOX_URL
184
+ else:
185
+ self.base_url = (
186
+ base_url
187
+ or os.environ.get("PROVISION_STACK_API_URL")
188
+ or API_BASE_URL_DEFAULT
189
+ )
190
+
191
+ def _headers(self) -> dict[str, str]:
192
+ headers: dict[str, str] = {}
193
+ if self.api_key:
194
+ headers["Authorization"] = f"Bearer {self.api_key}"
195
+ return headers
196
+
197
+ def _url(self, path: str) -> str:
198
+ return f"{self.base_url.rstrip('/')}{path}"
199
+
200
+ # ── Auth ──────────────────────────────────────────────────────
201
+
202
+ def signup(self, agent_id: str, scopes: list[str] | None = None) -> dict:
203
+ """
204
+ Create a new API token (self-serve signup).
205
+
206
+ Returns the token and next-step guidance.
207
+ """
208
+ return _http_post(
209
+ self._url("/auth/tokens"),
210
+ json={"agentId": agent_id, "scopes": scopes or []},
211
+ )
212
+
213
+ # ── Suggestions ───────────────────────────────────────────────
214
+
215
+ def suggest(
216
+ self,
217
+ outcome: str,
218
+ region: str | None = None,
219
+ provider: str | None = None,
220
+ cross_provider: bool = False,
221
+ max_monthly_cost_usd: float | None = None,
222
+ requirements: dict[str, Any] | None = None,
223
+ ) -> dict:
224
+ """
225
+ Get deployment suggestions for a desired outcome.
226
+
227
+ Parameters
228
+ ----------
229
+ outcome : str
230
+ Natural language description (e.g. "deploy a production web app").
231
+ region : str, optional
232
+ AWS/GCP/Azure region (e.g. "us-east-1").
233
+ provider : str, optional
234
+ Restrict to one provider ("aws", "gcp", "azure", "oracle").
235
+ cross_provider : bool
236
+ If True, compare across all providers.
237
+ max_monthly_cost_usd : float, optional
238
+ Filter out options above this monthly cost.
239
+ requirements : dict, optional
240
+ Business requirements: budget, availability, compliance,
241
+ trafficPattern, computePreference.
242
+
243
+ Returns
244
+ -------
245
+ dict
246
+ Suggestions with options, cost estimates, and optional
247
+ clarificationRequest.
248
+ """
249
+ body: dict[str, Any] = {"outcome": outcome}
250
+ if region:
251
+ body["region"] = region
252
+ if provider:
253
+ body["provider"] = provider
254
+ if cross_provider:
255
+ body["crossProvider"] = True
256
+ if max_monthly_cost_usd is not None:
257
+ body["maxMonthlyCostUsd"] = max_monthly_cost_usd
258
+ if requirements:
259
+ body["requirements"] = requirements
260
+
261
+ return _http_post(
262
+ self._url("/api/v1/suggestions"),
263
+ json=body,
264
+ headers=self._headers(),
265
+ )
266
+
267
+ # ── Quote ─────────────────────────────────────────────────────
268
+
269
+ def quote(
270
+ self,
271
+ outcome: str,
272
+ region: str | None = None,
273
+ tier: str | None = None,
274
+ max_monthly_cost_usd: float | None = None,
275
+ ) -> dict:
276
+ """
277
+ Get a pricing quote for a deployment.
278
+
279
+ Returns a quote ID and estimated monthly cost.
280
+ """
281
+ body: dict[str, Any] = {"outcome": outcome}
282
+ if region:
283
+ body["region"] = region
284
+ if tier:
285
+ body["tier"] = tier
286
+ if max_monthly_cost_usd is not None:
287
+ body["maxMonthlyCostUsd"] = max_monthly_cost_usd
288
+
289
+ return _http_post(
290
+ self._url("/api/v1/quote"),
291
+ json=body,
292
+ headers=self._headers(),
293
+ )
294
+
295
+ # ── Deploy ────────────────────────────────────────────────────
296
+
297
+ def deploy(
298
+ self,
299
+ outcome: str,
300
+ resources: list[dict[str, Any]] | None = None,
301
+ tier: str | None = None,
302
+ region: str | None = None,
303
+ max_monthly_cost_usd: float | None = None,
304
+ requirements: dict[str, Any] | None = None,
305
+ ) -> dict:
306
+ """
307
+ Deploy infrastructure for a desired outcome.
308
+
309
+ The deploy fee is held from your balance and captured only
310
+ after verification passes (failed deployments are free).
311
+
312
+ Parameters
313
+ ----------
314
+ outcome : str
315
+ Natural language description.
316
+ resources : list[dict], optional
317
+ Resource configs from a suggestion option.
318
+ tier : str, optional
319
+ Deployment tier (Starter/MVP/Startup/Enterprise).
320
+ region : str, optional
321
+ Target region.
322
+ max_monthly_cost_usd : float, optional
323
+ Monthly cost cap.
324
+ requirements : dict, optional
325
+ Business requirements for pattern selection.
326
+
327
+ Returns
328
+ -------
329
+ dict
330
+ Deployment ID, status, and provider info.
331
+ """
332
+ body: dict[str, Any] = {
333
+ "outcome": outcome,
334
+ }
335
+ if resources:
336
+ body["resources"] = resources
337
+ if tier:
338
+ body["tier"] = tier
339
+ if region:
340
+ body["region"] = region
341
+ if max_monthly_cost_usd is not None:
342
+ body["maxMonthlyCostUsd"] = max_monthly_cost_usd
343
+ if requirements:
344
+ body["requirements"] = requirements
345
+
346
+ return _http_post(
347
+ self._url("/api/v1/deployments"),
348
+ json=body,
349
+ headers=self._headers(),
350
+ )
351
+
352
+ # ── Status ────────────────────────────────────────────────────
353
+
354
+ def status(self, deployment_id: str) -> dict:
355
+ """
356
+ Check deployment status and verification results.
357
+
358
+ Returns status (pending/provisioning/active/failed),
359
+ verification details, and terraform files.
360
+ """
361
+ return _http_get(
362
+ self._url(f"/api/v1/deployments/{deployment_id}"),
363
+ headers=self._headers(),
364
+ )
365
+
366
+ # ── Destroy ───────────────────────────────────────────────────
367
+
368
+ def destroy(self, deployment_id: str) -> dict:
369
+ """
370
+ Destroy all resources owned by a deployment.
371
+ """
372
+ return _http_delete(
373
+ self._url(f"/api/v1/deployments/{deployment_id}"),
374
+ headers=self._headers(),
375
+ )
376
+
377
+ # ── List deployments ──────────────────────────────────────────
378
+
379
+ def list_deployments(self) -> dict:
380
+ """
381
+ List all tracked deployments.
382
+ """
383
+ return _http_get(
384
+ self._url("/api/v1/deployments"),
385
+ headers=self._headers(),
386
+ )
387
+
388
+ # ── Billing ───────────────────────────────────────────────────
389
+
390
+ def balance(self) -> dict:
391
+ """
392
+ Check account credit balance.
393
+
394
+ Returns total, held, and available USD.
395
+ """
396
+ return _http_get(
397
+ self._url("/api/v1/billing/balance"),
398
+ headers=self._headers(),
399
+ )
400
+
401
+ def topup_stripe(self, amount_usd: float) -> dict:
402
+ """
403
+ Create a Stripe Checkout session for adding credits.
404
+
405
+ Returns a checkout URL for the user to complete payment.
406
+ """
407
+ return _http_post(
408
+ self._url("/api/v1/billing/topup/stripe"),
409
+ json={"amountUsd": amount_usd},
410
+ headers=self._headers(),
411
+ )
412
+
413
+ def topup_xrp(self, amount_usd: float) -> dict:
414
+ """
415
+ Get XRP deposit instructions for adding credits.
416
+
417
+ Returns deposit address, destination tag, and exact amount.
418
+ """
419
+ return _http_post(
420
+ self._url("/api/v1/billing/topup/xrp"),
421
+ json={"amountUsd": amount_usd},
422
+ headers=self._headers(),
423
+ )
424
+
425
+ def topup_sandbox(self, amount_usd: float) -> dict:
426
+ """
427
+ Add free sandbox credits (sandbox mode only).
428
+
429
+ Returns the credited amount.
430
+ """
431
+ return _http_post(
432
+ self._url("/api/v1/billing/topup/sandbox"),
433
+ json={"amountUsd": amount_usd},
434
+ headers=self._headers(),
435
+ )
@@ -0,0 +1,101 @@
1
+ """
2
+ Branding and configuration — single source of truth.
3
+
4
+ This is the ONLY file you need to edit when rebranding.
5
+ All user-visible strings (product name, package name, URLs,
6
+ descriptions) are defined here.
7
+
8
+ Current brand: **Provision Stack** (finalized 2026-07-20).
9
+
10
+ If the brand ever changes again:
11
+
12
+ 1. Update PRODUCT_NAME below
13
+ 2. Update PYPI_PACKAGE to match the new PyPI package name
14
+ 3. Update API_BASE_URL_DEFAULT and SANDBOX_URL to the new domain
15
+ 4. Update the description strings (short_description, long_description)
16
+ 5. Rename the plugin directories:
17
+ plugins/python/provision-stack/ → plugins/python/<new-name>-sdk/
18
+ plugins/python/provision-stack-langchain/ → plugins/python/<new-name>-langchain/
19
+ plugins/python/provision-stack-crewai/ → plugins/python/<new-name>-crewai/
20
+ plugins/python/provision-stack-composio/ → plugins/python/<new-name>-composio/
21
+ 6. Rename Python package directories:
22
+ provision_stack/ → <new_name>/
23
+ provision_stack_langchain/ → <new_name>_langchain/
24
+ provision_stack_crewai/ → <new_name>_crewai/
25
+ provision_stack_composio/ → <new_name>_composio/
26
+ 7. Update pyproject.toml name/description in each package
27
+ 8. Update all import statements across the plugin packages
28
+
29
+ This is the ONLY file that needs to change for steps 1-4.
30
+ Steps 5-8 are mechanical find-replace.
31
+ """
32
+
33
+ # ── Product identity ──────────────────────────────────────────────
34
+ # Change these when rebranding. Everything else derives from them.
35
+
36
+ PRODUCT_NAME = "Provision Stack"
37
+ PYPI_PACKAGE = "provision-stack" # PyPI distribution name
38
+
39
+ # ── API endpoints ─────────────────────────────────────────────────
40
+ # These are the live API URLs. Sandbox uses SANDBOX_URL.
41
+
42
+ API_BASE_URL_DEFAULT = "https://api.provision-stack.com"
43
+ SANDBOX_URL = "https://sandbox.provision-stack.com"
44
+
45
+ # ── User-visible strings ──────────────────────────────────────────
46
+ # Used in tool descriptions, error messages, and docs.
47
+
48
+ SHORT_DESCRIPTION = (
49
+ "Deploy production infrastructure via AI agents — "
50
+ "AWS, GCP, Azure, and Oracle Cloud."
51
+ )
52
+
53
+ LONG_DESCRIPTION = (
54
+ "{product} lets AI agents provision and manage cloud "
55
+ "infrastructure through natural language. Describe your outcome "
56
+ "(e.g. 'deploy a production web application') and receive "
57
+ "verified, best-practice infrastructure on your own cloud account. "
58
+ "Supports AWS, Google Cloud, Azure, and Oracle Cloud."
59
+ )
60
+
61
+ TOOL_DESCRIPTION_SUGGEST = (
62
+ "Get deployment suggestions for a desired outcome. "
63
+ "Returns tier options (Starter/MVP/Startup/Enterprise) with "
64
+ "monthly cost estimates across multiple cloud providers."
65
+ )
66
+
67
+ TOOL_DESCRIPTION_DEPLOY = (
68
+ "Deploy infrastructure for a selected suggestion. "
69
+ "The deploy fee is held from your account balance and captured "
70
+ "only after verification passes (failed deployments are free)."
71
+ )
72
+
73
+ TOOL_DESCRIPTION_STATUS = (
74
+ "Check the status and verification results of a deployment."
75
+ )
76
+
77
+ TOOL_DESCRIPTION_DESTROY = (
78
+ "Destroy all resources owned by a deployment."
79
+ )
80
+
81
+ TOOL_DESCRIPTION_BALANCE = (
82
+ "Check your account credit balance."
83
+ )
84
+
85
+ TOOL_DESCRIPTION_TOPUP = (
86
+ "Add credits to your account via Stripe (card) or XRP (crypto)."
87
+ )
88
+
89
+ # ── Defaults ──────────────────────────────────────────────────────
90
+
91
+ DEFAULT_REGION = "us-east-1"
92
+ DEFAULT_TIER = "MVP"
93
+
94
+ # ── Sandbox note ──────────────────────────────────────────────────
95
+ # Shown to users when connecting to the sandbox.
96
+
97
+ SANDBOX_NOTE = (
98
+ "Sandbox mode: infrastructure is mocked (no real cloud resources "
99
+ "are created). Top up with POST /api/v1/billing/topup/sandbox "
100
+ "to get free credits."
101
+ )
@@ -0,0 +1,161 @@
1
+ """
2
+ Base tool definitions for the Provision Stack client.
3
+
4
+ These are framework-agnostic tool definitions that can be wrapped
5
+ by LangChain, CrewAI, Composio, or any other agent framework.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Callable, Optional
12
+
13
+ from provision_stack.client import ProvisionStackClient
14
+ from provision_stack.config import (
15
+ PRODUCT_NAME,
16
+ TOOL_DESCRIPTION_SUGGEST,
17
+ TOOL_DESCRIPTION_DEPLOY,
18
+ TOOL_DESCRIPTION_STATUS,
19
+ TOOL_DESCRIPTION_DESTROY,
20
+ TOOL_DESCRIPTION_BALANCE,
21
+ TOOL_DESCRIPTION_TOPUP,
22
+ )
23
+
24
+
25
+ @dataclass
26
+ class ToolResult:
27
+ """Standardized result from a tool call."""
28
+
29
+ success: bool
30
+ data: dict[str, Any]
31
+ error: str | None = None
32
+
33
+ def to_dict(self) -> dict[str, Any]:
34
+ result = {"success": self.success, **self.data}
35
+ if self.error:
36
+ result["error"] = self.error
37
+ return result
38
+
39
+
40
+ @dataclass
41
+ class BaseTool:
42
+ """
43
+ Base class for framework-specific tool wrappers.
44
+
45
+ Each tool has a name, description, and a ``run`` method that
46
+ delegates to the ProvisionStackClient.
47
+
48
+ Framework plugins (LangChain, CrewAI, Composio) subclass this
49
+ and add framework-specific metadata (args schemas, return types).
50
+ """
51
+
52
+ name: str
53
+ description: str
54
+ client: ProvisionStackClient
55
+ _run: Callable[..., Any]
56
+
57
+ def run(self, **kwargs: Any) -> ToolResult:
58
+ """Execute the tool with the given arguments."""
59
+ try:
60
+ data = self._run(**kwargs)
61
+ return ToolResult(success=True, data=data)
62
+ except Exception as e:
63
+ return ToolResult(success=False, data={}, error=str(e))
64
+
65
+
66
+ def create_suggest_tool(client: ProvisionStackClient) -> BaseTool:
67
+ """Create a 'suggest' tool bound to the given client."""
68
+ return BaseTool(
69
+ name="suggest",
70
+ description=TOOL_DESCRIPTION_SUGGEST,
71
+ client=client,
72
+ _run=lambda outcome, region=None, provider=None,
73
+ cross_provider=False, max_monthly_cost_usd=None,
74
+ requirements=None, **kw: client.suggest(
75
+ outcome=outcome,
76
+ region=region,
77
+ provider=provider,
78
+ cross_provider=cross_provider,
79
+ max_monthly_cost_usd=max_monthly_cost_usd,
80
+ requirements=requirements,
81
+ ),
82
+ )
83
+
84
+
85
+ def create_deploy_tool(client: ProvisionStackClient) -> BaseTool:
86
+ """Create a 'deploy' tool bound to the given client."""
87
+ return BaseTool(
88
+ name="deploy",
89
+ description=TOOL_DESCRIPTION_DEPLOY,
90
+ client=client,
91
+ _run=lambda outcome, resources=None, tier=None,
92
+ region=None, max_monthly_cost_usd=None,
93
+ requirements=None, **kw: client.deploy(
94
+ outcome=outcome,
95
+ resources=resources,
96
+ tier=tier,
97
+ region=region,
98
+ max_monthly_cost_usd=max_monthly_cost_usd,
99
+ requirements=requirements,
100
+ ),
101
+ )
102
+
103
+
104
+ def create_status_tool(client: ProvisionStackClient) -> BaseTool:
105
+ """Create a 'status' tool bound to the given client."""
106
+ return BaseTool(
107
+ name="status",
108
+ description=TOOL_DESCRIPTION_STATUS,
109
+ client=client,
110
+ _run=lambda deployment_id, **kw: client.status(deployment_id),
111
+ )
112
+
113
+
114
+ def create_destroy_tool(client: ProvisionStackClient) -> BaseTool:
115
+ """Create a 'destroy' tool bound to the given client."""
116
+ return BaseTool(
117
+ name="destroy",
118
+ description=TOOL_DESCRIPTION_DESTROY,
119
+ client=client,
120
+ _run=lambda deployment_id, **kw: client.destroy(deployment_id),
121
+ )
122
+
123
+
124
+ def create_balance_tool(client: ProvisionStackClient) -> BaseTool:
125
+ """Create a 'balance' tool bound to the given client."""
126
+ return BaseTool(
127
+ name="balance",
128
+ description=TOOL_DESCRIPTION_BALANCE,
129
+ client=client,
130
+ _run=lambda **kw: client.balance(),
131
+ )
132
+
133
+
134
+ def create_topup_tool(client: ProvisionStackClient) -> BaseTool:
135
+ """Create a 'topup' tool bound to the given client."""
136
+ return BaseTool(
137
+ name="topup",
138
+ description=TOOL_DESCRIPTION_TOPUP,
139
+ client=client,
140
+ _run=lambda amount_usd, rail="stripe", **kw: (
141
+ client.topup_stripe(amount_usd) if rail == "stripe"
142
+ else client.topup_xrp(amount_usd)
143
+ ),
144
+ )
145
+
146
+
147
+ def get_all_tools(client: ProvisionStackClient) -> list[BaseTool]:
148
+ """
149
+ Get all available tools bound to the given client.
150
+
151
+ Returns a list of BaseTool instances that can be used by any
152
+ framework plugin.
153
+ """
154
+ return [
155
+ create_suggest_tool(client),
156
+ create_deploy_tool(client),
157
+ create_status_tool(client),
158
+ create_destroy_tool(client),
159
+ create_balance_tool(client),
160
+ create_topup_tool(client),
161
+ ]
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: provision-stack
3
+ Version: 0.1.0
4
+ Summary: Python client for the Provision Stack Execution API — deploy cloud infrastructure via AI agents.
5
+ Project-URL: Homepage, https://provision-stack.com
6
+ Project-URL: Documentation, https://docs.provision-stack.com
7
+ Project-URL: Repository, https://github.com/TravisLinkey/provision-stack
8
+ Author: Provision Stack
9
+ License-Expression: MIT
10
+ Keywords: ai-agents,aws,azure,cloud,gcp,infrastructure,mcp,oracle,terraform
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.10
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest-mock>=3.12; extra == 'test'
24
+ Requires-Dist: pytest>=8.0; extra == 'test'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # provision-stack
28
+
29
+ > Python client for the Provision Stack Execution API — deploy cloud infrastructure via AI agents.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install provision-stack
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from provision_stack import ProvisionStackClient
41
+
42
+ client = ProvisionStackClient(api_key="your-token", sandbox=True)
43
+
44
+ # Get suggestions
45
+ suggestions = client.suggest("deploy a production web app")
46
+
47
+ # Deploy
48
+ result = client.deploy(outcome="deploy a production web app", tier="MVP")
49
+
50
+ # Check status
51
+ status = client.status(result["deploymentId"])
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ ```python
57
+ client = ProvisionStackClient(
58
+ api_key="your-token", # or PROVISION_STACK_API_KEY env var
59
+ base_url="https://...", # or PROVISION_STACK_API_URL env var
60
+ sandbox=True, # use sandbox (mock Terraform)
61
+ )
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### `suggest(outcome, **kwargs)`
67
+ Get deployment suggestions with tier options and cost estimates.
68
+
69
+ ### `deploy(outcome, **kwargs)`
70
+ Deploy infrastructure. Fee held from balance, captured after verification.
71
+
72
+ ### `status(deployment_id)`
73
+ Check deployment status and verification results.
74
+
75
+ ### `destroy(deployment_id)`
76
+ Destroy all resources owned by a deployment.
77
+
78
+ ### `balance()`
79
+ Check account credit balance.
80
+
81
+ ### `topup_stripe(amount_usd)` / `topup_xrp(amount_usd)`
82
+ Add credits via Stripe or XRP.
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,7 @@
1
+ provision_stack/__init__.py,sha256=2qg3jN18s86_qg5B8JRgU17OyEY1dfLBe3Eyn57-VnQ,1018
2
+ provision_stack/client.py,sha256=ybvaEktcbgowIH5tL8d8kwC_cLEtiL1qEV-a9d_p3Wc,13216
3
+ provision_stack/config.py,sha256=b9yKJW53SbdhMPUVBCos5UsNby70dPtlYpaaG4I5jps,4163
4
+ provision_stack/tools.py,sha256=5T_MaZCEXf0OWXoieKv0lu0W_CAXEtBW3htlBptFII8,4771
5
+ provision_stack-0.1.0.dist-info/METADATA,sha256=zPwpSsmmbjpecY8IUs3bOurp8dWclYZXALRkEwu-rK0,2514
6
+ provision_stack-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
7
+ provision_stack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any