aboutsummaryrefslogtreecommitdiff
path: root/bin/make_localpi.py
diff options
context:
space:
mode:
Diffstat (limited to 'bin/make_localpi.py')
-rwxr-xr-xbin/make_localpi.py244
1 files changed, 244 insertions, 0 deletions
diff --git a/bin/make_localpi.py b/bin/make_localpi.py
new file mode 100755
index 0000000..7661bb6
--- /dev/null
+++ b/bin/make_localpi.py
@@ -0,0 +1,244 @@
+#!/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])