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.
Files changed (42) hide show
  1. {boxd-0.1.2/src/boxd.egg-info → boxd-0.1.2.dev13}/PKG-INFO +1 -1
  2. {boxd-0.1.2 → boxd-0.1.2.dev13}/pyproject.toml +1 -1
  3. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/__init__.py +6 -0
  4. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_sync.py +33 -0
  5. boxd-0.1.2.dev13/src/boxd/billing.py +146 -0
  6. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/client.py +2 -0
  7. {boxd-0.1.2 → boxd-0.1.2.dev13/src/boxd.egg-info}/PKG-INFO +1 -1
  8. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/SOURCES.txt +1 -0
  9. {boxd-0.1.2 → boxd-0.1.2.dev13}/LICENSE +0 -0
  10. {boxd-0.1.2 → boxd-0.1.2.dev13}/README.md +0 -0
  11. {boxd-0.1.2 → boxd-0.1.2.dev13}/setup.cfg +0 -0
  12. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_generated/__init__.py +0 -0
  13. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_generated/api_pb2.py +0 -0
  14. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_generated/api_pb2_grpc.py +0 -0
  15. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_utils.py +0 -0
  16. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/_version_check.py +0 -0
  17. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/aio.py +0 -0
  18. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/auth.py +0 -0
  19. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/box.py +0 -0
  20. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/boxes.py +0 -0
  21. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/disks.py +0 -0
  22. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/domains.py +0 -0
  23. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/errors.py +0 -0
  24. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/exec.py +0 -0
  25. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/networks.py +0 -0
  26. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/templates.py +0 -0
  27. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/tokens.py +0 -0
  28. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd/types.py +0 -0
  29. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/dependency_links.txt +0 -0
  30. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/requires.txt +0 -0
  31. {boxd-0.1.2 → boxd-0.1.2.dev13}/src/boxd.egg-info/top_level.txt +0 -0
  32. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_auth.py +0 -0
  33. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_boxes.py +0 -0
  34. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_e2e.py +0 -0
  35. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_e2e_v2.py +0 -0
  36. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_exec.py +0 -0
  37. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_files.py +0 -0
  38. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_lifecycle.py +0 -0
  39. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_proxies.py +0 -0
  40. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_utils.py +0 -0
  41. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_v2.py +0 -0
  42. {boxd-0.1.2 → boxd-0.1.2.dev13}/tests/test_version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boxd
3
- Version: 0.1.2
3
+ Version: 0.1.2.dev13
4
4
  Summary: Python SDK for the boxd cloud VM platform
5
5
  Author: Azin
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boxd"
3
- version = "0.1.2"
3
+ version = "0.1.2.dev13"
4
4
  description = "Python SDK for the boxd cloud VM platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boxd
3
- Version: 0.1.2
3
+ Version: 0.1.2.dev13
4
4
  Summary: Python SDK for the boxd cloud VM platform
5
5
  Author: Azin
6
6
  License-Expression: MIT
@@ -7,6 +7,7 @@ src/boxd/_utils.py
7
7
  src/boxd/_version_check.py
8
8
  src/boxd/aio.py
9
9
  src/boxd/auth.py
10
+ src/boxd/billing.py
10
11
  src/boxd/box.py
11
12
  src/boxd/boxes.py
12
13
  src/boxd/client.py
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