git: 5806d293c4a9 - main - www/py-proxpi: new port
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Thu, 17 Sep 2026 14:12:13 UTC
The branch main has been updated by michaelo:
URL: https://cgit.FreeBSD.org/ports/commit/?id=5806d293c4a901c91800e524a2a1b38efd1c54e2
commit 5806d293c4a901c91800e524a2a1b38efd1c54e2
Author: Michael Osipov <michaelo@FreeBSD.org>
AuthorDate: 2026-09-17 13:53:28 +0000
Commit: Michael Osipov <michaelo@FreeBSD.org>
CommitDate: 2026-09-17 14:11:26 +0000
www/py-proxpi: new port
PyPI caching mirror implemented as a Flask WSGI app. Proxies and
caches package indexes and file downloads from an upstream index,
reducing redundant downloads and allowing installs to continue
during upstream outages.
---
www/Makefile | 1 +
www/py-proxpi/Makefile | 41 ++++++++++++++++
www/py-proxpi/distinfo | 3 ++
www/py-proxpi/files/extra-patch-file-option | 74 +++++++++++++++++++++++++++++
www/py-proxpi/pkg-descr | 4 ++
5 files changed, 123 insertions(+)
diff --git a/www/Makefile b/www/Makefile
index 40ba0270832a..bfb71c42c2e8 100644
--- a/www/Makefile
+++ b/www/Makefile
@@ -1917,6 +1917,7 @@
SUBDIR += py-priority1
SUBDIR += py-prometheus-fastapi-instrumentator
SUBDIR += py-protego
+ SUBDIR += py-proxpi
SUBDIR += py-proxy.py
SUBDIR += py-proxy_tools
SUBDIR += py-puppetboard
diff --git a/www/py-proxpi/Makefile b/www/py-proxpi/Makefile
new file mode 100644
index 000000000000..d7157c0970bc
--- /dev/null
+++ b/www/py-proxpi/Makefile
@@ -0,0 +1,41 @@
+PORTNAME= proxpi
+PORTVERSION= 1.3.0
+CATEGORIES= www python
+MASTER_SITES= PYPI
+PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX}
+
+MAINTAINER= michaelo@FreeBSD.org
+COMMENT= PyPI caching mirror
+WWW= https://github.com/EpicWink/proxpi
+
+LICENSE= MIT
+LICENSE_FILE= ${WRKSRC}/LICENSE
+
+BUILD_DEPENDS= ${PYTHON_PKGNAMEPREFIX}setuptools>=61.0:devel/py-setuptools@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}setuptools-scm>=3.4:devel/py-setuptools-scm@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}wheel>=0:devel/py-wheel@${PY_FLAVOR}
+RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}flask>=2.0<4.0:www/py-flask@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}Jinja2>=3.0<4.0:devel/py-Jinja2@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}lxml>=4.8<7.0:devel/py-lxml@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}requests>=2.27<3.0:www/py-requests@${PY_FLAVOR}
+TEST_DEPENDS= ${PYTHON_PKGNAMEPREFIX}hypercorn>=0.14<2.0:www/py-hypercorn@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}packaging>=0:devel/py-packaging@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}starlette>=0.29<2.0:www/py-starlette@${PY_FLAVOR}
+
+USES= python
+USE_PYTHON= autoplist concurrent pep517 pytest
+
+PYTEST_IGNORED_TESTS= test_pip_download
+
+NO_ARCH= yes
+# 1.4.0 replaces flask with starlette and is otherwise internally
+# incompatible; do not suggest it until the port is updated to match.
+PORTSCOUT= limit:^1\.[0-3]\.
+
+OPTIONS_DEFINE= FILE
+FILE_DESC= Support file:// index/package URLs
+
+FILE_RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}requests-file>=0:www/py-requests-file@${PY_FLAVOR}
+FILE_EXTRA_PATCHES= ${PATCHDIR}/extra-patch-file-option
+
+.include <bsd.port.mk>
diff --git a/www/py-proxpi/distinfo b/www/py-proxpi/distinfo
new file mode 100644
index 000000000000..14b39f1c0bcc
--- /dev/null
+++ b/www/py-proxpi/distinfo
@@ -0,0 +1,3 @@
+TIMESTAMP = 1789647249
+SHA256 (proxpi-1.3.0.tar.gz) = aea872e98010c0927084d3a47793761829a7686f950965bba02fd29fe41e6d89
+SIZE (proxpi-1.3.0.tar.gz) = 43206
diff --git a/www/py-proxpi/files/extra-patch-file-option b/www/py-proxpi/files/extra-patch-file-option
new file mode 100644
index 000000000000..6bd4ba178e8a
--- /dev/null
+++ b/www/py-proxpi/files/extra-patch-file-option
@@ -0,0 +1,74 @@
+Add support for the file:// URL scheme via requests_file's FileAdapter,
+so the index/package URL can point at a local on-disk PyPI mirror
+instead of only a remote HTTP(S) index.
+
+Obtained from: https://github.com/michael-o/proxpi/commit/14ea92360f051723f415e9c2d622d07a18946ff1
+
+--- pyproject.toml.orig
++++ pyproject.toml
+@@ -25,6 +25,7 @@ dependencies = [
+ "jinja2 ~= 3.0",
+ "lxml >= 4.8, < 7.0",
+ "requests ~= 2.27",
++ "requests_file",
+ ]
+ description = "PyPI caching mirror"
+ keywords = ["pypi", "index", "mirror", "cache"]
+--- src/proxpi/_cache.py.orig
++++ src/proxpi/_cache.py
+@@ -16,6 +16,7 @@
+ import urllib.parse
+
+ import requests
++from requests_file import FileAdapter
+ import lxml.etree
+
+ INDEX_URL = os.environ.get("PROXPI_INDEX_URL", "https://pypi.org/simple/")
+@@ -484,6 +485,7 @@ def __init__(self, index_url: str, ttl: int, session: requests.Session = None):
+ self.index_url = index_url
+ self.ttl = ttl
+ self.session = session or requests.Session()
++ self.session.mount("file://", FileAdapter())
+ self._index_t = None
+ self._index_lock = threading.Lock()
+ self._package_locks = _Locks()
+@@ -503,7 +505,11 @@ def _list_packages(self):
+ self._stats.add_miss(key="<index>")
+
+ logger.info(f"Listing packages in index '{self._index_url_masked}'")
+- response = self.session.get(self.index_url, headers=self._headers, stream=True)
++ index_url = self.index_url
++ splitted_index_url = urllib.parse.urlsplit(self.index_url)
++ if splitted_index_url.scheme == "file" and os.path.isdir(urllib.parse.unquote(splitted_index_url.path)):
++ index_url = urllib.parse.urljoin(self.index_url, "index.html")
++ response = self.session.get(index_url, headers=self._headers, stream=True)
+ response.raise_for_status()
+ self._index_t = _now()
+
+@@ -576,6 +582,9 @@ def _list_files(self, package_name: str):
+ raise NotFound(package_name)
+ package_url = self._index[package_name]
+ url = urllib.parse.urljoin(self.index_url, package_url)
++ splitted_url = urllib.parse.urlsplit(url)
++ if splitted_url.scheme == "file" and os.path.isdir(urllib.parse.unquote(splitted_url.path)):
++ url = urllib.parse.urljoin(url, "index.html")
+ response = self.session.get(url, headers=self._headers, stream=True)
+ response.raise_for_status()
+
+@@ -757,6 +766,7 @@ def __init__(
+ self.cache_dir = os.path.abspath(cache_dir or tempfile.mkdtemp())
+ self.download_timeout = download_timeout
+ self.session = session or requests.Session()
++ self.session.mount("file://", FileAdapter())
+ self._cache_dir_provided = cache_dir
+ self._files = {}
+ self._evict_lock = threading.Lock()
+@@ -794,7 +804,7 @@ def _populate_files_from_existing_cache_dir(self):
+ def _get_key(url: str) -> str:
+ """Get file cache reference key from file URL."""
+ urlsplit = urllib.parse.urlsplit(url)
+- parent = _hostname_normalise_pattern.sub("-", urlsplit.hostname)
++ parent = _hostname_normalise_pattern.sub("-", urlsplit.hostname) if urlsplit.hostname else "local"
+ return posixpath.join(parent, *_split_path(urlsplit.path, posixpath.split))
+
+ def _download_file(self, url: str, path: str):
diff --git a/www/py-proxpi/pkg-descr b/www/py-proxpi/pkg-descr
new file mode 100644
index 000000000000..d6bff8c7260f
--- /dev/null
+++ b/www/py-proxpi/pkg-descr
@@ -0,0 +1,4 @@
+proxpi is a PyPI caching mirror. It proxies and caches package indexes
+and file downloads from an upstream Python package index (PyPI or a
+private index), reducing redundant downloads and allowing installs to
+continue during upstream index outages.