boxd 0.1.2__tar.gz → 0.1.2.dev13__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.
- {boxd-0.1.2/src/boxd.egg-info → boxd-0.1.2.dev13}/PKG-INFO +1 -1
- {boxd-0.1.2 → boxd-0.1.2.dev13}/pyproject.toml +1 -1
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/__init__.py +6 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_sync.py +33 -0
- boxd-0.1.2.dev13/src/boxd/billing.py +146 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/client.py +2 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13/src/boxd.egg-info}/PKG-INFO +1 -1
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/SOURCES.txt +1 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/LICENSE +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/README.md +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/setup.cfg +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_generated/api_pb2_grpc.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_utils.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_version_check.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/aio.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/auth.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/box.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/boxes.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/disks.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/domains.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/errors.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/exec.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/networks.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/templates.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/tokens.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/types.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_auth.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_boxes.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_e2e.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_e2e_v2.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_exec.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_files.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_lifecycle.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_proxies.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_utils.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_v2.py +0 -0
- {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_version_check.py +0 -0
|
@@ -21,6 +21,7 @@ For the async API::
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
from ._sync import (
|
|
24
|
+
BillingService,
|
|
24
25
|
Box,
|
|
25
26
|
BoxService,
|
|
26
27
|
Compute,
|
|
@@ -31,6 +32,7 @@ from ._sync import (
|
|
|
31
32
|
TemplateService,
|
|
32
33
|
TokenService,
|
|
33
34
|
)
|
|
35
|
+
from .billing import Billing, Plan
|
|
34
36
|
from .exec import ExecResult, ExecProcess, StreamReader, StreamWriter
|
|
35
37
|
from .types import (
|
|
36
38
|
BoxConfig,
|
|
@@ -84,6 +86,10 @@ __all__ = [
|
|
|
84
86
|
"DomainService",
|
|
85
87
|
"NetworkService",
|
|
86
88
|
"TokenService",
|
|
89
|
+
"BillingService",
|
|
90
|
+
# Billing types
|
|
91
|
+
"Billing",
|
|
92
|
+
"Plan",
|
|
87
93
|
# Exec primitives
|
|
88
94
|
"ExecResult",
|
|
89
95
|
"ExecProcess",
|
|
@@ -393,6 +393,38 @@ class TokenService(_SyncBase):
|
|
|
393
393
|
self._run(self._async.revoke(jti))
|
|
394
394
|
|
|
395
395
|
|
|
396
|
+
class BillingService(_SyncBase):
|
|
397
|
+
"""Subscription billing (synchronous API)."""
|
|
398
|
+
|
|
399
|
+
def __init__(self, async_service, loop: asyncio.AbstractEventLoop) -> None:
|
|
400
|
+
from .billing import BillingService as AsyncBillingService
|
|
401
|
+
|
|
402
|
+
self._async: AsyncBillingService = async_service
|
|
403
|
+
self._loop = loop
|
|
404
|
+
|
|
405
|
+
def list_plans(self):
|
|
406
|
+
return self._run(self._async.list_plans())
|
|
407
|
+
|
|
408
|
+
def status(self):
|
|
409
|
+
return self._run(self._async.status())
|
|
410
|
+
|
|
411
|
+
def create_checkout_session(
|
|
412
|
+
self,
|
|
413
|
+
shape: str,
|
|
414
|
+
success_url: str = "",
|
|
415
|
+
cancel_url: str = "",
|
|
416
|
+
) -> str:
|
|
417
|
+
return self._run(
|
|
418
|
+
self._async.create_checkout_session(shape, success_url, cancel_url)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def create_billing_portal_session(self, return_url: str = "") -> str:
|
|
422
|
+
return self._run(self._async.create_billing_portal_session(return_url))
|
|
423
|
+
|
|
424
|
+
def change_shape(self, shape: str):
|
|
425
|
+
return self._run(self._async.change_shape(shape))
|
|
426
|
+
|
|
427
|
+
|
|
396
428
|
class Compute(_SyncBase):
|
|
397
429
|
"""Main client for the boxd API (synchronous API).
|
|
398
430
|
|
|
@@ -429,6 +461,7 @@ class Compute(_SyncBase):
|
|
|
429
461
|
self.domain = DomainService(self._async.domain, self._loop)
|
|
430
462
|
self.network = NetworkService(self._async.network, self._loop)
|
|
431
463
|
self.token = TokenService(self._async.token, self._loop)
|
|
464
|
+
self.billing = BillingService(self._async.billing, self._loop)
|
|
432
465
|
|
|
433
466
|
def whoami(self) -> WhoamiResult:
|
|
434
467
|
"""Return information about the authenticated user."""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""BillingService — list plans, check status, and manage subscriptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ._generated import api_pb2
|
|
9
|
+
from ._utils import GrpcCaller
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .client import Compute
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Plan:
|
|
17
|
+
"""One row of the static plan catalog."""
|
|
18
|
+
|
|
19
|
+
tier: str
|
|
20
|
+
"""``'free'`` or ``'individual'``."""
|
|
21
|
+
|
|
22
|
+
shape: str
|
|
23
|
+
"""``'2x8'`` | ``'4x16'`` | ``'8x32'`` | ``'16x64'``."""
|
|
24
|
+
|
|
25
|
+
monthly_eur: int
|
|
26
|
+
"""Price in EUR cents (e.g. ``4000`` for €40/mo)."""
|
|
27
|
+
|
|
28
|
+
lookup_key: str
|
|
29
|
+
"""Stripe ``lookup_key`` for this plan (``'individual-<shape>'``)."""
|
|
30
|
+
|
|
31
|
+
description: str
|
|
32
|
+
vcpu: int
|
|
33
|
+
memory_bytes: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Billing:
|
|
38
|
+
"""Current billing state for the authenticated user."""
|
|
39
|
+
|
|
40
|
+
tier: str
|
|
41
|
+
"""``'free'`` or ``'individual'``."""
|
|
42
|
+
|
|
43
|
+
shape: str
|
|
44
|
+
"""``'2x8'`` | ``'4x16'`` | ``'8x32'`` | ``'16x64'``."""
|
|
45
|
+
|
|
46
|
+
subscription_status: str
|
|
47
|
+
"""``'active'`` | ``'trialing'`` | ``'past_due'`` | ``'canceled'``."""
|
|
48
|
+
|
|
49
|
+
stripe_customer_id: str
|
|
50
|
+
"""Empty string when the user has never upgraded."""
|
|
51
|
+
|
|
52
|
+
stripe_subscription_id: str
|
|
53
|
+
"""Empty string when on the free tier."""
|
|
54
|
+
|
|
55
|
+
past_due_since: int
|
|
56
|
+
"""Unix timestamp of payment failure; ``0`` when not past due."""
|
|
57
|
+
|
|
58
|
+
max_vms: int
|
|
59
|
+
"""Effective quota at the current shape."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BillingService(GrpcCaller):
|
|
63
|
+
"""Subscription billing: list plans, get current status, drive checkout
|
|
64
|
+
and the Customer Portal hand-off, swap shapes in-place.
|
|
65
|
+
|
|
66
|
+
Usage::
|
|
67
|
+
|
|
68
|
+
plans = await c.billing.list_plans()
|
|
69
|
+
current = await c.billing.status()
|
|
70
|
+
if current.tier == "free":
|
|
71
|
+
url = await c.billing.create_checkout_session("4x16")
|
|
72
|
+
print(f"Upgrade at: {url}")
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, client: Compute) -> None:
|
|
76
|
+
self._client = client
|
|
77
|
+
|
|
78
|
+
async def list_plans(self) -> list[Plan]:
|
|
79
|
+
"""Return the static plan catalog (the same one the website renders)."""
|
|
80
|
+
resp = await self._call("ListPlans", api_pb2.ListPlansRequest())
|
|
81
|
+
return [
|
|
82
|
+
Plan(
|
|
83
|
+
tier=p.tier,
|
|
84
|
+
shape=p.shape,
|
|
85
|
+
monthly_eur=p.monthly_eur,
|
|
86
|
+
lookup_key=p.lookup_key,
|
|
87
|
+
description=p.description,
|
|
88
|
+
vcpu=p.vcpu,
|
|
89
|
+
memory_bytes=p.memory_bytes,
|
|
90
|
+
)
|
|
91
|
+
for p in resp.plans
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
async def status(self) -> Billing:
|
|
95
|
+
"""Read the current billing state for the authenticated user."""
|
|
96
|
+
resp = await self._call("GetBilling", api_pb2.GetBillingRequest())
|
|
97
|
+
b = resp.billing
|
|
98
|
+
return Billing(
|
|
99
|
+
tier=b.tier,
|
|
100
|
+
shape=b.shape,
|
|
101
|
+
subscription_status=b.subscription_status,
|
|
102
|
+
stripe_customer_id=b.stripe_customer_id,
|
|
103
|
+
stripe_subscription_id=b.stripe_subscription_id,
|
|
104
|
+
past_due_since=b.past_due_since,
|
|
105
|
+
max_vms=b.max_vms,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def create_checkout_session(
|
|
109
|
+
self,
|
|
110
|
+
shape: str,
|
|
111
|
+
success_url: str = "",
|
|
112
|
+
cancel_url: str = "",
|
|
113
|
+
) -> str:
|
|
114
|
+
"""Free → paid upgrade. Returns the Stripe Checkout URL — open it in
|
|
115
|
+
a browser to complete the upgrade. The subscription becomes active
|
|
116
|
+
via webhook on completion (14-day trial, EU VAT, quantity=1)."""
|
|
117
|
+
resp = await self._call(
|
|
118
|
+
"CreateCheckoutSession",
|
|
119
|
+
api_pb2.CreateCheckoutSessionRequest(
|
|
120
|
+
shape=shape,
|
|
121
|
+
success_url=success_url,
|
|
122
|
+
cancel_url=cancel_url,
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
return resp.checkout_url
|
|
126
|
+
|
|
127
|
+
async def create_billing_portal_session(self, return_url: str = "") -> str:
|
|
128
|
+
"""Open Stripe Customer Portal for payment-method updates, invoice
|
|
129
|
+
history, and self-cancel. Returns a short-lived URL."""
|
|
130
|
+
resp = await self._call(
|
|
131
|
+
"CreateBillingPortalSession",
|
|
132
|
+
api_pb2.CreateBillingPortalSessionRequest(return_url=return_url),
|
|
133
|
+
)
|
|
134
|
+
return resp.portal_url
|
|
135
|
+
|
|
136
|
+
async def change_shape(self, shape: str) -> Billing:
|
|
137
|
+
"""In-place subscription swap to a different shape. Prorated by Stripe.
|
|
138
|
+
Returns the post-swap billing state (echoed from Stripe)."""
|
|
139
|
+
resp = await self._call(
|
|
140
|
+
"ChangeShape",
|
|
141
|
+
api_pb2.ChangeShapeRequest(shape=shape),
|
|
142
|
+
)
|
|
143
|
+
# Server returns just shape + status; fold into a Billing-shaped view
|
|
144
|
+
# by re-reading status. Simpler than mirroring two slightly different
|
|
145
|
+
# response types in the public surface.
|
|
146
|
+
return await self.status()
|
|
@@ -16,6 +16,7 @@ from .disks import DiskService
|
|
|
16
16
|
from .domains import DomainService
|
|
17
17
|
from .errors import AuthenticationError, from_grpc_error
|
|
18
18
|
from .networks import NetworkService
|
|
19
|
+
from .billing import BillingService
|
|
19
20
|
from .templates import TemplateService
|
|
20
21
|
from .tokens import TokenService
|
|
21
22
|
from .types import ConfigResult, WhoamiResult
|
|
@@ -104,6 +105,7 @@ class Compute:
|
|
|
104
105
|
self.domain = DomainService(self)
|
|
105
106
|
self.network = NetworkService(self)
|
|
106
107
|
self.token = TokenService(self)
|
|
108
|
+
self.billing = BillingService(self)
|
|
107
109
|
|
|
108
110
|
async def _ensure_channel(self) -> api_pb2_grpc.BoxdApiStub:
|
|
109
111
|
"""Lazily create the gRPC channel and return the stub."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|