Show HN: Morgan – PyPI Mirror for Restricted/Offline Environments (github.com)
1. Depend on pip to download and cache package distributions. This means those downloads will probably only work in a similar environment (same Python interpreter, same libc), because of the nature of binary package distributions and the fact that packages have optional dependencies for different environments.
2. Depend on other PyPI packages, meaning installing the mirror in a restricted environment in itself is too difficult.
3. Cannot resolve dependencies of dependencies, meaning mirroring PyPI partially is extremely difficult, and PyPI is huge.
Morgan works differently. It creates a mirror based on a configuration file that defines target environments (using Python's standard Environment Markers specification from PEP 345) and a list of package requirement strings (e.g. "requests>=2.24.0"). It downloads all files relevant to the target environments from PyPI (both source and binary distributions), and recursively resolves and downloads their dependencies, again based on the target environments. It then extracts a single-file server to the mirror directory that works with Python 3.7+, has no outside dependencies, and implements the standard Simple API. This directory can be copied to the restricted network, through whatever security policies are in place, and deployed easily with a simple `python server.py` command.
I should note that Morgan can find dependencies from various metadata sources inside package distributions, including standard METADATA/PKG-INFO/pyproject.toml files, and non-standard files such as setuptools' requires.txt.
There's more information in the Git repository. If this is interesting to you, I'll be happy to receive your feedback.
Thanks!
27 comments
[ 3.0 ms ] story [ 72.3 ms ] threadI have been looking for similar solution and the whitelist used to fail with other tools as they weren't resolving the dependencies.
We were running with the same problem (supercomputer with clusters of different architecture and no outgoing connections permitted) and so we created "pypickup" [1,2]. nice to see that we came with similar solutions! I have some questions:
1. is the directory of packages you create compatible with the PEP 503? (so I can use `--index-url file://PATH_TO_LOCAL_CACHE` flat with pip and it should work)
2. is there some filtering mechanism? e.g. we are not interested in non-release versions ("dev" versions, "rc" versions, "post" versions, ...)
3. I guess that the way morgan resolves dependencies is by manually parsing files like "pyproject.toml" or "requirements.txt" and it does not ask the build-system for the dependencies. if so...
kudos for the good work[1] https://pypi.org/project/pypickup/ [2] https://github.com/UB-Quantic/pypickup
As for your questions:
1. I don't see any mention of directory structures in PEP 503. The Morgan server does implement PEP 503 though. In any case, I tried installing now straight from the directory and it didn't work. Are you sure you meant PEP 503?
2. Where Morgan differs from pypickup, as I can see, is that it interprets requirement strings as per PEP 508 (e.g. "requests>=2.40.0; python_version < '3.8'") instead of providing a command such as `pypickup add requests`. For every requirement string, it looks for the latest version in PyPI that satisfies it, and downloads that version. You can filter _in_ the requirement strings, other than that Morgan doesn't have any specific handling of dev/rc/etc.
3. Morgan detects and downloads the build system based either on the [build-system] section of pyproject.toml, or the setup_requires.txt file (from setuptools). These are the sources currently supported. It doesn't actually care what the build system is, it simply attempts to find where it is defined and download it as well.
As for complex dependency specifications, yes, they are supported and honored (Morgan relies on the "packaging" library to properly evaluate those). By the way, I recently moved from Poetry to Hatch for managing the Morgan project itself specifically because I got fed up with Poetry not honoring those specifications, and trying to download completely irrelevant packages.
1. Well, the flag "--index-url" explicitly says that "... should point to a repository compliant with PEP 503 (the simple repository API) or a local directory laid out in the same format". PEP 503 defines the directory structure where there is a folder per package, an "index.html" on the root with a link to each package and *an "index.html" in each package folder that has a link per available file*.
URLs are not limited to "https", they can also be relative paths. So the trick we do is to download the file to the folder of the package and add an anchor to that file in the "index.html" of the package. For example,
If you go to https://pypi.org/simple/numpy, you will find links like the following: <a href="https://files.pythonhosted.org/packages/f6/d8/ab692a75f584d1..." data-requires-python=">=3.8">numpy-1.22.4.zip</a>
But we download it and write, <a href="./numpy-1.22.4.zip" data-requires-python=">=3.8">numpy-1.22.4.zip</a>
This is specially important for us because we cannot setup any kind of server.
2. Okay nice. Yep, we thought that parsing would be more difficult and that relying on parsing would be problematic due to the different build-systems and that many packages still do not have the "pyproject.toml" file. We opted for a manual approach in which you do "pypickup add" until you have no more "dependency missing" errors. Your approach looks much better to me, but like you said is limited to "pyproject.toml" and "setuptools" right now.
Btw, does it also downloads extra dependencies?
3. Nice. I also stopped using Poetry for things like that, but now I manually write my "pyproject.toml" with "setuptools".
I like the idea on trying to parse the dependencies. I will probably try something but since we download all files (filtering some of them), it would be more costly. Maybe in some weeks when I'm more free.
As for extra dependencies, yes, they will be mirrored, but only if relevant, i.e. if they are included in a requirement string (be it a direct requirement or a dependency of a dependency).
Great about the extras.
Would you mind if we reference each other in the readmes?
As for doing partial mirroring of pypi with only what you are using, is that really a good idea anyway? it will break whenever you add or change any dependency.
[1] https://pypi.org/stats/
https://pip.pypa.io/en/stable/cli/pip_download/#cmdoption-pl...
https://peps.python.org/pep-0425/
If you're using something like Docker/containers, you can download the dependencies inside the container and be reasonably sure you get the right wheels. This becomes trickier when you have different setups like developers on Windows and production on Linux.
- how big is it if you exclude non-Python3 compatible? - how big if you only wanted the latest version of everything?
I'd hesitantly accepted the risk of serving a devpi server over vsock and into my (personal) restricted VLAN. I did so because using a shared folder meant I'd need have cached the module and any dependencies from my internet-connected VLAN first.
Combined with debmirror[0], vscodeoffline[1], and some nightly snatcher shell scripts, I think I have most of my needs covered.
[0] https://help.ubuntu.com/community/Debmirror
[1] https://github.com/LOLINTERNETZ/vscodeoffline
But Poetry and PDM don't add build dependencies to lock files - which I need - so I'm thinking of building a custom resolver.
Did you consider using resolvelib [2], which is what underlies both pip and PDM?
[1] https://github.com/jvolkman/rules_pycross/blob/main/examples...
[2] https://github.com/sarugaku/resolvelib
Then I had to take it to run in mainland China.
Nope.
BTW I also maintain resolvelib (mentioned in another comment), feel free to shoot any questions in the issue tracker or the PyPA Discord[2], or any other means. The documentation is a bit sparse and there are not many resources on dependency resolution in general, and there’s a few of us that help each other out on things.
[1]: https://github.com/uranusjr/simpleindex [2]: https://discord.com/invite/pypa