Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions
7a026f88e2 chore(rc-changes): update CHANGES.rst 2024-03-01 00:34:55 +00:00
17 changed files with 114 additions and 300 deletions

@ -12,26 +12,13 @@ on:
# Allowing to run on fork and other pull requests # Allowing to run on fork and other pull requests
pull_request: pull_request:
env:
LAST_SUPPORTED_PYTHON: "3.12"
jobs: jobs:
test-python: test-python:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# make sure to align the `python-version`s in the Matrix with env.LAST_SUPPORTED_PYTHON python-version: ["3.7", "3.8", "3.9", "3.10", "pypy3.9", "3.11"] # "3.12-dev"
python-version: [
"3.7",
"3.8",
"3.9",
"3.10",
"pypy3.9",
"3.11",
"3.12",
"3.x", # make sure to test the current stable Python version
]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -56,7 +43,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
# Use the current version of Python # Use the current version of Python
python-version: ${{ env.LAST_SUPPORTED_PYTHON }} python-version: "3.x"
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install -r "requirements/dev.pip" pip install -r "requirements/dev.pip"
@ -103,7 +90,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
# Use the current version of Python # Use the current version of Python
python-version: ${{ env.LAST_SUPPORTED_PYTHON }} python-version: "3.x"
- name: Install test dependencies - name: Install test dependencies
run: pip install -r "requirements/test.pip" run: pip install -r "requirements/test.pip"
- name: Install package - name: Install package
@ -129,15 +116,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- "tests" - "tests"
# only if a tag is pushed
if: startsWith(github.event.ref, 'refs/tags/v')
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ env.LAST_SUPPORTED_PYTHON }} python-version: 3.x
- name: Install dev dependencies
run: pip install -r "requirements/dev.pip"
- name: Build distribution _wheel_. - name: Build distribution _wheel_.
run: | run: |
@ -145,8 +131,6 @@ jobs:
- name: Publish distribution 📦 to PyPI. - name: Publish distribution 📦 to PyPI.
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
# Push to PyPi only if a tag is pushed
if: startsWith(github.event.ref, 'refs/tags/v')
with: with:
password: ${{ secrets.PYPI_API_TOKEN }} password: ${{ secrets.PYPI_API_TOKEN }}
print-hash: true print-hash: true
@ -170,7 +154,7 @@ jobs:
- uses: "actions/setup-python@v4" - uses: "actions/setup-python@v4"
with: with:
python-version: ${{ env.LAST_SUPPORTED_PYTHON }} python-version: "3.x"
# This script prints a JSON array of needed docker tags, depending on the # This script prints a JSON array of needed docker tags, depending on the
# ref. That array is then used to construct the matrix of the # ref. That array is then used to construct the matrix of the
@ -211,31 +195,27 @@ jobs:
${{ runner.os }}-buildx- ${{ runner.os }}-buildx-
- name: "Login to Docker Hub" - name: "Login to Docker Hub"
uses: "docker/login-action@v3" uses: "docker/login-action@v1"
with: with:
username: "${{ secrets.DOCKER_HUB_USER }}" username: "${{ secrets.DOCKER_HUB_USER }}"
password: "${{ secrets.DOCKER_HUB_TOKEN }}" password: "${{ secrets.DOCKER_HUB_TOKEN }}"
- name: "Login to GitHub Container Registry" - name: "Login to GitHub Container Registry"
uses: "docker/login-action@v3" uses: "docker/login-action@v2"
with: with:
registry: "ghcr.io" registry: "ghcr.io"
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: "Set up QEMU"
uses: "docker/setup-qemu-action@v3"
- name: "Set up Docker Buildx" - name: "Set up Docker Buildx"
id: "buildx" id: "buildx"
uses: "docker/setup-buildx-action@v3" uses: "docker/setup-buildx-action@v1"
- name: "Build and push" - name: "Build and push"
id: "docker_build" id: "docker_build"
uses: "docker/build-push-action@v5" uses: "docker/build-push-action@v2"
with: with:
context: "./" context: "./"
platforms: linux/amd64,linux/arm64
file: "./Dockerfile" file: "./Dockerfile"
builder: "${{ steps.buildx.outputs.name }}" builder: "${{ steps.buildx.outputs.name }}"
push: true push: true

@ -22,16 +22,14 @@ jobs:
if: ${{ github.ref_name == 'master' }} if: ${{ github.ref_name == 'master' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
CHANGE_FILE: CHANGES.rst CHANGES_FILE: CHANGES.rst
EXPECTED_DIFF_COUNT: 1 EXPECTED_DIFF_COUNT: 1
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- id: get-version - id: get-version
run: | run: |
CHANGE_FILE=${{ env.CHANGE_FILE }}
LAST_VERSION=$(grep -m1 -E ' \([0-9]+-[0-9]+-[0-9]+\)$' ${CHANGE_FILE} | awk '{ print $1 }') LAST_VERSION=$(grep -m1 -E ' \([0-9]+-[0-9]+-[0-9]+\)$' ${CHANGE_FILE} | awk '{ print $1 }')
echo "👀 Version detected: ${LAST_VERSION}"
echo "LAST_VERSION=${LAST_VERSION}" >> "$GITHUB_OUTPUT" echo "LAST_VERSION=${LAST_VERSION}" >> "$GITHUB_OUTPUT"
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
@ -47,7 +45,7 @@ jobs:
run: | run: |
echo ${{ inputs.dryrun && '💡 Running in dry-run mode' || 'Preparing release...' }} echo ${{ inputs.dryrun && '💡 Running in dry-run mode' || 'Preparing release...' }}
CHANGE_FILE=${{ env.CHANGE_FILE }} CHANGE_FILE=${{ env.CHANGES_FILE }}
LAST_VERSION=${{ steps.get-version.outputs.LAST_VERSION }} LAST_VERSION=${{ steps.get-version.outputs.LAST_VERSION }}
git config user.name github-actions git config user.name github-actions
git config user.email github-actions@github.com git config user.email github-actions@github.com

@ -4,21 +4,11 @@ Changelog
3.0.0 (tbd) 3.0.0 (tbd)
----------- -----------
2.1.1 (2024-04-24) 2.0.2rc03-01-2024 (__rc__)
-------------------------- --------------------------
- 31c9cf1 FIX: deprecated `setuptools.py` when building in `package.sh` (#568) - 50c7a78 chore: add tar xz test case (#538)
- 2619c17 FIX: use the right env variables in `release-tag` workflow (#569) - a558dbc Handle tar.xz archives (#536)
2.1.0 (2024-04-24)
--------------------------
- d588913 ENH: Bump github action versions and add multiarch support (#553)
- a558dbc ENH: Handle tar.xz archives (#536)
- 2f0a56c FIX: support Python 3.12 (#539)
- 84bf12c MAINT: make the last supported python version explicit in `ci.yaml` (#558)
- 946fbfe MAINT: Update setuptools requirement from <62.0.0,>=40.0 to >=40.0,<70.0.0 in /requirements (#557)
- 50c7a78 MAINT: add tar xz test case (#538)
2.0.1 (2023-10-01) 2.0.1 (2023-10-01)
-------------------------- --------------------------

104
README.md

@ -9,14 +9,14 @@
| name | description | | name | description |
| :---------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | :---------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Version | 2.1.1 | | Version | 2.0.1 |
| Date: | 2024-04-25 | | Date: | 2023-10-01 |
| Source | <https://github.com/pypiserver/pypiserver> | | Source | https://github.com/pypiserver/pypiserver |
| PyPI | <https://pypi.org/project/pypiserver/> | | PyPI | https://pypi.org/project/pypiserver/ |
| Tests | <https://github.com/pypiserver/pypiserver/actions> | | Tests | https://github.com/pypiserver/pypiserver/actions |
| Maintainers | Kostis Anagnostopoulos <ankostis@gmail.com>, Matthew Planchard <mplanchard@gmail.com>, Dmitrii Orlov <dmtree.dev@yahoo.com>, **Someone new?** We are looking for new maintainers! [#397](https://github.com/pypiserver/pypiserver/issues/397) | | Maintainers | Kostis Anagnostopoulos <ankostis@gmail.com>, Matthew Planchard <mplanchard@gmail.com>, Dmitrii Orlov <dmtree.dev@yahoo.com>, **Someone new?** We are looking for new maintainers! [#397](https://github.com/pypiserver/pypiserver/issues/397) |
| License | zlib/libpng + MIT | | License | zlib/libpng + MIT |
| Community | <https://pypiserver.zulipchat.com> | | Community | https://pypiserver.zulipchat.com |
Chat with us on [Zulip](https://pypiserver.zulipchat.com)! Chat with us on [Zulip](https://pypiserver.zulipchat.com)!
@ -44,47 +44,47 @@ making it much easier to get a running index server.
Table of Contents Table of Contents
- [pypiserver](#pypiserver) - [pypiserver - minimal PyPI server for use with pip/easy_install](#pypiserver---minimal-pypi-server-for-use-with-pipeasy_install)
- [Quickstart Installation and Usage](#quickstart-installation-and-usage) - [Quickstart Installation and Usage](#Quickstart-Installation-and-Usage)
- [More details about pypi server run](#more-details-about-pypi-server-run) - [More details about pypi-server run](#More-details-about-pypi-server-run)
- [More details about pypi-server update](#more-details-about-pypi-server-update) - [More details about pypi-server update](#More-details-about-pypi-server-update)
- [Client-Side Configurations](#client-side-configurations) - [Client-Side Configurations](#Client-Side-Configurations)
- [Configuring pip](#configuring-pip) - [Configuring pip](#Configuring-pip)
- [Configuring easy_install](#configuring-easy_install) - [Configuring easy_install](#Configuring-easy_install)
- [Uploading Packages Remotely](#uploading-packages-remotely) - [Uploading Packages Remotely](#Uploading-Packages-Remotely)
- [Apache Like Authentication (htpasswd)](#apache-like-authentication-htpasswd) - [Apache like Authentication ( htpasswd )](#Apache-like-Authentication)
- [Upload with setuptools](#upload-with-setuptools) - [Upload with setuptools](#Upload-with-setuptools)
- [Upload with twine](#upload-with-twine) - [Upload with twine](#Upload-with-twine)
- [Using the Docker Image](#using-the-docker-image) - [Using the Docker Image](#Using-the-Docker-Image)
- [Alternative Installation Methods](#alternative-installation-methods) - [Alternative Installation methods](#Alternative-Installation-methods)
- [Installing the Very Latest Version](#installing-the-very-latest-version) - [Installing the Very Latest Version](#Installing-the-Very-Latest-Version)
- [Recipes](#recipes) - [Recipes](#Recipes)
- [Managing the Package Directory](#managing-the-package-directory) - [Managing the Package Directory](#Managing-the-Package-Directory)
- [Serving Thousands of Packages](#serving-thousands-of-packages) - [Serving Thousands of Packages](#Serving-Thousands-of-Packages)
- [Managing Automated Startup](#managing-automated-startup) - [Managing Automated Startup](#Managing-Automated-Startup)
- [Running As a systemd Service](#running-as-a-systemd-service) - [Running as a systemd service](#Running-as-a-systemd-service)
- [Launching through supervisor](#launching-through-supervisor) - [Launching through supervisor](#Launching-through-supervisor)
- [Running As a service with NSSM](#running-as-a-service-with-nssm) - [Running as a service with NSSM (Windows)](#Running-as-a-service-with-NSSM)
- [Using a Different WSGI Server](#using-a-different-wsgi-server) - [Using a Different WSGI Server](#Using-a-Different-WSGI-Server)
- [Apache](#apache) - [Apache](#Apache)
- [gunicorn](#gunicorn) - [Gunicorn](#Gunicorn)
- [paste](#paste) - [Paste](#Paste)
- [Behind a Reverse Proxy](#behind-a-reverse-proxy) - [Behind a Reverse Proxy](#Behind-a-Reverse-Proxy)
- [Nginx](#nginx) - [Nginx](#Nginx)
- [Supporting HTTPS](#supporting-https) - [Supporting HTTPS](#Supporting-HTTPS)
- [Traefik](#traefik) - [Traefik](#Traefik)
- [Utilizing the API](#utilizing-the-api) - [Utilizing the API](#Utilizing-the-API)
- [Using Ad-Hoc Authentication Providers](#using-ad-hoc-authentication-providers) - [Using Ad-Hoc Authentication Providers](#Using-Ad-Hoc-Authentication-Providers)
- [Use with MicroPython](#use-with-micropython) - [Use with MicroPython](#Use-with-MicroPython)
- [Custom Health Check Endpoint](#custom-health-check-endpoint) - [Custom Health Check Endpoint](#Custom-Health-Check-Endpoint)
- [Configure a custom health endpoint by CLI arguments](#configure-a-custom-health-endpoint-by-cli-arguments) - [Configure a custom health check by CLI arguments](#Configure-a-custom-health-check-by-CLI-arguments)
- [Configure a custom health endpoint by script](#configure-a-custom-health-endpoint-by-script) - [Configure a custom health endpoint by script](#Configure-a-custom-health-endpoint-by-script)
- [Sources](#sources) - [Sources](#Sources)
- [Known Limitations](#known-limitations) - [Known Limitations](#known-limitations)
- [Similar Projects](#similar-projects) - [Similar Projects](#similar-projects)
- [Unmaintained or archived](#unmaintained-or-archived) - [Unmaintained or archived](#unmaintained-or-archived)
- [Related Software](#related-software) - [Related Projects](#related-projects)
- [Licensing](#licensing) - [License](#license)
## Quickstart Installation and Usage ## Quickstart Installation and Usage
@ -134,7 +134,7 @@ See also [Alternative Installation methods](<>)
# Note that pip search does not currently work with the /simple/ endpoint. # Note that pip search does not currently work with the /simple/ endpoint.
``` ```
See also [Client-side configurations](#client-side-configurations) for avoiding tedious typing. See also [Client-side configurations](#Client-Side-Configurations) for avoiding tedious typing.
4. Enter **pypi-server -h** in the cmd-line to print a detailed usage message 4. Enter **pypi-server -h** in the cmd-line to print a detailed usage message
@ -461,7 +461,7 @@ Please see `Using Ad-hoc authentication providers`\_ for more information.
password: <some_passwd> password: <some_passwd>
``` ```
1. Then from within the directory of the python-project you wish to upload, 2. Then from within the directory of the python-project you wish to upload,
issue this command: issue this command:
```shell ```shell
@ -693,7 +693,7 @@ Adjusting the paths and adding this file as **pypiserver.service** into your
**systemctl**, e.g. **systemctl start pypiserver**. **systemctl**, e.g. **systemctl start pypiserver**.
More useful information about *systemd* can be found at More useful information about *systemd* can be found at
<https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units> https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units
#### Launching through supervisor #### Launching through supervisor
@ -716,7 +716,7 @@ From there, the process can be managed via **supervisord** using **supervisorctl
#### Running As a service with NSSM #### Running As a service with NSSM
For Windows download NSSM from <https://nssm.cc> unzip to a desired location such as Program Files. Decide whether you are going For Windows download NSSM from https://nssm.cc unzip to a desired location such as Program Files. Decide whether you are going
to use win32 or win64, and add that exe to environment PATH. to use win32 or win64, and add that exe to environment PATH.
Create a start_pypiserver.bat Create a start_pypiserver.bat
@ -762,7 +762,7 @@ Other useful commands
``` ```
For detailed information please visit <https://nssm.cc> For detailed information please visit https://nssm.cc
### Using a Different WSGI Server ### Using a Different WSGI Server
@ -1057,7 +1057,7 @@ these steps:
3. Invoke the python-script to start-up **pypiserver** 3. Invoke the python-script to start-up **pypiserver**
```shell ```shell
python pypiserver-start.py $ python pypiserver-start.py
``` ```
Note Note
@ -1092,7 +1092,7 @@ Installing packages from the REPL of an embedded device works in this way:
upip.install("micropython-foobar") upip.install("micropython-foobar")
``` ```
Further information on micropython-packaging can be found here: <https://docs.micropython.org/en/latest/reference/packages.html> Further information on micropython-packaging can be found here: https://docs.micropython.org/en/latest/reference/packages.html
### Custom Health Check Endpoint ### Custom Health Check Endpoint
@ -1127,7 +1127,7 @@ Run pypiserver with **--health-endpoint** argument:
bottle.run(app=app, host="0.0.0.0", port=8080, server="auto") bottle.run(app=app, host="0.0.0.0", port=8080, server="auto")
```` ````
Try **curl <http://localhost:8080/action/health>** Try **curl http://localhost:8080/action/health**
## Sources ## Sources

@ -6,4 +6,4 @@ my_dir=`dirname "$0"`
cd $my_dir/.. cd $my_dir/..
rm -r build/* dist/* || echo "no build/* or dist/* folder is found" rm -r build/* dist/* || echo "no build/* or dist/* folder is found"
python3 -m build python3 setup.py bdist_wheel sdist

@ -396,7 +396,6 @@ class TestBasics:
"-m", "-m",
"pip", "pip",
"install", "install",
"--force-reinstall",
"--index-url", "--index-url",
f"http://localhost:{container.port}/simple", f"http://localhost:{container.port}/simple",
TEST_DEMO_PIP_PACKAGE, TEST_DEMO_PIP_PACKAGE,
@ -563,7 +562,6 @@ class TestAuthed:
"-m", "-m",
"pip", "pip",
"install", "install",
"--force-reinstall",
"--index-url", "--index-url",
f"http://a:a@localhost:{self.HOST_PORT}/simple", f"http://a:a@localhost:{self.HOST_PORT}/simple",
TEST_DEMO_PIP_PACKAGE, TEST_DEMO_PIP_PACKAGE,
@ -583,10 +581,9 @@ class TestAuthed:
"-m", "-m",
"pip", "pip",
"install", "install",
"--force-reinstall",
"--no-cache", "--no-cache",
"--index-url", "--index-url",
f"http://foo:bar@localhost:{self.HOST_PORT}/simple", f"http://localhost:{self.HOST_PORT}/simple",
TEST_DEMO_PIP_PACKAGE, TEST_DEMO_PIP_PACKAGE,
check_code=lambda c: c != 0, check_code=lambda c: c != 0,
) )

@ -7,9 +7,9 @@ import typing as t
from pypiserver.bottle import Bottle from pypiserver.bottle import Bottle
from pypiserver.config import Config, RunConfig, strtobool from pypiserver.config import Config, RunConfig, strtobool
version = __version__ = "2.1.1" version = __version__ = "2.0.1"
__version_info__ = tuple(_re.split("[.-]", __version__)) __version_info__ = tuple(_re.split("[.-]", __version__))
__updated__ = "2024-04-25 01:23:25" __updated__ = "2023-10-01 16:14:10"
__title__ = "pypiserver" __title__ = "pypiserver"
__summary__ = "A minimal PyPI server for use with pip/easy_install." __summary__ = "A minimal PyPI server for use with pip/easy_install."

@ -14,7 +14,6 @@ from urllib.parse import urljoin, urlparse
from pypiserver.config import RunConfig from pypiserver.config import RunConfig
from . import __version__ from . import __version__
from . import core from . import core
from . import mirror_cache
from .bottle import ( from .bottle import (
static_file, static_file,
redirect, redirect,
@ -287,9 +286,7 @@ def simple(project):
key=lambda x: (x.parsed_version, x.relfn), key=lambda x: (x.parsed_version, x.relfn),
) )
if not packages: if not packages:
if config.mirror: if not config.disable_fallback:
return mirror_cache.MirrorCache.add(project=project, config=config)
elif not config.disable_fallback:
return redirect(f"{config.fallback_url.rstrip('/')}/{project}/") return redirect(f"{config.fallback_url.rstrip('/')}/{project}/")
return HTTPError(404, f"Not Found ({normalized} does not exist)\n\n") return HTTPError(404, f"Not Found ({normalized} does not exist)\n\n")
@ -367,8 +364,7 @@ def server_static(filename):
"Cache-Control", f"public, max-age={config.cache_control}" "Cache-Control", f"public, max-age={config.cache_control}"
) )
return response return response
if config.mirror and mirror_cache.MirrorCache.has_project(filename):
return mirror_cache.MirrorCache.get_static_file(filename=filename, config=config)
return HTTPError(404, f"Not Found ({filename} does not exist)\n\n") return HTTPError(404, f"Not Found ({filename} does not exist)\n\n")

@ -43,27 +43,10 @@ import re
import sys import sys
import textwrap import textwrap
import typing as t import typing as t
from distutils.util import strtobool as strtoint
try:
# `importlib_resources` is required for Python versions below 3.12
# See more in the package docs: https://pypi.org/project/importlib-resources/
try:
from importlib_resources import files as import_files
except ImportError:
from importlib.resources import files as import_files
def get_resource_bytes(package: str, resource: str) -> bytes:
ref = import_files(package).joinpath(resource)
return ref.read_bytes()
except ImportError:
# The `pkg_resources` is deprecated in Python 3.12
import pkg_resources import pkg_resources
def get_resource_bytes(package: str, resource: str) -> bytes:
return pkg_resources.resource_string(package, resource)
from pypiserver.backend import ( from pypiserver.backend import (
SimpleFileBackend, SimpleFileBackend,
CachingFileBackend, CachingFileBackend,
@ -80,29 +63,10 @@ except ImportError:
HtpasswdFile = None HtpasswdFile = None
def legacy_strtoint(val: str) -> int: # The "strtobool" function in distutils does a nice job at parsing strings,
"""Convert a string representation of truth to true (1) or false (0). # but returns an integer. This just wraps it in a boolean call so that we
# get a bool.
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values strtobool: t.Callable[[str], bool] = lambda val: bool(strtoint(val))
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'val' is anything else.
The "strtobool" function in distutils does a nice job at parsing strings,
but returns an integer. This just wraps it in a boolean call so that we
get a bool.
Borrowed from deprecated distutils.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return 1
elif val in ("n", "no", "f", "false", "off", "0"):
return 0
else:
raise ValueError("invalid truth value {!r}".format(val))
strtobool: t.Callable[[str], bool] = lambda val: bool(legacy_strtoint(val))
# Specify defaults here so that we can use them in tests &c. and not need # Specify defaults here so that we can use them in tests &c. and not need
@ -187,7 +151,9 @@ def health_endpoint_arg(arg: str) -> str:
def html_file_arg(arg: t.Optional[str]) -> str: def html_file_arg(arg: t.Optional[str]) -> str:
"""Parse the provided HTML file and return its contents.""" """Parse the provided HTML file and return its contents."""
if arg is None or arg == "pypiserver/welcome.html": if arg is None or arg == "pypiserver/welcome.html":
return get_resource_bytes(__name__, "welcome.html").decode("utf-8") return pkg_resources.resource_string(__name__, "welcome.html").decode(
"utf-8"
)
with open(arg, "r", encoding="utf-8") as f: with open(arg, "r", encoding="utf-8") as f:
msg = f.read() msg = f.read()
return msg return msg
@ -517,14 +483,6 @@ def get_parser() -> argparse.ArgumentParser:
"to '%%s' to see them all." "to '%%s' to see them all."
), ),
) )
run_parser.add_argument(
"--mirror",
default=0,
action="count",
help=(
"Mirror packages to local disk"
),
)
update_parser = subparsers.add_parser( update_parser = subparsers.add_parser(
"update", "update",
@ -728,7 +686,6 @@ class RunConfig(_ConfigCommon):
overwrite: bool, overwrite: bool,
welcome_msg: str, welcome_msg: str,
cache_control: t.Optional[int], cache_control: t.Optional[int],
mirror: bool,
log_req_frmt: str, log_req_frmt: str,
log_res_frmt: str, log_res_frmt: str,
log_err_frmt: str, log_err_frmt: str,
@ -754,7 +711,6 @@ class RunConfig(_ConfigCommon):
# Derived properties # Derived properties
self._derived_properties = self._derived_properties + ("auther",) self._derived_properties = self._derived_properties + ("auther",)
self.auther = self.get_auther(auther) self.auther = self.get_auther(auther)
self.mirror = mirror
@classmethod @classmethod
def kwargs_from_namespace( def kwargs_from_namespace(
@ -774,7 +730,6 @@ class RunConfig(_ConfigCommon):
"overwrite": namespace.overwrite, "overwrite": namespace.overwrite,
"welcome_msg": namespace.welcome, "welcome_msg": namespace.welcome,
"cache_control": namespace.cache_control, "cache_control": namespace.cache_control,
"mirror": namespace.mirror,
"log_req_frmt": namespace.log_req_frmt, "log_req_frmt": namespace.log_req_frmt,
"log_res_frmt": namespace.log_res_frmt, "log_res_frmt": namespace.log_res_frmt,
"log_err_frmt": namespace.log_err_frmt, "log_err_frmt": namespace.log_err_frmt,

@ -5,8 +5,7 @@ from __future__ import absolute_import, print_function, unicode_literals
import itertools import itertools
import os import os
import sys import sys
from distutils.version import LooseVersion
from packaging.version import parse as packaging_parse
from pathlib import Path from pathlib import Path
from subprocess import call from subprocess import call
from xmlrpc.client import Server from xmlrpc.client import Server
@ -113,14 +112,12 @@ class PipCmd:
@staticmethod @staticmethod
def update_root(pip_version): def update_root(pip_version):
"""Yield an appropriate root command depending on pip version. """Yield an appropriate root command depending on pip version."""
# legacy_pip = StrictVersion(pip_version) < StrictVersion('10.0')
Use `pip install` for `pip` 9 or lower, and `pip download` otherwise. legacy_pip = LooseVersion(pip_version) < LooseVersion("10.0")
""" for part in ("pip", "-q"):
legacy_pip = packaging_parse(pip_version).major < 10
pip_command = "install" if legacy_pip else "download"
for part in ("pip", "-q", pip_command):
yield part yield part
yield "install" if legacy_pip else "download"
@staticmethod @staticmethod
def update( def update(

@ -1,91 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import logging
from collections import OrderedDict
from pypiserver.bottle import HTTPError, redirect
from pypiserver.config import RunConfig
log = logging.getLogger(__name__)
try:
import requests
from bs4 import BeautifulSoup
import_ok = True
except ImportError:
import_ok = False
logging.error("mirror_cache import dependencies error")
class CacheElement:
def __init__(self, project: str):
self.project = project
self.html = ""
self.cache = dict()
def add(self, href: str):
targz = href.split("/")[-1]
pkg_name = targz.split("#")[0]
self.cache[f"{self.project}/{pkg_name}"] = href
return f"/packages/{self.project}/{targz}"
class MirrorCache:
cache: OrderedDict[str, CacheElement] = dict()
cache_limit = 10
@classmethod
def add(cls, project: str, config: RunConfig) -> str:
if not import_ok:
return redirect(f"{config.fallback_url.rstrip('/')}/{project}/")
if project in cls.cache:
log.info(f"mirror_cache serve html from cache {project}")
return cls.cache[project].html
element = CacheElement(project=project)
resp = requests.get(f"{config.fallback_url.rstrip('/')}/{project}/")
soup = BeautifulSoup(resp.content, "html.parser")
links = soup.find_all("a")
for link in links:
# new href with mapping to old href for later
new_href = element.add(href=link["href"])
# create new link
new_link = soup.new_tag("a")
new_link.string = link.text.strip()
new_link["href"] = new_href
link.replace_with(new_link)
element.html = str(soup)
cls.cache[project] = element
log.info(f"mirror_cache add project '{project}' to cache")
# purge
if len(cls.cache) > cls.cache_limit:
item = cls.cache.popitem(last=False)
log.info(f"mirror_cache limit '{cls.cache_limit}' exceeded, purged last item - {item}")
return element.html
@classmethod
def has_project(cls, filename):
project = filename.split("/")[0]
return project in cls.cache
@classmethod
def get_static_file(cls, filename, config: RunConfig):
if not import_ok:
return HTTPError(404, f"Not Found ({filename} does not exist)\n\n")
project = filename.split("/")[0]
element = cls.cache[project]
if filename in element.cache:
href = element.cache[filename]
resp = requests.get(href)
cls.add_to_cache(filename=filename, resp=resp, config=config)
return resp
log.info(f"mirror_cache not found in cache {filename} ")
return HTTPError(404, f"Not Found ({filename} does not exist)\n\n")
@classmethod
def add_to_cache(cls, filename: str, resp: requests.Response, config: RunConfig):
project = filename.split("/")[0]
os.makedirs(os.path.join(config.package_root, project), exist_ok=True)
log.info(f"mirror_cache add file '{filename}' to cache")
with open(f"{config.package_root}/{filename}", "wb+") as f:
f.write(resp.content)

@ -18,7 +18,6 @@ with the following keyword arguments
In the future, the plugin callable may be called with additional keyword In the future, the plugin callable may be called with additional keyword
arguments, so a plugin should accept a **kwargs variadic keyword argument. arguments, so a plugin should accept a **kwargs variadic keyword argument.
""" """
from pypiserver.backend import SimpleFileBackend, CachingFileBackend from pypiserver.backend import SimpleFileBackend, CachingFileBackend
from pypiserver import get_file_backend from pypiserver import get_file_backend

@ -1,2 +0,0 @@
beautifulsoup4==4.12.3
requests==2.31.0

@ -4,12 +4,11 @@ pip
passlib>=1.6 passlib>=1.6
pytest>=6.2.2 pytest>=6.2.2
pytest-cov pytest-cov
setuptools>=40.0,<70.0.0 setuptools>=40.0,<62.0.0
tox tox
twine twine
webtest webtest
wheel>=0.25.0 wheel>=0.25.0
build>=1.2.0; python_version >= '3.8'
mdformat-gfm mdformat-gfm
mdformat-frontmatter mdformat-frontmatter
mdformat-footnote mdformat-footnote

@ -11,19 +11,10 @@ tests_require = [
"twine", "twine",
"passlib>=1.6", "passlib>=1.6",
"webtest", "webtest",
"build>=1.2.0;python_version>='3.8'",
] ]
setup_requires = [ setup_requires = ["setuptools", "setuptools-git >= 0.3", "wheel >= 0.25.0"]
"setuptools", install_requires = ["pip>=7"]
"setuptools-git>=0.3",
"wheel>=0.25.0",
]
install_requires = [
"pip>=7",
"packaging>=23.2",
"importlib_resources;python_version>'3.8' and python_version<'3.12'",
]
def read_file(rel_path: str): def read_file(rel_path: str):

@ -1,7 +1,6 @@
""" """
Test module for app initialization Test module for app initialization
""" """
# Standard library imports # Standard library imports
import logging import logging
import pathlib import pathlib

@ -16,7 +16,6 @@ import itertools
import os import os
import shutil import shutil
import socket import socket
import re
import sys import sys
import time import time
import typing as t import typing as t
@ -100,14 +99,10 @@ def build_url(port: t.Union[int, str], user: str = "", pswd: str = "") -> str:
return f"http://{auth}localhost:{port}" return f"http://{auth}localhost:{port}"
def run_setup_py(path: Path, arguments: str) -> int: def run_setup_py(path: Path, arguments: str):
return os.system(f"{sys.executable} {path / 'setup.py'} {arguments}") return os.system(f"{sys.executable} {path / 'setup.py'} {arguments}")
def run_py_build(srcdir: Path, flags: str) -> int:
return os.system(f"{sys.executable} -m build {flags} {srcdir}")
# A test-distribution to check if # A test-distribution to check if
# bottle supports uploading 100's of packages, # bottle supports uploading 100's of packages,
# see: https://github.com/pypiserver/pypiserver/issues/82 # see: https://github.com/pypiserver/pypiserver/issues/82
@ -145,13 +140,8 @@ def server_root(tmp_path_factory):
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def wheel_file(project, tmp_path_factory): def wheel_file(project, tmp_path_factory):
distdir = tmp_path_factory.mktemp("dist") distdir = tmp_path_factory.mktemp("dist")
if re.match("^3\.7", sys.version):
assert run_setup_py(project, f"bdist_wheel -d {distdir}") == 0 assert run_setup_py(project, f"bdist_wheel -d {distdir}") == 0
else: return list(distdir.glob("centodeps*.whl"))[0]
assert run_py_build(project, f"--wheel --outdir {distdir}") == 0
wheels = list(distdir.glob("centodeps*.whl"))
assert len(wheels) > 0
return wheels[0]
@pytest.fixture() @pytest.fixture()
@ -327,6 +317,22 @@ def test_pip_install_authed_succeeds(authed_server, hosted_wheel_file, pipdir):
assert pipdir.joinpath(hosted_wheel_file.name).is_file() assert pipdir.joinpath(hosted_wheel_file.name).is_file()
@pytest.mark.parametrize("pkg_frmt", ["bdist", "bdist_wheel"])
@pytest.mark.parametrize(["server_fixture", "pypirc_fixture"], all_servers)
def test_setuptools_upload(
server_fixture, pypirc_fixture, project, pkg_frmt, server_root, request
):
request.getfixturevalue(server_fixture)
request.getfixturevalue(pypirc_fixture)
assert len(list(server_root.iterdir())) == 0
for i in range(5):
print(f"++Attempt #{i}")
assert run_setup_py(project, f"-vvv {pkg_frmt} upload -r test") == 0
assert len(list(server_root.iterdir())) == 1
def test_partial_authed_open_download(partial_authed_server): def test_partial_authed_open_download(partial_authed_server):
"""Validate that partial auth still allows downloads.""" """Validate that partial auth still allows downloads."""
url = build_url(partial_authed_server.port) + "/simple" url = build_url(partial_authed_server.port) + "/simple"