zppy 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.
zppy-0.1.0/LICENSE ADDED
@@ -0,0 +1,121 @@
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
zppy-0.1.0/MANIFEST.in ADDED
@@ -0,0 +1,2 @@
1
+ recursive-include tests *.py
2
+ recursive-include examples *.py
zppy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,359 @@
1
+ Metadata-Version: 2.4
2
+ Name: zppy
3
+ Version: 0.1.0
4
+ Summary: Zig progress protocol client for Python
5
+ License-Expression: CC0-1.0
6
+ Project-URL: Repository, https://github.com/asciimoth/zppy
7
+ Project-URL: Issues, https://github.com/asciimoth/zppy/issues
8
+ Project-URL: Documentation, https://github.com/asciimoth/zppy#readme
9
+ Keywords: zig,progress,zig-progress-protocol
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: POSIX
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # zppy
22
+
23
+ `zppy` is a Python library that reports task progress through the Zig progress
24
+ protocol. Use it to report tasks, subtasks, and progress from child processes.
25
+
26
+ `zppy` requires Python 3.11 or later. The library has no runtime dependencies.
27
+ The pipe transport supports POSIX systems, including Linux and macOS.
28
+
29
+ ## Installation
30
+
31
+ ```sh
32
+ python -m pip install zppy
33
+ ```
34
+
35
+ The package uses the [CC0 1.0 Universal license](LICENSE).
36
+
37
+ ## Zig progress protocol
38
+
39
+ The Zig progress protocol lets a process send a tree of tasks to its parent
40
+ through a pipe. Each task has a name, a completed item count, and an estimated
41
+ total. The `ZIG_PROGRESS` environment variable identifies the pipe. The parent
42
+ can combine progress from child processes and display it in one place.
43
+
44
+ You can use the protocol outside the Zig toolchain. For example,
45
+ [`zpg`](https://github.com/asciimoth/zpg) runs commands and displays their
46
+ progress in a terminal. With `zppy`, Python programs can send progress to
47
+ `zpg` or another compatible receiver.
48
+
49
+ See Andrew Kelley's [Zig progress protocol specification][protocol] for the
50
+ protocol design.
51
+
52
+ ![zpg displaying a task tree](https://raw.githubusercontent.com/asciimoth/zpg/master/demo.gif)
53
+
54
+ ## Library usage
55
+
56
+ If you use this library, [open an issue][issues] or [send a pull
57
+ request][pulls] with a link to your project. This helps us track use of the
58
+ Zig progress protocol outside the Zig toolchain.
59
+
60
+ Add the [`zig-progress-protocol` topic][topic] to your GitHub repository. Use
61
+ the same tag on other platforms so people can find related projects.
62
+
63
+ [issues]: https://github.com/asciimoth/zppy/issues
64
+ [pulls]: https://github.com/asciimoth/zppy/pulls
65
+ [topic]: https://github.com/topics/zig-progress-protocol
66
+
67
+ ### Report tasks
68
+
69
+ Save this example as `build.py`:
70
+
71
+ ```python
72
+ import time
73
+
74
+ from zppy import Progress
75
+
76
+ with Progress("build", estimated_total_items=2) as progress:
77
+ for name in ("assets", "code"):
78
+ with progress.root.start(name, estimated_total_items=10) as task:
79
+ for _ in range(10):
80
+ time.sleep(0.1) # Replace this with one item of work.
81
+ task.complete_one()
82
+ ```
83
+
84
+ Run `build.py` with a progress display:
85
+
86
+ ```sh
87
+ zpg python build.py
88
+ ```
89
+
90
+ Each child task can start more child tasks. When a child task ends, it leaves
91
+ the tree and adds one completed item to its parent task. Do not also call
92
+ `parent.complete_one()` for the same child task.
93
+
94
+ A context manager ends its task on normal exit and on exceptions. The context
95
+ manager does not suppress exceptions or report task success or failure.
96
+
97
+ Without a valid `ZIG_PROGRESS` channel, reporting is disabled. Task operations
98
+ have no effect. The library does not print to stdout or stderr. It does not
99
+ draw its own terminal display.
100
+
101
+ ### Run the combined example
102
+
103
+ From the repository root, run:
104
+
105
+ ```sh
106
+ PYTHONPATH=src zpg python -u examples/mock.py
107
+ ```
108
+
109
+ This example requires `zpg` on `PATH` and a system with `os.fork()`. It runs
110
+ `zpg --mock` as a subprocess and a Python worker as a forked process at the
111
+ same time. Both task trees appear under `demo`. Python task labels start with
112
+ `PY`. Both workers also write messages to stdout and stderr.
113
+
114
+ ### Child processes
115
+
116
+ Use `Node.spawn()` to start a process with its own progress pipe.
117
+ `Node.spawn()` accepts the arguments to `subprocess.Popen` and returns a
118
+ `Popen` object:
119
+
120
+ ```python
121
+ import sys
122
+
123
+ from zppy import Progress
124
+
125
+ with Progress("build", estimated_total_items=1) as progress:
126
+ with progress.root.start("compile") as task:
127
+ with task.spawn([sys.executable, "worker.py"]) as process:
128
+ returncode = process.wait()
129
+ ```
130
+
131
+ In `worker.py`, use `Progress`. The parent process combines the child's task
132
+ tree with its own tasks and sends the combined tree upstream. A child process
133
+ can use `Node.spawn()` to start more processes. Use a separate task node for
134
+ each process. Processes on different nodes can run at the same time.
135
+
136
+ `Node.spawn()` follows [Zig's child progress behavior][zig-child]. The child's
137
+ root replaces the task node in each report. The child root's descendants
138
+ appear below that node. If the child root has no name, the task keeps its
139
+ local name. Until the first complete report arrives, the task keeps its local
140
+ state. The last complete tree stays visible after the child closes its pipe or
141
+ exits.
142
+
143
+ Wait for the process, then end its task, as in the example above. Process exit
144
+ does not end the task or mark success or failure. Ending the task completes
145
+ one item in its parent.
146
+
147
+ Ending a task or closing the reporter closes its child progress pipes. Neither
148
+ operation waits for or terminates processes. Use one process per active node.
149
+ A second `spawn()` or `fork()` on the same node raises `ValueError`.
150
+
151
+ The update thread collects child reports. With `refresh_rate=None`, call
152
+ `flush()` regularly while children run.
153
+
154
+ `spawn()` copies the child environment and sets `ZIG_PROGRESS` to the new
155
+ pipe. It preserves other environment entries and adds the pipe to `pass_fds`.
156
+ `close_fds` must be true. `spawn()` does not change the parent environment.
157
+ Disabled, ended, or closed nodes still start processes, but remove
158
+ `ZIG_PROGRESS` from the child environment. Do not pass the upstream progress
159
+ descriptor to children.
160
+
161
+ All processes in the tree must use the same `name_size`. There is no format
162
+ negotiation.
163
+
164
+ [zig-child]: https://github.com/ziglang/zig/blob/0.15.2/lib/std/process/Child.zig#L96-L104
165
+
166
+ ### Forked processes
167
+
168
+ On systems with `os.fork()`, use `Node.fork()` to attach a forked process to a
169
+ task. It returns the child's PID in the parent and `0` in the child:
170
+
171
+ ```python
172
+ import os
173
+
174
+ from zppy import Progress
175
+
176
+ with Progress("parent", estimated_total_items=1) as progress:
177
+ with progress.root.start("worker") as task:
178
+ pid = task.fork()
179
+ if pid == 0:
180
+ with Progress("worker", estimated_total_items=10) as child:
181
+ for _ in range(10):
182
+ child.root.complete_one()
183
+ os._exit(0)
184
+ _, status = os.waitpid(pid, 0)
185
+ ```
186
+
187
+ The child gets a private `ZIG_PROGRESS` pipe. Create a new `Progress` in the
188
+ child, with the same `name_size` as its parent. All inherited reporters and
189
+ their nodes are disabled in the child. Caller-owned descriptors remain the
190
+ caller's responsibility. The parent's reporters and environment stay
191
+ unchanged.
192
+
193
+ The forked child's tree replaces its task node, as for `spawn()`. Forked
194
+ children can use either `fork()` or `spawn()` to create more children. Wait
195
+ for the child with `os.waitpid()`, then end its task in the parent. Close the
196
+ child's reporter before `os._exit()`, which skips normal cleanup. Handle child
197
+ errors and exit status as required by your application.
198
+
199
+ Forks on disabled, ended, or closed nodes still run, but the child has no
200
+ `ZIG_PROGRESS` channel. A failed fork leaves the parent reporter usable.
201
+
202
+ Use `Node.fork()` to connect a forked process to a task. Direct `os.fork()`
203
+ calls and `multiprocessing` do not create this connection. `Node.fork()`
204
+ protects `zppy`'s own state. Application code and other libraries must also
205
+ meet the platform's [fork
206
+ requirements](https://docs.python.org/3/library/os.html#os.fork).
207
+
208
+ ### API reference
209
+
210
+ `Progress(root_name="", estimated_total_items=0, *, refresh_rate=0.08,
211
+ initial_delay=0.2, fd=None, disable=False, name_size=120)` creates a reporter.
212
+ Use `progress.root` to access its root task.
213
+
214
+ | Option | Meaning |
215
+ | --- | --- |
216
+ | `root_name` | Root label, as `str` or `bytes`. |
217
+ | `estimated_total_items` | Estimated root item count. Zero means unknown. |
218
+ | `refresh_rate` | Seconds between reports. Use `None` for manual reports. |
219
+ | `initial_delay` | Seconds before the first automatic report. |
220
+ | `fd` | Explicit pipe descriptor. `None` reads `ZIG_PROGRESS`. |
221
+ | `disable` | Disable all reporting when true. |
222
+ | `name_size` | Name field size: `120` by default, or `40` for the old format. |
223
+
224
+ Timer values must be finite numbers no greater than `threading.TIMEOUT_MAX`.
225
+ `refresh_rate` must be positive, and `initial_delay` must be nonnegative.
226
+
227
+ `progress.enabled` shows whether the channel is open. `progress.flush()` tries
228
+ to send a snapshot immediately. It returns `True` if the current snapshot was
229
+ sent in full, or `False` if reporting is disabled or the write could not
230
+ finish. It does not wait for pipe space.
231
+
232
+ `progress.close()` stops the update thread, tries to send the final state, and
233
+ releases its descriptor. Use a context manager or call `close()` directly.
234
+ Closing the reporter does not set counts to their totals or remove the final
235
+ tree from the receiver. Ending the root also closes the reporter.
236
+
237
+ Nodes provide these methods:
238
+
239
+ | Method | Action |
240
+ | --- | --- |
241
+ | `start(name, estimated_total_items=0)` | Create a child task. |
242
+ | `spawn(args, **kwargs)` | Start a process and collect its progress. |
243
+ | `fork()` | Fork and collect the child's progress at this node. |
244
+ | `complete_one()` | Add one completed item. |
245
+ | `set_completed_items(count)` | Set the absolute completed count. |
246
+ | `set_estimated_total_items(count)` | Set the estimated total. |
247
+ | `increase_estimated_total_items(count)` | Add to the estimated total. |
248
+ | `set_name(name)` | Change the task label. |
249
+ | `end()` | Remove this branch and complete one item in its parent. |
250
+
251
+ Calls are thread-safe. Repeated `end()` calls have no effect. Task updates on
252
+ ended nodes also have no effect. Calls to `spawn()` and `fork()` still start a
253
+ process on ended nodes, as described in the child process sections. Invalid
254
+ arguments raise `TypeError` or `ValueError`, including when reporting is
255
+ disabled.
256
+
257
+ Counts must be nonnegative integers. Completed counts are limited to
258
+ `4294967295`. Totals are limited to `4294967294`, because Zig reserves the
259
+ next value. The library reduces larger values to these limits. Completed
260
+ counts can exceed totals.
261
+
262
+ Names are zero-padded to the field size. Text is encoded as UTF-8 and
263
+ shortened at a character boundary. Byte names are shortened without decoding.
264
+ Bytes after the first NUL are ignored.
265
+
266
+ The reporter tracks up to 253 active nodes, including the root. Additional
267
+ nodes have no effect. Ending nodes makes space for new nodes. Combined reports
268
+ also have a 253-node limit. Local nodes take priority, followed by child trees
269
+ in process creation order. Child roots replace local nodes without using more
270
+ slots. The reporter omits excess descendants and their branches. These
271
+ descendants can appear in later reports when space is available.
272
+
273
+ ## Technical details
274
+
275
+ ### Protocol compatibility
276
+
277
+ The default uses a 120-byte name field. Each node has two little-endian 32-bit
278
+ counters and 120 name bytes. A packet contains the node count, all node
279
+ records, then all parent indices. The root's parent is `255`.
280
+
281
+ The [published specification][protocol] uses 40-byte names. Select that format
282
+ explicitly with `Progress(name_size=40)`. The two formats are not wire
283
+ compatible. Both use `ZIG_PROGRESS`, and there is no version handshake. Select
284
+ the format that matches your receiver.
285
+
286
+ [protocol]: https://andrewkelley.me/post/zig-new-cli-progress-bar-explained.html#the-zig-progress-protocol-specification
287
+
288
+ Use one reporter per pipe. Each packet replaces the previous tree. A packet
289
+ does not add tasks to the previous tree. Multiple writers can replace each
290
+ other's progress or interleave packet bytes. Use `Node.spawn()` or
291
+ `Node.fork()` to combine progress from separate processes through private
292
+ pipes. Create a new reporter in each child, and use the same `name_size`
293
+ throughout the process tree.
294
+
295
+ ### Platforms and pipe handling
296
+
297
+ The reporter accepts only writable POSIX pipes, such as those on Linux and
298
+ macOS. Other descriptors, including regular files and read-only pipes, disable
299
+ reporting without changing their data or blocking mode. Reporting is disabled
300
+ on other platforms.
301
+
302
+ The reporter duplicates the supplied descriptor and never closes the original.
303
+ The duplicate is not inherited across exec. The reporter sets the channel to
304
+ nonblocking mode. This flag also applies to the original descriptor. When you
305
+ create a reporter, it does not change `ZIG_PROGRESS` in the environment.
306
+ `Node.fork()` sets `ZIG_PROGRESS` in the forked child only.
307
+
308
+ A full pipe can delay or drop updates. The reporter resumes partial packets
309
+ before it sends a new snapshot. A permanent write error disables reporting.
310
+
311
+ Closing the reporter never waits for pipe space. The final report can be lost.
312
+ If the receiver stops reading, a large partial packet can remain incomplete.
313
+ If final delivery is required, retry `flush()` while the receiver is active
314
+ before you close the reporter.
315
+
316
+ ## Release notes
317
+
318
+ ### 0.1.0
319
+
320
+ Initial release:
321
+
322
+ - Report tasks and nested tasks through the Zig progress protocol.
323
+ - Combine progress from subprocesses and forked Python workers.
324
+ - Support both 120-byte and 40-byte task names.
325
+ - Send updates through POSIX pipes without runtime dependencies.
326
+
327
+ ## Manual releases
328
+
329
+ Use the Nix development shell on this machine:
330
+
331
+ ```sh
332
+ nix develop
333
+ ```
334
+
335
+ The shell includes the build tools and Twine. Before each release:
336
+
337
+ 1. Set the version in `pyproject.toml`. The Nix package reads this version too.
338
+ 2. Update the release notes above.
339
+ 3. Run `just check`, then `just build` and `just check-dist`.
340
+ 4. Install the wheel in a fresh virtual environment outside the source tree
341
+ and check that it works.
342
+ 5. Commit the release changes and create an annotated tag, for example
343
+ `git tag -a v0.1.0 -m "Release 0.1.0"`.
344
+ 6. Run `just publish` to upload the checked source archive and wheel to PyPI.
345
+ 7. Push the release commit and tag to the repository.
346
+
347
+ `just publish` checks the files again but does not rebuild them. It selects
348
+ only the two files for the version in `pyproject.toml`, so older releases in
349
+ `dist/` are not uploaded. If you change the source, rebuild and check it before
350
+ you publish.
351
+
352
+ For the first upload, create a PyPI account, verify its email address, enable
353
+ two-factor authentication, and create an API token with account-wide scope.
354
+ Enter the token at Twine's password prompt. After the first upload, replace
355
+ that token with a token scoped to `zppy`. Do not commit tokens to the
356
+ repository. See the [PyPI publishing guide][pypi-publishing] for account and
357
+ authentication details.
358
+
359
+ [pypi-publishing]: https://docs.pypi.org/publishing/publishing-to-pypi/