forked from github.com/pypiserver
Compare commits
8 Commits
dependabot
...
master
Author | SHA1 | Date | |
---|---|---|---|
f65bc5bf6e | |||
|
acff1bbab8 | ||
|
5ca6004ebe | ||
|
31c9cf14d1 | ||
|
2619c17602 | ||
|
d5886ae3d5 | ||
|
6bfeddc1fc | ||
4ddfc3a077 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -129,8 +129,6 @@ 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
|
||||||
@ -138,12 +136,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ env.LAST_SUPPORTED_PYTHON }}
|
python-version: ${{ env.LAST_SUPPORTED_PYTHON }}
|
||||||
|
|
||||||
|
- name: Install dev dependencies
|
||||||
|
run: pip install -r "requirements/dev.pip"
|
||||||
|
|
||||||
- name: Build distribution _wheel_.
|
- name: Build distribution _wheel_.
|
||||||
run: |
|
run: |
|
||||||
./bin/package.sh
|
./bin/package.sh
|
||||||
|
|
||||||
- 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
|
||||||
|
6
.github/workflows/rt.yml
vendored
6
.github/workflows/rt.yml
vendored
@ -22,14 +22,16 @@ jobs:
|
|||||||
if: ${{ github.ref_name == 'master' }}
|
if: ${{ github.ref_name == 'master' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
CHANGES_FILE: CHANGES.rst
|
CHANGE_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
|
||||||
@ -45,7 +47,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.CHANGES_FILE }}
|
CHANGE_FILE=${{ env.CHANGE_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,6 +4,22 @@ Changelog
|
|||||||
3.0.0 (tbd)
|
3.0.0 (tbd)
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
2.1.1 (2024-04-24)
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
- 31c9cf1 FIX: deprecated `setuptools.py` when building in `package.sh` (#568)
|
||||||
|
- 2619c17 FIX: use the right env variables in `release-tag` workflow (#569)
|
||||||
|
|
||||||
|
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)
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
| name | description |
|
| name | description |
|
||||||
| :---------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :---------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Version | 2.0.1 |
|
| Version | 2.1.1 |
|
||||||
| Date: | 2023-10-01 |
|
| Date: | 2024-04-25 |
|
||||||
| 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> |
|
||||||
|
@ -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 setup.py bdist_wheel sdist
|
python3 -m build
|
||||||
|
@ -8,4 +8,4 @@ bcrypt==3.2.0
|
|||||||
# whatever bottle chooses as a default. Since the wsgiref server is not
|
# whatever bottle chooses as a default. Since the wsgiref server is not
|
||||||
# production-ready, install waitress as a fallback for these cases.
|
# production-ready, install waitress as a fallback for these cases.
|
||||||
waitress==2.1.2
|
waitress==2.1.2
|
||||||
watchdog==4.0.0
|
watchdog==1.0.2
|
||||||
|
@ -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.0.1"
|
version = __version__ = "2.1.1"
|
||||||
__version_info__ = tuple(_re.split("[.-]", __version__))
|
__version_info__ = tuple(_re.split("[.-]", __version__))
|
||||||
__updated__ = "2023-10-01 16:14:10"
|
__updated__ = "2024-04-25 01:23:25"
|
||||||
|
|
||||||
__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,6 +14,7 @@ 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,
|
||||||
@ -286,7 +287,9 @@ 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 not config.disable_fallback:
|
if config.mirror:
|
||||||
|
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")
|
||||||
|
|
||||||
@ -364,7 +367,8 @@ 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")
|
||||||
|
|
||||||
|
|
||||||
|
@ -517,6 +517,14 @@ 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",
|
||||||
@ -720,6 +728,7 @@ 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,
|
||||||
@ -745,6 +754,7 @@ 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(
|
||||||
@ -764,6 +774,7 @@ 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,
|
||||||
|
91
pypiserver/mirror_cache.py
Normal file
91
pypiserver/mirror_cache.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/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)
|
2
requirements/mirror-cache-requirements.txt
Normal file
2
requirements/mirror-cache-requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
beautifulsoup4==4.12.3
|
||||||
|
requests==2.31.0
|
Loading…
Reference in New Issue
Block a user