wafer-cli 0.2.14__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.
- wafer/GUIDE.md +118 -0
- wafer/__init__.py +3 -0
- wafer/analytics.py +306 -0
- wafer/api_client.py +195 -0
- wafer/auth.py +432 -0
- wafer/autotuner.py +1080 -0
- wafer/billing.py +233 -0
- wafer/cli.py +7289 -0
- wafer/config.py +105 -0
- wafer/corpus.py +366 -0
- wafer/evaluate.py +4593 -0
- wafer/global_config.py +350 -0
- wafer/gpu_run.py +307 -0
- wafer/inference.py +148 -0
- wafer/kernel_scope.py +552 -0
- wafer/ncu_analyze.py +651 -0
- wafer/nsys_analyze.py +1042 -0
- wafer/nsys_profile.py +510 -0
- wafer/output.py +248 -0
- wafer/problems.py +357 -0
- wafer/rocprof_compute.py +490 -0
- wafer/rocprof_sdk.py +274 -0
- wafer/rocprof_systems.py +520 -0
- wafer/skills/wafer-guide/SKILL.md +129 -0
- wafer/ssh_keys.py +261 -0
- wafer/target_lock.py +270 -0
- wafer/targets.py +842 -0
- wafer/targets_ops.py +717 -0
- wafer/templates/__init__.py +0 -0
- wafer/templates/ask_docs.py +61 -0
- wafer/templates/optimize_kernel.py +71 -0
- wafer/templates/optimize_kernelbench.py +137 -0
- wafer/templates/trace_analyze.py +74 -0
- wafer/tracelens.py +218 -0
- wafer/wevin_cli.py +577 -0
- wafer/workspaces.py +852 -0
- wafer_cli-0.2.14.dist-info/METADATA +16 -0
- wafer_cli-0.2.14.dist-info/RECORD +41 -0
- wafer_cli-0.2.14.dist-info/WHEEL +5 -0
- wafer_cli-0.2.14.dist-info/entry_points.txt +2 -0
- wafer_cli-0.2.14.dist-info/top_level.txt +1 -0
wafer/billing.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Billing CLI - Manage credits and subscription.
|
|
2
|
+
|
|
3
|
+
This module provides the implementation for the `wafer billing` subcommand.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .api_client import get_api_url
|
|
11
|
+
from .auth import get_auth_headers
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_client() -> tuple[str, dict[str, str]]:
|
|
15
|
+
"""Get API URL and auth headers."""
|
|
16
|
+
api_url = get_api_url()
|
|
17
|
+
headers = get_auth_headers()
|
|
18
|
+
|
|
19
|
+
assert api_url, "API URL must be configured"
|
|
20
|
+
assert api_url.startswith("http"), "API URL must be a valid HTTP(S) URL"
|
|
21
|
+
|
|
22
|
+
return api_url, headers
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_cents(cents: int) -> str:
|
|
26
|
+
"""Format cents as a dollar amount.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
cents: Amount in cents (e.g., 2500 for $25.00)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Formatted string (e.g., "$25.00")
|
|
33
|
+
"""
|
|
34
|
+
dollars = cents / 100
|
|
35
|
+
return f"${dollars:.2f}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_topup_amount(amount_cents: int) -> None:
|
|
39
|
+
"""Validate topup amount is within allowed range.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
amount_cents: Amount in cents
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If amount is out of range ($10-$500)
|
|
46
|
+
"""
|
|
47
|
+
min_cents = 1000 # $10
|
|
48
|
+
max_cents = 50000 # $500
|
|
49
|
+
|
|
50
|
+
if amount_cents < min_cents:
|
|
51
|
+
raise ValueError(f"Amount must be at least ${min_cents // 100}")
|
|
52
|
+
|
|
53
|
+
if amount_cents > max_cents:
|
|
54
|
+
raise ValueError(f"Amount must be at most ${max_cents // 100}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def format_usage_text(usage: dict) -> str:
|
|
58
|
+
"""Format billing usage as human-readable text.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
usage: Usage data from API
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Formatted multi-line string
|
|
65
|
+
"""
|
|
66
|
+
tier = usage.get("tier", "unknown")
|
|
67
|
+
status = usage.get("status", "unknown")
|
|
68
|
+
credits_used = usage.get("credits_used_cents", 0)
|
|
69
|
+
credits_limit = usage.get("credits_limit_cents", 0)
|
|
70
|
+
credits_remaining = usage.get("credits_remaining_cents", 0)
|
|
71
|
+
topup_balance = usage.get("topup_balance_cents", 0)
|
|
72
|
+
has_hw_counters = usage.get("has_hardware_counters", False)
|
|
73
|
+
has_slack = usage.get("has_slack_access", False)
|
|
74
|
+
period_ends = usage.get("period_ends_at")
|
|
75
|
+
|
|
76
|
+
# Capitalize tier for display
|
|
77
|
+
tier_display = tier.capitalize()
|
|
78
|
+
|
|
79
|
+
lines = [
|
|
80
|
+
"Billing Summary",
|
|
81
|
+
"===============",
|
|
82
|
+
"",
|
|
83
|
+
f" Tier: {tier_display}",
|
|
84
|
+
f" Status: {status}",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# Status warnings
|
|
88
|
+
if status == "past_due":
|
|
89
|
+
lines.append(" ⚠ Warning: Payment past due. Please update payment method.")
|
|
90
|
+
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
# Credits section - different handling for enterprise (unlimited)
|
|
94
|
+
if credits_limit == -1 or tier.lower() == "enterprise":
|
|
95
|
+
lines.extend([
|
|
96
|
+
"Credits:",
|
|
97
|
+
f" Used this period: {format_cents(credits_used)}",
|
|
98
|
+
" Limit: Unlimited",
|
|
99
|
+
])
|
|
100
|
+
else:
|
|
101
|
+
lines.extend([
|
|
102
|
+
"Credits:",
|
|
103
|
+
f" Used: {format_cents(credits_used)}",
|
|
104
|
+
f" Limit: {format_cents(credits_limit)}",
|
|
105
|
+
f" Remaining: {format_cents(credits_remaining)}",
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
# Topup balance
|
|
109
|
+
if topup_balance > 0:
|
|
110
|
+
lines.append(f" Topup balance: {format_cents(topup_balance)}")
|
|
111
|
+
|
|
112
|
+
lines.append("")
|
|
113
|
+
|
|
114
|
+
# Features
|
|
115
|
+
lines.append("Features:")
|
|
116
|
+
lines.append(f" Hardware counters: {'Yes' if has_hw_counters else 'No'}")
|
|
117
|
+
lines.append(f" Slack support: {'Yes' if has_slack else 'No'}")
|
|
118
|
+
|
|
119
|
+
# Period end date
|
|
120
|
+
if period_ends:
|
|
121
|
+
lines.append("")
|
|
122
|
+
lines.append(f"Period ends: {period_ends}")
|
|
123
|
+
|
|
124
|
+
# Upgrade prompt for Start tier
|
|
125
|
+
if tier.lower() == "start":
|
|
126
|
+
lines.extend([
|
|
127
|
+
"",
|
|
128
|
+
"Upgrade to Pro for hardware counters and credit topups:",
|
|
129
|
+
" wafer billing portal",
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
return "\n".join(lines)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_usage(json_output: bool = False) -> str:
|
|
136
|
+
"""Get billing usage information.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
json_output: If True, return raw JSON; otherwise return formatted text
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Usage info as string (JSON or formatted text)
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
RuntimeError: On authentication or API errors
|
|
146
|
+
"""
|
|
147
|
+
api_url, headers = _get_client()
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
with httpx.Client(timeout=30.0, headers=headers) as client:
|
|
151
|
+
response = client.get(f"{api_url}/v1/billing/usage")
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
usage = response.json()
|
|
154
|
+
except httpx.HTTPStatusError as e:
|
|
155
|
+
if e.response.status_code == 401:
|
|
156
|
+
raise RuntimeError("Not authenticated. Run: wafer login") from e
|
|
157
|
+
raise RuntimeError(f"API error: {e.response.status_code} - {e.response.text}") from e
|
|
158
|
+
except httpx.RequestError as e:
|
|
159
|
+
raise RuntimeError(f"Could not reach API: {e}") from e
|
|
160
|
+
|
|
161
|
+
if json_output:
|
|
162
|
+
return json.dumps(usage, indent=2)
|
|
163
|
+
|
|
164
|
+
return format_usage_text(usage)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def create_topup(amount_cents: int) -> dict:
|
|
168
|
+
"""Create a topup checkout session.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
amount_cents: Amount to add in cents (1000-50000)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Dict with checkout_url and session_id
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
RuntimeError: On authentication, validation, or API errors
|
|
178
|
+
"""
|
|
179
|
+
api_url, headers = _get_client()
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
with httpx.Client(timeout=30.0, headers=headers) as client:
|
|
183
|
+
response = client.post(
|
|
184
|
+
f"{api_url}/v1/billing/topup",
|
|
185
|
+
json={"amount_cents": amount_cents},
|
|
186
|
+
)
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
return response.json()
|
|
189
|
+
except httpx.HTTPStatusError as e:
|
|
190
|
+
if e.response.status_code == 401:
|
|
191
|
+
raise RuntimeError("Not authenticated. Run: wafer login") from e
|
|
192
|
+
if e.response.status_code == 400:
|
|
193
|
+
# Invalid amount
|
|
194
|
+
try:
|
|
195
|
+
detail = e.response.json().get("detail", e.response.text)
|
|
196
|
+
except Exception:
|
|
197
|
+
detail = e.response.text
|
|
198
|
+
raise RuntimeError(f"Invalid amount: {detail}") from e
|
|
199
|
+
if e.response.status_code == 403:
|
|
200
|
+
# Start tier or other restriction
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
"Topup not available for your subscription tier.\n"
|
|
203
|
+
"Upgrade your subscription first: wafer billing portal"
|
|
204
|
+
) from e
|
|
205
|
+
if e.response.status_code == 503:
|
|
206
|
+
raise RuntimeError("Billing service temporarily unavailable. Please try again later.") from e
|
|
207
|
+
raise RuntimeError(f"API error: {e.response.status_code} - {e.response.text}") from e
|
|
208
|
+
except httpx.RequestError as e:
|
|
209
|
+
raise RuntimeError(f"Could not reach API: {e}") from e
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_portal_url() -> dict:
|
|
213
|
+
"""Get Stripe billing portal URL.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dict with portal_url
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
RuntimeError: On authentication or API errors
|
|
220
|
+
"""
|
|
221
|
+
api_url, headers = _get_client()
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
with httpx.Client(timeout=30.0, headers=headers) as client:
|
|
225
|
+
response = client.post(f"{api_url}/v1/billing/portal")
|
|
226
|
+
response.raise_for_status()
|
|
227
|
+
return response.json()
|
|
228
|
+
except httpx.HTTPStatusError as e:
|
|
229
|
+
if e.response.status_code == 401:
|
|
230
|
+
raise RuntimeError("Not authenticated. Run: wafer login") from e
|
|
231
|
+
raise RuntimeError(f"API error: {e.response.status_code} - {e.response.text}") from e
|
|
232
|
+
except httpx.RequestError as e:
|
|
233
|
+
raise RuntimeError(f"Could not reach API: {e}") from e
|