toybox-3d-api 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sgarrity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: toybox-3d-api
3
+ Version: 0.1.0
4
+ Summary: Python API client for ToyBox 3D printers (make.toys) via Meteor DDP
5
+ Author: stgarrity
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/stgarrity/toybox-api
8
+ Project-URL: Repository, https://github.com/stgarrity/toybox-api
9
+ Project-URL: Issues, https://github.com/stgarrity/toybox-api/issues
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: aiohttp>=3.9.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # toybox-api — Python Client for ToyBox 3D Printers
20
+
21
+ Async Python client for [ToyBox 3D printers](https://www.toybox.com/) via the [make.toys](https://www.make.toys) Meteor DDP protocol.
22
+
23
+ Used by the [Home Assistant integration](https://github.com/stgarrity/homeassistant-toybox).
24
+
25
+ ## Features
26
+
27
+ - Meteor DDP WebSocket client (no REST — make.toys is DDP-only)
28
+ - Authentication via Meteor accounts
29
+ - Real-time printer state via DDP subscriptions (`printerStates` collection)
30
+ - Print job tracking via `toyPrints` collection
31
+ - Time remaining / elapsed / progress calculations matching make.toys web app logic
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install toybox-api
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ import asyncio
43
+ from toybox_api import ToyBoxClient
44
+
45
+ async def main():
46
+ async with ToyBoxClient() as client:
47
+ await client.connect()
48
+ await client.authenticate("email@example.com", "password")
49
+ await client.subscribe_to_printer_data(["printer_id"])
50
+
51
+ data = await client.get_all_data()
52
+ print(f"Printer: {data.printer.display_name}")
53
+ print(f"Online: {data.printer.is_online}")
54
+ print(f"State: {data.print_state}")
55
+
56
+ if data.current_request:
57
+ print(f"Printing: {data.current_request.print_name}")
58
+ print(f"Remaining: {data.current_request.remaining_seconds}s")
59
+
60
+ asyncio.run(main())
61
+ ```
62
+
63
+ ## Data Model
64
+
65
+ | Class | Source | Description |
66
+ |-------|--------|-------------|
67
+ | `PrinterStatus` | `printerStates` collection | Online state, model, hardware ID, firmware |
68
+ | `PrintRequest` | `toyPrints` collection | Active/completed prints with timing data |
69
+ | `ToyBoxData` | Coordinator container | Combines printer + current/last print request |
70
+
71
+ ## Status
72
+
73
+ 🟡 **Beta** — Implements real Meteor DDP protocol. Needs live testing with active prints.
74
+
75
+ ## Project Structure
76
+
77
+ ```
78
+ ha-toybox/
79
+ ├── toybox_api/
80
+ │ ├── __init__.py
81
+ │ ├── client.py # DDP WebSocket client
82
+ │ ├── models.py # PrinterStatus, PrintRequest, ToyBoxData
83
+ │ ├── exceptions.py # ToyBoxError hierarchy
84
+ │ └── const.py # DDP URLs, subscription/method names
85
+ ├── pyproject.toml
86
+ ├── LICENSE
87
+ └── README.md
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,74 @@
1
+ # toybox-api — Python Client for ToyBox 3D Printers
2
+
3
+ Async Python client for [ToyBox 3D printers](https://www.toybox.com/) via the [make.toys](https://www.make.toys) Meteor DDP protocol.
4
+
5
+ Used by the [Home Assistant integration](https://github.com/stgarrity/homeassistant-toybox).
6
+
7
+ ## Features
8
+
9
+ - Meteor DDP WebSocket client (no REST — make.toys is DDP-only)
10
+ - Authentication via Meteor accounts
11
+ - Real-time printer state via DDP subscriptions (`printerStates` collection)
12
+ - Print job tracking via `toyPrints` collection
13
+ - Time remaining / elapsed / progress calculations matching make.toys web app logic
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install toybox-api
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```python
24
+ import asyncio
25
+ from toybox_api import ToyBoxClient
26
+
27
+ async def main():
28
+ async with ToyBoxClient() as client:
29
+ await client.connect()
30
+ await client.authenticate("email@example.com", "password")
31
+ await client.subscribe_to_printer_data(["printer_id"])
32
+
33
+ data = await client.get_all_data()
34
+ print(f"Printer: {data.printer.display_name}")
35
+ print(f"Online: {data.printer.is_online}")
36
+ print(f"State: {data.print_state}")
37
+
38
+ if data.current_request:
39
+ print(f"Printing: {data.current_request.print_name}")
40
+ print(f"Remaining: {data.current_request.remaining_seconds}s")
41
+
42
+ asyncio.run(main())
43
+ ```
44
+
45
+ ## Data Model
46
+
47
+ | Class | Source | Description |
48
+ |-------|--------|-------------|
49
+ | `PrinterStatus` | `printerStates` collection | Online state, model, hardware ID, firmware |
50
+ | `PrintRequest` | `toyPrints` collection | Active/completed prints with timing data |
51
+ | `ToyBoxData` | Coordinator container | Combines printer + current/last print request |
52
+
53
+ ## Status
54
+
55
+ 🟡 **Beta** — Implements real Meteor DDP protocol. Needs live testing with active prints.
56
+
57
+ ## Project Structure
58
+
59
+ ```
60
+ ha-toybox/
61
+ ├── toybox_api/
62
+ │ ├── __init__.py
63
+ │ ├── client.py # DDP WebSocket client
64
+ │ ├── models.py # PrinterStatus, PrintRequest, ToyBoxData
65
+ │ ├── exceptions.py # ToyBoxError hierarchy
66
+ │ └── const.py # DDP URLs, subscription/method names
67
+ ├── pyproject.toml
68
+ ├── LICENSE
69
+ └── README.md
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=45", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "toybox-3d-api"
7
+ version = "0.1.0"
8
+ description = "Python API client for ToyBox 3D printers (make.toys) via Meteor DDP"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "stgarrity"}
14
+ ]
15
+ dependencies = [
16
+ "aiohttp>=3.9.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=7.0",
22
+ "pytest-asyncio>=0.21.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/stgarrity/toybox-api"
27
+ Repository = "https://github.com/stgarrity/toybox-api"
28
+ Issues = "https://github.com/stgarrity/toybox-api/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python3
2
+ """Minimal connection test - no credentials needed."""
3
+ import asyncio
4
+ import sys
5
+ import os
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
7
+
8
+ from toybox_api.client import ToyBoxClient
9
+
10
+ async def main():
11
+ client = ToyBoxClient()
12
+ await client.connect()
13
+ print("Connected:", client._connected)
14
+ await client.close()
15
+
16
+ asyncio.run(main())
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """Dump raw DDP collection data for debugging."""
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
9
+
10
+ from toybox_api.client import ToyBoxClient
11
+
12
+ async def main():
13
+ # Read creds from file
14
+ with open("/tmp/toybox_creds") as f:
15
+ lines = f.read().strip().split("\n")
16
+ email, password = lines[0], lines[1]
17
+
18
+ client = ToyBoxClient()
19
+ await client.connect()
20
+ await client.authenticate(email, password)
21
+ await client.setup()
22
+
23
+ # Dump ALL collections raw
24
+ for coll_name, docs in client._collections.items():
25
+ print(f"\n{'='*60}")
26
+ print(f"Collection: {coll_name} ({len(docs)} docs)")
27
+ print(f"{'='*60}")
28
+ for doc_id, doc in docs.items():
29
+ print(json.dumps(doc, indent=2, default=str))
30
+
31
+ # Also try the getPrintRequestsByIds method
32
+ users = client._collections.get("users", {})
33
+ for uid, udata in users.items():
34
+ profile = udata.get("profile", {})
35
+ last_print_id = profile.get("last_completed_print")
36
+ if last_print_id:
37
+ print(f"\n{'='*60}")
38
+ print(f"Fetching print request: {last_print_id}")
39
+ print(f"{'='*60}")
40
+ result = await client._call_method(
41
+ "getPrintRequestsByIds",
42
+ [{"requestIds": [last_print_id]}],
43
+ )
44
+ print(json.dumps(result, indent=2, default=str))
45
+
46
+ await client.close()
47
+
48
+ asyncio.run(main())
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """Live test of the ToyBox DDP client.
3
+
4
+ Usage:
5
+ TOYBOX_EMAIL=stgarrity TOYBOX_PASSWORD=xxx python3 tests/test_live.py
6
+ """
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ import os
11
+ import sys
12
+
13
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
14
+ logging.basicConfig(
15
+ level=logging.DEBUG,
16
+ stream=sys.stderr,
17
+ format="%(levelname)s %(name)s: %(message)s",
18
+ )
19
+
20
+ from toybox_api.client import ToyBoxClient
21
+
22
+
23
+ async def test():
24
+ email = os.environ.get("TOYBOX_EMAIL")
25
+ password = os.environ.get("TOYBOX_PASSWORD")
26
+
27
+ # Also try reading from .env file or creds file
28
+ if not email or not password:
29
+ for creds_path in [
30
+ os.path.join(os.path.dirname(__file__), "..", ".env"),
31
+ "/tmp/toybox_creds",
32
+ ]:
33
+ if os.path.exists(creds_path):
34
+ with open(creds_path) as f:
35
+ content = f.read().strip()
36
+ if "=" in content:
37
+ for line in content.split("\n"):
38
+ line = line.strip()
39
+ if "=" in line and not line.startswith("#"):
40
+ k, v = line.split("=", 1)
41
+ os.environ[k] = v
42
+ else:
43
+ lines = content.split("\n")
44
+ if len(lines) >= 2:
45
+ os.environ["TOYBOX_EMAIL"] = lines[0].strip()
46
+ os.environ["TOYBOX_PASSWORD"] = lines[1].strip()
47
+ email = os.environ.get("TOYBOX_EMAIL")
48
+ password = os.environ.get("TOYBOX_PASSWORD")
49
+ if email and password:
50
+ break
51
+
52
+ if not email or not password:
53
+ print("Set TOYBOX_EMAIL and TOYBOX_PASSWORD env vars or create .env file")
54
+ sys.exit(1)
55
+
56
+ client = ToyBoxClient()
57
+ try:
58
+ print("Connecting to DDP...")
59
+ await client.connect()
60
+ print("Connected ✓")
61
+
62
+ print("Authenticating...")
63
+ try:
64
+ await client.authenticate(email, password)
65
+ except Exception:
66
+ # Retry with username-style login
67
+ msg_id = client._next_id()
68
+ future = asyncio.get_event_loop().create_future()
69
+ client._pending[msg_id] = future
70
+ await client._send(
71
+ {
72
+ "msg": "method",
73
+ "method": "login",
74
+ "id": msg_id,
75
+ "params": [
76
+ {"user": {"username": email}, "password": password}
77
+ ],
78
+ }
79
+ )
80
+ result = await asyncio.wait_for(future, timeout=15)
81
+ client._login_token = result.get("token")
82
+ client._user_id = result.get("id")
83
+
84
+ print(f"Authenticated ✓ (user_id={client._user_id})")
85
+
86
+ print("Running setup (discover printers, subscribe)...")
87
+ await client.setup()
88
+ print(f"Printer IDs: {client.printer_ids}")
89
+ print(f"Collections: {list(client._collections.keys())}")
90
+ for coll_name, docs in client._collections.items():
91
+ print(f" {coll_name}: {len(docs)} docs")
92
+
93
+ data = await client.get_all_data()
94
+ print(f"\n=== Printer ===")
95
+ print(f" Name: {data.printer.display_name}")
96
+ print(f" Model: {data.printer.model}")
97
+ print(f" Online: {data.printer.is_online}")
98
+ print(f" State: {data.print_state}")
99
+ print(f" Hardware ID: {data.printer.hardware_id}")
100
+ print(f" Firmware: {data.printer.firmware_version}")
101
+ print(f" Last Completed Print ID: {data.printer.last_completed_print}")
102
+
103
+ if data.current_request:
104
+ r = data.current_request
105
+ print(f"\n=== Current Print ===")
106
+ print(f" Name: {r.print_name}")
107
+ print(f" State: {r.state}")
108
+ print(f" Remaining: {r.remaining_seconds}s")
109
+ print(f" Progress: {r.progress_percent}%")
110
+ else:
111
+ print("\n No active print")
112
+
113
+ if data.last_completed_request:
114
+ r = data.last_completed_request
115
+ print(f"\n=== Last Completed ===")
116
+ print(f" Name: {r.print_name}")
117
+ print(f" State: {r.state} (end_reason={r.end_reason})")
118
+ if r.print_completion_time:
119
+ print(f" Completed at: {r.print_completion_time}")
120
+ else:
121
+ print("\n No last completed in subscriptions")
122
+ if data.printer.last_completed_print:
123
+ print(
124
+ f" Fetching via method call for ID={data.printer.last_completed_print}..."
125
+ )
126
+ details = await client.get_print_request_details(
127
+ [data.printer.last_completed_print]
128
+ )
129
+ for d in details:
130
+ apm = d.get("active_print_model", {})
131
+ print(f" Name: {apm.get('name')}")
132
+ print(
133
+ f" State: {d.get('state')} end_reason={d.get('end_reason')}"
134
+ )
135
+
136
+ print(f"\n=== Raw printerStates ===")
137
+ for doc_id, doc in client._collections.get("printerStates", {}).items():
138
+ print(json.dumps(doc, indent=2, default=str)[:800])
139
+
140
+ print(f"\n=== Raw toyPrints ===")
141
+ for doc_id, doc in client._collections.get("toyPrints", {}).items():
142
+ apm = doc.get("active_print_model", {})
143
+ name = apm.get("name") if isinstance(apm, dict) else None
144
+ print(
145
+ f" {doc_id}: state={doc.get('state')} "
146
+ f"active={doc.get('is_active')} name={name}"
147
+ )
148
+
149
+ # Also dump raw users collection to see printer ID structure
150
+ print(f"\n=== Raw users (printer fields only) ===")
151
+ for doc_id, doc in client._collections.get("users", {}).items():
152
+ filtered = {
153
+ k: v
154
+ for k, v in doc.items()
155
+ if k in ("_id", "printers", "profile", "username", "emails")
156
+ }
157
+ print(json.dumps(filtered, indent=2, default=str)[:600])
158
+
159
+ except Exception:
160
+ import traceback
161
+
162
+ traceback.print_exc()
163
+ finally:
164
+ await client.close()
165
+
166
+
167
+ if __name__ == "__main__":
168
+ asyncio.run(test())
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: toybox-3d-api
3
+ Version: 0.1.0
4
+ Summary: Python API client for ToyBox 3D printers (make.toys) via Meteor DDP
5
+ Author: stgarrity
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/stgarrity/toybox-api
8
+ Project-URL: Repository, https://github.com/stgarrity/toybox-api
9
+ Project-URL: Issues, https://github.com/stgarrity/toybox-api/issues
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: aiohttp>=3.9.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # toybox-api — Python Client for ToyBox 3D Printers
20
+
21
+ Async Python client for [ToyBox 3D printers](https://www.toybox.com/) via the [make.toys](https://www.make.toys) Meteor DDP protocol.
22
+
23
+ Used by the [Home Assistant integration](https://github.com/stgarrity/homeassistant-toybox).
24
+
25
+ ## Features
26
+
27
+ - Meteor DDP WebSocket client (no REST — make.toys is DDP-only)
28
+ - Authentication via Meteor accounts
29
+ - Real-time printer state via DDP subscriptions (`printerStates` collection)
30
+ - Print job tracking via `toyPrints` collection
31
+ - Time remaining / elapsed / progress calculations matching make.toys web app logic
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install toybox-api
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ import asyncio
43
+ from toybox_api import ToyBoxClient
44
+
45
+ async def main():
46
+ async with ToyBoxClient() as client:
47
+ await client.connect()
48
+ await client.authenticate("email@example.com", "password")
49
+ await client.subscribe_to_printer_data(["printer_id"])
50
+
51
+ data = await client.get_all_data()
52
+ print(f"Printer: {data.printer.display_name}")
53
+ print(f"Online: {data.printer.is_online}")
54
+ print(f"State: {data.print_state}")
55
+
56
+ if data.current_request:
57
+ print(f"Printing: {data.current_request.print_name}")
58
+ print(f"Remaining: {data.current_request.remaining_seconds}s")
59
+
60
+ asyncio.run(main())
61
+ ```
62
+
63
+ ## Data Model
64
+
65
+ | Class | Source | Description |
66
+ |-------|--------|-------------|
67
+ | `PrinterStatus` | `printerStates` collection | Online state, model, hardware ID, firmware |
68
+ | `PrintRequest` | `toyPrints` collection | Active/completed prints with timing data |
69
+ | `ToyBoxData` | Coordinator container | Combines printer + current/last print request |
70
+
71
+ ## Status
72
+
73
+ 🟡 **Beta** — Implements real Meteor DDP protocol. Needs live testing with active prints.
74
+
75
+ ## Project Structure
76
+
77
+ ```
78
+ ha-toybox/
79
+ ├── toybox_api/
80
+ │ ├── __init__.py
81
+ │ ├── client.py # DDP WebSocket client
82
+ │ ├── models.py # PrinterStatus, PrintRequest, ToyBoxData
83
+ │ ├── exceptions.py # ToyBoxError hierarchy
84
+ │ └── const.py # DDP URLs, subscription/method names
85
+ ├── pyproject.toml
86
+ ├── LICENSE
87
+ └── README.md
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tests/test_connection.py
5
+ tests/test_dump.py
6
+ tests/test_live.py
7
+ toybox_3d_api.egg-info/PKG-INFO
8
+ toybox_3d_api.egg-info/SOURCES.txt
9
+ toybox_3d_api.egg-info/dependency_links.txt
10
+ toybox_3d_api.egg-info/requires.txt
11
+ toybox_3d_api.egg-info/top_level.txt
12
+ toybox_api/__init__.py
13
+ toybox_api/client.py
14
+ toybox_api/const.py
15
+ toybox_api/exceptions.py
16
+ toybox_api/models.py
@@ -0,0 +1,5 @@
1
+ aiohttp>=3.9.0
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ pytest-asyncio>=0.21.0
@@ -0,0 +1 @@
1
+ toybox_api
@@ -0,0 +1,33 @@
1
+ """ToyBox API client - Python client for make.toys."""
2
+
3
+ from toybox_api.client import ToyBoxClient
4
+ from toybox_api.models import (
5
+ PrinterStatus,
6
+ PrintRequest,
7
+ ActivePrintModel,
8
+ PrintState,
9
+ PrintRequestState,
10
+ ToyBoxData,
11
+ )
12
+ from toybox_api.exceptions import (
13
+ ToyBoxError,
14
+ AuthenticationError,
15
+ ConnectionError,
16
+ APIError,
17
+ SessionExpiredError,
18
+ )
19
+
20
+ __all__ = [
21
+ "ToyBoxClient",
22
+ "PrinterStatus",
23
+ "PrintRequest",
24
+ "ActivePrintModel",
25
+ "PrintState",
26
+ "PrintRequestState",
27
+ "ToyBoxData",
28
+ "ToyBoxError",
29
+ "AuthenticationError",
30
+ "ConnectionError",
31
+ "APIError",
32
+ "SessionExpiredError",
33
+ ]