drukbox-python-sdk 0.0.7__py3-none-any.whl → 0.2.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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: drukbox-python-sdk
3
- Version: 0.0.7
3
+ Version: 0.2.0
4
4
  Summary: Async Python client for Drukbox.
5
5
  Project-URL: Homepage, https://github.com/clawhaven/drukbox-python-sdk
6
6
  Project-URL: Repository, https://github.com/clawhaven/drukbox-python-sdk
@@ -15,10 +15,10 @@ Description-Content-Type: text/markdown
15
15
 
16
16
  Async Python client for the [Drukbox] host API.
17
17
 
18
- The SDK provisions sandbox VMs, reads host state, deletes hosts, and
19
- returns the SSH connection details a caller needs. It speaks HTTP only:
20
- SSH sessions, file transfer, command execution, and retry orchestration
21
- belong in the caller.
18
+ The SDK builds reusable sandbox templates, provisions sandbox VMs, reads host
19
+ state, deletes hosts and templates, and returns the SSH connection details a
20
+ caller needs. It speaks HTTP only: SSH sessions, file transfer, command
21
+ execution, and retry orchestration belong in the caller.
22
22
 
23
23
  ## Install
24
24
 
@@ -70,6 +70,9 @@ Public exports live in `drukbox_sdk`:
70
70
 
71
71
  - `SandboxAPI`
72
72
  - `SandboxHost`
73
+ - `SandboxTemplate`
74
+ - `Secret`
75
+ - `Issuer`
73
76
  - `DoctorReport` and `DoctorCheck`
74
77
  - `HTTPProxy` and `HTTPProxyAttachment`
75
78
  - `SandboxAPIError` and typed subclasses for auth, not found, conflict,
@@ -87,16 +90,54 @@ Supported host operations:
87
90
  - `aclose`
88
91
 
89
92
  `create_host` supports the service's optional `image`, `env`, `expires_at`,
90
- `provider`, `instance_type`, `disk_gb`, and `Idempotency-Key` inputs.
91
- `instance_type` (provider-native size, e.g. `t3.xlarge` / `cx33`) and `disk_gb`
92
- pin the VM shape; omit either for the provider default. `expires_at` mirrors the
93
- wire contract: omit it for the default lease, pass a datetime for an explicit
94
- expiry, or pass `None` for a never-reaped (permanent) host.
93
+ `permanent`, `provider`, `instance_type`, `disk_gb`, `template`, `secrets`,
94
+ and `Idempotency-Key` inputs. `instance_type` (provider-native size, e.g.
95
+ `t3.xlarge` / `cx33`) and `disk_gb` pin the VM shape; omit either for the
96
+ provider default. Omit `expires_at` for the default lease, pass a datetime for
97
+ an explicit expiry, or pass `permanent=True` for a never-reaped host.
98
+ `template` accepts a template ID. An explicit `image` wins over it.
99
+
100
+ `secrets` gives the box credentials it never sees. Each entry is a `Secret`,
101
+ a value the caller holds, or an `Issuer`, a URL Drukbox fetches the value
102
+ from. The box gets a placeholder per entry in the service's variable, and the
103
+ Drukbox swaps it on the way out. A custom service names its `host` and
104
+ `auth_variable`:
105
+
106
+ ```python
107
+ from drukbox_sdk import Issuer, Secret
108
+
109
+ host = await sandbox.create_host(
110
+ secrets={
111
+ "anthropic": Issuer(
112
+ "https://mint.example/box-1/anthropic",
113
+ headers={"Authorization": "Bearer ..."},
114
+ refresh="1h",
115
+ ),
116
+ "openai": Secret("sk-..."),
117
+ "acme": Secret("ak_...", host="api.acme.test", auth_variable="ACME_TOKEN"),
118
+ },
119
+ )
120
+ ```
121
+
122
+ The response carries no secret. A service name Drukbox does not know raises
123
+ `SandboxValidationError`. A deployment without the secrets proxy raises
124
+ `SandboxConflictError`.
95
125
 
96
126
  `renew_host` extends a host's lease via `POST /hosts/{id}/renew`. Omit
97
127
  `expires_at` to extend by the service's default TTL; renewal never makes a host
98
128
  permanent.
99
129
 
130
+ Supported template operations:
131
+
132
+ - `create_template`
133
+ - `get_template`
134
+ - `list_templates`
135
+ - `delete_template`
136
+
137
+ `create_template` returns immediately with a `building` record. Poll
138
+ `get_template` until the status is `available` or `failed`. A failed record
139
+ carries the reason in `last_error`.
140
+
100
141
  Supported HTTP-proxy operations:
101
142
 
102
143
  - `create_http_proxy`
@@ -132,4 +173,4 @@ uv run pytest
132
173
  Tests use `respx` to fake the Drukbox HTTP API. They do not need a
133
174
  real network, VM provider, or Drukbox service.
134
175
 
135
- [Drukbox]: https://github.com/clawhaven/drukbox
176
+ [Drukbox]: https://github.com/czpython/drukbox
@@ -0,0 +1,7 @@
1
+ drukbox_sdk/__init__.py,sha256=7LHAef5BXUaEN_pk9pXeowyV8CuQxWnbghANSZZTcPM,970
2
+ drukbox_sdk/api.py,sha256=tcVaHQom-O2Xq8BbThFTjiRt7zHkWw3a6WYwC4rW3c0,22300
3
+ drukbox_sdk/exceptions.py,sha256=Vzs4zVOywWsWfiZm6vgQ65ScI3Nn7zLGVj-8eT1YAR0,2224
4
+ drukbox_python_sdk-0.2.0.dist-info/METADATA,sha256=3XKJgIsi_BHqaCuNhNaOSa6MSje_67u40BlCATpm3s4,5870
5
+ drukbox_python_sdk-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ drukbox_python_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=zM66rWL3BWGJBa_mZPnM0WUomSSZ1xtWVtGQovQ71M8,1078
7
+ drukbox_python_sdk-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
drukbox_sdk/__init__.py CHANGED
@@ -9,8 +9,11 @@ from .api import (
9
9
  DoctorReport,
10
10
  HTTPProxy,
11
11
  HTTPProxyAttachment,
12
+ Issuer,
12
13
  SandboxAPI,
13
14
  SandboxHost,
15
+ SandboxTemplate,
16
+ Secret,
14
17
  )
15
18
  from .exceptions import (
16
19
  SandboxAPIError,
@@ -28,6 +31,7 @@ __all__ = [
28
31
  "DoctorReport",
29
32
  "HTTPProxy",
30
33
  "HTTPProxyAttachment",
34
+ "Issuer",
31
35
  "SandboxAPI",
32
36
  "SandboxAPIError",
33
37
  "SandboxAuthError",
@@ -36,6 +40,8 @@ __all__ = [
36
40
  "SandboxNotFoundError",
37
41
  "SandboxProvisioningError",
38
42
  "SandboxResponseError",
43
+ "SandboxTemplate",
39
44
  "SandboxUnavailableError",
40
45
  "SandboxValidationError",
46
+ "Secret",
41
47
  ]
drukbox_sdk/api.py CHANGED
@@ -26,13 +26,20 @@ Usage::
26
26
  await sandbox.delete_host(host.id)
27
27
  await sandbox.aclose()
28
28
 
29
+ Templates bake a setup script into a reusable image; hosts fork from it::
30
+
31
+ template = await sandbox.create_template(setup_script=script)
32
+ while template.status == "building":
33
+ template = await sandbox.get_template(template.id)
34
+ host = await sandbox.create_host(template=template.id)
35
+
29
36
  For env-backed config use :meth:`SandboxAPI.from_env`.
30
37
  """
31
38
 
32
39
  import asyncio
33
40
  import os
34
41
  import uuid
35
- from dataclasses import dataclass
42
+ from dataclasses import dataclass, field
36
43
  from datetime import datetime
37
44
  from typing import Any, Self
38
45
 
@@ -58,22 +65,6 @@ _SANDBOX_HTTP_LIMITS = httpx.Limits(
58
65
  )
59
66
 
60
67
 
61
- class _Unset:
62
- """Sentinel distinguishing an omitted argument from an explicit ``None``.
63
-
64
- ``create_host`` mirrors the service's tri-state ``expires_at``: an
65
- omitted field means "default lease", an explicit ``null`` means
66
- "permanent". A plain ``datetime | None`` default can't tell those two
67
- apart, so the omitted case uses this sentinel.
68
- """
69
-
70
- def __repr__(self) -> str:
71
- return "UNSET"
72
-
73
-
74
- _UNSET = _Unset()
75
-
76
-
77
68
  @dataclass(frozen=True)
78
69
  class SandboxHost:
79
70
  """Snapshot of a provisioned host as returned by the service.
@@ -117,6 +108,73 @@ class SandboxHost:
117
108
  expires_at: str | None
118
109
 
119
110
 
111
+ @dataclass(frozen=True, kw_only=True)
112
+ class _Service:
113
+ """The custom service fields of a secret. None is unset. An empty
114
+ ``auth_prefix`` is set, and Drukbox reads it as no prefix."""
115
+
116
+ host: str | None = None
117
+ auth_variable: str | None = None
118
+ auth_header: str | None = None
119
+ auth_prefix: str | None = None
120
+
121
+ def get_service(self) -> dict[str, Any]:
122
+ fields = {
123
+ "host": self.host,
124
+ "auth_variable": self.auth_variable,
125
+ "auth_header": self.auth_header,
126
+ "auth_prefix": self.auth_prefix,
127
+ }
128
+ return {name: value for name, value in fields.items() if value is not None}
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class Secret(_Service):
133
+ """A credential the caller holds."""
134
+
135
+ value: str = field(repr=False)
136
+
137
+ def entry(self) -> dict[str, Any]:
138
+ return {**self.get_service(), "value": self.value}
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class Issuer(_Service):
143
+ """A credential Drukbox fetches from ``url`` with ``headers``, and again
144
+ every ``refresh`` when the answer carries no ``expires_at``."""
145
+
146
+ url: str
147
+ headers: dict[str, str] = field(repr=False)
148
+ refresh: str
149
+
150
+ def entry(self) -> dict[str, Any]:
151
+ issuer = {"url": self.url, "headers": dict(self.headers), "refresh": self.refresh}
152
+ return {**self.get_service(), "issuer": issuer}
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class SandboxTemplate:
157
+ """Snapshot of a reusable sandbox template as returned by the service.
158
+
159
+ ``status`` moves from ``"building"`` to ``"available"`` or
160
+ ``"failed"``. While the build runs, ``image`` is empty. A failed
161
+ build carries its reason in ``last_error``. The record has no
162
+ ``setup_script`` because the service never echoes it back.
163
+ """
164
+
165
+ id: str
166
+ provider: str
167
+ base_image: str
168
+ setup_script_hash: str
169
+ label: str
170
+ image: str
171
+ status: str
172
+ last_error: str
173
+ created_at: str
174
+ updated_at: str
175
+ last_used_at: str | None
176
+
177
+
120
178
  @dataclass(frozen=True)
121
179
  class DoctorCheck:
122
180
  """One dependency probe. ``hint`` is a remediation slug, set only on failures."""
@@ -215,20 +273,19 @@ class SandboxAPI:
215
273
  timeout = float(os.environ.get(f"{prefix}SERVICE_TIMEOUT", "300"))
216
274
  return cls(base_url=url, token=token, timeout=timeout)
217
275
 
218
- # ------------------------------------------------------------------
219
- # Host lifecycle
220
- # ------------------------------------------------------------------
221
-
222
276
  async def create_host(
223
277
  self,
224
278
  *,
225
279
  disk_gb: int | None = None,
226
280
  env: dict[str, str] | None = None,
227
- expires_at: datetime | None | _Unset = _UNSET,
281
+ expires_at: datetime | None = None,
228
282
  idempotency_key: str | None = None,
229
283
  image: str | None = None,
230
284
  instance_type: str | None = None,
285
+ permanent: bool = False,
231
286
  provider: str | None = None,
287
+ secrets: dict[str, Secret | Issuer] | None = None,
288
+ template: uuid.UUID | str | None = None,
232
289
  ) -> SandboxHost:
233
290
  """Provision a new host.
234
291
 
@@ -248,45 +305,58 @@ class SandboxAPI:
248
305
  use the service default. An unknown provider raises
249
306
  :class:`SandboxResponseError` (the service rejects it with 400).
250
307
 
308
+ ``template`` names a template by its ID, from
309
+ :meth:`create_template` or :meth:`get_template`. An explicit
310
+ ``image`` wins when you pass both. A template that is not
311
+ ``available`` raises :class:`SandboxConflictError`. An unknown
312
+ ID raises :class:`SandboxResponseError`.
313
+
251
314
  ``instance_type`` (provider-native size, e.g. ``t3.xlarge`` /
252
315
  ``cx33``) and ``disk_gb`` (root disk size) pin the VM shape; omit
253
316
  either to use the provider's configured default. A size the active
254
317
  provider can't serve raises :class:`SandboxResponseError` (400).
255
318
 
256
- Lease (mirrors the wire contract's tri-state ``expires_at``): omit
257
- it for the service's default lease, pass a ``datetime`` for an
258
- explicit expiry, or pass ``None`` for a never-reaped (permanent)
259
- host — exactly as an omitted field vs. an explicit ``null`` behave
260
- on the API.
319
+ Lease: omit ``expires_at`` for the service's default lease, pass a
320
+ ``datetime`` for an explicit expiry, or pass ``permanent=True`` for
321
+ a host the janitor never reaps.
322
+
323
+ ``secrets`` maps a service name to a :class:`Secret` or an
324
+ :class:`Issuer`. The response carries no secret.
261
325
  """
262
326
 
327
+ if expires_at and permanent:
328
+ raise ValueError("a permanent host cannot carry an expires_at")
329
+
263
330
  payload: dict[str, Any] = {}
264
331
  if disk_gb is not None:
265
332
  payload["disk_gb"] = disk_gb
266
333
  if env is not None:
267
334
  payload["env"] = env
268
- # Sentinel default lets us tell "caller omitted expires_at" (default
269
- # lease) from "caller passed None" (explicit null → permanent host).
270
- if not isinstance(expires_at, _Unset):
271
- payload["expires_at"] = None if expires_at is None else expires_at.isoformat()
335
+ if expires_at:
336
+ payload["expires_at"] = expires_at.isoformat()
337
+ if permanent:
338
+ # An explicit null on the wire is the opt-out from expiry.
339
+ payload["expires_at"] = None
272
340
  if image is not None:
273
341
  payload["image"] = image
274
342
  if instance_type is not None:
275
343
  payload["instance_type"] = instance_type
276
344
  if provider is not None:
277
345
  payload["provider"] = provider
346
+ if secrets:
347
+ payload["secrets"] = {name: entry.entry() for name, entry in secrets.items()}
348
+ if template is not None:
349
+ payload["template"] = str(template)
278
350
 
279
351
  headers: dict[str, str] = {}
280
352
  if idempotency_key is not None:
281
353
  headers["Idempotency-Key"] = idempotency_key
282
354
 
283
355
  data = await self._request("POST", "/hosts", json=payload, headers=headers)
284
- assert isinstance(data, dict)
285
356
  return SandboxHost(**data)
286
357
 
287
358
  async def get_host(self, host_id: uuid.UUID | str) -> SandboxHost:
288
359
  data = await self._request("GET", f"/hosts/{host_id}")
289
- assert isinstance(data, dict)
290
360
  return SandboxHost(**data)
291
361
 
292
362
  async def attach(self, host_id: uuid.UUID | str) -> SandboxHost:
@@ -303,7 +373,6 @@ class SandboxAPI:
303
373
 
304
374
  async def list_hosts(self) -> list[SandboxHost]:
305
375
  data = await self._request("GET", "/hosts")
306
- assert isinstance(data, list)
307
376
  return [SandboxHost(**item) for item in data]
308
377
 
309
378
  async def renew_host(
@@ -327,12 +396,61 @@ class SandboxAPI:
327
396
  if expires_at is not None:
328
397
  payload["expires_at"] = expires_at.isoformat()
329
398
  data = await self._request("POST", f"/hosts/{host_id}/renew", json=payload)
330
- assert isinstance(data, dict)
331
399
  return SandboxHost(**data)
332
400
 
333
401
  async def delete_host(self, host_id: uuid.UUID | str) -> None:
334
402
  await self._request("DELETE", f"/hosts/{host_id}")
335
403
 
404
+ async def create_template(
405
+ self,
406
+ *,
407
+ setup_script: str,
408
+ provider: str | None = None,
409
+ base_image: str | None = None,
410
+ label: str | None = None,
411
+ ) -> SandboxTemplate:
412
+ """Start a template build and return immediately.
413
+
414
+ The service responds 202 with ``status="building"``. Poll
415
+ :meth:`get_template` until the status becomes ``"available"`` or
416
+ ``"failed"``. A failed record carries the reason in
417
+ ``last_error``. A repeated post with the same provider, base
418
+ image, and setup script returns the existing record without a
419
+ rebuild.
420
+ """
421
+
422
+ payload: dict[str, Any] = {"setup_script": setup_script}
423
+ if provider is not None:
424
+ payload["provider"] = provider
425
+ if base_image is not None:
426
+ payload["base_image"] = base_image
427
+ if label is not None:
428
+ payload["label"] = label
429
+
430
+ data = await self._request("POST", "/templates", json=payload)
431
+ return SandboxTemplate(**data)
432
+
433
+ async def get_template(self, template_id: uuid.UUID | str) -> SandboxTemplate:
434
+ """Fetch one template. Callers poll this after :meth:`create_template`."""
435
+
436
+ data = await self._request("GET", f"/templates/{template_id}")
437
+ return SandboxTemplate(**data)
438
+
439
+ async def list_templates(self) -> list[SandboxTemplate]:
440
+ """List templates newest first."""
441
+
442
+ data = await self._request("GET", "/templates")
443
+ return [SandboxTemplate(**item) for item in data]
444
+
445
+ async def delete_template(self, template_id: uuid.UUID | str) -> None:
446
+ """Delete a template.
447
+
448
+ A template whose status is ``building`` raises
449
+ :class:`SandboxConflictError`.
450
+ """
451
+
452
+ await self._request("DELETE", f"/templates/{template_id}")
453
+
336
454
  async def doctor(self) -> DoctorReport:
337
455
  """Fetch ``GET /doctor`` — read-only dependency health.
338
456
 
@@ -341,7 +459,6 @@ class SandboxAPI:
341
459
  """
342
460
 
343
461
  data = await self._request("GET", "/doctor")
344
- assert isinstance(data, dict)
345
462
  return DoctorReport(
346
463
  ok=data["ok"],
347
464
  active_provider=data["active_provider"],
@@ -349,10 +466,6 @@ class SandboxAPI:
349
466
  checks=[DoctorCheck(**check) for check in data["checks"]],
350
467
  )
351
468
 
352
- # ------------------------------------------------------------------
353
- # HTTP proxies
354
- # ------------------------------------------------------------------
355
-
356
469
  async def create_http_proxy(
357
470
  self,
358
471
  *,
@@ -371,7 +484,6 @@ class SandboxAPI:
371
484
 
372
485
  payload: dict[str, Any] = {"name": name, "target": target, "headers": headers}
373
486
  data = await self._request("POST", "/http-proxies", json=payload)
374
- assert isinstance(data, dict)
375
487
  return HTTPProxy(**data)
376
488
 
377
489
  async def delete_http_proxy(self, name: str) -> None:
@@ -393,7 +505,6 @@ class SandboxAPI:
393
505
  """
394
506
 
395
507
  data = await self._request("POST", f"/http-proxies/{name}/hosts/{host_id}")
396
- assert isinstance(data, dict)
397
508
  return HTTPProxyAttachment(**data)
398
509
 
399
510
  async def detach_http_proxy(self, name: str, host_id: uuid.UUID | str) -> None:
@@ -403,15 +514,10 @@ class SandboxAPI:
403
514
  await self._request("DELETE", f"/http-proxies/{name}/hosts/{host_id}")
404
515
 
405
516
  async def aclose(self) -> None:
406
- if self._client is None:
407
- return
408
- await self._client.aclose()
409
- self._client = None
410
- self._client_loop = None
411
-
412
- # ------------------------------------------------------------------
413
- # Internals
414
- # ------------------------------------------------------------------
517
+ if self._client:
518
+ await self._client.aclose()
519
+ self._client = None
520
+ self._client_loop = None
415
521
 
416
522
  async def _request(
417
523
  self,
@@ -420,13 +526,13 @@ class SandboxAPI:
420
526
  *,
421
527
  headers: dict[str, str] | None = None,
422
528
  json: dict[str, Any] | None = None,
423
- ) -> dict[str, Any] | list[Any]:
529
+ ) -> Any:
424
530
  client = await self._get_client()
425
531
  request_headers = {
426
532
  "Authorization": f"Bearer {self.token}",
427
533
  "Accept": "application/json",
428
534
  }
429
- if headers is not None:
535
+ if headers:
430
536
  request_headers.update(headers)
431
537
 
432
538
  try:
@@ -466,9 +572,8 @@ class SandboxAPI:
466
572
  if response.status_code == 422:
467
573
  # FastAPI's 422 detail is a list of error dicts; join the messages
468
574
  # so it reads as a line, not a raw list repr.
469
- detail = json_response.get("detail", "validation error")
470
- if isinstance(detail, list):
471
- detail = "; ".join(item.get("msg", "") for item in detail) or "validation error"
575
+ errors = json_response.get("detail") or []
576
+ detail = "; ".join(error.get("msg", "") for error in errors) or "validation error"
472
577
  raise SandboxValidationError(detail)
473
578
 
474
579
  if response.status_code >= 400:
@@ -477,20 +582,16 @@ class SandboxAPI:
477
582
 
478
583
  async def _get_client(self) -> httpx.AsyncClient:
479
584
  running_loop = asyncio.get_running_loop()
480
- # Fast-path: client exists and is bound to the current loop.
481
- if self._client is not None and self._client_loop is running_loop:
585
+ if self._client and self._client_loop is running_loop:
482
586
  return self._client
483
- # Slow-path: either no client yet, or the client is bound to a
484
- # stale loop (e.g. fixture teardown). Serialize through the
485
- # lock.
486
- if self._client_lock is None:
587
+ if not self._client_lock:
487
588
  self._client_lock = asyncio.Lock()
488
589
  async with self._client_lock:
489
590
  # Re-check under the lock; another coroutine may have raced
490
591
  # ahead.
491
- if self._client is not None and self._client_loop is running_loop:
592
+ if self._client and self._client_loop is running_loop:
492
593
  return self._client
493
- if self._client is not None:
594
+ if self._client:
494
595
  # Stale-loop client: closing on its own loop is unsafe,
495
596
  # so drop the reference and rely on GC. httpx will emit
496
597
  # an "unclosed client" warning, which is the correct
drukbox_sdk/exceptions.py CHANGED
@@ -37,8 +37,8 @@ class SandboxNotFoundError(SandboxAPIError):
37
37
  class SandboxConflictError(SandboxAPIError):
38
38
  """409 — the operation conflicts with the current state.
39
39
 
40
- e.g. provisioning a Linux user on a host that already has one with
41
- the same name.
40
+ e.g. a template that is still building, or secrets on a deployment
41
+ without the secrets proxy configured.
42
42
  """
43
43
 
44
44
 
@@ -53,9 +53,10 @@ class SandboxProvisioningError(SandboxAPIError):
53
53
  class SandboxValidationError(SandboxAPIError):
54
54
  """422 — the service rejected the request payload as invalid.
55
55
 
56
- e.g. an env key that isn't a valid environment-variable name, or a
57
- proxy target URL carrying a path or credentials. The request is
58
- malformed, so retrying it unchanged won't help — fix the input.
56
+ e.g. an env key that isn't a valid environment-variable name, a
57
+ proxy target URL carrying a path or credentials, or a secret for a
58
+ service name Drukbox does not know. The request is malformed, so
59
+ retrying it unchanged won't help — fix the input.
59
60
  """
60
61
 
61
62
 
@@ -1,7 +0,0 @@
1
- drukbox_sdk/__init__.py,sha256=pMksdq5D6wBPVQH1PEfMQwXxwyRN7F8Py6ShUQst3Sg,874
2
- drukbox_sdk/api.py,sha256=SZFo2DhDGbbdENBhdS1yNxvA4QkIAySuGfUJkn9as2s,19123
3
- drukbox_sdk/exceptions.py,sha256=YecxsuwpHhwzgw-_h9KbXf1WqY6R0JIJrwPY3Eg4e6I,2146
4
- drukbox_python_sdk-0.0.7.dist-info/METADATA,sha256=X8ym547ccA8-8Bw_WuldPHmi2Qrtg8Tk9d1seJCepxg,4523
5
- drukbox_python_sdk-0.0.7.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
6
- drukbox_python_sdk-0.0.7.dist-info/licenses/LICENSE,sha256=zM66rWL3BWGJBa_mZPnM0WUomSSZ1xtWVtGQovQ71M8,1078
7
- drukbox_python_sdk-0.0.7.dist-info/RECORD,,