git: 5806d293c4a9 - main - www/py-proxpi: new port

From: Michael Osipov <michaelo_at_FreeBSD.org>
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.