blob: 7661bb6c0580fd7563463be9ce1446c1f988e866 [file] [log] [blame]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Utility script to create a pypi-like directory structure (localpi)
from a number of Python packages in a directory of the local filesystem.
DIRECTORY STRUCTURE (before):
+-- downloads/
+-- alice-1.0.zip
+-- alice-1.0.tar.gz
+-- bob-1.3.0.tar.gz
+-- bob-1.4.2.tar.gz
+-- charly-1.0.tar.bz2
DIRECTORY STRUCTURE (afterwards):
+-- downloads/
+-- simple/
| +-- alice/index.html --> ../../alice-*.*
| +-- bob/index.html --> ../../bob-*.*
| +-- charly/index.html --> ../../charly-*.*
| +-- index.html --> alice/, bob/, ...
+-- alice-1.0.zip
+-- alice-1.0.tar.gz
+-- bob-1.3.0.tar.gz
+-- bob-1.4.2.tar.gz
+-- charly-1.0.tar.bz2
USAGE EXAMPLE:
mkdir -p /tmp/downloads
pip install --download=/tmp/downloads argparse Jinja2
make_localpi.py /tmp/downloads
pip install --index-url=file:///tmp/downloads/simple argparse Jinja2
ALTERNATIVE:
pip install --download=/tmp/downloads argparse Jinja2
pip install --find-links=/tmp/downloads --no-index argparse Jinja2
"""
from __future__ import with_statement, print_function
from fnmatch import fnmatch
import os.path
import shutil
import sys
__author__ = "Jens Engel"
__version__ = "0.2"
__license__ = "BSD"
__copyright__ = "(c) 2013 by Jens Engel"
class Package(object):
"""
Package entity that keeps track of:
* one or more versions of this package
* one or more archive types
"""
PATTERNS = [
"*.egg", "*.exe", "*.whl", "*.zip", "*.tar.gz", "*.tar.bz2", "*.7z"
]
def __init__(self, filename, name=None):
if not name and filename:
name = self.get_pkgname(filename)
self.name = name
self.files = []
if filename:
self.files.append(filename)
@property
def versions(self):
versions_info = [ self.get_pkgversion(p) for p in self.files ]
return versions_info
@classmethod
def get_pkgversion(cls, filename):
parts = os.path.basename(filename).rsplit("-", 1)
version = ""
if len(parts) >= 2:
version = parts[1]
for pattern in cls.PATTERNS:
assert pattern.startswith("*")
suffix = pattern[1:]
if version.endswith(suffix):
version = version[:-len(suffix)]
break
return version
@staticmethod
def get_pkgname(filename):
name = os.path.basename(filename).rsplit("-", 1)[0]
if name.startswith("http%3A") or name.startswith("https%3A"):
# -- PIP DOWNLOAD-CACHE PACKAGE FILE NAME SCHEMA:
pos = name.rfind("%2F")
name = name[pos+3:]
return name
@staticmethod
def splitext(filename):
fname = os.path.splitext(filename)[0]
if fname.endswith(".tar"):
fname = os.path.splitext(fname)[0]
return fname
@classmethod
def isa(cls, filename):
basename = os.path.basename(filename)
if basename.startswith("."):
return False
for pattern in cls.PATTERNS:
if fnmatch(filename, pattern):
return True
return False
def make_index_for(package, index_dir, verbose=True):
"""
Create an 'index.html' for one package.
:param package: Package object to use.
:param index_dir: Where 'index.html' should be created.
"""
index_template = """\
<html>
<head><title>{title}</title></head>
<body>
<h1>{title}</h1>
<ul>
{packages}
</ul>
</body>
</html>
"""
item_template = '<li><a href="{1}">{0}</a></li>'
index_filename = os.path.join(index_dir, "index.html")
if not os.path.isdir(index_dir):
os.makedirs(index_dir)
parts = []
for pkg_filename in package.files:
pkg_name = os.path.basename(pkg_filename)
if pkg_name == "index.html":
# -- ROOT-INDEX:
pkg_name = os.path.basename(os.path.dirname(pkg_filename))
else:
pkg_name = package.splitext(pkg_name)
pkg_relpath_to = os.path.relpath(pkg_filename, index_dir)
parts.append(item_template.format(pkg_name, pkg_relpath_to))
if not parts:
print("OOPS: Package %s has no files" % package.name)
return
if verbose:
root_index = not Package.isa(package.files[0])
if root_index:
info = "with %d package(s)" % len(package.files)
else:
package_versions = sorted(set(package.versions))
info = ", ".join(reversed(package_versions))
message = "%-30s %s" % (package.name, info)
print(message)
with open(index_filename, "w") as f:
packages = "\n".join(parts)
text = index_template.format(title=package.name, packages=packages)
f.write(text.strip())
f.close()
def make_package_index(download_dir):
"""
Create a pypi server like file structure below download directory.
:param download_dir: Download directory with packages.
EXAMPLE BEFORE:
+-- downloads/
+-- alice-1.0.zip
+-- alice-1.0.tar.gz
+-- bob-1.3.0.tar.gz
+-- bob-1.4.2.tar.gz
+-- charly-1.0.tar.bz2
EXAMPLE AFTERWARDS:
+-- downloads/
+-- simple/
| +-- alice/index.html --> ../../alice-*.*
| +-- bob/index.html --> ../../bob-*.*
| +-- charly/index.html --> ../../charly-*.*
| +-- index.html --> alice/index.html, bob/index.html, ...
+-- alice-1.0.zip
+-- alice-1.0.tar.gz
+-- bob-1.3.0.tar.gz
+-- bob-1.4.2.tar.gz
+-- charly-1.0.tar.bz2
"""
if not os.path.isdir(download_dir):
raise ValueError("No such directory: %r" % download_dir)
pkg_rootdir = os.path.join(download_dir, "simple")
if os.path.isdir(pkg_rootdir):
shutil.rmtree(pkg_rootdir, ignore_errors=True)
os.mkdir(pkg_rootdir)
# -- STEP: Collect all packages.
package_map = {}
packages = []
for filename in sorted(os.listdir(download_dir)):
if not Package.isa(filename):
continue
pkg_filepath = os.path.join(download_dir, filename)
package_name = Package.get_pkgname(pkg_filepath)
package = package_map.get(package_name, None)
if not package:
# -- NEW PACKAGE DETECTED: Store/register package.
package = Package(pkg_filepath)
package_map[package.name] = package
packages.append(package)
else:
# -- SAME PACKAGE: Collect other variant/version.
package.files.append(pkg_filepath)
# -- STEP: Make local PYTHON PACKAGE INDEX.
root_package = Package(None, "Python Package Index")
root_package.files = [ os.path.join(pkg_rootdir, pkg.name, "index.html")
for pkg in packages ]
make_index_for(root_package, pkg_rootdir)
for package in packages:
index_dir = os.path.join(pkg_rootdir, package.name)
make_index_for(package, index_dir)
# -----------------------------------------------------------------------------
# MAIN:
# -----------------------------------------------------------------------------
if __name__ == "__main__":
if (len(sys.argv) != 2) or "-h" in sys.argv[1:] or "--help" in sys.argv[1:]:
print("USAGE: %s DOWNLOAD_DIR" % os.path.basename(sys.argv[0]))
print(__doc__)
sys.exit(1)
make_package_index(sys.argv[1])