pip2pi-dev 0.9.0__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.
Potentially problematic release.
This version of pip2pi-dev might be problematic. Click here for more details.
- libpip2pi/__init__.py +1 -0
- libpip2pi/commands.py +615 -0
- pip2pi_dev-0.9.0.dist-info/METADATA +257 -0
- pip2pi_dev-0.9.0.dist-info/RECORD +8 -0
- pip2pi_dev-0.9.0.dist-info/WHEEL +5 -0
- pip2pi_dev-0.9.0.dist-info/entry_points.txt +4 -0
- pip2pi_dev-0.9.0.dist-info/licenses/LICENSE.txt +29 -0
- pip2pi_dev-0.9.0.dist-info/top_level.txt +1 -0
libpip2pi/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = (0, 9, 0)
|
libpip2pi/commands.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
import atexit
|
|
6
|
+
import tempfile
|
|
7
|
+
import warnings
|
|
8
|
+
import textwrap
|
|
9
|
+
import functools
|
|
10
|
+
from subprocess import check_call
|
|
11
|
+
import glob
|
|
12
|
+
import optparse
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from html import escape
|
|
16
|
+
except ImportError:
|
|
17
|
+
from cgi import escape
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import wheel as _; _
|
|
21
|
+
has_wheel = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
has_wheel = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def try_int(x):
|
|
27
|
+
try:
|
|
28
|
+
return int(x)
|
|
29
|
+
except ValueError:
|
|
30
|
+
return x
|
|
31
|
+
|
|
32
|
+
pip_download_command = 'download --dest'
|
|
33
|
+
pip_no_binary_command = '--no-binary=:all:'
|
|
34
|
+
|
|
35
|
+
class PipError(Exception):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def warn_wheel():
|
|
39
|
+
print(
|
|
40
|
+
"ERROR: .whl packages were downloaded but the wheel package "
|
|
41
|
+
"is not installed, so they cannot be correctly processed.\n"
|
|
42
|
+
"Install it with:\n"
|
|
43
|
+
" pip install wheel"
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
import setuptools
|
|
47
|
+
setuptools_version = setuptools.__version__
|
|
48
|
+
except ImportError:
|
|
49
|
+
setuptools_version = ''
|
|
50
|
+
if setuptools_version < '1.0.0':
|
|
51
|
+
print(
|
|
52
|
+
"WARNING: your setuptools package is out of date. This "
|
|
53
|
+
"could lead to bad things.\n"
|
|
54
|
+
"You should likely update it:\n"
|
|
55
|
+
" pip install -U setuptools"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def dedent(text):
|
|
59
|
+
return textwrap.dedent(text.lstrip("\n")).rstrip()
|
|
60
|
+
|
|
61
|
+
def maintain_cwd(f):
|
|
62
|
+
@functools.wraps(f)
|
|
63
|
+
def maintain_cwd_helper(*args, **kwargs):
|
|
64
|
+
orig_dir = os.getcwd()
|
|
65
|
+
try:
|
|
66
|
+
return f(*args, **kwargs)
|
|
67
|
+
finally:
|
|
68
|
+
os.chdir(orig_dir)
|
|
69
|
+
return maintain_cwd_helper
|
|
70
|
+
|
|
71
|
+
class InvalidFilePackageName(ValueError):
|
|
72
|
+
def __init__(self, file, basedir=None):
|
|
73
|
+
msg = "unexpected file name: %r " %(file, )
|
|
74
|
+
msg += "(not in 'pkg-name-version.xxx' format"
|
|
75
|
+
if basedir:
|
|
76
|
+
msg += "; found in directory: %r" %(basedir)
|
|
77
|
+
msg += ")"
|
|
78
|
+
super(InvalidFilePackageName, self).__init__(msg)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def safe_name(name):
|
|
82
|
+
"""Convert an arbitrary string to a standard distribution name
|
|
83
|
+
|
|
84
|
+
Any runs of non-alphanumeric/. characters are replaced with a single '-'.
|
|
85
|
+
Copied from pkg_resources.
|
|
86
|
+
"""
|
|
87
|
+
return re.sub('[^A-Za-z0-9.]+', '-', name)
|
|
88
|
+
|
|
89
|
+
# Copied from pkg_resources.
|
|
90
|
+
EGG_NAME = re.compile(
|
|
91
|
+
r"""
|
|
92
|
+
(?P<name>[^-]+) (
|
|
93
|
+
-(?P<ver>[^-]+) (
|
|
94
|
+
-py(?P<pyver>[^-]+) (
|
|
95
|
+
-(?P<plat>.+)
|
|
96
|
+
)?
|
|
97
|
+
)?
|
|
98
|
+
)?
|
|
99
|
+
""",
|
|
100
|
+
re.VERBOSE | re.IGNORECASE,
|
|
101
|
+
).match
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def egg_project_name(filename):
|
|
105
|
+
project_name, version, py_version, platform = [None] * 4
|
|
106
|
+
basename, ext = os.path.splitext(filename)
|
|
107
|
+
match = EGG_NAME(basename)
|
|
108
|
+
if match:
|
|
109
|
+
project_name = match.group("name")
|
|
110
|
+
return safe_name(project_name or 'Unknown')
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def file_to_package(file, basedir=None):
|
|
114
|
+
""" Returns the package name for a given file, or raises an
|
|
115
|
+
``InvalidFilePackageName`` exception if the file name is
|
|
116
|
+
not valid::
|
|
117
|
+
|
|
118
|
+
>>> file_to_package("foo-1.2.3_rc1.tar.gz")
|
|
119
|
+
('foo', '1.2.3-rc1.tar.gz')
|
|
120
|
+
>>> file_to_package("foo-bar-1.2.tgz")
|
|
121
|
+
('foo-bar', '1.2.tgz')
|
|
122
|
+
>>> file_to_package("kafka-quixey-0.8.1-1.tar.gz")
|
|
123
|
+
('kafka-quixey', '0.8.1-1.tar.gz')
|
|
124
|
+
>>> file_to_package("foo-bar-1.2-py27-none-any.whl")
|
|
125
|
+
('foo-bar', '1.2-py27-none-any.whl')
|
|
126
|
+
>>> file_to_package("Cython-0.17.2-cp26-none-linux_x86_64.whl")
|
|
127
|
+
('Cython', '0.17.2-cp26-none-linux_x86_64.whl')
|
|
128
|
+
>>> file_to_package("PyYAML-3.10-py2.7-macosx-10.7-x86_64.egg")
|
|
129
|
+
('PyYAML', '3.10-py2.7-macosx-10.7-x86_64.egg')
|
|
130
|
+
>>> file_to_package("python_ldap-2.3.9-py2.7-macosx-10.3-fat.egg")
|
|
131
|
+
('python-ldap', '2.3.9-py2.7-macosx-10.3-fat.egg')
|
|
132
|
+
>>> file_to_package("python_ldap-2.4.19-cp27-none-macosx_10_10_x86_64.whl")
|
|
133
|
+
('python-ldap', '2.4.19-cp27-none-macosx_10_10_x86_64.whl')
|
|
134
|
+
>>> file_to_package("foo.whl")
|
|
135
|
+
Traceback (most recent call last):
|
|
136
|
+
...
|
|
137
|
+
InvalidFilePackageName: unexpected file name: 'foo.whl' (not in 'pkg-name-version.xxx' format)
|
|
138
|
+
>>> file_to_package("foo.png")
|
|
139
|
+
Traceback (most recent call last):
|
|
140
|
+
...
|
|
141
|
+
InvalidFilePackageName: unexpected file name: 'foo.png' (not in 'pkg-name-version.xxx' format)
|
|
142
|
+
"""
|
|
143
|
+
file = os.path.basename(file)
|
|
144
|
+
file_ext = os.path.splitext(file)[1].lower()
|
|
145
|
+
if file_ext == ".egg":
|
|
146
|
+
name = egg_project_name(file)
|
|
147
|
+
split = (name, file[len(name)+1:])
|
|
148
|
+
to_safe_name = lambda x: x
|
|
149
|
+
to_safe_rest = lambda x: x
|
|
150
|
+
elif file_ext == ".whl":
|
|
151
|
+
bits = file.rsplit("-", 4)
|
|
152
|
+
split = (bits[0], "-".join(bits[1:]))
|
|
153
|
+
to_safe_name = safe_name
|
|
154
|
+
to_safe_rest = lambda x: x
|
|
155
|
+
else:
|
|
156
|
+
match = re.search(r"(?P<pkg>.*?)-(?P<rest>\d+.*)", file)
|
|
157
|
+
if not match:
|
|
158
|
+
raise InvalidFilePackageName(file, basedir)
|
|
159
|
+
split = (match.group("pkg"), match.group("rest"))
|
|
160
|
+
to_safe_name = safe_name
|
|
161
|
+
to_safe_rest = safe_name
|
|
162
|
+
|
|
163
|
+
if len(split) != 2 or not split[1]:
|
|
164
|
+
raise InvalidFilePackageName(file, basedir)
|
|
165
|
+
|
|
166
|
+
return (to_safe_name(split[0]), to_safe_rest(split[1]))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def pip_run_command(pip_args):
|
|
170
|
+
check_call(["pip"] + pip_args)
|
|
171
|
+
|
|
172
|
+
OS_HAS_SYMLINK = hasattr(os, "symlink")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Pip2PiOptionParser(optparse.OptionParser):
|
|
176
|
+
def format_description(self, formatter):
|
|
177
|
+
# Parent implementation reformats all the hard line breaks
|
|
178
|
+
return self.get_description() + "\n"
|
|
179
|
+
|
|
180
|
+
def add_index_options(self):
|
|
181
|
+
self.add_option(
|
|
182
|
+
'-n', '--normalize-package-names', dest="normalize_package_names",
|
|
183
|
+
default=True, action="store_true",
|
|
184
|
+
help=dedent("""
|
|
185
|
+
Normalize package names in the simple index using whatever
|
|
186
|
+
scheme is supported by the most recent version of pip (default).
|
|
187
|
+
"""))
|
|
188
|
+
self.add_option(
|
|
189
|
+
'-N', '--no-normalize-package-names', dest="normalize_package_names",
|
|
190
|
+
action="store_false")
|
|
191
|
+
self.add_option(
|
|
192
|
+
'-a', '--aggressive-normalization', dest="aggressive_normalization",
|
|
193
|
+
default=False, action="store_true",
|
|
194
|
+
help=dedent("""
|
|
195
|
+
Aggressive create symlinks to many different package name
|
|
196
|
+
normalizations to support multiple pip versions (for a package
|
|
197
|
+
named "Spam_Eggs--cheese.shop", the following normalizations
|
|
198
|
+
will be used: "spam-eggs-cheese-shop" (PEP-503),
|
|
199
|
+
"spam-eggs-cheese.shop" (pip 6, pip 7),
|
|
200
|
+
"Spam_Eggs--cheese.shop" (pip < 6)).
|
|
201
|
+
"""))
|
|
202
|
+
self.add_option(
|
|
203
|
+
'--recursive', dest="recursive", default=True, action="store_true",
|
|
204
|
+
help=dedent("""
|
|
205
|
+
Also scan sub-directories of PACKAGE_DIR, at any depth, for
|
|
206
|
+
packages (default). Files in sub-directories are only indexed
|
|
207
|
+
when their name ends in one of: .whl, .egg, .zip, .tgz, .tar,
|
|
208
|
+
.tar.gz, .tar.bz2, .tar.xz.
|
|
209
|
+
"""))
|
|
210
|
+
self.add_option(
|
|
211
|
+
'--no-recursive', dest="recursive", action="store_false",
|
|
212
|
+
help=dedent("""
|
|
213
|
+
Only look at the top level of PACKAGE_DIR.
|
|
214
|
+
"""))
|
|
215
|
+
self.add_option(
|
|
216
|
+
'-x', '--no-build-html', dest="build_html", action="store_false",
|
|
217
|
+
default=True, help=dedent("""
|
|
218
|
+
Don't build index files for each directory, you might want to
|
|
219
|
+
to disable this if your server dynamically builds index files
|
|
220
|
+
using something like mod_autoindex and you want to make older
|
|
221
|
+
package/module versions continue to be available.
|
|
222
|
+
"""))
|
|
223
|
+
self.add_option(
|
|
224
|
+
'-z', '--also-get-source', dest="get_source", action="store_true",
|
|
225
|
+
default=False, help=dedent("""
|
|
226
|
+
In addition to downloading wheels, eggs or any other package
|
|
227
|
+
format also download the source tar.gz files in case the
|
|
228
|
+
platform using this index does not support the wheel/egg/etc
|
|
229
|
+
"""))
|
|
230
|
+
self.add_option(
|
|
231
|
+
'-s', '--symlink', dest="use_symlink",
|
|
232
|
+
default=OS_HAS_SYMLINK, action="store_true",
|
|
233
|
+
help=dedent("""
|
|
234
|
+
Use symlinks in PACKAGE_DIR/simple/ rather than copying files.
|
|
235
|
+
Default: %default
|
|
236
|
+
"""))
|
|
237
|
+
self.add_option(
|
|
238
|
+
'-S', '--no-symlink', dest="use_symlink", action="store_false")
|
|
239
|
+
self.add_option(
|
|
240
|
+
'-v', '--verbose', dest="verbose", action="store_true")
|
|
241
|
+
|
|
242
|
+
def _process_args(self, largs, rargs, values):
|
|
243
|
+
"""
|
|
244
|
+
An unknown option pass-through implementation of OptionParser.
|
|
245
|
+
|
|
246
|
+
When unknown arguments are encountered, bundle with largs and try again,
|
|
247
|
+
until rargs is depleted.
|
|
248
|
+
|
|
249
|
+
sys.exit(status) will still be called if a known argument is passed
|
|
250
|
+
incorrectly (e.g. missing arguments or bad argument types, etc.)
|
|
251
|
+
|
|
252
|
+
From http://stackoverflow.com/a/9307174/6364
|
|
253
|
+
"""
|
|
254
|
+
while rargs:
|
|
255
|
+
try:
|
|
256
|
+
optparse.OptionParser._process_args(self, largs, rargs, values)
|
|
257
|
+
except (optparse.BadOptionError, optparse.AmbiguousOptionError) as e:
|
|
258
|
+
largs.append(e.opt_str)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def dir2pi(argv=sys.argv, use_symlink=None):
|
|
262
|
+
parser = Pip2PiOptionParser(
|
|
263
|
+
usage="usage: %prog PACKAGE_DIR",
|
|
264
|
+
description=dedent("""
|
|
265
|
+
Creates the directory PACKAGE_DIR/simple/ and populates it with the
|
|
266
|
+
directory structure required to use with pip's --index-url.
|
|
267
|
+
|
|
268
|
+
Assumes that PACKAGE_DIR (and, unless --no-recursive is given, its
|
|
269
|
+
sub-directories) contain a bunch of archives named
|
|
270
|
+
'package-name-version.ext' (ex 'foo-2.1.tar.gz' or
|
|
271
|
+
'foo-bar-1.3rc1.bz2').
|
|
272
|
+
|
|
273
|
+
This makes the most sense if PACKAGE_DIR is somewhere inside a
|
|
274
|
+
webserver's htdocs directory.
|
|
275
|
+
|
|
276
|
+
For example:
|
|
277
|
+
|
|
278
|
+
$ ls packages/
|
|
279
|
+
foo-1.2.tar.gz
|
|
280
|
+
$ dir2pi packages/
|
|
281
|
+
$ find packages/
|
|
282
|
+
packages/
|
|
283
|
+
packages/foo-1.2.tar.gz
|
|
284
|
+
packages/simple/
|
|
285
|
+
packages/simple/foo/
|
|
286
|
+
packages/simple/foo/index.html
|
|
287
|
+
packages/simple/foo/foo-1.2.tar.gz
|
|
288
|
+
"""))
|
|
289
|
+
|
|
290
|
+
if use_symlink is not None:
|
|
291
|
+
warnings.warn("dir2pi(use_symlink=...) is deprecated", stacklevel=1)
|
|
292
|
+
|
|
293
|
+
parser.add_index_options()
|
|
294
|
+
|
|
295
|
+
option, argv = parser.parse_args(argv)
|
|
296
|
+
if option.aggressive_normalization and not option.use_symlink:
|
|
297
|
+
sys.stderr.write(
|
|
298
|
+
"Error: cannot use --aggressive-normalization without --symlink\n")
|
|
299
|
+
return 1
|
|
300
|
+
|
|
301
|
+
if len(argv) != 2:
|
|
302
|
+
parser.print_help()
|
|
303
|
+
parser.exit()
|
|
304
|
+
return _dir2pi(option, argv)
|
|
305
|
+
|
|
306
|
+
def normalize_pep503(pkg_name):
|
|
307
|
+
# As per https://www.python.org/dev/peps/pep-0503/#normalized-names
|
|
308
|
+
return re.sub(r"[-_.]+", "-", pkg_name).lower()
|
|
309
|
+
|
|
310
|
+
def normalize_pip67(pkg_name):
|
|
311
|
+
# Normalize for pip 6 and 7
|
|
312
|
+
return re.sub(r"([-.])+", r"\1", pkg_name.replace("_", "-")).lower()
|
|
313
|
+
|
|
314
|
+
def try_symlink(option, source, target):
|
|
315
|
+
try:
|
|
316
|
+
os.symlink(source, target)
|
|
317
|
+
if option.verbose:
|
|
318
|
+
print('linking %s to %s' %(source, target))
|
|
319
|
+
except OSError as e:
|
|
320
|
+
if e.errno == 17:
|
|
321
|
+
# ignore errors when the file exists
|
|
322
|
+
return
|
|
323
|
+
sys.stderr.write(
|
|
324
|
+
'ERROR linking %s to %s (skipping): %s\n'
|
|
325
|
+
%(source, target, e)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# Files in sub-directories are only treated as packages when their name looks
|
|
330
|
+
# like a package archive: a directory full of downloads often also holds a
|
|
331
|
+
# README, an index.html, etc. The top level keeps the historical behaviour of
|
|
332
|
+
# treating every file it finds as a package.
|
|
333
|
+
PACKAGE_FILE_EXTS = (
|
|
334
|
+
".whl", ".egg", ".zip", ".tgz", ".tar", ".tar.gz", ".tar.bz2", ".tar.xz",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def is_package_file(filename):
|
|
338
|
+
return filename.lower().endswith(PACKAGE_FILE_EXTS)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def iter_package_files(pkgdir, recursive=True, skip_dirs=()):
|
|
342
|
+
""" Yields the packages to index, depth first.
|
|
343
|
+
|
|
344
|
+
The top level of ``pkgdir`` is always scanned. Sub-directories are
|
|
345
|
+
scanned too when ``recursive`` is true (the default), but only files
|
|
346
|
+
that look like a package archive are taken from them. Hidden files and
|
|
347
|
+
directories are ignored, and so is anything in ``skip_dirs`` (absolute
|
|
348
|
+
paths -- the generated ``simple`` directory).
|
|
349
|
+
"""
|
|
350
|
+
skip_dirs = set(os.path.abspath(x) for x in skip_dirs)
|
|
351
|
+
if not recursive:
|
|
352
|
+
for filename in sorted(os.listdir(pkgdir)):
|
|
353
|
+
filepath = os.path.join(pkgdir, filename)
|
|
354
|
+
if not filename.startswith(".") and os.path.isfile(filepath):
|
|
355
|
+
yield filepath
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
for dirpath, dirnames, filenames in os.walk(pkgdir):
|
|
359
|
+
at_top = os.path.abspath(dirpath) == os.path.abspath(pkgdir)
|
|
360
|
+
dirnames[:] = sorted(
|
|
361
|
+
dirname for dirname in dirnames
|
|
362
|
+
if not dirname.startswith(".")
|
|
363
|
+
and os.path.abspath(os.path.join(dirpath, dirname)) not in skip_dirs
|
|
364
|
+
)
|
|
365
|
+
for filename in sorted(filenames):
|
|
366
|
+
if filename.startswith("."):
|
|
367
|
+
continue
|
|
368
|
+
if not at_top and not is_package_file(filename):
|
|
369
|
+
continue
|
|
370
|
+
filepath = os.path.join(dirpath, filename)
|
|
371
|
+
if os.path.isfile(filepath):
|
|
372
|
+
yield filepath
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _dir2pi(option, argv):
|
|
376
|
+
pkgdir = argv[1]
|
|
377
|
+
if not os.path.isdir(pkgdir):
|
|
378
|
+
raise ValueError("no such directory: %r" %(pkgdir, ))
|
|
379
|
+
pkgdirpath = lambda *x: os.path.join(pkgdir, *x)
|
|
380
|
+
|
|
381
|
+
shutil.rmtree(pkgdirpath("simple"), ignore_errors=True)
|
|
382
|
+
os.mkdir(pkgdirpath("simple"))
|
|
383
|
+
pkg_index = ("<html><head><title>Simple Index</title>"
|
|
384
|
+
"<meta name='api-version' value='2' /></head><body>\n")
|
|
385
|
+
|
|
386
|
+
# The same archive can be present more than once -- nightly builds keep a
|
|
387
|
+
# directory per build -- and only one of them is indexed: the last one in
|
|
388
|
+
# lexicographic order, so the newest build directory wins.
|
|
389
|
+
pkg_files = {}
|
|
390
|
+
for pkg_filepath in iter_package_files(
|
|
391
|
+
pkgdir,
|
|
392
|
+
recursive=option.recursive,
|
|
393
|
+
skip_dirs=[pkgdirpath("simple")],
|
|
394
|
+
):
|
|
395
|
+
pkg_basename = os.path.basename(pkg_filepath)
|
|
396
|
+
pkg_name, pkg_rest = file_to_package(
|
|
397
|
+
pkg_basename, os.path.dirname(pkg_filepath),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
pkg_dir_name = pkg_name
|
|
401
|
+
if option.normalize_package_names:
|
|
402
|
+
pkg_dir_name = normalize_pep503(pkg_dir_name)
|
|
403
|
+
|
|
404
|
+
pkg_key = (pkg_dir_name, pkg_basename)
|
|
405
|
+
previous = pkg_files.get(pkg_key)
|
|
406
|
+
if previous is None or (
|
|
407
|
+
os.path.normpath(pkg_filepath) > os.path.normpath(previous[0])
|
|
408
|
+
):
|
|
409
|
+
if previous is not None and option.verbose:
|
|
410
|
+
print('replacing duplicate %s with %s' %(
|
|
411
|
+
previous[0], pkg_filepath,
|
|
412
|
+
))
|
|
413
|
+
pkg_files[pkg_key] = (pkg_filepath, pkg_name)
|
|
414
|
+
elif option.verbose:
|
|
415
|
+
print('ignoring duplicate %s (keeping %s)' %(
|
|
416
|
+
pkg_filepath, previous[0],
|
|
417
|
+
))
|
|
418
|
+
|
|
419
|
+
processed_pkg = set()
|
|
420
|
+
written_pkg_pages = set()
|
|
421
|
+
index_links = []
|
|
422
|
+
for (pkg_dir_name, pkg_basename), (pkg_filepath, pkg_name) in pkg_files.items():
|
|
423
|
+
pkg_dir = pkgdirpath("simple", pkg_dir_name)
|
|
424
|
+
if not os.path.exists(pkg_dir):
|
|
425
|
+
os.mkdir(pkg_dir)
|
|
426
|
+
|
|
427
|
+
if option.aggressive_normalization:
|
|
428
|
+
try_symlink(option, pkg_dir_name, pkgdirpath("simple", normalize_pip67(pkg_name)))
|
|
429
|
+
try_symlink(option, pkg_dir_name, pkgdirpath("simple", pkg_name))
|
|
430
|
+
|
|
431
|
+
symlink_target = os.path.join(pkg_dir, pkg_basename)
|
|
432
|
+
symlink_source = os.path.relpath(pkg_filepath, pkg_dir)
|
|
433
|
+
if option.use_symlink:
|
|
434
|
+
try_symlink(option, symlink_source, symlink_target)
|
|
435
|
+
else:
|
|
436
|
+
if option.verbose:
|
|
437
|
+
print('copying %s to %s' % (symlink_target, pkg_filepath))
|
|
438
|
+
shutil.copy2(pkg_filepath, symlink_target)
|
|
439
|
+
|
|
440
|
+
if pkg_name not in processed_pkg:
|
|
441
|
+
index_links.append((pkg_dir_name, pkg_name))
|
|
442
|
+
processed_pkg.add(pkg_name)
|
|
443
|
+
|
|
444
|
+
if option.build_html:
|
|
445
|
+
pkg_page = os.path.join(pkg_dir, "index.html")
|
|
446
|
+
if pkg_page not in written_pkg_pages:
|
|
447
|
+
with open(pkg_page, "w") as fp:
|
|
448
|
+
fp.write(
|
|
449
|
+
"<html><head><title>Links for %(name)s</title>"
|
|
450
|
+
"<meta name='api-version' value='2' />"
|
|
451
|
+
"</head><body>\n"
|
|
452
|
+
"<h1>Links for %(name)s</h1>\n" %{
|
|
453
|
+
"name": escape(pkg_name),
|
|
454
|
+
}
|
|
455
|
+
)
|
|
456
|
+
written_pkg_pages.add(pkg_page)
|
|
457
|
+
with open(pkg_page, "a") as fp:
|
|
458
|
+
fp.write("<a href='%(name)s'>%(name)s</a><br />\n" %{
|
|
459
|
+
"name": escape(pkg_basename),
|
|
460
|
+
})
|
|
461
|
+
# The order in which the archives are found depends on the filesystem (and
|
|
462
|
+
# on the directory they live in), so the index is sorted here rather than
|
|
463
|
+
# relying on the order of the scan: alphabetically, ignoring case.
|
|
464
|
+
pkg_index += "".join(
|
|
465
|
+
"<a href='%(dir)s/'>%(name)s</a><br />\n" %{
|
|
466
|
+
"dir": escape(pkg_dir_name),
|
|
467
|
+
"name": escape(pkg_name),
|
|
468
|
+
}
|
|
469
|
+
for pkg_dir_name, pkg_name in sorted(
|
|
470
|
+
index_links, key=lambda link: (link[1].lower(), link[1]),
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
pkg_index += "</body></html>\n"
|
|
474
|
+
|
|
475
|
+
if option.build_html:
|
|
476
|
+
with open(pkgdirpath("simple/index.html"), "w") as fp:
|
|
477
|
+
fp.write(pkg_index)
|
|
478
|
+
for pkg_page in written_pkg_pages:
|
|
479
|
+
with open(pkg_page, "a") as fp:
|
|
480
|
+
fp.write("</body></html>\n")
|
|
481
|
+
|
|
482
|
+
return 0
|
|
483
|
+
|
|
484
|
+
def globall(globs):
|
|
485
|
+
result = []
|
|
486
|
+
for g in globs:
|
|
487
|
+
result.extend(glob.glob(g))
|
|
488
|
+
return result
|
|
489
|
+
|
|
490
|
+
@maintain_cwd
|
|
491
|
+
def pip2tgz(argv=sys.argv):
|
|
492
|
+
glob_exts = ['*.whl', '*.tgz', '*.gz']
|
|
493
|
+
parser = Pip2PiOptionParser(
|
|
494
|
+
usage="usage: %prog OUTPUT_DIRECTORY [PIP_OPTIONS] PACKAGES ...",
|
|
495
|
+
description=dedent("""
|
|
496
|
+
Where PACKAGES are any names accepted by pip (ex, `foo`,
|
|
497
|
+
`foo==1.2`, `-r requirements.txt`), and [PIP_OPTIONS] can be any
|
|
498
|
+
options accepted by `pip %s`.
|
|
499
|
+
|
|
500
|
+
pip2tgz will download all packages required to install PACKAGES and
|
|
501
|
+
save them to sanely-named tarballs or wheel files in OUTPUT_DIRECTORY.
|
|
502
|
+
|
|
503
|
+
For example:
|
|
504
|
+
|
|
505
|
+
$ pip2tgz /var/www/packages/ -r requirements.txt foo==1.2 baz/
|
|
506
|
+
$ pip2tgz /var/www/packages/ \\
|
|
507
|
+
%s \\
|
|
508
|
+
--index-url https://example.com/simple \\
|
|
509
|
+
bar==3.1
|
|
510
|
+
""" % (pip_download_command, pip_no_binary_command)))
|
|
511
|
+
|
|
512
|
+
option, argv = parser.parse_args(argv)
|
|
513
|
+
if len(argv) < 3:
|
|
514
|
+
parser.print_help()
|
|
515
|
+
parser.exit()
|
|
516
|
+
|
|
517
|
+
outdir = os.path.abspath(argv[1])
|
|
518
|
+
if not os.path.exists(outdir):
|
|
519
|
+
os.mkdir(outdir)
|
|
520
|
+
|
|
521
|
+
full_glob_paths = [
|
|
522
|
+
os.path.join(outdir, g) for g in glob_exts
|
|
523
|
+
]
|
|
524
|
+
pkg_file_set = lambda: set(globall(full_glob_paths))
|
|
525
|
+
old_pkgs = pkg_file_set()
|
|
526
|
+
|
|
527
|
+
pip_run_command(pip_download_command.split() + [outdir] + argv[2:])
|
|
528
|
+
|
|
529
|
+
os.chdir(outdir)
|
|
530
|
+
new_pkgs = pkg_file_set() - old_pkgs
|
|
531
|
+
num_pkgs = len(pkg_file_set() - old_pkgs)
|
|
532
|
+
print("\nDone. %s new archives currently saved in %r." %(num_pkgs, argv[1]))
|
|
533
|
+
return 0
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
WINDOWS_PATH_RE = re.compile(r"^[A-Za-z]:\\")
|
|
537
|
+
|
|
538
|
+
def is_remote_target(target):
|
|
539
|
+
return ":" in target and not WINDOWS_PATH_RE.match(target)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def pip2pi(argv=sys.argv):
|
|
543
|
+
parser = Pip2PiOptionParser(
|
|
544
|
+
usage="usage: %prog TARGET [PIP_OPTIONS] PACKAGES ...",
|
|
545
|
+
description=dedent("""
|
|
546
|
+
Adds packages PACKAGES to PyPI-compatible package index at TARGET.
|
|
547
|
+
|
|
548
|
+
If TARGET contains ':' it will be treated as a remote path. The
|
|
549
|
+
package index will be built locally and rsync will be used to copy
|
|
550
|
+
it to the remote host.
|
|
551
|
+
|
|
552
|
+
PIP_OPTIONS can be any options accepted by `pip %s`, like
|
|
553
|
+
`--index-url` or `%s`.
|
|
554
|
+
|
|
555
|
+
For example, to create a remote index:
|
|
556
|
+
|
|
557
|
+
$ pip2pi example.com:/var/www/packages/ -r requirements.txt
|
|
558
|
+
|
|
559
|
+
To create a local index:
|
|
560
|
+
|
|
561
|
+
$ pip2pi ~/Sites/packages/ foo==1.2
|
|
562
|
+
|
|
563
|
+
To pass arguments to pip:
|
|
564
|
+
|
|
565
|
+
$ pip2pi ~/Sites/packages/ \\
|
|
566
|
+
%s \\
|
|
567
|
+
--index-url https://example.com/simple \\
|
|
568
|
+
-r requirements-base.txt \\
|
|
569
|
+
-r requirements-dev.txt \\
|
|
570
|
+
bar==3.1
|
|
571
|
+
|
|
572
|
+
""" % (pip_download_command, pip_no_binary_command, pip_no_binary_command)))
|
|
573
|
+
parser.add_index_options()
|
|
574
|
+
|
|
575
|
+
option, argv = parser.parse_args(argv)
|
|
576
|
+
if len(argv) < 3:
|
|
577
|
+
parser.print_help()
|
|
578
|
+
parser.exit()
|
|
579
|
+
|
|
580
|
+
target = argv[1]
|
|
581
|
+
pip_argv = argv[2:]
|
|
582
|
+
if is_remote_target(target):
|
|
583
|
+
is_remote = True
|
|
584
|
+
working_dir = tempfile.mkdtemp(prefix="pip2pi-working-dir")
|
|
585
|
+
atexit.register(lambda: shutil.rmtree(working_dir))
|
|
586
|
+
else:
|
|
587
|
+
is_remote = False
|
|
588
|
+
working_dir = os.path.abspath(target)
|
|
589
|
+
|
|
590
|
+
subcmd_argv = [argv[0], working_dir] + pip_argv
|
|
591
|
+
res = pip2tgz(subcmd_argv)
|
|
592
|
+
if res:
|
|
593
|
+
print("pip2tgz returned an error; aborting.")
|
|
594
|
+
return res
|
|
595
|
+
if option.get_source:
|
|
596
|
+
subcmd_argv_source = subcmd_argv[:]
|
|
597
|
+
subcmd_argv_source.insert(2, pip_no_binary_command)
|
|
598
|
+
res = pip2tgz(subcmd_argv_source)
|
|
599
|
+
if res:
|
|
600
|
+
print("pip2tgz returned an error; aborting.")
|
|
601
|
+
return res
|
|
602
|
+
|
|
603
|
+
res = _dir2pi(option, subcmd_argv)
|
|
604
|
+
if res:
|
|
605
|
+
print("dir2pi returned an error; aborting.")
|
|
606
|
+
return res
|
|
607
|
+
|
|
608
|
+
if is_remote:
|
|
609
|
+
print("copying temporary index at %r to %r..." %(working_dir, target))
|
|
610
|
+
check_call([
|
|
611
|
+
"rsync",
|
|
612
|
+
"--recursive", "--progress", "--links",
|
|
613
|
+
working_dir + "/", target + "/",
|
|
614
|
+
])
|
|
615
|
+
return 0
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pip2pi-dev
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: pip2pi builds a PyPI-compatible package repository from pip requirements
|
|
5
|
+
Author-email: David Wolever <david@wolever.net>
|
|
6
|
+
Maintainer-email: Md Safiyat Reza <safiyat@voereir.com>
|
|
7
|
+
License-Expression: BSD-2-Clause
|
|
8
|
+
Project-URL: Homepage, https://github.com/wolever/pip2pi
|
|
9
|
+
Keywords: pypi,mirror,offline,package index,pip
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
|
+
License-File: LICENSE.txt
|
|
26
|
+
Requires-Dist: pip>=10.0.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
``pip2pi`` builds a PyPI-compatible package repository from ``pip`` requirements
|
|
30
|
+
================================================================================
|
|
31
|
+
|
|
32
|
+
PyPI can go down, package maintainers can remove old tarballs, and downloading
|
|
33
|
+
tarballs can take a long time. ``pip2pi`` helps to alleviate these problems by
|
|
34
|
+
making it blindingly simple to maintain a PyPI-compatible repository of packages
|
|
35
|
+
your software depends on.
|
|
36
|
+
|
|
37
|
+
::
|
|
38
|
+
|
|
39
|
+
$ pip2pi --help
|
|
40
|
+
Usage: pip2pi TARGET [PIP_OPTIONS] PACKAGES ...
|
|
41
|
+
|
|
42
|
+
Adds packages PACKAGES to PyPI-compatible package index at TARGET.
|
|
43
|
+
|
|
44
|
+
If TARGET contains ':' it will be treated as a remote path. The
|
|
45
|
+
package index will be built locally and rsync will be used to copy
|
|
46
|
+
it to the remote host.
|
|
47
|
+
|
|
48
|
+
PIP_OPTIONS can be any options accepted by `pip install -d`, like
|
|
49
|
+
`--index-url` or `--no-use-wheel`.
|
|
50
|
+
|
|
51
|
+
For example, to create a remote index:
|
|
52
|
+
|
|
53
|
+
$ pip2pi example.com:/var/www/packages/ -r requirements.txt
|
|
54
|
+
|
|
55
|
+
To create a local index:
|
|
56
|
+
|
|
57
|
+
$ pip2pi ~/Sites/packages/ foo==1.2
|
|
58
|
+
|
|
59
|
+
To pass arguments to pip:
|
|
60
|
+
|
|
61
|
+
$ pip2pi ~/Sites/packages/ \
|
|
62
|
+
--index-url https://example.com/simple \
|
|
63
|
+
--no-use-wheel \
|
|
64
|
+
-r requirements-base.txt \
|
|
65
|
+
-r requirements-dev.txt \
|
|
66
|
+
bar==3.1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Requirements
|
|
70
|
+
------------
|
|
71
|
+
|
|
72
|
+
0. Python 3.11 or newer
|
|
73
|
+
1. ``pip``
|
|
74
|
+
2. A ``requirements.txt`` file for your project (optional, but useful)
|
|
75
|
+
3. An HTTP server (optional, but useful)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
Setup
|
|
79
|
+
-----
|
|
80
|
+
|
|
81
|
+
Install ``pip2pi``::
|
|
82
|
+
|
|
83
|
+
$ pip install pip2pi
|
|
84
|
+
|
|
85
|
+
And create the directory which will contain the tarballs of required packages,
|
|
86
|
+
preferably somewhere under your web server's document root::
|
|
87
|
+
|
|
88
|
+
$ mkdir /var/www/packages/
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
Mirroring Packages
|
|
92
|
+
------------------
|
|
93
|
+
|
|
94
|
+
To mirror a package and all of its requirements, use ``pip2tgz``::
|
|
95
|
+
|
|
96
|
+
$ pip2tgz packages/ foo==1.2
|
|
97
|
+
...
|
|
98
|
+
$ ls packages/
|
|
99
|
+
foo-1.2.tar.gz
|
|
100
|
+
bar-0.8.tar.gz
|
|
101
|
+
|
|
102
|
+
Note that ``pip2tgz`` passes package arguments directly to ``pip``, so packages
|
|
103
|
+
can be specified in any format that ``pip`` recognizes::
|
|
104
|
+
|
|
105
|
+
$ cat requirements.txt
|
|
106
|
+
foo==1.2
|
|
107
|
+
http://example.com/baz-0.3.tar.gz
|
|
108
|
+
$ pip2tgz packages/ -r requirements.txt bam-2.3/
|
|
109
|
+
...
|
|
110
|
+
$ ls packages/
|
|
111
|
+
foo-1.2.tar.gz
|
|
112
|
+
bar-0.8.tar.gz
|
|
113
|
+
baz-0.3.tar.gz
|
|
114
|
+
bam-2.3.tar.gz
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
Building a Package Index
|
|
118
|
+
------------------------
|
|
119
|
+
|
|
120
|
+
A directory full of ``.tar.gz`` files can be turned into PyPI-compatible
|
|
121
|
+
"simple" package index using the ``dir2pi`` command::
|
|
122
|
+
|
|
123
|
+
$ ls packages/
|
|
124
|
+
bar-0.8.tar.gz
|
|
125
|
+
baz-0.3.tar.gz
|
|
126
|
+
foo-1.2.tar.gz
|
|
127
|
+
$ dir2pi packages/
|
|
128
|
+
$ find packages/
|
|
129
|
+
packages/
|
|
130
|
+
packages/bar-0.8.tar.gz
|
|
131
|
+
packages/baz-0.3.tar.gz
|
|
132
|
+
packages/foo-1.2.tar.gz
|
|
133
|
+
packages/simple
|
|
134
|
+
packages/simple/bar
|
|
135
|
+
packages/simple/bar/bar-0.8.tar.gz
|
|
136
|
+
packages/simple/baz
|
|
137
|
+
packages/simple/baz/baz-0.3.tar.gz
|
|
138
|
+
packages/simple/foo
|
|
139
|
+
packages/simple/foo/foo-1.2.tar.gz
|
|
140
|
+
|
|
141
|
+
The archives do not have to sit at the top level of ``packages/``: unless
|
|
142
|
+
``--no-recursive`` is given, every sub-directory is scanned too, and only
|
|
143
|
+
files named like a package archive (``.whl``, ``.egg``, ``.zip``, ``.tgz``,
|
|
144
|
+
``.tar``, ``.tar.gz``, ``.tar.bz2``, ``.tar.xz``) are picked up from them::
|
|
145
|
+
|
|
146
|
+
$ find packages/ -name '*.whl'
|
|
147
|
+
packages/flask/flask-3.1.3-py3-none-any.whl
|
|
148
|
+
packages/click/v8.5.0/click-8.5.0-py3-none-any.whl
|
|
149
|
+
$ dir2pi packages/
|
|
150
|
+
|
|
151
|
+
If the same archive (same file name) is present in more than one place -- as
|
|
152
|
+
happens with nightly builds, kept in a directory per build -- it is indexed
|
|
153
|
+
once, using the last one in lexicographic order.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
But that's a lot of work...
|
|
157
|
+
---------------------------
|
|
158
|
+
|
|
159
|
+
If running two commands seems like too much work... Take heart! The ``pip2pi``
|
|
160
|
+
command will run both of them for you... **And** it will use ``rsync`` to copy
|
|
161
|
+
the new packages and index to a remote host! ::
|
|
162
|
+
|
|
163
|
+
$ pip2pi example.com:/var/www/packages/ foo==1.2
|
|
164
|
+
...
|
|
165
|
+
$ curl -I http://example.com/packages/simple/foo/foo-1.2.tar.gz | head -n1
|
|
166
|
+
HTTP/1.1 200 OK
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
But that's still too much work...
|
|
170
|
+
.................................
|
|
171
|
+
|
|
172
|
+
Take heart! Your shell's ``alias`` command can help. Add an alias like this to
|
|
173
|
+
your shell's runtime configuration file (hint: ``~/.bashrc`` or similar)::
|
|
174
|
+
|
|
175
|
+
alias pip2acmeco="pip2pi dev.acmeco.com:/var/www/packages/"
|
|
176
|
+
|
|
177
|
+
Now updating your package index will be as simple as::
|
|
178
|
+
|
|
179
|
+
$ pip2acmeco foo==1.2 -r bar/requirements.txt
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
Using Your New Package Index
|
|
183
|
+
----------------------------
|
|
184
|
+
|
|
185
|
+
To use the new package index, pass the ``--index-url=`` argument to ``pip``::
|
|
186
|
+
|
|
187
|
+
$ pip install --index-url=http://example.com/packages/simple/ foo
|
|
188
|
+
|
|
189
|
+
Or, once it has been mirrored, prefix you ``requirements.txt`` with
|
|
190
|
+
``--index-url=...``::
|
|
191
|
+
|
|
192
|
+
$ cat requirements.txt
|
|
193
|
+
--index-url=http://example.com/packages/simple/
|
|
194
|
+
foo==1.2
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
Without a web server
|
|
198
|
+
--------------------
|
|
199
|
+
|
|
200
|
+
You can use your package index offline, too::
|
|
201
|
+
|
|
202
|
+
$ pip install --index-url=file:///var/www/packages/simple foo==1.2
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
Some Tips
|
|
206
|
+
---------
|
|
207
|
+
|
|
208
|
+
When installing packages from source via ``python setup.py install`` or
|
|
209
|
+
``python setup.py install``, you may need to create a ``setup.cfg``, which
|
|
210
|
+
points to your package index. Here are some examples for an offline package
|
|
211
|
+
index in your Windows, Linux, or Mac file system::
|
|
212
|
+
|
|
213
|
+
[easy_install]
|
|
214
|
+
# Windows
|
|
215
|
+
# index_url = file:///C:/pip2pi/simple/
|
|
216
|
+
|
|
217
|
+
# Linux
|
|
218
|
+
# index_url = file:///home/myusername/.pip2pi/simple/
|
|
219
|
+
|
|
220
|
+
# Mac
|
|
221
|
+
index_url = file:///Users/myusername/.pip2pi/simple/
|
|
222
|
+
|
|
223
|
+
Note the triple ``///`` after ``file:`` -- two for the protocol,
|
|
224
|
+
the third for the root of the local file system.
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
Building and releasing
|
|
228
|
+
----------------------
|
|
229
|
+
|
|
230
|
+
The wheel is built with `uv <https://docs.astral.sh/uv/>`_. Locally::
|
|
231
|
+
|
|
232
|
+
$ uv build --wheel
|
|
233
|
+
$ ls dist/
|
|
234
|
+
dist/pip2pi_dev-0.9.0-py3-none-any.whl
|
|
235
|
+
|
|
236
|
+
Two GitHub Actions workflows share the work: ``.github/workflows/test.yml`` runs
|
|
237
|
+
the test suite on every push and pull request, and
|
|
238
|
+
``.github/workflows/publish.yml`` builds the wheel with uv and publishes it to
|
|
239
|
+
PyPI when a version tag is pushed::
|
|
240
|
+
|
|
241
|
+
$ git tag v0.9.0
|
|
242
|
+
$ git push origin v0.9.0
|
|
243
|
+
|
|
244
|
+
The tag must match ``libpip2pi.__version__``. A manual run of ``publish.yml``
|
|
245
|
+
can publish to TestPyPI (or to PyPI) without tagging, or just build the wheel.
|
|
246
|
+
The two workflows are independent, so only tag a commit whose tests are green.
|
|
247
|
+
Publishing uses trusted publishing when it is configured for this repository on
|
|
248
|
+
PyPI (workflow ``publish.yml``, environment ``pypi``), otherwise the
|
|
249
|
+
``PYPI_API_TOKEN`` secret (``TEST_PYPI_API_TOKEN`` for TestPyPI).
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
Keywords
|
|
253
|
+
========
|
|
254
|
+
|
|
255
|
+
* Mirror PyPI
|
|
256
|
+
* Offline PyPI
|
|
257
|
+
* Create offline PyPI mirror
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
libpip2pi/__init__.py,sha256=gQN1WAxqYmHO6KwIyhBVtLCC-RMVLc-gPIaHx4V4OyE,24
|
|
2
|
+
libpip2pi/commands.py,sha256=IPp3OHtMUQ44lC1pC4l60nLj8kP_wkRnINXszpRPe3o,21978
|
|
3
|
+
pip2pi_dev-0.9.0.dist-info/licenses/LICENSE.txt,sha256=3p5Bye0lYadUXzOcVsryrKzo-mXtJJaMaTv8bfAvwGU,1649
|
|
4
|
+
pip2pi_dev-0.9.0.dist-info/METADATA,sha256=o4deB0zMkfCn6tGx-M78aUEqE5rA9KHjMoTYi7GfXoc,7714
|
|
5
|
+
pip2pi_dev-0.9.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
pip2pi_dev-0.9.0.dist-info/entry_points.txt,sha256=KuDQoTc_gN24UyyRfQo1HGWl2J2PU4u2ET9vV_KzxFQ,125
|
|
7
|
+
pip2pi_dev-0.9.0.dist-info/top_level.txt,sha256=c7EW__NGhQIdxV9AbKti_A6JqTzW62f0_DrYDPIbUoY,10
|
|
8
|
+
pip2pi_dev-0.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
tl;dr: all code code is licensed under simplified BSD, unless stated otherwise.
|
|
2
|
+
|
|
3
|
+
Unless stated otherwise in the source files, all code is copyright 2010 David
|
|
4
|
+
Wolever <david@wolever.net>. All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND ANY EXPRESS OR
|
|
17
|
+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
|
19
|
+
EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
20
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
21
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
22
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
23
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
|
24
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
25
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
|
|
27
|
+
The views and conclusions contained in the software and documentation are those
|
|
28
|
+
of the authors and should not be interpreted as representing official policies,
|
|
29
|
+
either expressed or implied, of David Wolever.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
libpip2pi
|