ezpkg 0.0.1__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.
@@ -0,0 +1,595 @@
1
+ Metadata-Version: 2.1
2
+ Name: ezpkg
3
+ Version: 0.0.1
4
+ Summary: Automatic Package Fetching for One-File Programs
5
+ Requires-Python: >=3.11
6
+ License: MIT No Attribution
7
+ Author: Wayne C. Gramlich
8
+ Project-URL: Homepage, https://github.com/waynegramlich/ezpkg
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: System :: Installation/Setup
14
+ Description-Content-Type: text/markdown
15
+
16
+ # `EZPkg`: Easy Python Packages
17
+
18
+ `EZPkg` (pronounced "easy package") grew out of the desire to send a simple
19
+ one-file Python program to a friend. When said friend tried to run it,
20
+ a few packages were missing, so the friend had to create a virtual environment
21
+ and install them. It worked, but it was annoying. So the question became
22
+ "Could this be made really easy for the recipient?" Thus was born `EZPkg`,
23
+ which lets a single-file Python program download its missing packages all
24
+ by itself.
25
+
26
+ To get started with `EZPkg`, you paste a modest bootstrap at the front of
27
+ your program and go. After that, `EZPkg` fetches your packages over the net
28
+ on demand. Once everything is working with `EZPkg`, `EZPkg` can easily make
29
+ a release in one of three formats - "email", "zipapp", or "uv".
30
+
31
+ Table of Contents:
32
+ * [Quick Start](#quick-start)
33
+ * [Running a Program That Uses `EZPkg`](#running-a-program-that-uses-ezpkg)
34
+ * [Enabling `EZPkg` for a Program](#enabling-ezpkg-for-a-program)
35
+ * [Releasing a Packaged Program](#releasing-a-packaged-program)
36
+ * [The Bootstrap](#the-bootstrap)
37
+ * [Security](#security)
38
+ * [Miscellaneous](#miscellaneous)
39
+ * [How `EZPkg` Actually Works](#how-ezpkg-actually-works)
40
+ * [Troubleshooting](#troubleshooting)
41
+ * [Project Information](#project-information)
42
+ * [Conclusion](#conclusion)
43
+
44
+ ## Quick Start
45
+
46
+ To get started, please take the program below and paste it into a file
47
+ named `quick_start.py` on your computer. The bulk of this program is the
48
+ `EZPkg` bootstrap, Part 2. This is the "magic" that skips the
49
+ hassle of virtual environments. Then run it as the next section shows.
50
+
51
+ ```python
52
+ #!/usr/bin/env python3
53
+ # Part 1: These lines look like `#` comments, but they are Python's inline
54
+ # script metadata (PEP 723) that the EZPkg bootstrap needs. Never remove them.
55
+ # /// script
56
+ # requires-python = ">=3.11"
57
+ # dependencies = ["ezpkg>=0.0.1.dev0,<0.1", "cowsay"]
58
+ # ///
59
+ """Try `EZPkg`: fetch the `cowsay` package on first use, then run."""
60
+
61
+ # Part 2: The EZPkg bootstrap. Paste all of it, `# Begin` to `# End`,
62
+ # below your docstring and any `from __future__` lines.
63
+ # Begin the EZPkg bootstrap. Never edit it.
64
+ import hashlib
65
+ import importlib
66
+ import json
67
+ import pathlib
68
+ import subprocess
69
+ import sys
70
+ import urllib.request as req
71
+
72
+ try: # Do not edit try block! See https://pypi.org/project/ezpkg/
73
+ s = "find ezpkg"; t = pathlib.Path.home()/".ezpkg"; w = t/"ezpkg.whl"
74
+ def g(u): # Fetch u, with curl if this Python has no certificates.
75
+ try: return req.urlopen(u, None, 60).read()
76
+ except req.URLError as y: z = y # As on python.org's macOS Python.
77
+ try: return subprocess.run(["curl", "-fsSL", u],
78
+ capture_output=True, check=True).stdout
79
+ except Exception: raise z from None
80
+ try: m = None; m = importlib.import_module("ezpkg") # Already importable?
81
+ except ImportError: pass
82
+ if m is None and not w.is_file(): # Not downloaded yet.
83
+ s = "download ezpkg"; d = g("https://pypi.org/pypi/ezpkg/json")
84
+ e = next(e for e in json.loads(d)["urls"] if e["url"][-4:] == ".whl")
85
+ r = g(e["url"])
86
+ if hashlib.sha256(r).hexdigest() != e["digests"]["sha256"]:
87
+ raise ValueError(f"{e['filename']} fails its SHA-256 check")
88
+ t.mkdir(parents=True, exist_ok=True); p = t/"ezpkg.part"
89
+ p.write_bytes(r); p.replace(w) # The rename is atomic.
90
+ if m is None: # Python imports it straight from the wheel.
91
+ s = "import ezpkg"; sys.path.insert(0, str(w))
92
+ m = importlib.import_module("ezpkg")
93
+ s = "fetch packages"; m.EZPkg().install(bootstrap="2026-10-23")
94
+ except Exception as x:
95
+ raise SystemExit(f"ezpkg: {s} failed: {x!r}") # Do not edit try block!
96
+ # End the EZPkg bootstrap. Your imports go below it.
97
+
98
+ # Part 3: Your imports, all after the bootstrap.
99
+ import cowsay # type: ignore[import-not-found] # For mypy.
100
+
101
+ # Part 4: The program itself.
102
+ cowsay.cow("EZPkg works!")
103
+ print(f"The cow above was drawn by cowsay {cowsay.__version__}.")
104
+ ```
105
+
106
+ ## Running a Program That Uses `EZPkg`
107
+
108
+ The `quick_start.py` program is run as follows:
109
+ * Windows: `py quick_start.py` or `python quick_start.py`
110
+ * Linux/macOS: `python3 quick_start.py`
111
+
112
+ The first time `quick_start.py` runs, it prints the following:
113
+
114
+ ```
115
+ The author of `quick_start.py` has selected some software packages from the
116
+ internet. If you agree, they are retained in `~/.ezpkg`. If you do not trust
117
+ the author, answer `n` and `~/.ezpkg` will simply go away without running
118
+ `quick_start.py`.
119
+
120
+ Retain `~/.ezpkg` and run `quick_start.py`?
121
+ [y=>Yes|n=>No|?=>Explain]:
122
+ ```
123
+
124
+ The three options are:
125
+ * `y=>Yes`:
126
+ Answer `y`, and `EZPkg` lists what it retained in `~/.ezpkg` (only this
127
+ once), and then the program runs.
128
+ * `n=>No`:
129
+ The newly downloaded packages are removed and the program does not run.
130
+ On the very first run, all of `~/.ezpkg` is removed. Pressing Control-C
131
+ or not answering within 5 minutes is treated as `n`.
132
+ * `?=>Explain`:
133
+ This lists the packages in question and repeats the question.
134
+
135
+ Lastly, the prompt is skipped when there is nobody at a terminal to answer it
136
+ (e.g. `cron` or a test system), and `y` is assumed.
137
+
138
+ After a `y`, the first run generally looks as follows, though the version
139
+ numbers may differ:
140
+
141
+ ```
142
+ The following 3 package(s) are now retained in `~/.ezpkg`:
143
+ 1. pip 26.2.1 (used by `ezpkg`): The PyPA recommended tool for installing Py...
144
+ 2. ezpkg 0.0.1: Automatic Package Fetching for One-File Programs
145
+ 3. cowsay 6.1: The famous cowsay for GNU/Linux is now available for python
146
+ `ezpkg` (https://pypi.org/project/ezpkg/) manages `~/.ezpkg`.
147
+
148
+ ____________
149
+ | EZPkg works! |
150
+ ============
151
+ \
152
+ \
153
+ ^__^
154
+ (oo)\_______
155
+ (__)\ )\/\
156
+ ||----w |
157
+ || ||
158
+ The cow above was drawn by cowsay 6.1.
159
+ ```
160
+
161
+ The list includes `pip` and `ezpkg` only on a machine's very first run,
162
+ when they have just been fetched; `pip` is the tool `ezpkg` uses,
163
+ not one the program's author chose. On Windows, the full path, such as
164
+ `C:\Users\YOUR_NAME\.ezpkg`, is shown instead of `~/.ezpkg`.
165
+
166
+ Now run it a second time. There is no question and no download, and it
167
+ starts in a fraction of a second, because `cowsay` is already in
168
+ `~/.ezpkg/`. The next time you run the program, you will not be asked for
169
+ permission to store files in `~/.ezpkg/`.
170
+
171
+ There are basically 4 things that can go wrong:
172
+ * **No Network**:
173
+ The network is needed only when a program needs packages that are not
174
+ yet in `~/.ezpkg`.
175
+ * **No Version Available**:
176
+ The version the author chose is no longer on PyPI, or none was built
177
+ for your computer and Python.
178
+ * **`pip` Failure**:
179
+ `pip` ("`pip` Installs Packages") is the standard tool that fetches
180
+ Python packages, and it can fail for other reasons.
181
+ * **Not Approved**:
182
+ The author sent a `.py` that has not been produced as an `EZPkg` release,
183
+ and this computer needs a package the author's computer did not. The
184
+ solution is to ask the author for a released version of the program; see
185
+ [Releasing a Packaged Program](#releasing-a-packaged-program).
186
+
187
+ For each issue above, one line is printed and the program does not run.
188
+ For further details of what went wrong, read `~/.ezpkg/last_failure.txt`,
189
+ which every failure rewrites.
190
+
191
+ If you want to remove the `~/.ezpkg` directory and its contents, do the
192
+ following:
193
+ * Windows (PowerShell): `py $HOME\.ezpkg\remove.py`
194
+ * Windows (`cmd.exe`): `py %USERPROFILE%\.ezpkg\remove.py`
195
+ * Linux/macOS: `python3 ~/.ezpkg/remove.py`
196
+
197
+ While it is always safe to remove `~/.ezpkg`, please note that if you run
198
+ `quick_start.py` again, the `~/.ezpkg` directory will return.
199
+
200
+ ## Enabling `EZPkg` for a Program
201
+
202
+ To enable `EZPkg` in your program `PROG_NAME.py` (with `PROG_NAME` replaced
203
+ by your program's name):
204
+ 1. Copy Part 2 of `quick_start.py`, from `# Begin the EZPkg bootstrap.`
205
+ through `# End the EZPkg bootstrap.`, and paste it below your program's
206
+ top-level docstring (and any `from __future__` lines), but above your own
207
+ imports.
208
+ 2. Beside `PROG_NAME.py`, create an empty `PROG_NAME.py.toml`:
209
+ * Windows (PowerShell): `ni PROG_NAME.py.toml`
210
+ * Windows (`cmd.exe`): `type nul > PROG_NAME.py.toml`
211
+ * Linux/macOS: `touch PROG_NAME.py.toml`
212
+ 3. Run `PROG_NAME.py`.
213
+
214
+ The `PROG_NAME.py.toml` makes you the program's developer: `EZPkg` asks you
215
+ nothing and fetches whatever your imports need. Your `PROG_NAME.py` is never
216
+ edited; a release writes the packages and their versions into the copies
217
+ it makes. On the first run, `EZPkg` fills the empty `PROG_NAME.py.toml` with
218
+ its settings, each commented out at its default; see
219
+ [`PROG_NAME.py.toml` Settings](#progpytoml-settings). Without
220
+ `PROG_NAME.py.toml`, whoever runs `PROG_NAME.py` is an end user, the same
221
+ as in the Quick Start.
222
+
223
+ ### What are packages and where do they come from?
224
+
225
+ Since the primary purpose of `EZPkg` is to manage Python packages, it is
226
+ prudent to discuss a little bit more about them.
227
+
228
+ In short, a package is a chunk of code that can be imported into a Python
229
+ program for subsequent execution. The most common place to fetch packages
230
+ from is the [Python Package Index](https://pypi.org) (PyPI), which is where
231
+ the Python community submits freely accessible packages.
232
+
233
+ A package evolves over time, undergoing both bug fixes and feature
234
+ enhancements. These changes are managed by giving each released package a
235
+ different version number. Furthermore, for performance reasons, some packages
236
+ are compiled to run on the native machine architecture (e.g. Intel, ARM,
237
+ RISC-V). Likewise, some packages require specific features available only on a
238
+ specific operating system (e.g. Windows, Linux (Red Hat, Ubuntu), macOS).
239
+
240
+ Finding a set of packages that can interoperate with one another can be quite
241
+ challenging. Ultimately, `EZPkg` encodes the package configuration in Python
242
+ inline script metadata format at the beginning of each released copy of the
243
+ Python file. It is unlikely that you will ever need to read any of this
244
+ metadata. If you look at the `quick_start.py` example, the information between
245
+ the `# /// script` and `# ///` lines is a very short example of the metadata.
246
+ `EZPkg` accomplishes all of this by scanning your Python program
247
+ for all `import` statements and reconciling them into a single, coherent,
248
+ versioned package set.
249
+
250
+ Frankly, `EZPkg` is trying to do all of the hard work of managing the
251
+ metadata and other stuff so that you do not need to. However, if you are curious,
252
+ the following references provide a great deal of additional information:
253
+ * [Python Package Index](https://pypi.org/):
254
+ The most common place to store freely accessible Python packages.
255
+ * [Inline script metadata](
256
+ https://packaging.python.org/en/latest/specifications/inline-script-metadata):
257
+ The Python standard inline configuration information.
258
+ * [TOML (Tom's Obvious Minimal Language)](https://toml.io/en/) format:
259
+ This is the syntax used for the inline script metadata.
260
+ * [Dependency specifiers](
261
+ https://packaging.python.org/en/latest/specifications/dependency-specifiers):
262
+ This is the syntax of each entry in `dependencies=[...]`.
263
+
264
+ ### `PROG_NAME.py.toml` Settings
265
+
266
+ When PROG_NAME.py runs and finds an empty PROG_NAME.py.toml file, EZPkg
267
+ fills it with exactly the content shown below (i.e. from
268
+ "# Start of PROG_NAME.py.toml:" to "# End of PROG_NAME.py.toml".)
269
+
270
+ ```toml
271
+ # Start of PROG_NAME.py.toml:
272
+
273
+ # This file is meant to be self-documenting. It contains the EZPkg settings
274
+ # for PROG_NAME.py (i.e. your program). More: https://pypi.org/project/ezpkg/
275
+
276
+ # This file is created as an empty file next to PROG_NAME.py, in the same
277
+ # directory. Depending upon which operating system you are running, one of
278
+ # the following commands creates the empty file:
279
+ # * Windows (PowerShell): `ni PROG_NAME.py.toml`
280
+ # * Windows (`cmd.exe`): `type nul > PROG_NAME.py.toml`
281
+ # * Linux/macOS: `touch PROG_NAME.py.toml`
282
+
283
+ # The mere existence of PROG_NAME.py.toml classifies you as a developer of
284
+ # PROG_NAME.py. Conversely, if you remove this file, you are no longer
285
+ # treated as a developer. Instead, you will be asked on the program's first
286
+ # run whether its packages can be kept in the ~/.ezpkg directory.
287
+
288
+ # Whenever PROG_NAME.py.toml is empty and PROG_NAME.py is run, its contents
289
+ # are initialized to the text you are currently reading (i.e. from
290
+ # "# Start of PROG_NAME.py.toml:" to "# End of PROG_NAME.py.toml".) After
291
+ # this file is created, only you (the developer) get to edit it. If you
292
+ # want it reinitialized, empty the file and run PROG_NAME.py again.
293
+
294
+ # This file is the only place where your EZPkg rules are configured. Any
295
+ # additional information collected by EZPkg is stored in various files in
296
+ # the `~/.ezpkg/` directory. EZPkg only ever fills this file when it is
297
+ # empty, and never modifies it after that.
298
+
299
+ # All possible settings that can be set are shown as commented-out TOML
300
+ # lines. To enable an option, just remove the preceding "# ". For a list,
301
+ # such as "release", remove the "#" from each entry you want. If you
302
+ # accidentally enable two conflicting options, EZPkg will produce an error
303
+ # message with line numbers for you to fix.
304
+
305
+ # The following configuration options exist:
306
+
307
+ # "verbosity" specifies how much EZPkg prints while the program runs.
308
+ # There are 4 levels of verbosity. The default is "low":
309
+ # verbosity = "high" # Also what EZPkg decided, and why.
310
+ # verbosity = "medium" # Also each change to the packages.
311
+ # verbosity = "low" # Only one-off notices (default).
312
+ # verbosity = "none" # Nothing, except errors that stop the program.
313
+
314
+ # "update-frequency" specifies how often your runs move to newer package
315
+ # versions. A release always carries the versions your last run used, so
316
+ # whoever receives it gets exactly what you tested.
317
+ # update-frequency = "never" # Leave the current versions alone (default).
318
+ # update-frequency = "monthly" # Move to the newest versions once a month.
319
+ # update-frequency = "weekly" # Move to the newest versions once a week.
320
+ # update-frequency = "daily" # Move to the newest versions once a day.
321
+
322
+ # "purge-unused-after" specifies how long a package may go unused before
323
+ # EZPkg removes it from ~/.ezpkg (it is fetched again if it is needed).
324
+ # purge-unused-after = "month" # Unused for a month (default).
325
+ # purge-unused-after = "week" # Unused for a week.
326
+ # purge-unused-after = "day" # Unused for a day.
327
+
328
+ # "version" specifies the release string. Whenever it differs from the
329
+ # last version released, the next run builds the releases listed in
330
+ # "release", and then runs the program as usual. Change it whenever you
331
+ # like. If you really need to, you can even reuse a previous value.
332
+ # version = "1.0.0" # 1.0.0 is usually the first functional release number.
333
+
334
+ # "release" lists which releases to generate, each named with the VERSION
335
+ # above. Remove the "#" from 0, 1, or more of the entries in the list.
336
+ release = [
337
+ # "email", # PROG_NAME-VERSION.py; email .py (network needed).
338
+ # "zipapp", # PROG_NAME-VERSION.pyz; pure Python (no network needed).
339
+ # "uv", # PROG_NAME-VERSION-uv.py and its lock (`uv` needed).
340
+ ]
341
+ # Each release, and anything worth a second look, is recorded in
342
+ # PROG_NAME.py.releases.
343
+
344
+ # "bootstrap" specifies which EZPkg bootstrap the program carries. Each
345
+ # bootstrap is named by the date it was published ("YYYY-MM-DD").
346
+ # bootstrap = "update" # A release replaces an older one (default).
347
+ # bootstrap = "2026-10-23" # Keep exactly this one.
348
+
349
+ # The next options are rarely needed. Most people can stop reading now.
350
+ # They are needed for difficult situations that EZPkg simply cannot figure
351
+ # out on its own.
352
+
353
+ # "requires-python" specifies the oldest Python the program runs on. EZPkg
354
+ # works this out from the program and its packages (never older than 3.11,
355
+ # which EZPkg itself needs). Set it only when the program needs a newer
356
+ # Python for its own reasons, such as a library function that is new.
357
+ # requires-python = ">=3.11" # The default: EZPkg itself needs 3.11.
358
+
359
+ # "dependencies" provides a way to manually specify values to be inserted
360
+ # into the dependencies portion of the Python inline script metadata. They
361
+ # use the standard Python dependency specifier format. See:
362
+ #
363
+ # https://packaging.python.org/en/latest/specifications/dependency-specifiers/
364
+ #
365
+ # Put every entry in the one list, which may span several lines. If an
366
+ # entry conflicts with what EZPkg would choose, your entry wins.
367
+ # dependencies = ["numpy>=2.1", "requests[socks]"] # For example.
368
+
369
+ # End of PROG_NAME.py.toml.
370
+ ```
371
+
372
+ ## Releasing a Packaged Program
373
+
374
+ Once you are satisfied with your program, you may choose to release it in
375
+ a more durable single file package. Currently three formats are supported:
376
+ * **`email`**:
377
+ Email format is just a single Python file, `PROG_NAME-VERSION.py`: a copy
378
+ of `PROG_NAME.py` with its inline metadata filled in. When emailed to
379
+ another person, the `EZPkg` bootstrap ensures that all of the required
380
+ packages are downloaded prior to running the program.
381
+ * **`zipapp`**:
382
+ This works for pure Python programs (i.e. no packages with C, C++, Rust,
383
+ etc.) and produces a `.pyz` file. The file can be quite large since it
384
+ includes all of the needed code. All that the recipient needs is a Python
385
+ interpreter; no network is needed. It produces a file
386
+ `PROG_NAME-X.Y.Z.pyz`, where `PROG_NAME` is the program name and `X.Y.Z`
387
+ is the version number.
388
+ * **`uv`**:
389
+ `uv` is a popular packaging system written in Rust. It handles both
390
+ compiled and pure Python packages together. The recipient must have `uv`
391
+ installed, and `uv` fetches the packages over the network on the first run.
392
+
393
+ The requirement is that at least one of the values for `release = [ ... ]`
394
+ is enabled (`email`, `zipapp` or `uv`), and that `version` is set. Once
395
+ both are set, run your program. As a side effect of running your program
396
+ the appropriate files are generated. Whenever `version` changes, the
397
+ appropriate files are (re)generated.
398
+
399
+ ## The Bootstrap
400
+
401
+ Never edit the bootstrap, not even a space or a comment. Indenting the
402
+ whole of it, as under an `if`, is the one change allowed. Anything else
403
+ stops the program, which shows each line that differs from the published
404
+ bootstrap.
405
+
406
+ An older bootstrap that has not been edited still runs, so a program that
407
+ nobody maintains will keep working. When you are the developer, `EZPkg` tells
408
+ you once about each newer bootstrap, and the next release replaces it.
409
+
410
+ ## Security
411
+
412
+ `EZPkg` fetches code from the internet and runs it, so here is who you
413
+ are trusting:
414
+ * **The program's author**:
415
+ The one question on a first run is about them. If you do not trust
416
+ them, answer `n`. A `y` is remembered, so you are not asked again.
417
+ * **PyPI**:
418
+ Every package, `ezpkg` included, comes from the
419
+ [Python Package Index](https://pypi.org), and each download is checked
420
+ against the checksum PyPI publishes for it.
421
+ * **No substitutions allowed**:
422
+ Once its author has run a program with `EZPkg`, the program carries a
423
+ checksum of every package file it ran with. A file that differs, such
424
+ as one re-uploaded to PyPI later, is refused before anything in it
425
+ can run.
426
+ * **All content is stored in the `~/.ezpkg` directory**, except:
427
+ * `PROG_NAME.py.toml`, which is filled only when it is empty.
428
+ * Up to 4 output files in total for the three possible release
429
+ formats: "email" (1), "zipapp" (1) and "uv" (2).
430
+
431
+ ## Miscellaneous
432
+
433
+ Here are some miscellaneous observations:
434
+ * **Garbage collection**:
435
+ Packages that no program has used for 30 days are removed, unless a
436
+ program's `PROG_NAME.py.toml` sets `purge-unused-after` for its own
437
+ packages.
438
+ * **No duplication**:
439
+ Programs that need the same package share one copy.
440
+ * **Versions side by side**:
441
+ Programs that need different versions of a package each get the correct one.
442
+ * **One version per program**:
443
+ A running program can import only one version of a package, so two
444
+ parts of one program cannot use different versions of the same one.
445
+ * **Where it has been tested**:
446
+ Linux, Windows and macOS (Intel and Apple Silicon) on GitHub's test
447
+ machines, Python 3.11 to 3.15; not yet tried with the Microsoft Store,
448
+ Homebrew or Apple's own Pythons.
449
+
450
+ ## How `EZPkg` Actually Works
451
+
452
+ This section is for the curious-minded. Feel free to skip it unless you
453
+ are interested in puzzling out how the `EZPkg` bootstrap actually works
454
+ on your own.
455
+
456
+ Please open `quick_start.py` and go to `# Part 2: ...`. The entire
457
+ bootstrap runs inside a Python `try` block. Before each step, the
458
+ variable `s` is set to the step's name, so that if anything goes wrong,
459
+ the `except` at the bottom stops the program with
460
+ `ezpkg: <step> failed: ...`, naming the step that broke. The steps are:
461
+ * `s = "find ezpkg"`:
462
+ * This names `~/.ezpkg/ezpkg.whl`, where `ezpkg` is kept, and tries
463
+ `import ezpkg`. That succeeds when something like `uv run` has
464
+ already provided it.
465
+ * If `ezpkg` imported, or `~/.ezpkg/ezpkg.whl` already exists, the
466
+ download is skipped. It runs only the first time any program uses
467
+ `EZPkg` on a machine.
468
+ * `def g(u)`:
469
+ * This fetches a URL for the download. If Python cannot, it asks
470
+ `curl` instead. That happens with the Python installer from
471
+ python.org on macOS, which has no certificates (and so cannot check
472
+ that it is really talking to PyPI) until its
473
+ `Install Certificates.command` has been run. Every Mac has `curl`,
474
+ which uses the Mac's own certificates.
475
+ * `s = "download ezpkg"`:
476
+ * This asks [PyPI](https://pypi.org/) for the newest `ezpkg` wheel,
477
+ downloads it, checks its SHA-256 against the one PyPI publishes, and
478
+ keeps it as `~/.ezpkg/ezpkg.whl`, which is actually just a zip file.
479
+ * `s = "import ezpkg"`:
480
+ * This puts `~/.ezpkg/ezpkg.whl` at the front of `sys.path` and
481
+ imports `ezpkg` straight from it. Since Python can import pure
482
+ Python code from inside a zip file, nothing is actually installed.
483
+ * `s = "fetch packages"`:
484
+ * `m.EZPkg()` sets up the `~/.ezpkg` directory, and `.install()`
485
+ reads Part 1, downloads any packages not already there, and puts
486
+ them on `sys.path`, so that the imports in Part 3 succeed. (This is
487
+ why the user's import statements must be after the bootstrap.)
488
+ * `bootstrap="YYYY-MM-DD"` names the bootstrap this program carries.
489
+ They are always referred to by their published date.
490
+ * If Part 1's `ezpkg` entry asks for a different version, this `ezpkg`
491
+ fetches that one and restarts the program using it instead.
492
+
493
+ `ezpkg` fetches packages with `pip`, the standard Python tool. It downloads
494
+ its own copy of `pip` (the newest one your Python can run) and keeps it
495
+ as `~/.ezpkg/pip.whl`. This is because some commonly used Linux releases
496
+ (e.g. Ubuntu) do not include `pip` with the base Python installation and
497
+ other releases refuse to let it install into the system's Python
498
+ ([PEP 668](https://peps.python.org/pep-0668/)). The machine's own
499
+ `pip`, if it has one, is never changed.
500
+
501
+ A release also uses [`uv`](https://docs.astral.sh/uv/), the fast package
502
+ manager written in Rust. `pip` only works out what *this* computer needs,
503
+ so a release asks `uv` to work out what every other computer needs too
504
+ (Windows, macOS, Linux, and each Python the program allows), held to the
505
+ versions your own runs approved. If `uv` is not installed, `ezpkg`
506
+ fetches its own copy into `~/.ezpkg`, just as it does for `pip`.
507
+
508
+ ## Troubleshooting
509
+
510
+ These are the messages a developer is most likely to meet, and what to
511
+ do about each. An end user sees only the one-line failures in [Running a
512
+ Program That Uses `EZPkg`](#running-a-program-that-uses-ezpkg).
513
+ * **``PROG_NAME.py.toml line 3: unknown setting `relase` (did you mean
514
+ `release`?)``**:
515
+ Fix the spelling in `PROG_NAME.py.toml`; the program does not run until
516
+ every setting is known.
517
+ * **``PROG_NAME.py.toml lines LINENO1 and LINENO2: `update-frequency` is set
518
+ twice; keep only one of them``**:
519
+ The `#` was removed from two lines of one setting. Put it back on the
520
+ one you did not mean. (The message gives the actual line numbers.)
521
+ * **``PROG_NAME.py line 7: `import tabulat` needs a package, but PyPI has none
522
+ called `tabulat` ``**:
523
+ Usually a misspelt import. If the package's name on PyPI differs from
524
+ the module's, add the right name to `dependencies` yourself.
525
+ * **``line 12: `cowsay` is imported above the bootstrap; move it below,
526
+ with the other imports``**:
527
+ On a computer whose `~/.ezpkg` is still empty, that import runs before
528
+ anything has been fetched, so it fails there even though it works for
529
+ you.
530
+ * **An edited bootstrap**:
531
+ `EZPkg` stops, showing each line that differs from the published
532
+ bootstrap, with a `|` under the first different character. Paste the
533
+ bootstrap again from `quick_start.py`.
534
+ * **``PROG_NAME.py line 14 imports `PKG_NAME`, which is newer than
535
+ YYYY-MM-DD, so PROG_NAME.py moves to today's versions ...``**:
536
+ Nothing to do. Your versions were frozen at the date of your last update
537
+ (see `update-frequency`), and `PKG_NAME` is newer than that, so `EZPkg`
538
+ moved this program, and only this program, to today's versions of all
539
+ its packages. The next release carries them.
540
+ * **``PROG_NAME.py line 14 imports `PKG_NAME`, but no release of it existed
541
+ before the date ... (line 5, YYYY-MM-DD) ...``**:
542
+ Part 1 of `PROG_NAME.py` has an `exclude-newer` date of your own, which
543
+ `EZPkg` never changes. Delete that line, as the message says, to move
544
+ everything to today's versions.
545
+ * **``PROG_NAME.py 1.0.1 was not released: ...``**:
546
+ The release stopped for the reason given; `PROG_NAME.py` still runs, and the
547
+ next run tries the release again, so fix the problem and rerun it.
548
+
549
+ ## Project Information
550
+
551
+ * **Source**:
552
+ [`ezpkg`](https://github.com/waynegramlich/ezpkg)
553
+ * **How it was built**:
554
+ `EZPkg` was developed primarily with
555
+ [Claude Code](https://claude.com/claude-code), Anthropic's coding agent,
556
+ using the Claude Opus 5.5 model, with the author directing the design
557
+ and the documentation. Most of the effort went into testing: the test
558
+ suite and its tools (coverage, scripted first-run sessions, and checks
559
+ on GitHub's Linux, Windows and macOS machines) are about 13,000 lines,
560
+ against about 9,600 for `ezpkg.py` itself.
561
+ * **Bug Reports and Feature Requests**:
562
+ [EZPkg Issues](https://github.com/waynegramlich/ezpkg/issues) is read very
563
+ infrequently by the principal author. A much better strategy is to use
564
+ the standard GitHub cycle: Fork, Clone, Commit, and Pull Request. A pull
565
+ request must pass the associated test suites without decreasing the code
566
+ coverage percentage before it will even remotely be considered for a
567
+ merge. How to run the tests, coverage and the GitHub Actions checks is
568
+ explained at the top of
569
+ [`ezpkg_test.py`](https://github.com/waynegramlich/ezpkg/blob/main/ezpkg_test.py).
570
+ * **License**: MIT No Attribution (`MIT-0`); see
571
+ [`LICENSE`](https://github.com/waynegramlich/ezpkg/blob/main/LICENSE).
572
+
573
+ ## Conclusion
574
+
575
+ `EZPkg` does all of the package downloading and dependency checking for you.
576
+ When you are done, you can literally email someone one of the following
577
+ results:
578
+ * **`email`**:
579
+ A single `.py` file with the `EZPkg` bootstrap embedded in it. The
580
+ resulting program needs network access to download needed packages the
581
+ first time. After they are downloaded, the network is no longer needed.
582
+ * **`zipapp`**:
583
+ A single `.pyz` file that can be directly executed by a Python interpreter.
584
+ `zipapp` requires that all packages be pure Python without any packages
585
+ that contain non-Python code (e.g. C, C++, Rust). No network access
586
+ is needed for a `zipapp` `.pyz` file.
587
+ * **`uv`**:
588
+ In addition to having `uv` installed, the user needs two files:
589
+ * `PROG_NAME-VERSION-uv.py`, the program, run with `uv run`.
590
+ * `PROG_NAME-VERSION-uv.py.lock`, which pins every package's version
591
+ and checksum.
592
+
593
+ Note that `uv` needs network access for the first run to download packages.
594
+
595
+ All of the remaining hassle of managing packages is handled by `EZPkg`.
@@ -0,0 +1,4 @@
1
+ ezpkg.py,sha256=ELTgyi6-7GlSG9SIgldf2_nSi_stfvN_3F1ch2igKmw,385234
2
+ ezpkg-0.0.1.dist-info/METADATA,sha256=9SeAOWW7uLSgHv1NUA8QbZCHKj_0yNuLPJUK6sLBAnI,28590
3
+ ezpkg-0.0.1.dist-info/WHEEL,sha256=6pkS2c9JK_fPmi8WAKQeiWxwY5RyVNwiVbIjGUYP9Sg,81
4
+ ezpkg-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: ezpkg_test
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any