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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ dir2pi = libpip2pi.commands:dir2pi
3
+ pip2pi = libpip2pi.commands:pip2pi
4
+ pip2tgz = libpip2pi.commands:pip2tgz
@@ -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