Commit fb07b5cf authored by Igor Ponomarev's avatar Igor Ponomarev Committed by Nejc Habjan
Browse files

feat(api): Add argument that appends extra HTTP headers to a request

Currently the only way to manipulate the headers for a request
is to use `Gitlab.headers` attribute. However, this makes it
very concurrently unsafe because the `Gitlab` object can be shared
between multiple requests at the same time.

Instead add a new keyword argument `extra_headers` which will update
the headers dictionary with new values just before the request is sent.

For example, this can be used to download a part of a artifacts file
using the `Range` header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests



Signed-off-by: default avatarIgor Ponomarev <igor.ponomarev@collabora.com>
parent e4673d8a
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -211,3 +211,20 @@ on your own, such as for nested API responses and ``Union`` return types. For ex

   if TYPE_CHECKING:
      assert isinstance(license["plan"], str)

Per request HTTP headers override
---------------------------------

The ``extra_headers`` keyword argument can be used to add and override
the HTTP headers for a specific request. For example, it can be used do add ``Range``
header to download a part of artifacts archive:

.. code-block:: python

   import gitlab

   gl = gitlab.Gitlab(url, token)
   project = gl.projects.get(1)
   job = project.jobs.get(123)

   artifacts = job.artifacts(extra_headers={"Range": "bytes=0-9"})
+5 −0
Original line number Diff line number Diff line
@@ -654,6 +654,7 @@ class Gitlab:
        obey_rate_limit: bool = True,
        retry_transient_errors: Optional[bool] = None,
        max_retries: int = 10,
        extra_headers: Optional[Dict[str, Any]] = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Make an HTTP request to the Gitlab server.
@@ -675,6 +676,7 @@ class Gitlab:
                or 52x responses. Defaults to False.
            max_retries: Max retries after 429 or transient errors,
                               set to -1 to retry forever. Defaults to 10.
            extra_headers: Add and override HTTP headers for the request.
            **kwargs: Extra options to send to the server (e.g. sudo)

        Returns:
@@ -721,6 +723,9 @@ class Gitlab:
        send_data = self._backend.prepare_send_data(files, post_data, raw)
        opts["headers"]["Content-type"] = send_data.content_type

        if extra_headers is not None:
            opts["headers"].update(extra_headers)

        retry = utils.Retry(
            max_retries=max_retries,
            obey_rate_limit=obey_rate_limit,
+24 −0
Original line number Diff line number Diff line
@@ -35,6 +35,20 @@ def resp_project_artifacts_delete():
        yield rsps


@pytest.fixture
def resp_job_artifact_bytes_range(binary_content):
    with responses.RequestsMock() as rsps:
        rsps.add(
            method=responses.GET,
            url="http://localhost/api/v4/projects/1/jobs/123/artifacts",
            body=binary_content[:10],
            content_type="application/octet-stream",
            status=206,
            match=[responses.matchers.header_matcher({"Range": "bytes=0-9"})],
        )
        yield rsps


def test_project_artifacts_delete(gl, resp_project_artifacts_delete):
    project = gl.projects.get(1, lazy=True)
    project.artifacts.delete()
@@ -46,3 +60,13 @@ def test_project_artifacts_download_by_ref_name(
    project = gl.projects.get(1, lazy=True)
    artifacts = project.artifacts.download(ref_name=ref_name, job=job)
    assert artifacts == binary_content


def test_job_artifact_download_bytes_range(
    gl, binary_content, resp_job_artifact_bytes_range
):
    project = gl.projects.get(1, lazy=True)
    job = project.jobs.get(123, lazy=True)

    artifacts = job.artifacts(extra_headers={"Range": "bytes=0-9"})
    assert len(artifacts) == 10
+23 −0
Original line number Diff line number Diff line
@@ -117,6 +117,29 @@ def test_http_request_with_retry_on_method_for_transient_failures(gl):
    assert len(responses.calls) == calls_before_success


@responses.activate
def test_http_request_extra_headers(gl):
    path = "/projects/123/jobs/123456"
    url = "http://localhost/api/v4" + path

    range_headers = {"Range": "bytes=0-99"}

    responses.add(
        method=responses.GET,
        url=url,
        body=b"a" * 100,
        status=206,
        content_type="application/octet-stream",
        match=helpers.MATCH_EMPTY_QUERY_PARAMS
        + [responses.matchers.header_matcher(range_headers)],
    )

    http_r = gl.http_request("get", path, extra_headers=range_headers)

    assert http_r.status_code == 206
    assert len(http_r.content) == 100


@responses.activate
@pytest.mark.parametrize(
    "exception",