diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 2277336..24374cb 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -51,7 +51,7 @@ jobs: environment: name: pypi url: https://pypi.org/p/simple-repository-server - + steps: - name: Retrieve release distributions uses: actions/download-artifact@v4 diff --git a/simple_repository_server/__main__.py b/simple_repository_server/__main__.py index 704b612..e5bd6cb 100644 --- a/simple_repository_server/__main__.py +++ b/simple_repository_server/__main__.py @@ -59,6 +59,11 @@ def configure_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument("--host", default="0.0.0.0") parser.add_argument("--port", type=int, default=8000) + parser.add_argument( + "--stream-http-resources", + action="store_true", + help="Stream HTTP resources through this server instead of redirecting (default: redirect)", + ) parser.add_argument("repository_url", metavar="repository-url", type=str, nargs="+") @@ -88,7 +93,7 @@ def create_repository( return MetadataInjectorRepository(repo, http_client) -def create_app(repository_urls: list[str]) -> fastapi.FastAPI: +def create_app(repository_urls: list[str], *, stream_http_resources: bool = False) -> fastapi.FastAPI: @asynccontextmanager async def lifespan(app: FastAPI) -> typing.AsyncIterator[None]: # Configure httpx client with netrc support if netrc file exists @@ -100,7 +105,13 @@ async def lifespan(app: FastAPI) -> typing.AsyncIterator[None]: async with httpx.AsyncClient(auth=auth, follow_redirects=True) as http_client: repo = create_repository(repository_urls, http_client=http_client) - app.include_router(simple.build_router(repo, http_client=http_client)) + app.include_router( + simple.build_router( + repo, + http_client=http_client, + stream_http_resources=stream_http_resources, + ), + ) yield app = FastAPI( @@ -114,8 +125,9 @@ def handler(args: typing.Any) -> None: host: str = args.host port: int = args.port repository_urls: list[str] = args.repository_url + stream_http_resources: bool = args.stream_http_resources uvicorn.run( - app=create_app(repository_urls), + app=create_app(repository_urls, stream_http_resources=stream_http_resources), host=host, port=port, ) diff --git a/simple_repository_server/routers/simple.py b/simple_repository_server/routers/simple.py index 66b6641..0470ae9 100644 --- a/simple_repository_server/routers/simple.py +++ b/simple_repository_server/routers/simple.py @@ -64,6 +64,7 @@ def build_router( http_client: httpx.AsyncClient, prefix: str = "/simple/", repo_factory: typing.Optional[typing.Callable[..., SimpleRepository]] = None, + stream_http_resources: bool = False, ) -> APIRouter: """ Build a FastAPI router for the given repository and http_client. @@ -188,16 +189,19 @@ async def resources( ) if isinstance(resource, model.HttpResource): - response_iterator = await HttpResponseIterator.create_iterator( - http_client=http_client, - url=resource.url, - request_headers=request.headers, - ) - return StreamingResponse( - content=response_iterator, - status_code=response_iterator.status_code, - headers=response_iterator.headers, - ) + if stream_http_resources: + response_iterator = await HttpResponseIterator.create_iterator( + http_client=http_client, + url=resource.url, + request_headers=request.headers, + ) + return StreamingResponse( + content=response_iterator, + status_code=response_iterator.status_code, + headers=response_iterator.headers, + ) + else: + return RedirectResponse(url=resource.url, status_code=302) if isinstance(resource, model.LocalResource): ctx_etag = resource.context.get("etag") diff --git a/simple_repository_server/tests/api/test_simple_router.py b/simple_repository_server/tests/api/test_simple_router.py index eef57c3..59b00e9 100644 --- a/simple_repository_server/tests/api/test_simple_router.py +++ b/simple_repository_server/tests/api/test_simple_router.py @@ -120,7 +120,22 @@ async def test_simple_package_releases__package_not_found(client: TestClient, mo } -def test_get_resource__remote(mock_repo: mock.AsyncMock, httpx_mock: HTTPXMock) -> None: +def test_get_resource__http_redirect(mock_repo: mock.AsyncMock) -> None: + mock_repo.get_resource.return_value = model.HttpResource( + url="http://my_url", + ) + + http_client = httpx.AsyncClient() + app = FastAPI() + app.include_router(simple_router.build_router(mock_repo, http_client=http_client)) + client = TestClient(app) + + response = client.get("/resources/numpy/numpy-1.0-ciao.whl", follow_redirects=False) + assert response.status_code == 302 + assert response.headers["location"] == "http://my_url" + + +def test_get_resource__http_streaming(mock_repo: mock.AsyncMock, httpx_mock: HTTPXMock) -> None: mock_repo.get_resource.return_value = model.HttpResource( url="http://my_url", ) @@ -132,7 +147,7 @@ def test_get_resource__remote(mock_repo: mock.AsyncMock, httpx_mock: HTTPXMock) ) http_client = httpx.AsyncClient() app = FastAPI() - app.include_router(simple_router.build_router(mock_repo, http_client=http_client)) + app.include_router(simple_router.build_router(mock_repo, http_client=http_client, stream_http_resources=True)) client = TestClient(app) response = client.get("/resources/numpy/numpy-1.0-ciao.whl", follow_redirects=False)