Skip to content

Commit

Permalink
🔖 Release 3.3.3 (#50)
Browse files Browse the repository at this point in the history
3.3.2 (2023-11-26)
------------------

**Added**
- Hook `on_upload` that allows you to monitor / track the upload
progress.
- Model `TransferProgress` that is used in `PreparedRequest` as public
property `upload_progress`.

**Changed**
- urllib3.future minimal version raised to 2.3.901 in order to be able
to track upload progress.
  This version also ships with various performance improvements.
  • Loading branch information
Ousret authored Nov 26, 2023
1 parent 8f18ad7 commit 77c0de8
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 43 deletions.
19 changes: 0 additions & 19 deletions .github/workflows/lock-issues.yml

This file was deleted.

11 changes: 11 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Release History
===============

3.3.2 (2023-11-26)
------------------

**Added**
- Hook `on_upload` that allows you to monitor / track the upload progress.
- Model `TransferProgress` that is used in `PreparedRequest` as public property `upload_progress`.

**Changed**
- urllib3.future minimal version raised to 2.3.901 in order to be able to track upload progress.
This version also ships with various performance improvements.

3.3.2 (2023-11-19)
------------------

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Niquests, is the “**Safest**, **Fastest<sup>*</sup>**, **Easiest**, and **Most
>>> r.json()
{'authenticated': True, ...}
>>> r
<Reponse HTTP/2 [200]>
<Response HTTP/2 [200]>
>>> r.ocsp_verified
True
```
Expand Down Expand Up @@ -72,13 +72,18 @@ Niquests is ready for the demands of building robust and reliable HTTP–speakin

## Why did we pursue this?

For many years now, **Requests** has been frozen and blocked millions of developers, left in a vegetative state
regarding evolutions in the field.

We don't have to reinvent the wheel all over again, HTTP client **Requests** is well established and
really plaisant in its usage. We believe that **Requests** have the most inclusive, and developer friendly interfaces.
We intend to keep it that way. As long as we can, long live Niquests!

How about a nice refresher with a mere `CTRL+H` _import requests_ **to** _import niquests as requests_ ?

---

<small><sup>(*)</sup> performance measured when leveraging a multiplexed connection with or without uses of any form of concurrency as of november 2023. The research compared `httpx`, `requests`, `aiohttp` against `niquests`. Niquests is faster than Requests in every scenarii.</small>
<small><sup>(*)</sup> performance measured when leveraging a multiplexed connection with or without uses of any form of concurrency as of november 2023. The research compared `httpx`, `requests`, `aiohttp` against `niquests`.</small>

---

Expand Down
10 changes: 1 addition & 9 deletions docs/community/support.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@
Support
=======

If you have questions or issues about Niquests, there are several options:

Stack Overflow
--------------

If your question does not contain sensitive (possibly proprietary)
information or can be properly anonymized, please ask a question on
`Stack Overflow <https://stackoverflow.com/questions/tagged/python-requests>`_
and use the tag ``python-niquests``.
If you have questions or issues about Niquests, you may:


File an Issue
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ is designed to be a drop-in replacement for **Requests** that is no longer under
>>> r.json()
{'private_gists': 419, 'total_private_repos': 77, ...}
>>> r
<Reponse HTTP/2 [200]>
<Response HTTP/2 [200]>
>>> r.ocsp_verified
True

Expand Down
52 changes: 43 additions & 9 deletions docs/user/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,9 @@ Available hooks:
The response generated from a Request.
``pre_send``:
The prepared request got his ConnectionInfo injected. This event is triggered just after picking a live connection from the pool.
``on_upload``:
Permit to monitor the upload progress of passed body. This event is triggered each time a block of data is transmitted to the remote peer.
Use this hook carefully as it may impact the overall performance.
``pre_request``:
The prepared request just got built. You may alter it prior to be sent through HTTP.

Expand Down Expand Up @@ -508,11 +511,40 @@ You can explore the following data in it.
List of tangible use-cases:


- Doing a OCSP request to verify whenever a certificate is revoked.
- Displaying cool stuff on the screen for CLI based tools.
- Also debugging, obviously.
- Among others thing.

Track upload progress
---------------------

You may use the ``on_upload`` hook to track the upload progress of a request.
The callable will receive the ``PreparedRequest`` that will contain a property named ``upload_progress``.

.. note:: ``upload_progress`` is a ``TransferProgress`` instance.

You may find bellow a plausible example::

import niquests

if __name__ == "__main__":
def track(req):
print(req.upload_progress)

with niquests.Session() as s:
s.post("https://pie.dev/post", data=b"foo"*16800*1024, hooks={"on_upload": [track]})

.. note:: Niquests recommend the excellent tqdm library to create progress bars with ease.

``upload_progress`` contains the following properties:


- **percentage** (optional) Basic percentage expressed via float from 0% to 100%
- **content_length** (optional) The expected total bytes to be sent (may be unset due to some body formats, e.g. blind iterator / generator)
- **total** : Amount of bytes sent to the remote peer
- **is_completed** : Determine if the transfer ended
- **any_error** : Simple boolean that indicate whenever a error occurred during transfer (like early response from peer)

.. _custom-auth:

Custom Authentication
Expand Down Expand Up @@ -551,6 +583,8 @@ Then, we can make a request using our Pizza Auth::
>>> niquests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))
<Response HTTP/2 [200]>

.. note:: In case you want a clever shortcut to passing a ``Bearer`` token, you can pass directly (as a string) the token to ``auth=...`` instead.

.. _streaming-requests:

Streaming Requests
Expand Down Expand Up @@ -1091,15 +1125,15 @@ backoff, within a Niquests :class:`Session <niquests.Session>` using the
Blocking Or Non-Blocking?
-------------------------

With the default Transport Adapter in place, Niquests does not provide any kind
of non-blocking IO. The :attr:`Response.content <niquests.Response.content>`
property will block until the entire response has been downloaded. If
you require more granularity, the streaming features of the library (see
:ref:`streaming-requests`) allow you to retrieve smaller quantities of the
response at a time. However, these calls will still block.
The :attr:`Response.content <niquests.Response.content>`
property will block until the entire response has been downloaded by default in HTTP/1.1
In HTTP/2 onward, non-consumed response (body, aka. stream=True) will no longer block the connection.

But if you leverage a full multiplexed connection, Niquests no longer block your synchronous
loop. You are free of the IO blocking per request.

We plan to support multiplexed requests when using HTTP/2 or HTTP/3. Async usage
will be partially rendered useless. There won't be any IO blocking per request.
You may also use the ``AsyncSession`` that provide you with the same methods as the regular
``Session`` but with asyncio support.

Header Ordering
---------------
Expand Down
4 changes: 2 additions & 2 deletions src/niquests/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
__url__: str = "https://niquests.readthedocs.io"

__version__: str
__version__ = "3.3.2"
__version__ = "3.3.3"

__build__: int = 0x030302
__build__: int = 0x030303
__author__: str = "Kenneth Reitz"
__author_email__: str = "me@kennethreitz.org"
__license__: str = "Apache-2.0"
Expand Down
5 changes: 5 additions & 0 deletions src/niquests/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ def send(
cert: TLSClientCertType | None = None,
proxies: ProxyType | None = None,
on_post_connection: typing.Callable[[typing.Any], None] | None = None,
on_upload_body: typing.Callable[[int, int | None, bool, bool], None]
| None = None,
multiplexed: bool = False,
) -> Response:
"""Sends PreparedRequest object. Returns Response object.
Expand Down Expand Up @@ -612,6 +614,8 @@ def send(
cert: TLSClientCertType | None = None,
proxies: ProxyType | None = None,
on_post_connection: typing.Callable[[typing.Any], None] | None = None,
on_upload_body: typing.Callable[[int, int | None, bool, bool], None]
| None = None,
multiplexed: bool = False,
) -> Response:
"""Sends PreparedRequest object. Returns Response object.
Expand Down Expand Up @@ -706,6 +710,7 @@ def send(
timeout=timeout,
chunked=chunked,
on_post_connection=on_post_connection,
on_upload_body=on_upload_body,
multiplexed=multiplexed,
)

Expand Down
4 changes: 4 additions & 0 deletions src/niquests/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
The prepared request just got built. You may alter it prior to be sent through HTTP.
``pre_send``:
The prepared request got his ConnectionInfo injected. This event is triggered just after picking a live connection from the pool.
``on_upload``:
Permit to monitor the upload progress of passed body. This event is triggered each time a block of data is transmitted to the remote peer.
Use this hook carefully as it may impact the overall performance.
``response``:
The response generated from a Request.
"""
Expand All @@ -22,6 +25,7 @@
HOOKS = [
"pre_request",
"pre_send",
"on_upload",
"response",
]

Expand Down
24 changes: 24 additions & 0 deletions src/niquests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@
ITER_CHUNK_SIZE = 512


class TransferProgress:
def __init__(self):
self.total: int = 0
self.content_length: int | None = None
self.is_completed: bool = False
self.any_error: bool = False

@property
def percentage(self) -> float | None:
if self.content_length is None:
return None

return round((self.total / self.content_length) * 100.0, 3)

def __repr__(self) -> str:
if self.content_length:
return (
f"<Progress {self.percentage} % ({self.total} / {self.content_length})>"
)
return f"<Progress {self.total} bytes sent ({'in progress' if self.is_completed is False else 'completed'})>"


class Request:
"""A user-created :class:`Request <Request>` object.
Expand Down Expand Up @@ -275,6 +297,8 @@ def __init__(self) -> None:
self.conn_info: ConnectionInfo | None = None
#: marker about if OCSP post-handshake verification took place.
self.ocsp_verified: bool | None = None
#: upload progress if any.
self.upload_progress: TransferProgress | None = None

@property
def oheaders(self) -> Headers:
Expand Down
21 changes: 21 additions & 0 deletions src/niquests/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
PreparedRequest,
Request,
Response,
TransferProgress,
)
from .status_codes import codes
from .structures import CaseInsensitiveDict, QuicSharedCache
Expand Down Expand Up @@ -996,7 +997,27 @@ def on_post_connection(conn_info: ConnectionInfo) -> None:
if ptr_request == request:
dispatch_hook("pre_send", hooks, ptr_request) # type: ignore[arg-type]

def handle_upload_progress(
total_sent: int,
content_length: int | None,
is_completed: bool,
any_error: bool,
) -> None:
nonlocal ptr_request, request, kwargs
if ptr_request != request:
return
if request.upload_progress is None:
request.upload_progress = TransferProgress()

request.upload_progress.total = total_sent
request.upload_progress.content_length = content_length
request.upload_progress.is_completed = is_completed
request.upload_progress.any_error = any_error

dispatch_hook("on_upload", hooks, request) # type: ignore[arg-type]

kwargs.setdefault("on_post_connection", on_post_connection)
kwargs.setdefault("on_upload_body", handle_upload_progress)
kwargs.setdefault("multiplexed", self.multiplexed)

assert request.url is not None
Expand Down
7 changes: 6 additions & 1 deletion tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ def test_hooks_with_kwargs(hooks_list, result):


def test_default_hooks():
assert hooks.default_hooks() == {"pre_request": [], "pre_send": [], "response": []}
assert hooks.default_hooks() == {
"pre_request": [],
"pre_send": [],
"on_upload": [],
"response": [],
}
17 changes: 17 additions & 0 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ def test_override_content_length(self, httpbin):
assert "Content-Length" in r.headers
assert r.headers["Content-Length"] == "not zero"

def test_upload_progress(self, httpbin):
pulse = []

def track(req):
nonlocal pulse
pulse.append(req.upload_progress)

r = niquests.post(
httpbin("post"), data=b"foo" * 1024 * 10, hooks={"on_upload": [track]}
)
assert r.status_code == 200

assert all(item is not None for item in pulse)
assert pulse[-1].is_completed
assert pulse[0].content_length == 3 * 1024 * 10
assert len(pulse) > 1

def test_path_is_not_double_encoded(self):
request = niquests.Request("GET", "http://0.0.0.0/get/test case").prepare()

Expand Down

0 comments on commit 77c0de8

Please sign in to comment.