forked from github.com/pypiserver
Compare commits
1 Commits
master
...
auto-relea
Author | SHA1 | Date | |
---|---|---|---|
|
7a026f88e2 |
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
6
.github/workflows/rt.yml
vendored
6
.github/workflows/rt.yml
vendored
@ -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
|
||||||
|
16
CHANGES.rst
16
CHANGES.rst
@ -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
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
|
||||||
|
13
setup.py
13
setup.py
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user