Compare commits
4 Commits
9e3eea4d08
...
feature/tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e5a2948e | ||
|
|
b4e23d9899 | ||
|
|
aa3bd05d46 | ||
|
|
810e024d09 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -7,17 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Added transparent PyPI proxy implementing PEP 503 Simple API
|
- Added transparent PyPI proxy implementing PEP 503 Simple API (#108)
|
||||||
- `GET /pypi/simple/` - package index (proxied from upstream)
|
- `GET /pypi/simple/` - package index (proxied from upstream)
|
||||||
- `GET /pypi/simple/{package}/` - version list with rewritten download links
|
- `GET /pypi/simple/{package}/` - version list with rewritten download links
|
||||||
- `GET /pypi/simple/{package}/{filename}` - download with automatic caching
|
- `GET /pypi/simple/{package}/{filename}` - download with automatic caching
|
||||||
- Allows `pip install --index-url https://orchard.../pypi/simple/ <package>`
|
- Allows `pip install --index-url https://orchard.../pypi/simple/ <package>`
|
||||||
- Artifacts cached on first access through configured upstream sources
|
- Artifacts cached on first access through configured upstream sources
|
||||||
|
- Added `POST /api/v1/cache/resolve` endpoint to cache packages by coordinates instead of URL (#108)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Upstream sources table text is now centered under column headers
|
- Upstream sources table text is now centered under column headers (#108)
|
||||||
- ENV badge now appears inline with source name instead of separate column
|
- ENV badge now appears inline with source name instead of separate column (#108)
|
||||||
- Test and Edit buttons now have more prominent button styling
|
- Test and Edit buttons now have more prominent button styling (#108)
|
||||||
|
- Reduced footer padding for cleaner layout (#108)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed purge_seed_data crash when deleting access permissions - was comparing UUID to VARCHAR column (#107)
|
- Fixed purge_seed_data crash when deleting access permissions - was comparing UUID to VARCHAR column (#107)
|
||||||
|
|||||||
@@ -156,10 +156,7 @@ async def pypi_simple_index(
|
|||||||
|
|
||||||
simple_url = source.url.rstrip('/') + '/simple/'
|
simple_url = source.url.rstrip('/') + '/simple/'
|
||||||
|
|
||||||
timeout = httpx.Timeout(
|
timeout = httpx.Timeout(PROXY_READ_TIMEOUT, connect=PROXY_CONNECT_TIMEOUT)
|
||||||
connect=PROXY_CONNECT_TIMEOUT,
|
|
||||||
read=PROXY_READ_TIMEOUT,
|
|
||||||
)
|
|
||||||
|
|
||||||
with httpx.Client(timeout=timeout, follow_redirects=False) as client:
|
with httpx.Client(timeout=timeout, follow_redirects=False) as client:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -247,10 +244,7 @@ async def pypi_package_versions(
|
|||||||
|
|
||||||
package_url = source.url.rstrip('/') + f'/simple/{normalized_name}/'
|
package_url = source.url.rstrip('/') + f'/simple/{normalized_name}/'
|
||||||
|
|
||||||
timeout = httpx.Timeout(
|
timeout = httpx.Timeout(PROXY_READ_TIMEOUT, connect=PROXY_CONNECT_TIMEOUT)
|
||||||
connect=PROXY_CONNECT_TIMEOUT,
|
|
||||||
read=PROXY_READ_TIMEOUT,
|
|
||||||
)
|
|
||||||
|
|
||||||
with httpx.Client(timeout=timeout, follow_redirects=False) as client:
|
with httpx.Client(timeout=timeout, follow_redirects=False) as client:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -385,10 +379,7 @@ async def pypi_download_file(
|
|||||||
headers.update(_build_auth_headers(matched_source))
|
headers.update(_build_auth_headers(matched_source))
|
||||||
auth = _get_basic_auth(matched_source) if matched_source else None
|
auth = _get_basic_auth(matched_source) if matched_source else None
|
||||||
|
|
||||||
timeout = httpx.Timeout(
|
timeout = httpx.Timeout(300.0, connect=PROXY_CONNECT_TIMEOUT) # 5 minutes for large files
|
||||||
connect=PROXY_CONNECT_TIMEOUT,
|
|
||||||
read=300.0, # 5 minutes for large files
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fetch the file
|
# Fetch the file
|
||||||
logger.info(f"PyPI proxy: fetching {filename} from {upstream_url}")
|
logger.info(f"PyPI proxy: fetching {filename} from {upstream_url}")
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
"""Integration tests for PyPI transparent proxy."""
|
"""Integration tests for PyPI transparent proxy."""
|
||||||
|
|
||||||
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_url():
|
||||||
|
"""Get the base URL for the Orchard server from environment."""
|
||||||
|
return os.environ.get("ORCHARD_TEST_URL", "http://localhost:8080")
|
||||||
|
|
||||||
|
|
||||||
class TestPyPIProxyEndpoints:
|
class TestPyPIProxyEndpoints:
|
||||||
"""Tests for PyPI proxy endpoints.
|
"""Tests for PyPI proxy endpoints.
|
||||||
|
|
||||||
@@ -13,8 +19,7 @@ class TestPyPIProxyEndpoints:
|
|||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
def test_pypi_simple_index_no_sources(self):
|
def test_pypi_simple_index_no_sources(self):
|
||||||
"""Test that /pypi/simple/ returns 503 when no sources configured."""
|
"""Test that /pypi/simple/ returns 503 when no sources configured."""
|
||||||
# Use unauthenticated client since PyPI proxy is public
|
with httpx.Client(base_url=get_base_url(), timeout=30.0) as client:
|
||||||
with httpx.Client(base_url="http://localhost:8080") as client:
|
|
||||||
response = client.get("/pypi/simple/")
|
response = client.get("/pypi/simple/")
|
||||||
# Should return 503 when no PyPI upstream sources are configured
|
# Should return 503 when no PyPI upstream sources are configured
|
||||||
assert response.status_code == 503
|
assert response.status_code == 503
|
||||||
@@ -23,7 +28,7 @@ class TestPyPIProxyEndpoints:
|
|||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
def test_pypi_package_no_sources(self):
|
def test_pypi_package_no_sources(self):
|
||||||
"""Test that /pypi/simple/{package}/ returns 503 when no sources configured."""
|
"""Test that /pypi/simple/{package}/ returns 503 when no sources configured."""
|
||||||
with httpx.Client(base_url="http://localhost:8080") as client:
|
with httpx.Client(base_url=get_base_url(), timeout=30.0) as client:
|
||||||
response = client.get("/pypi/simple/requests/")
|
response = client.get("/pypi/simple/requests/")
|
||||||
assert response.status_code == 503
|
assert response.status_code == 503
|
||||||
assert "No PyPI upstream sources configured" in response.json()["detail"]
|
assert "No PyPI upstream sources configured" in response.json()["detail"]
|
||||||
@@ -31,7 +36,7 @@ class TestPyPIProxyEndpoints:
|
|||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
def test_pypi_download_missing_upstream_param(self):
|
def test_pypi_download_missing_upstream_param(self):
|
||||||
"""Test that /pypi/simple/{package}/{filename} requires upstream param."""
|
"""Test that /pypi/simple/{package}/{filename} requires upstream param."""
|
||||||
with httpx.Client(base_url="http://localhost:8080") as client:
|
with httpx.Client(base_url=get_base_url(), timeout=30.0) as client:
|
||||||
response = client.get("/pypi/simple/requests/requests-2.31.0.tar.gz")
|
response = client.get("/pypi/simple/requests/requests-2.31.0.tar.gz")
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "upstream" in response.json()["detail"].lower()
|
assert "upstream" in response.json()["detail"].lower()
|
||||||
@@ -75,7 +80,7 @@ class TestPyPIPackageNormalization:
|
|||||||
# requests, Requests, requests_, requests-
|
# requests, Requests, requests_, requests-
|
||||||
# The endpoint normalizes to lowercase with hyphens
|
# The endpoint normalizes to lowercase with hyphens
|
||||||
|
|
||||||
with httpx.Client(base_url="http://localhost:8080") as client:
|
with httpx.Client(base_url=get_base_url(), timeout=30.0) as client:
|
||||||
# Without upstream sources, we get 503, but the normalization
|
# Without upstream sources, we get 503, but the normalization
|
||||||
# happens before the source lookup
|
# happens before the source lookup
|
||||||
response = client.get("/pypi/simple/Requests/")
|
response = client.get("/pypi/simple/Requests/")
|
||||||
|
|||||||
Reference in New Issue
Block a user