boxd 0.2.18.dev892__tar.gz → 0.2.18.dev896__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.2.18.dev892/src/boxd.egg-info → boxd-0.2.18.dev896}/PKG-INFO +3 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/pyproject.toml +6 -4
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_client.py +1 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_credentials.py +2 -3
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_generated/api_pb2_grpc.py +21 -23
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_mappers.py +1 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_requests.py +6 -6
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_urls.py +2 -2
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/models.py +1 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/machines.py +6 -6
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/orgs.py +4 -4
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896/src/boxd.egg-info}/PKG-INFO +3 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_credentials.py +2 -2
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_delete_self.py +1 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_namespaces.py +7 -8
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_requests.py +1 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_transport.py +4 -4
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_urls.py +1 -1
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/LICENSE +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/README.md +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/setup.cfg +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/__init__.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_generated/__init__.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_generated/api_pb2.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_streaming.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_transport.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/_version_check.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/errors.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/__init__.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/account.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/credentials.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/disks.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/domains.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/snapshots.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/teams.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd/resources/vars.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd.egg-info/SOURCES.txt +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd.egg-info/dependency_links.txt +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd.egg-info/requires.txt +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/src/boxd.egg-info/top_level.txt +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_mappers.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_streaming.py +0 -0
- {boxd-0.2.18.dev892 → boxd-0.2.18.dev896}/tests/test_version_check.py +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.2.18.
|
|
3
|
+
Version: 0.2.18.dev896
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://boxd.sh
|
|
8
|
+
Project-URL: Repository, https://github.com/azin-tech/boxd-sdk-python
|
|
9
|
+
Project-URL: Issues, https://github.com/azin-tech/boxd-sdk-python/issues
|
|
8
10
|
Keywords: boxd,vm,microvm,sandbox,compute,grpc,sdk
|
|
9
11
|
Classifier: Development Status :: 4 - Beta
|
|
10
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boxd"
|
|
3
|
-
version = "0.2.18.
|
|
3
|
+
version = "0.2.18.dev896"
|
|
4
4
|
description = "Python SDK for the boxd cloud VM platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -25,8 +25,8 @@ dependencies = [
|
|
|
25
25
|
# 1.80 needs grpcio>=1.80 / protobuf>=6.31.1 at runtime, never older).
|
|
26
26
|
# Keep these in lockstep with the grpcio-tools pin below — bumping
|
|
27
27
|
# the codegen toolchain without bumping these floors reintroduces
|
|
28
|
-
#
|
|
29
|
-
# the newer gencode
|
|
28
|
+
# broken installs: resolvers pick an old protobuf/grpcio that can't
|
|
29
|
+
# load the newer gencode.
|
|
30
30
|
"grpcio>=1.80",
|
|
31
31
|
"protobuf>=6.31.1",
|
|
32
32
|
"pydantic>=2",
|
|
@@ -42,13 +42,15 @@ dependencies = [
|
|
|
42
42
|
|
|
43
43
|
[project.urls]
|
|
44
44
|
Homepage = "https://boxd.sh"
|
|
45
|
+
Repository = "https://github.com/azin-tech/boxd-sdk-python"
|
|
46
|
+
Issues = "https://github.com/azin-tech/boxd-sdk-python/issues"
|
|
45
47
|
|
|
46
48
|
[project.optional-dependencies]
|
|
47
49
|
dev = [
|
|
48
50
|
# Capped at 1.80: newer grpcio-tools stamps gencode against protobuf
|
|
49
51
|
# 7.x, which bumps the runtime floor above what shared environments
|
|
50
52
|
# (e.g. anything depending on beta9's protobuf<7) can satisfy. See
|
|
51
|
-
#
|
|
53
|
+
# the dependency floors above: bump this only alongside them.
|
|
52
54
|
"grpcio-tools>=1.60,<=1.80",
|
|
53
55
|
"pytest>=8",
|
|
54
56
|
"pytest-asyncio>=0.23",
|
|
@@ -31,7 +31,7 @@ class Boxd:
|
|
|
31
31
|
|
|
32
32
|
boxd = Boxd() # production
|
|
33
33
|
boxd = Boxd(api_key="bxd_...")
|
|
34
|
-
boxd = Boxd(base_url="https://boxd
|
|
34
|
+
boxd = Boxd(base_url="https://boxd.example.com:9443") # any other cluster
|
|
35
35
|
|
|
36
36
|
Args:
|
|
37
37
|
api_key: A `bxd_` API key. Defaults to `BOXD_API_KEY`.
|
|
@@ -28,8 +28,7 @@ import httpx
|
|
|
28
28
|
|
|
29
29
|
from .errors import AuthenticationError, RateLimitError
|
|
30
30
|
|
|
31
|
-
# The in-VM metadata adapter
|
|
32
|
-
# the VM's default gateway.
|
|
31
|
+
# The in-VM metadata adapter listens on the VM's default gateway.
|
|
33
32
|
METADATA_PORT = 9002
|
|
34
33
|
METADATA_TOKEN_PATH = "/v1/auth/token"
|
|
35
34
|
# Proxy-minted, HMAC-signed identity token exposed to every guest process by
|
|
@@ -57,7 +56,7 @@ _NO_CREDENTIALS = (
|
|
|
57
56
|
def detect_gateway() -> str | None:
|
|
58
57
|
"""Read the default gateway from ``/proc/net/route``.
|
|
59
58
|
|
|
60
|
-
Same approach as `
|
|
59
|
+
Same approach as the in-VM `boxd` CLI: the gateway column of the
|
|
61
60
|
default route is a hex-encoded little-endian IPv4 address.
|
|
62
61
|
"""
|
|
63
62
|
try:
|
|
@@ -658,7 +658,7 @@ class BoxdApiServicer(object):
|
|
|
658
658
|
raise NotImplementedError('Method not implemented!')
|
|
659
659
|
|
|
660
660
|
def SetVmNetworks(self, request, context):
|
|
661
|
-
"""Replaces a VM's whole network-tag set
|
|
661
|
+
"""Replaces a VM's whole network-tag set — not add/remove, the
|
|
662
662
|
caller sends the complete desired set. Networks drive east-west
|
|
663
663
|
reachability within the caller's org; see CreateVmRequest.networks.
|
|
664
664
|
"""
|
|
@@ -667,7 +667,7 @@ class BoxdApiServicer(object):
|
|
|
667
667
|
raise NotImplementedError('Method not implemented!')
|
|
668
668
|
|
|
669
669
|
def SetVmEgressAllow(self, request, context):
|
|
670
|
-
"""Replaces a VM's egress allowlist
|
|
670
|
+
"""Replaces a VM's egress allowlist: hostnames / wildcards and
|
|
671
671
|
public IPv4 addresses / CIDRs the machine may reach. Empty = no
|
|
672
672
|
allowlist. Only reachable through the public API (no CLI, no in-VM route).
|
|
673
673
|
"""
|
|
@@ -676,7 +676,7 @@ class BoxdApiServicer(object):
|
|
|
676
676
|
raise NotImplementedError('Method not implemented!')
|
|
677
677
|
|
|
678
678
|
def UpdateVmLabels(self, request, context):
|
|
679
|
-
"""Edits a machine's labels
|
|
679
|
+
"""Edits a machine's labels: user-set key=value metadata for
|
|
680
680
|
grouping and finding machines. Returns the labels after the edit.
|
|
681
681
|
"""
|
|
682
682
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
@@ -708,7 +708,7 @@ class BoxdApiServicer(object):
|
|
|
708
708
|
rejects it exactly like ListVms omits it and Exec refuses it, so a
|
|
709
709
|
caller polling GetVm to confirm teardown never sees a stale "running"
|
|
710
710
|
(or any other live-looking) status for a machine it already destroyed
|
|
711
|
-
|
|
711
|
+
.
|
|
712
712
|
"""
|
|
713
713
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
714
714
|
context.set_details('Method not implemented!')
|
|
@@ -786,7 +786,7 @@ class BoxdApiServicer(object):
|
|
|
786
786
|
Cancelling the call — a client-side timeout expiring, or the caller
|
|
787
787
|
half-closing/cancelling the stream — does NOT just walk away from the
|
|
788
788
|
remote command: it kills it. The in-VM agent runs every exec'd command
|
|
789
|
-
in its own process group
|
|
789
|
+
in its own process group and, once it notices this RPC has
|
|
790
790
|
been torn down while the command is still running, sends SIGTERM to
|
|
791
791
|
that whole group, then SIGKILL after a short grace period if it hasn't
|
|
792
792
|
exited. Both SDKs' `timeout` / `stream.close()` (`close()`) go through
|
|
@@ -1045,7 +1045,7 @@ class BoxdApiServicer(object):
|
|
|
1045
1045
|
raise NotImplementedError('Method not implemented!')
|
|
1046
1046
|
|
|
1047
1047
|
def CreateSnapshot(self, request, context):
|
|
1048
|
-
"""Templates are
|
|
1048
|
+
"""Templates are managed by the platform and used only as the fast default
|
|
1049
1049
|
path of `new` when a prepped template matches the image + size — there is no
|
|
1050
1050
|
public API to create/list/delete templates or create a VM from a specific one.
|
|
1051
1051
|
Snapshots: user/org point-in-time VM captures (memory + disk), replicated
|
|
@@ -1238,14 +1238,14 @@ class BoxdApiServicer(object):
|
|
|
1238
1238
|
|
|
1239
1239
|
def GetOrgBilling(self, request, context):
|
|
1240
1240
|
"""The org's shared plan: aggregate quota + current usage. Any member may
|
|
1241
|
-
view it (read-only; the
|
|
1241
|
+
view it (read-only; the platform sets the quota).
|
|
1242
1242
|
"""
|
|
1243
1243
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
1244
1244
|
context.set_details('Method not implemented!')
|
|
1245
1245
|
raise NotImplementedError('Method not implemented!')
|
|
1246
1246
|
|
|
1247
1247
|
def GetOrgUsage(self, request, context):
|
|
1248
|
-
"""Usage and cost
|
|
1248
|
+
"""Usage and cost: what each machine used and cost over a window,
|
|
1249
1249
|
and billing activity (what was actually charged). Org owners and admins
|
|
1250
1250
|
only; API keys fenced to the org work, tokens minted inside a VM don't.
|
|
1251
1251
|
"""
|
|
@@ -1266,8 +1266,7 @@ class BoxdApiServicer(object):
|
|
|
1266
1266
|
raise NotImplementedError('Method not implemented!')
|
|
1267
1267
|
|
|
1268
1268
|
def SetBackupSchedule(self, request, context):
|
|
1269
|
-
"""Disaster recovery: scheduled VM disk backups to object storage
|
|
1270
|
-
(plans/2026-08-10-dr-vm-backups.md). Off by default — SetBackupSchedule
|
|
1269
|
+
"""Disaster recovery: scheduled VM disk backups to object storage. Off by default — SetBackupSchedule
|
|
1271
1270
|
is refused server-side if the cluster has no [backups] object store
|
|
1272
1271
|
configured.
|
|
1273
1272
|
"""
|
|
@@ -1294,7 +1293,7 @@ class BoxdApiServicer(object):
|
|
|
1294
1293
|
raise NotImplementedError('Method not implemented!')
|
|
1295
1294
|
|
|
1296
1295
|
def TriggerBackup(self, request, context):
|
|
1297
|
-
"""On-demand "back up now"
|
|
1296
|
+
"""On-demand "back up now" — captures immediately, independent of
|
|
1298
1297
|
(and without disturbing) any schedule. Retention resolves from the VM's
|
|
1299
1298
|
schedule, else the org default, else the platform default.
|
|
1300
1299
|
"""
|
|
@@ -1303,7 +1302,7 @@ class BoxdApiServicer(object):
|
|
|
1303
1302
|
raise NotImplementedError('Method not implemented!')
|
|
1304
1303
|
|
|
1305
1304
|
def DeleteBackup(self, request, context):
|
|
1306
|
-
"""Manual deletion of a single backup by id
|
|
1305
|
+
"""Manual deletion of a single backup by id. Refused for a backup
|
|
1307
1306
|
still being captured or one a newer dedup pointer depends on.
|
|
1308
1307
|
"""
|
|
1309
1308
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
@@ -1321,8 +1320,7 @@ class BoxdApiServicer(object):
|
|
|
1321
1320
|
def SetOrgBackupsDefault(self, request, context):
|
|
1322
1321
|
"""The org's default backup schedule for NEW VMs (existing VMs are
|
|
1323
1322
|
unchanged; a fork always copies its own source's schedule instead of
|
|
1324
|
-
this default
|
|
1325
|
-
counterpart to the operator-only `boxd-ctl org set-backups-default`.
|
|
1323
|
+
this default). Self-service: org admins set it themselves.
|
|
1326
1324
|
Set/Clear require org admin; Get is readable by any member — same shape
|
|
1327
1325
|
as SetOrgDomain/GetOrgDomain above.
|
|
1328
1326
|
"""
|
|
@@ -1337,7 +1335,7 @@ class BoxdApiServicer(object):
|
|
|
1337
1335
|
raise NotImplementedError('Method not implemented!')
|
|
1338
1336
|
|
|
1339
1337
|
def SetOrgMachineDefaults(self, request, context):
|
|
1340
|
-
"""The org's default machine SIZE
|
|
1338
|
+
"""The org's default machine SIZE: what a machine gets at CREATE
|
|
1341
1339
|
when the request names no size. Distinct from the quota: the quota stays
|
|
1342
1340
|
the operator-owned ceiling and the per-request maximum; this is the
|
|
1343
1341
|
org-admin-owned preference below it (clamped to the ceiling). Every
|
|
@@ -1356,12 +1354,12 @@ class BoxdApiServicer(object):
|
|
|
1356
1354
|
raise NotImplementedError('Method not implemented!')
|
|
1357
1355
|
|
|
1358
1356
|
def SetOrgIdleIgnoreOutboundDefault(self, request, context):
|
|
1359
|
-
"""The org's default for idle.ignore-outbound
|
|
1357
|
+
"""The org's default for idle.ignore-outbound: whether a NEW
|
|
1360
1358
|
machine in the org starts with its idle clock ignoring outbound traffic
|
|
1361
1359
|
when its create request does not say. An org without one follows the
|
|
1362
1360
|
platform's setting. Setting it also sets it on the existing machines and
|
|
1363
1361
|
snapshots the caller may manage, unless the Set asks for
|
|
1364
|
-
`new_machines_only
|
|
1362
|
+
`new_machines_only`; a fork or a restore keeps its source's
|
|
1365
1363
|
value either way.
|
|
1366
1364
|
Set/Clear require org admin; Get is readable by any member.
|
|
1367
1365
|
"""
|
|
@@ -1376,7 +1374,7 @@ class BoxdApiServicer(object):
|
|
|
1376
1374
|
raise NotImplementedError('Method not implemented!')
|
|
1377
1375
|
|
|
1378
1376
|
def SetOrgBotProtectionDefault(self, request, context):
|
|
1379
|
-
"""The org's default bot protection mode
|
|
1377
|
+
"""The org's default bot protection mode: what a NEW machine in
|
|
1380
1378
|
the org starts with. An org without one follows the cluster's setting
|
|
1381
1379
|
for new machines. Every machine keeps the mode it has, so changing this
|
|
1382
1380
|
only affects machines created afterwards; a fork or a restore keeps its
|
|
@@ -1394,8 +1392,8 @@ class BoxdApiServicer(object):
|
|
|
1394
1392
|
raise NotImplementedError('Method not implemented!')
|
|
1395
1393
|
|
|
1396
1394
|
def CreateOrg(self, request, context):
|
|
1397
|
-
"""Org creation, roster, and membership (self-serve,
|
|
1398
|
-
|
|
1395
|
+
"""Org creation, roster, and membership (self-serve, same business rules as
|
|
1396
|
+
the console). Any signed-in
|
|
1399
1397
|
user may create an org (becomes its owner+admin); invite/revoke/remove/
|
|
1400
1398
|
set-role require org admin.
|
|
1401
1399
|
|
|
@@ -1438,7 +1436,7 @@ class BoxdApiServicer(object):
|
|
|
1438
1436
|
raise NotImplementedError('Method not implemented!')
|
|
1439
1437
|
|
|
1440
1438
|
def RemoveOrgMember(self, request, context):
|
|
1441
|
-
"""Refused for an org owner/admin (
|
|
1439
|
+
"""Refused for an org owner/admin (only the platform can remove those) or a member
|
|
1442
1440
|
who still owns VMs billed to this org — same guardrails as the console.
|
|
1443
1441
|
"""
|
|
1444
1442
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
@@ -1447,7 +1445,7 @@ class BoxdApiServicer(object):
|
|
|
1447
1445
|
|
|
1448
1446
|
def SetOrgMemberRole(self, request, context):
|
|
1449
1447
|
"""Promote a member to admin, or demote an admin back to member. Refused for
|
|
1450
|
-
the owner (
|
|
1448
|
+
the owner (ownership is transferred by the platform) and for a target
|
|
1451
1449
|
that isn't a member. Demoting ANOTHER admin is owner-only (an admin may
|
|
1452
1450
|
only step down themselves) — otherwise any admin could demote a peer and
|
|
1453
1451
|
then RemoveOrgMember them, defeating that RPC's admin guard. Promoting to
|
|
@@ -1458,7 +1456,7 @@ class BoxdApiServicer(object):
|
|
|
1458
1456
|
raise NotImplementedError('Method not implemented!')
|
|
1459
1457
|
|
|
1460
1458
|
def ListTeams(self, request, context):
|
|
1461
|
-
"""Teams
|
|
1459
|
+
"""Teams: org-scoped sub-grouping of members, each with an
|
|
1462
1460
|
optional default snapshot new team machines boot from. Creating a team
|
|
1463
1461
|
requires org admin; every other write requires team-admin (inherited by
|
|
1464
1462
|
every org admin, or held directly via a team_members.role='admin' row).
|
|
@@ -556,7 +556,7 @@ def api_key(msg) -> ApiKey:
|
|
|
556
556
|
org=msg.org,
|
|
557
557
|
kind=msg.kind,
|
|
558
558
|
created_by=msg.created_by,
|
|
559
|
-
#
|
|
559
|
+
# An older server lists only the caller's own keys and
|
|
560
560
|
# sends neither field.
|
|
561
561
|
mine=msg.mine or not msg.created_by,
|
|
562
562
|
)
|
|
@@ -77,7 +77,7 @@ def vm_config(
|
|
|
77
77
|
srf.auto_suspend_timeout_secs = auto_suspend_timeout
|
|
78
78
|
config.srf.CopyFrom(srf)
|
|
79
79
|
if idle_ignore_outbound is not None:
|
|
80
|
-
# `optional` on the wire
|
|
80
|
+
# `optional` on the wire: unset means "not requested", which
|
|
81
81
|
# is not the same as False. A new machine then takes its org's default
|
|
82
82
|
# and a fork or restore keeps the source's value. Only touch it when
|
|
83
83
|
# asked, and send an explicit False as False.
|
|
@@ -420,9 +420,9 @@ def set_org_idle_ignore_outbound_default(
|
|
|
420
420
|
`enabled` is `optional` on the wire, and absence is what clears the
|
|
421
421
|
default, so None must stay unset rather than become False.
|
|
422
422
|
|
|
423
|
-
Existing machines change unless `new_machines_only
|
|
424
|
-
also carries `apply_existing` as its opposite, so
|
|
425
|
-
|
|
423
|
+
Existing machines change unless `new_machines_only`. The wire
|
|
424
|
+
also carries `apply_existing` as its opposite, so an older server,
|
|
425
|
+
which only reads that field, does the same. `apply_existing` is
|
|
426
426
|
accepted from callers written against 0.2.16 and changes nothing, but
|
|
427
427
|
asking for it together with `new_machines_only` is a contradiction.
|
|
428
428
|
"""
|
|
@@ -434,7 +434,7 @@ def set_org_idle_ignore_outbound_default(
|
|
|
434
434
|
request = api_pb2.SetOrgIdleIgnoreOutboundDefaultRequest(
|
|
435
435
|
org_id=org or "",
|
|
436
436
|
new_machines_only=new_machines_only,
|
|
437
|
-
# A clear has nothing to apply, and
|
|
437
|
+
# A clear has nothing to apply, and an older server refuses a
|
|
438
438
|
# clear that asks for it.
|
|
439
439
|
apply_existing=enabled is not None and not new_machines_only,
|
|
440
440
|
)
|
|
@@ -458,7 +458,7 @@ def unix_seconds(t: datetime | int | float | None) -> int:
|
|
|
458
458
|
|
|
459
459
|
|
|
460
460
|
def set_delete_after(machine_id: str, seconds: int, cron: str | None):
|
|
461
|
-
"""`SetDeleteAfter` for a duration or a cron expression
|
|
461
|
+
"""`SetDeleteAfter` for a duration or a cron expression. The
|
|
462
462
|
server rejects both at once; here we catch it before the round trip."""
|
|
463
463
|
if seconds and cron:
|
|
464
464
|
raise ValueError("pass seconds or cron, not both")
|
|
@@ -13,7 +13,7 @@ from typing import NamedTuple
|
|
|
13
13
|
|
|
14
14
|
# gRPC front door, over TLS. The proxy still accepts plain HTTP/2 on the same
|
|
15
15
|
# port for older clients, so an explicit http:// base_url keeps working against
|
|
16
|
-
# any cluster
|
|
16
|
+
# any cluster.
|
|
17
17
|
DEFAULT_BASE_URL = "https://boxd.sh:9443"
|
|
18
18
|
|
|
19
19
|
EXCHANGE_PATH = "/api/v1/auth/token"
|
|
@@ -40,7 +40,7 @@ def resolve_base_url(explicit: str | None) -> str | None:
|
|
|
40
40
|
def console_base_url(base_url: str) -> str:
|
|
41
41
|
"""Map a gRPC base URL to the web-console origin that fronts `/api/v1/...`.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Same mapping as the `boxd` CLI uses, with one
|
|
44
44
|
deliberate difference: the port is dropped. The CLI's input is a bare
|
|
45
45
|
`https://boxd.sh`, but the SDK's default carries the gRPC port
|
|
46
46
|
(`:9443`) — preserving it would aim the exchange at the gRPC listener. A
|
|
@@ -77,7 +77,7 @@ class MachineAccess(BaseModel):
|
|
|
77
77
|
the client has no endpoint to derive the console origin from. Opening it
|
|
78
78
|
needs a signed-in browser; the machine is woken if it is asleep."""
|
|
79
79
|
custom_domains: list[CustomDomain]
|
|
80
|
-
"""Per-VM custom domains bound to this machine
|
|
80
|
+
"""Per-VM custom domains bound to this machine, e.g.
|
|
81
81
|
`app.example.com`. Distinct from `domain` above, which is the org's
|
|
82
82
|
wildcard domain or the cluster zone. Empty if none are bound."""
|
|
83
83
|
|
|
@@ -42,7 +42,7 @@ from ..models import (
|
|
|
42
42
|
)
|
|
43
43
|
from . import Namespace
|
|
44
44
|
|
|
45
|
-
# ── delete_self
|
|
45
|
+
# ── delete_self ────────────────────────────────────────────
|
|
46
46
|
|
|
47
47
|
_SELF_DESTROY_PATH = "/v1/self/destroy"
|
|
48
48
|
_SELF_DELETE_TIMEOUT_SECS = 30.0
|
|
@@ -209,7 +209,7 @@ def _exec_init(
|
|
|
209
209
|
# Same reasoning as `env`: no cwd field on ExecChunk, so `cd` ahead of
|
|
210
210
|
# the command through the same shell. `&&` means a `cwd` that doesn't
|
|
211
211
|
# exist fails fast with `cd`'s own error instead of silently running
|
|
212
|
-
# in the default directory
|
|
212
|
+
# in the default directory.
|
|
213
213
|
line = f"cd {shlex.quote(cwd)} && {line}"
|
|
214
214
|
return req.exec_chunk(
|
|
215
215
|
machine_id=machine_id, command=line, tty=tty, cols=cols, rows=rows
|
|
@@ -378,7 +378,7 @@ class Machines(Namespace):
|
|
|
378
378
|
created = self._transport.call(method, request)
|
|
379
379
|
self._apply_delete_after(created.vm_id, delete_after, delete_after_cron)
|
|
380
380
|
# Non-zero only for a machine made from a snapshot, a team's default
|
|
381
|
-
# one included
|
|
381
|
+
# one included.
|
|
382
382
|
return _with_paused_automations(
|
|
383
383
|
self._reread(created), created.paused_automations, from_snapshot=True
|
|
384
384
|
)
|
|
@@ -400,7 +400,7 @@ class Machines(Namespace):
|
|
|
400
400
|
Raises `NotFoundError` for a destroyed machine's id/name, same as
|
|
401
401
|
`delete()` followed by `list()` omitting it — polling `get(id)`
|
|
402
402
|
after `delete(id)` returns is a reliable way to confirm teardown
|
|
403
|
-
completed
|
|
403
|
+
completed.
|
|
404
404
|
"""
|
|
405
405
|
return m.machine(
|
|
406
406
|
self._transport.call("GetVm", api_pb2.GetVmRequest(vm_id=machine_id)),
|
|
@@ -476,7 +476,7 @@ class Machines(Namespace):
|
|
|
476
476
|
|
|
477
477
|
def delete_self(self, *, confirm: str) -> SelfDeleteResult:
|
|
478
478
|
"""Delete **the machine this code is running on** — call it inside a
|
|
479
|
-
boxd machine when its task is done
|
|
479
|
+
boxd machine when its task is done.
|
|
480
480
|
|
|
481
481
|
This is the SDK twin of ``boxd machine remove --self``, not a plain
|
|
482
482
|
:meth:`delete` of your own id: the same server checks apply, and the
|
|
@@ -1291,7 +1291,7 @@ class AsyncMachines(Namespace):
|
|
|
1291
1291
|
Raises `NotFoundError` for a destroyed machine's id/name, same as
|
|
1292
1292
|
`delete()` followed by `list()` omitting it — polling `get(id)`
|
|
1293
1293
|
after `delete(id)` returns is a reliable way to confirm teardown
|
|
1294
|
-
completed
|
|
1294
|
+
completed.
|
|
1295
1295
|
"""
|
|
1296
1296
|
return m.machine(
|
|
1297
1297
|
await self._transport.call("GetVm", api_pb2.GetVmRequest(vm_id=machine_id)),
|
|
@@ -97,7 +97,7 @@ class Orgs(Namespace):
|
|
|
97
97
|
def remove_member(self, user_id: str, org: str = "") -> None:
|
|
98
98
|
"""Remove a plain member from the org. Requires org admin.
|
|
99
99
|
|
|
100
|
-
Refused for an org owner/admin (
|
|
100
|
+
Refused for an org owner/admin (only the platform can remove those) or for a
|
|
101
101
|
member who still owns machines billed to this org — move them to
|
|
102
102
|
personal billing or destroy them first.
|
|
103
103
|
"""
|
|
@@ -111,7 +111,7 @@ class Orgs(Namespace):
|
|
|
111
111
|
|
|
112
112
|
Requires org admin. Demoting another admin is owner-only (you can
|
|
113
113
|
always demote yourself). Refused for the org owner, whose ownership
|
|
114
|
-
is transferred
|
|
114
|
+
is transferred by the platform instead.
|
|
115
115
|
"""
|
|
116
116
|
self._transport.call(
|
|
117
117
|
"SetOrgMemberRole",
|
|
@@ -462,7 +462,7 @@ class AsyncOrgs(Namespace):
|
|
|
462
462
|
async def remove_member(self, user_id: str, org: str = "") -> None:
|
|
463
463
|
"""Remove a plain member from the org. Requires org admin.
|
|
464
464
|
|
|
465
|
-
Refused for an org owner/admin (
|
|
465
|
+
Refused for an org owner/admin (only the platform can remove those) or for a
|
|
466
466
|
member who still owns machines billed to this org — move them to
|
|
467
467
|
personal billing or destroy them first.
|
|
468
468
|
"""
|
|
@@ -476,7 +476,7 @@ class AsyncOrgs(Namespace):
|
|
|
476
476
|
|
|
477
477
|
Requires org admin. Demoting another admin is owner-only (you can
|
|
478
478
|
always demote yourself). Refused for the org owner, whose ownership
|
|
479
|
-
is transferred
|
|
479
|
+
is transferred by the platform instead.
|
|
480
480
|
"""
|
|
481
481
|
await self._transport.call(
|
|
482
482
|
"SetOrgMemberRole",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boxd
|
|
3
|
-
Version: 0.2.18.
|
|
3
|
+
Version: 0.2.18.dev896
|
|
4
4
|
Summary: Python SDK for the boxd cloud VM platform
|
|
5
5
|
Author: Azin
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://boxd.sh
|
|
8
|
+
Project-URL: Repository, https://github.com/azin-tech/boxd-sdk-python
|
|
9
|
+
Project-URL: Issues, https://github.com/azin-tech/boxd-sdk-python/issues
|
|
8
10
|
Keywords: boxd,vm,microvm,sandbox,compute,grpc,sdk
|
|
9
11
|
Classifier: Development Status :: 4 - Beta
|
|
10
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -137,7 +137,7 @@ def test_both_signals_mint_a_token_and_report_the_cluster(monkeypatch):
|
|
|
137
137
|
return {
|
|
138
138
|
"token": "vm-jwt",
|
|
139
139
|
"expires_at": time.time() + 900,
|
|
140
|
-
"api_url": "http://
|
|
140
|
+
"api_url": "http://staging.example.com:9443",
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
class FakeClient:
|
|
@@ -161,7 +161,7 @@ def test_both_signals_mint_a_token_and_report_the_cluster(monkeypatch):
|
|
|
161
161
|
assert c.token() == "vm-jwt"
|
|
162
162
|
assert seen["url"] == "http://10.42.0.1:9002/v1/auth/token"
|
|
163
163
|
# An unconfigured client learns its cluster from the machine.
|
|
164
|
-
assert c.discovered_base_url == "http://
|
|
164
|
+
assert c.discovered_base_url == "http://staging.example.com:9443"
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
|
|
@@ -173,7 +173,7 @@ def _forked(name):
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
def test_create_sends_idle_ignore_outbound_only_when_asked():
|
|
176
|
-
# `optional` on the wire
|
|
176
|
+
# `optional` on the wire: unset lets a new machine take its
|
|
177
177
|
# org's default, so a defaulted False must never go out.
|
|
178
178
|
transport = FakeTransport(
|
|
179
179
|
[*_created("a"), *_created("b"), *_created("c"), *_created("d")]
|
|
@@ -421,7 +421,7 @@ async def test_async_fork_reports_paused_automations_too():
|
|
|
421
421
|
|
|
422
422
|
|
|
423
423
|
def test_create_from_snapshot_reports_the_automations_it_came_with_paused():
|
|
424
|
-
#
|
|
424
|
+
# A machine from a snapshot is born with its automations paused,
|
|
425
425
|
# like a fork.
|
|
426
426
|
transport = FakeTransport(
|
|
427
427
|
[
|
|
@@ -513,9 +513,8 @@ def test_exec_folds_the_stream_into_one_result():
|
|
|
513
513
|
|
|
514
514
|
|
|
515
515
|
def test_exec_default_encoding_corrupts_invalid_utf8():
|
|
516
|
-
#
|
|
517
|
-
#
|
|
518
|
-
# is about, alongside the encoding="buffer" escape hatch below.
|
|
516
|
+
# The default string mode is lossy — invalid bytes become U+FFFD and
|
|
517
|
+
# the byte length changes. Documents that failure, alongside the encoding="buffer" escape hatch below.
|
|
519
518
|
transport = FakeTransport([[api_pb2.ExecChunk(data=b"\x00\x01\xff\xfe", exit_code=0)]])
|
|
520
519
|
result = Machines(transport).exec("alpha", "printf '\\000\\001\\377\\376'")
|
|
521
520
|
assert result.stdout == "\x00\x01��"
|
|
@@ -969,7 +968,7 @@ def test_orgs_set_get_clear_bot_protection_default():
|
|
|
969
968
|
)
|
|
970
969
|
ns = Orgs(transport)
|
|
971
970
|
|
|
972
|
-
# By default existing machines are changed too
|
|
971
|
+
# By default existing machines are changed too.
|
|
973
972
|
applied = ns.set_bot_protection_default("o1", mode="sleep_only")
|
|
974
973
|
method, request = transport.calls[0]
|
|
975
974
|
assert method == "SetOrgBotProtectionDefault"
|
|
@@ -1037,7 +1036,7 @@ def test_orgs_set_get_clear_idle_ignore_outbound_default():
|
|
|
1037
1036
|
)
|
|
1038
1037
|
ns = Orgs(transport)
|
|
1039
1038
|
|
|
1040
|
-
#
|
|
1039
|
+
# Existing machines change by default. `apply_existing` rides
|
|
1041
1040
|
# along as the opposite of `new_machines_only`, so a server that predates
|
|
1042
1041
|
# `new_machines_only` does the same.
|
|
1043
1042
|
applied = ns.set_idle_ignore_outbound_default("o1", enabled=True)
|
|
@@ -1094,7 +1093,7 @@ def test_orgs_set_get_clear_idle_ignore_outbound_default():
|
|
|
1094
1093
|
assert (unset.is_set, unset.enabled, unset.platform_enabled) == (False, True, True)
|
|
1095
1094
|
|
|
1096
1095
|
# Clearing is the same RPC with `enabled` ABSENT, not False, and asks for
|
|
1097
|
-
# nothing to be applied (
|
|
1096
|
+
# nothing to be applied (an older server refuses that).
|
|
1098
1097
|
ns.clear_idle_ignore_outbound_default("o1")
|
|
1099
1098
|
method, request = transport.calls[5]
|
|
1100
1099
|
assert method == "SetOrgIdleIgnoreOutboundDefault"
|
|
@@ -69,7 +69,7 @@ def _vm_config_on_the_wire(config):
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def test_idle_ignore_outbound_left_unset_when_not_given():
|
|
72
|
-
# `optional` on the wire
|
|
72
|
+
# `optional` on the wire: unset lets a new machine take its
|
|
73
73
|
# org's default, so a defaulted False must never go out.
|
|
74
74
|
config = req.vm_config(vcpu=2)
|
|
75
75
|
assert not config.HasField("idle_ignore_outbound")
|
|
@@ -169,7 +169,7 @@ def test_bearer_token_and_version_header_are_sent():
|
|
|
169
169
|
|
|
170
170
|
def test_explicit_base_url_wins_over_the_discovered_one():
|
|
171
171
|
credentials = FakeCredentials()
|
|
172
|
-
credentials.discovered_base_url = "http://
|
|
172
|
+
credentials.discovered_base_url = "http://staging.example.com:9443"
|
|
173
173
|
t = SyncTransport(
|
|
174
174
|
base_url="http://explicit:9443",
|
|
175
175
|
credentials=credentials,
|
|
@@ -181,11 +181,11 @@ def test_explicit_base_url_wins_over_the_discovered_one():
|
|
|
181
181
|
|
|
182
182
|
def test_discovered_base_url_is_used_when_none_was_given():
|
|
183
183
|
credentials = FakeCredentials()
|
|
184
|
-
credentials.discovered_base_url = "http://
|
|
184
|
+
credentials.discovered_base_url = "http://staging.example.com:9443"
|
|
185
185
|
t = SyncTransport(
|
|
186
186
|
base_url=None, credentials=credentials, timeout=1.0, max_retries=2
|
|
187
187
|
)
|
|
188
|
-
assert t.base_url == "http://
|
|
188
|
+
assert t.base_url == "http://staging.example.com:9443"
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
def test_production_is_the_last_resort():
|
|
@@ -298,7 +298,7 @@ async def _no_sleep(_seconds):
|
|
|
298
298
|
return None
|
|
299
299
|
|
|
300
300
|
|
|
301
|
-
# --- stream wrapping
|
|
301
|
+
# --- stream wrapping ---
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
class FakeStreamCall:
|
|
@@ -56,7 +56,7 @@ def test_empty_string_is_not_a_choice(monkeypatch):
|
|
|
56
56
|
[
|
|
57
57
|
("http://boxd.sh:9443", "https://app.boxd.sh"),
|
|
58
58
|
("https://boxd.sh", "https://app.boxd.sh"),
|
|
59
|
-
("http://
|
|
59
|
+
("http://staging.example.com:9443", "https://app.staging.example.com"),
|
|
60
60
|
("http://boxd.sh:9443", "https://app.boxd.sh"),
|
|
61
61
|
# A path is stripped along with the port.
|
|
62
62
|
("http://boxd.sh:9443/grpc", "https://app.boxd.sh"),
|
|
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
|