pybricks-cli 1.0.0b1__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.
pb_cli/ui.py ADDED
@@ -0,0 +1,454 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 The Pybricks Authors
3
+
4
+ """What the commands put on screen.
5
+
6
+ Everything a command draws goes through here, for two reasons. Commands stay
7
+ thin, which is what keeps them readable as "ask the service, then say what
8
+ happened"; and the whole of the tool's appearance can be redirected at once,
9
+ which is how :mod:`tools.screenshot` records a real session for the README.
10
+
11
+ One rule governs all of it. **Data goes to stdout and everything else goes to
12
+ stderr** -- progress, spinners, tables meant to be looked at rather than
13
+ parsed, prompts, errors. That single split is what keeps ``pb list | jq`` and
14
+ ``pb scan > hubs.txt`` working, and it is why :func:`console` takes ``err``
15
+ rather than being one console everything shares.
16
+
17
+ The second rule is that anything drawn with colour must also read without it.
18
+ A terminal that strips styling, a log file, a reader who cannot tell green from
19
+ red: none of them should lose information. So the signal meter varies its
20
+ characters and not only its colour, and the chooser marks its selected row with
21
+ an arrow as well as a style.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import contextlib
27
+ from collections.abc import Collection, Generator, Iterable, Mapping
28
+ from contextlib import AbstractContextManager
29
+ from typing import Final
30
+
31
+ from rich.console import Console, RenderableType
32
+ from rich.live import Live
33
+ from rich.table import Table
34
+ from rich.text import Text
35
+
36
+ import pb_client
37
+ from pb_cli import errors
38
+
39
+ __all__ = [
40
+ "HUB_WARNINGS",
41
+ "SELECTED_MARK",
42
+ "SHOW_HUB_IDS",
43
+ "SIGNAL_BLOCKS",
44
+ "SIGNAL_EMPTY",
45
+ "chooser_table",
46
+ "connected_table",
47
+ "console",
48
+ "hub_warnings",
49
+ "live",
50
+ "plain",
51
+ "rows",
52
+ "scan_table",
53
+ "signal_bars",
54
+ "spinner",
55
+ "usage",
56
+ ]
57
+
58
+ SHOW_HUB_IDS: bool = False
59
+ """Whether to print hub ids.
60
+
61
+ Off for now. A hub id is built from its Bluetooth address, which is not stable
62
+ -- and on macOS is a per-machine UUID standing in for one -- so printing it
63
+ invites users to write down something that will stop working. Ids remain in the
64
+ wire protocol and are still accepted as arguments; only the display is off.
65
+
66
+ Turn this on once a hub has a durable identifier worth showing -- see "Wanted
67
+ from the firmware" in the design notes, since the hub does not have one yet.
68
+ Annotated as ``bool`` rather than left to inference so that the code behind it
69
+ keeps being type-checked while it is switched off.
70
+ """
71
+
72
+ SIGNAL_BLOCKS = "▁▃▅▇"
73
+ """The signal meter, one character per level, rising left to right."""
74
+
75
+ SIGNAL_EMPTY = "·"
76
+ """What stands in for a bar that is not lit.
77
+
78
+ A dim copy of the block would say the same thing in colour alone, which is no
79
+ use to a colour-blind reader or to anything that strips styling. A dot is
80
+ plainly not a bar.
81
+ """
82
+
83
+ SELECTED_MARK = "▸"
84
+ """Marks the row a chooser is on, so the selection reads without colour."""
85
+
86
+ _SIGNAL_FLOORS = (-90, -80, -67, -55)
87
+ """How many dBm each level needs, weakest first.
88
+
89
+ Bluetooth reports signal strength as a negative number of dBm, roughly -30 for
90
+ a hub on the desk down to about -100 for one that can barely be heard. The
91
+ boundaries are the ones Wi-Fi tools have long used, which put a hub in the same
92
+ room at four bars and one through a wall at two. Anything at or below the first
93
+ floor still gets one bar rather than none: it was heard, so it is there.
94
+ """
95
+
96
+ HUB_WARNINGS: Final[Mapping[str, str]] = {
97
+ "BATTERY_LOW_VOLTAGE_WARNING": "battery low",
98
+ "BATTERY_LOW_VOLTAGE_SHUTDOWN": "battery empty, the hub is switching off",
99
+ "BATTERY_HIGH_CURRENT": "drawing too much current",
100
+ "BATTERY_HIGH_TEMP_WARNING": "battery hot",
101
+ "BATTERY_HIGH_TEMP_SHUTDOWN": "battery too hot, the hub is switching off",
102
+ "BLE_LOW_SIGNAL": "weak Bluetooth signal",
103
+ "SHUTDOWN_REQUEST": "switching off",
104
+ "SHUTDOWN": "switching off",
105
+ }
106
+ """Hub status flags that are worth saying out loud, in the user's words.
107
+
108
+ Keyed on the wire name of the flag rather than on the bit, because the CLI
109
+ deliberately knows nothing about the hub protocol. Flags that describe a normal
110
+ state -- advertising, connected, a program running -- are missing on purpose:
111
+ they are either uninteresting or already a line of their own.
112
+ """
113
+
114
+
115
+ def console(*, err: bool = False) -> Console:
116
+ """Build a console for data (stdout) or for everything else (stderr).
117
+
118
+ Args:
119
+ err: True for the console that progress, prompts and messages go to.
120
+
121
+ Returns:
122
+ A fresh console. Fresh rather than shared so that a caller can be
123
+ given a different one -- which is how the session in the README is
124
+ recorded.
125
+ """
126
+ # Highlighting off. Rich's default highlighter reads a string as a Python
127
+ # repr and colours what it finds there, which is right for a value dumped
128
+ # to a terminal and wrong for a sentence: "firmware 4.1.0b3-134-gb4bd99edd"
129
+ # came out with "4.1" and "134" in cyan and everything between them plain,
130
+ # as though the version were two numbers with debris around them, and every
131
+ # bracketed aside was drawn in bold. Nothing printed here is a repr -- data
132
+ # goes to stdout through `plain` and `rows`, which do not use print at all
133
+ # -- so any emphasis in this output should be markup we chose.
134
+ return Console(stderr=err, highlight=False)
135
+
136
+
137
+ @contextlib.contextmanager
138
+ def _null() -> Generator[None]:
139
+ """Stand in for a spinner when output is not a terminal."""
140
+ yield
141
+
142
+
143
+ def spinner(err: Console, message: str) -> AbstractContextManager[object]:
144
+ """Show progress while a step runs.
145
+
146
+ Args:
147
+ err: The stderr console. Progress is never data.
148
+ message: What is happening, in the present tense.
149
+
150
+ Returns:
151
+ A context manager, which does nothing at all when stderr is not a
152
+ terminal -- a spinner in a log file is just escape codes.
153
+ """
154
+ return err.status(message) if err.is_terminal else _null()
155
+
156
+
157
+ def live(renderable: RenderableType, target: Console) -> Live:
158
+ """Open a display that is redrawn in place.
159
+
160
+ Args:
161
+ renderable: The first frame.
162
+ target: Where to draw.
163
+
164
+ Returns:
165
+ The display. Manual refresh, because every caller here redraws in
166
+ response to something arriving rather than on a clock.
167
+ """
168
+ return Live(renderable, console=target, auto_refresh=False)
169
+
170
+
171
+ def usage(message: str, hint: str | None = None) -> int:
172
+ """Report a problem with the command line itself.
173
+
174
+ Presented like any other expected failure -- one sentence and a suggestion
175
+ -- rather than through a cyclopts group validator, whose wording ("Invalid
176
+ values for group Program...") is written for whoever wrote the CLI rather
177
+ than for whoever is using it.
178
+
179
+ Args:
180
+ message: What is wrong with the arguments.
181
+ hint: What to type instead.
182
+
183
+ Returns:
184
+ The process exit code.
185
+ """
186
+ return errors.render(console(err=True), [errors.Problem(message, hint)])
187
+
188
+
189
+ _TRANSPORT_NAMES: Final[Mapping[str, str]] = {
190
+ "ble": "Bluetooth",
191
+ "rfcomm": "Bluetooth Classic",
192
+ "usb": "USB",
193
+ "fake": "simulated",
194
+ }
195
+ """What to call each way of reaching a hub.
196
+
197
+ The service's words are ``ble``, ``rfcomm`` and ``usb``, which are the right
198
+ names on the wire and the wrong ones on a screen: nobody calls it BLE or RFCOMM
199
+ except us. Classic is Bluetooth's own name for the older radio the EV3 and NXT
200
+ use, and it keeps the two apart where both are in use.
201
+ """
202
+
203
+
204
+ def transport_name(transport: str) -> str | None:
205
+ """Say how a hub is reached, in a user's words.
206
+
207
+ Args:
208
+ transport: What the service reported.
209
+
210
+ Returns:
211
+ The wording, or ``None`` if the service said nothing -- which is what an
212
+ older service looks like, and is a reason to leave the line out rather
213
+ than to show an empty one. An unrecognised transport is passed through
214
+ as it came, since a name we do not have is still better than no answer.
215
+ """
216
+ if not transport:
217
+ return None
218
+
219
+ return _TRANSPORT_NAMES.get(transport, transport)
220
+
221
+
222
+ def hub_warnings(status_names: Iterable[str]) -> list[str]:
223
+ """Translate the hub's status flags into things worth telling the user.
224
+
225
+ Args:
226
+ status_names: Flag names as the hub reported them.
227
+
228
+ Returns:
229
+ The wording for each flag that says something actionable, in the order
230
+ given and without repeats -- two flags can mean the same thing to a
231
+ user, and saying it twice would look like a fault in the tool.
232
+ """
233
+ said: list[str] = []
234
+
235
+ for name in status_names:
236
+ warning = HUB_WARNINGS.get(name)
237
+ if warning is not None and warning not in said:
238
+ said.append(warning)
239
+
240
+ return said
241
+
242
+
243
+ def signal_bars(rssi: int | None) -> str:
244
+ """Draw a signal strength as a bar chart, in Rich markup.
245
+
246
+ Args:
247
+ rssi: The received signal strength in dBm, as Bluetooth reports it, or
248
+ ``None`` when the transport does not measure one.
249
+
250
+ Returns:
251
+ Four characters wide however strong the signal, so that rows stay
252
+ aligned. A number in dBm means nothing to most people, but "how many
253
+ bars" is something everyone already reads off a phone.
254
+ """
255
+
256
+ if rssi is None:
257
+ return f"[dim]{SIGNAL_EMPTY * len(SIGNAL_BLOCKS)}[/]"
258
+
259
+ # At least one bar: the hub was heard, so it is out there somewhere.
260
+ level = max(1, sum(1 for floor in _SIGNAL_FLOORS if rssi >= floor))
261
+ style = "green" if level >= 3 else "yellow" if level == 2 else "red"
262
+ filled = SIGNAL_BLOCKS[:level]
263
+ empty = SIGNAL_EMPTY * (len(SIGNAL_BLOCKS) - level)
264
+ return f"[{style}]{filled}[/][dim]{empty}[/]"
265
+
266
+
267
+ def plain(out: Console, line: str) -> None:
268
+ """Write one line to be read by another program, exactly as given.
269
+
270
+ Not through :meth:`rich.console.Console.print`, which is for text meant for
271
+ a person. Rich expands tabs as it renders and wraps at the console width --
272
+ 80 columns when it cannot see a terminal -- so a tab-separated line printed
273
+ through it arrives with its tabs turned into spaces, and a long one arrives
274
+ folded in half. Both quietly break ``cut -f2`` and everything like it, and
275
+ neither shows up until somebody pipes the output somewhere.
276
+
277
+ Flushed, because some of what is written this way is written while a
278
+ command is still running -- hubs as a scan hears them, lines as the service
279
+ logs them -- and a pipe buffers in blocks. Without this, ``pb scan |
280
+ head -1`` waits for the scan to end rather than answering the moment there
281
+ is an answer, and ``pb service log -f`` piped anywhere shows nothing at all
282
+ until it is stopped. The cost is nothing at these volumes: a hub's own
283
+ output does not come this way.
284
+
285
+ Args:
286
+ out: The stdout console. Only data is written this way.
287
+ line: Exactly what to write, without its newline.
288
+ """
289
+ out.file.write(line + "\n")
290
+ out.file.flush()
291
+
292
+
293
+ def rows(out: Console, pairs: list[tuple[str, str]]) -> None:
294
+ """Render key/value rows.
295
+
296
+ Args:
297
+ out: Where to write. This is data, so it is stdout.
298
+ pairs: Name and value for each row, in the order to show them.
299
+ """
300
+
301
+ if out.is_terminal:
302
+ table = Table(show_header=False, box=None)
303
+
304
+ for key, value in pairs:
305
+ table.add_row(key, value)
306
+
307
+ out.print(table)
308
+ else:
309
+ # Tab-separated when piped, so cut and awk can read it.
310
+ for key, value in pairs:
311
+ plain(out, f"{key}\t{value}")
312
+
313
+
314
+ def signal_cell(device: pb_client.DeviceInfo, connected: Collection[str] = ()) -> str:
315
+ """Say how well a hub can be reached, in Rich markup.
316
+
317
+ Args:
318
+ device: The hub.
319
+ connected: The ids of hubs the service is holding open.
320
+
321
+ Returns:
322
+ A meter for a hub that was heard over Bluetooth, and a word for the
323
+ cases where a meter would be a lie. A connected hub is not advertising,
324
+ so there is no signal to draw; a hub on a cable has no signal at all,
325
+ and drawing an empty meter for one would read as a hub that can barely
326
+ be heard when it is in fact the best-connected hub on the list. A hub
327
+ listed over Bluetooth Classic was not heard at all: it is there because
328
+ it is paired, which says nothing about whether it is switched on.
329
+ """
330
+ if device.id in connected:
331
+ return "[green]connected[/]"
332
+
333
+ if device.transport == "usb":
334
+ return "[green]USB[/]"
335
+
336
+ if device.transport == "rfcomm":
337
+ return "[dim]paired[/]"
338
+
339
+ return signal_bars(device.rssi)
340
+
341
+
342
+ def scan_table(
343
+ devices: Iterable[pb_client.DeviceInfo],
344
+ caption: str = "",
345
+ selected: int | None = None,
346
+ connected: Collection[str] = (),
347
+ ) -> Table:
348
+ """Build the table of hubs seen so far.
349
+
350
+ Args:
351
+ devices: What has been seen, in the order it was first seen. Not sorted
352
+ by signal, tempting as that is: rows that swap places under the
353
+ reader's eyes while they are trying to read one are worse than a
354
+ list that is not in the ideal order.
355
+ caption: A line under the table saying what is happening.
356
+ selected: Which row the user is on, when this is a chooser rather than
357
+ a report. Marked with an arrow as well as with a style, so it is
358
+ still visible where colour is not.
359
+ connected: The ids of hubs that are connected. A connected hub does
360
+ not advertise, so there is no signal to report for one and the
361
+ column says so instead of drawing an empty meter.
362
+
363
+ Returns:
364
+ The table, ready to print or hand to a live display.
365
+ """
366
+ table = Table(caption=caption or None, caption_justify="left")
367
+
368
+ if selected is not None:
369
+ table.add_column("")
370
+
371
+ table.add_column("Name")
372
+ # Known before connecting, because a Pybricks hub puts its Plug and Play ID
373
+ # in the scan response. Two hubs called "Pybricks Hub" are told apart by
374
+ # this and by their signal, which is the whole reason to show either.
375
+ table.add_column("Type")
376
+
377
+ if SHOW_HUB_IDS:
378
+ table.add_column("Address", style="dim")
379
+
380
+ table.add_column("Signal", justify="right")
381
+
382
+ for index, device in enumerate(devices):
383
+ row = [
384
+ device.name,
385
+ device.hub_type or "",
386
+ device.id,
387
+ signal_cell(device, connected),
388
+ ]
389
+
390
+ if not SHOW_HUB_IDS:
391
+ del row[2]
392
+
393
+ if selected is not None:
394
+ row.insert(0, SELECTED_MARK if index == selected else " ")
395
+
396
+ table.add_row(*row, style="bold" if index == selected else None)
397
+
398
+ return table
399
+
400
+
401
+ def connected_table(hubs: Iterable[pb_client.HubInfo], acting: str | None) -> Table:
402
+ """Build the table of hubs the service is holding open.
403
+
404
+ Args:
405
+ hubs: The connected hubs.
406
+ acting: The id of the one a command with no hub named would act
407
+ on, marked with a star.
408
+
409
+ Returns:
410
+ The table.
411
+
412
+ There is no state column. This lists what is connected, so it could only
413
+ ever say "connected"; it comes back if the service ever tracks hubs it
414
+ knows about but is not holding.
415
+ """
416
+ table = Table()
417
+ table.add_column("")
418
+ table.add_column("Name")
419
+ table.add_column("Firmware")
420
+ table.add_column("Used by", justify="right")
421
+
422
+ for info in hubs:
423
+ table.add_row(
424
+ "*" if info.id == acting else "",
425
+ info.name,
426
+ info.firmware,
427
+ str(info.subscribers),
428
+ )
429
+
430
+ return table
431
+
432
+
433
+ def chooser_table(
434
+ devices: list[pb_client.DeviceInfo],
435
+ selected: int,
436
+ connected: Collection[str] = (),
437
+ ) -> RenderableType:
438
+ """Draw the chooser's list.
439
+
440
+ Args:
441
+ devices: The hubs heard so far.
442
+ selected: Which one the user is on.
443
+ connected: The ids of hubs that are connected rather than heard.
444
+
445
+ Returns:
446
+ What to show, which is a line of text until a hub has been heard -- an
447
+ empty table with headings would look like a list of no hubs rather than
448
+ like still looking for them.
449
+ """
450
+
451
+ if not devices:
452
+ return Text("Looking for hubs...", style="dim")
453
+
454
+ return scan_table(devices, selected=selected, connected=connected)
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybricks-cli
3
+ Version: 1.0.0b1
4
+ Summary: The Pybricks command line interface
5
+ Keywords: pybricks,lego,micropython,bluetooth,robotics
6
+ Author: The Pybricks Authors
7
+ Author-email: The Pybricks Authors <dev@pybricks.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Natural Language :: English
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Software Development :: Embedded Systems
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: pybricks-core>=1.0.0b1
19
+ Requires-Dist: pybricks-client>=1.0.0b1
20
+ Requires-Dist: pybricks-service>=1.0.0b1
21
+ Requires-Dist: anyio>=4.14
22
+ Requires-Dist: cyclopts>=4.22
23
+ Requires-Dist: rich>=13.7
24
+ Requires-Dist: typing-extensions>=4.15
25
+ Requires-Dist: watchfiles>=1.2
26
+ Maintainer: David Lechner
27
+ Maintainer-email: David Lechner <david@pybricks.com>
28
+ Requires-Python: >=3.11
29
+ Project-URL: homepage, https://pybricks.com
30
+ Project-URL: repository, https://github.com/pybricks/pybricks-tools
31
+ Project-URL: documentation, https://pybricks.com/pybricks-tools/cli/
32
+ Project-URL: Changelog, https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-cli/CHANGELOG.md
33
+ Project-URL: Support, https://github.com/pybricks/support/discussions
34
+ Project-URL: Issues, https://github.com/pybricks/support/issues
35
+ Description-Content-Type: text/markdown
36
+
37
+ # pybricks-cli
38
+
39
+ The `pb` command: run Python programs on a [Pybricks](https://pybricks.com) hub
40
+ from your computer.
41
+
42
+ ```bash
43
+ uv tool install pybricks-cli
44
+ ```
45
+
46
+ Then switch a hub on:
47
+
48
+ ```bash
49
+ pb connect # find a hub and connect to it
50
+ pb run demo.py # compile the script, send it, and run it
51
+ ```
52
+
53
+ **The hub stays connected after the command finishes**, so the second `pb run`
54
+ starts instantly, and so does the hundredth.
55
+
56
+ `pb repl` talks to the hub's interpreter a line at a time, and `pb run -c` runs
57
+ a line or two of Python without a file. `pb --help` lists the rest.
58
+
59
+ > **Status:** beta. Commands and APIs may still change before 1.0.0.
@@ -0,0 +1,26 @@
1
+ pb_cli/__init__.py,sha256=OOzz50wu4R3xbPJmGzcd1cgVOMMFXCT-t1xSqsWWc3Y,169
2
+ pb_cli/__main__.py,sha256=daU1PjUYs4oc_iRv6SrANPvszOQ4JT2XUahD4xAloR4,159
3
+ pb_cli/app.py,sha256=_FGyMwKnrtcfDd_ZPm_14xlkV4B1E0zFUb8AGPdn2Lw,4331
4
+ pb_cli/banner.py,sha256=_TvHn0SnYLqaoEimZ0lWfy3r0QUUAvwB4qbPnWHbLf8,10910
5
+ pb_cli/cli.py,sha256=44dmk8U_qUrsT7Q5zOdybOG5g254EohhxsHCxx8qAzo,4766
6
+ pb_cli/commands/__init__.py,sha256=Q776Ajsv54PlCL4hViY5amUAwSTtsKEHvWYnMgBFBtA,667
7
+ pb_cli/commands/hub.py,sha256=vahVx51zNcxa9FuDSy2NRsncr1D32CpLcFK7XlTXLts,22493
8
+ pb_cli/commands/program.py,sha256=5cA74hDjg91LSr0quLaHKy_7Jk0VMVKVdicHOyCQvnE,14207
9
+ pb_cli/commands/repl.py,sha256=1zBumG_Qf5UgNKq76SUNmwmkAM4vvV0fX5gM52KJ_MQ,5111
10
+ pb_cli/commands/service.py,sha256=4NL17Rk6XyK6rn5xuX85EisOlFZjF0SOl-RidMu3ilU,12378
11
+ pb_cli/config.py,sha256=F154YJ7wdDEwk7WF4KlDqKNsVqtV7hGQjMYfzdjbhLY,7565
12
+ pb_cli/console/__init__.py,sha256=0Gc7a1TEmustjYYN_XFmp8AaoAZOvxRFD96KN4AGcIc,4189
13
+ pb_cli/console/posix.py,sha256=OFC4XeS-MtCXCjObrArK10fAGTN1_gBOdpkeEUtRbAw,3264
14
+ pb_cli/console/windows.py,sha256=R_cwNr0rbO-wXmxfWdglzlXyNmQck_Uv5uc7Ez5vxzI,20578
15
+ pb_cli/errors.py,sha256=N7gHXWpTiKgcqsWcR2bJUqj2yO50GuI5L1bF-6aRc9c,24741
16
+ pb_cli/escape.py,sha256=mjy3KR2Ro3Akp2GkqaXF_Snu4hhRbq_4Q_RVuXiROCs,10130
17
+ pb_cli/keys.py,sha256=cHhUmO1H96VSHGRKlarF_hwkiVeZhjP1FBuOsUH-Ut8,12533
18
+ pb_cli/picker.py,sha256=1N6TTUxEWOYMaYz7v2xvq6g8CydTwLl168KdE1l-No4,5026
19
+ pb_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ pb_cli/session.py,sha256=3-QOOwXa8NIh7h32kR8jSzQsAjLqJpeNOewEdzq66zc,29910
21
+ pb_cli/ui.py,sha256=sjGcaHRVH9hJUjMo43F5XrsKLSe8xeV0JAZZD2WaT5E,16154
22
+ pybricks_cli-1.0.0b1.dist-info/licenses/LICENSE,sha256=JndG-VHDIn3qDRbnL3i1F01gYV64xJKk3bjgY8hSe2o,1077
23
+ pybricks_cli-1.0.0b1.dist-info/WHEEL,sha256=9IUYslYuG_asT4WlWvfk_K76nveuLvSTKDoLeL0cV-Q,80
24
+ pybricks_cli-1.0.0b1.dist-info/entry_points.txt,sha256=mwERougZL-mI4AcHM3DhWGhA3hBuWjwLEqHcoM5nnK8,40
25
+ pybricks_cli-1.0.0b1.dist-info/METADATA,sha256=vvbhNYgynuNAvj-JXT-VnltRwSWhQIfTiD-9Si3SvzE,2117
26
+ pybricks_cli-1.0.0b1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pb = pb_cli.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Pybricks Authors
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.