Skip to content

Commit

Permalink
🔖 Release 3.2.0 (#40)
Browse files Browse the repository at this point in the history
Multiplexed is now a thing in Python
Async is now a thing for Niquests

See HISTORY.md for more
  • Loading branch information
Ousret authored Nov 5, 2023
1 parent 92c7548 commit 17678b8
Show file tree
Hide file tree
Showing 25 changed files with 1,882 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ repos:
- id: mypy
args: [--check-untyped-defs]
exclude: 'tests/'
additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.1.902', 'wassima>=1.0.1', 'idna', 'kiss_headers']
additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.2.901', 'wassima>=1.0.1', 'idna', 'kiss_headers']
135 changes: 135 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,141 @@
Release History
===============

3.2.0 (2023-11-05)
------------------

**Changed**
- Changed method `raise_for_status` in class `Response` to return **self** in order to make the call chainable.
Idea taken from upstream https://github.com/psf/requests/issues/6215
- Bump minimal version supported for `urllib3.future` to 2.2.901 for recently introduced added features (bellow).

**Added**
- Support for multiplexed connection in HTTP/2 and HTTP/3. Concurrent requests per connection are now a thing, in synchronous code.
This feature is the real advantage of using binaries HTTP protocols.
It is disabled by default and can be enabled through `Session(multiplexed=True)`, each `Response` object will
be 'lazy' loaded. Accessing anything from returned `Response` will block the code until target response is retrieved.
Use `Session.gather()` to efficiently receive responses. You may also give a list of responses that you want to load.

**Example A)** Emitting concurrent requests and loading them via `Session.gather()`
```python
from niquests import Session
from time import time

s = Session(multiplexed=True)

before = time()
responses = []

responses.append(
s.get("https://pie.dev/delay/3")
)

responses.append(
s.get("https://pie.dev/delay/1")
)

s.gather()

print(f"waited {time() - before} second(s)") # will print 3s
```

**Example B)** Emitting concurrent requests and loading them via direct access
```python
from niquests import Session
from time import time

s = Session(multiplexed=True)

before = time()
responses = []

responses.append(
s.get("https://pie.dev/delay/3")
)

responses.append(
s.get("https://pie.dev/delay/1")
)

# internally call gather with self (Response)
print(responses[0].status_code) # 200! :! Hidden call to s.gather(responses[0])
print(responses[1].status_code) # 200!

print(f"waited {time() - before} second(s)") # will print 3s
```
You have nothing to do, everything from streams to connection pooling are handled automagically!
- Support for in-memory intermediary/client certificate (mTLS).
Thanks for support within `urllib3.future`. Unfortunately this feature may not be available depending on your platform.
Passing `cert=(a, b, c)` where **a** or/and **b** contains directly the certificate is supported.
See https://urllib3future.readthedocs.io/en/latest/advanced-usage.html#in-memory-client-mtls-certificate for more information.
It is proposed to circumvent recent pyOpenSSL complete removal.
- Detect if a new (stable) version is available when invoking `python -m niquests.help` and propose it for installation.
- Add the possibility to disable a specific protocol (e.g. HTTP/2, and/or HTTP/3) when constructing `Session`.
Like so: `s = Session(disable_http2=..., disable_http3=...)` both options are set to `False`, thus letting them enabled.
urllib3.future does not permit to disable HTTP/1.1 for now.
- Support passing a single `str` to `auth=...` in addition to actually supported types. It will be treated as a
**Bearer** token, by default to the `Authorization` header. It's a shortcut. You may keep your own token prefix in given
string (e.g. if not Bearer).
- Added `MultiplexingError` exception for anything related to failure with a multiplexed connection.
- Added **async** support through `AsyncSession` that utilize an underlying thread pool.
```python
from niquests import AsyncSession
import asyncio
from time import time

async def emit() -> None:
responses = []

async with AsyncSession(multiplexed=True) as s:
responses.append(await s.get("https://pie.dev/get"))
responses.append(await s.get("https://pie.dev/head"))

await s.gather()

print(responses)

async def main() -> None:
foo = asyncio.create_task(emit())
bar = asyncio.create_task(emit())
await foo
await bar

if __name__ == "__main__":
before = time()
asyncio.run(main())
print(time() - before)
```
Or without `multiplexing` if you want to keep multiple connections open per host per request.
```python
from niquests import AsyncSession
import asyncio
from time import time

async def emit() -> None:
responses = []

async with AsyncSession() as s:
responses.append(await s.get("https://pie.dev/get"))
responses.append(await s.get("https://pie.dev/head"))

print(responses)

async def main() -> None:
foo = asyncio.create_task(emit())
bar = asyncio.create_task(emit())
await foo
await bar

if __name__ == "__main__":
before = time()
asyncio.run(main())
print(time() - before)
```
You may disable concurrent threads by setting `AsyncSession.no_thread = True`.

**Security**
- Certificate revocation verification may not be fired for subsequents requests in a specific condition (redirection).

3.1.4 (2023-10-23)
------------------

Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
**Niquests** is a simple, yet elegant, HTTP library. It is a drop-in replacement for **Requests** that is no longer under
feature freeze.

Why did we pursue this? 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.
Niquests, is the “**Safest**, **Fastest**, **Easiest**, and **Most advanced**” Python HTTP Client.

```python
>>> import niquests
Expand All @@ -28,8 +26,6 @@ True

Niquests allows you to send HTTP requests extremely easily. There’s no need to manually add query strings to your URLs, or to form-encode your `PUT` & `POST` data — but nowadays, just use the `json` method!

Niquests is one of the least downloaded Python packages today, pulling in around `100+ download / week`— according to GitHub, Niquests is currently depended upon by `1+` repositories. But, that may change..! Starting with you.

[![Downloads](https://static.pepy.tech/badge/niquests/month)](https://pepy.tech/project/niquests)
[![Supported Versions](https://img.shields.io/pypi/pyversions/niquests.svg)](https://pypi.org/project/niquests)

Expand All @@ -41,7 +37,7 @@ Niquests is available on PyPI:
$ python -m pip install niquests
```

Niquests officially supports Python 3.7+.
Niquests officially supports Python or PyPy 3.7+.

## Supported Features & Best–Practices

Expand All @@ -66,6 +62,14 @@ Niquests is ready for the demands of building robust and reliable HTTP–speakin
- Streaming Downloads
- HTTP/2 by default
- HTTP/3 over QUIC
- Multiplexed!
- Async!

## Why did we pursue this?

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!

---

Expand Down
2 changes: 1 addition & 1 deletion docs/community/recommended.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ whenever you're making a lot of web niquests.
Requests-Toolbelt
-----------------

`Requests-Toolbelt`_ is a collection of utilities that some users of Requests may desire,
`Requests-Toolbelt`_ is a collection of utilities that some users of Niquests may desire,
but do not belong in Niquests proper. This library is actively maintained
by members of the Requests core team, and reflects the functionality most
requested by users within the community.
Expand Down
14 changes: 1 addition & 13 deletions docs/community/vulnerabilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,4 @@ if upgrading is not an option.
Previous CVEs
-------------

- Fixed in 2.20.0
- `CVE 2018-18074 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2018-18074>`_

- Fixed in 2.6.0

- `CVE 2015-2296 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2015-2296>`_,
reported by Matthew Daley of `BugFuzz <https://bugfuzz.com/>`_.

- Fixed in 2.3.0

- `CVE 2014-1829 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2014-1829>`_

- `CVE 2014-1830 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2014-1830>`_
None to date.
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ Niquests is ready for today's web.
- Streaming Downloads
- HTTP/2 by default
- HTTP/3 over QUIC
- Multiplexed!
- Async!

Niquests officially supports Python 3.7+, and runs great on PyPy.

Expand Down
23 changes: 23 additions & 0 deletions docs/user/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1228,3 +1228,26 @@ by passing a custom ``QuicSharedCache`` instance like so::
.. note:: Passing ``None`` to max size actually permit the cache to grow indefinitely. This is unwise and can lead to significant RAM usage.

When the cache is full, the oldest entry is removed.

Disable HTTP/2, and/or HTTP/3
-----------------------------

You can at your own discretion disable a protocol by passing ``disable_http2=True`` or
``disable_http3=True`` within your ``Session`` constructor.

.. warning:: It is actually forbidden to disable HTTP/1.1 as the underlying library (urllib3.future) does not permit it for now.

Having a session without HTTP/2 enabled should be done that way::

import niquests

session = niquests.Session(disable_http2=True)


Passing a bearer token
----------------------

You may use ``auth=my_token`` as a shortcut to passing ``headers={"Authorization": f"Bearer {my_token}"}`` in
get, post, request, etc...

.. note:: If you pass a token with its custom prefix, it will be taken and passed as-is. e.g. ``auth="NotBearer eyDdx.."``
109 changes: 109 additions & 0 deletions docs/user/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ failed response (e.g. error details with HTTP 500). Such JSON will be decoded
and returned. To check that a request is successful, use
``r.raise_for_status()`` or check ``r.status_code`` is what you expect.

.. note:: Since Niquests 3.2, ``r.raise_for_status()`` is chainable as it returns self if everything went fine.

Raw Response Content
--------------------
Expand Down Expand Up @@ -621,6 +622,114 @@ It is saved in-memory by Niquests.
You may also run the following command ``python -m niquests.help`` to find out if you support HTTP/3.
In 95 percents of the case, the answer is yes!

Multiplexed Connection
----------------------

Starting from Niquests 3.2 you can issue concurrent requests without having multiple connections.
It can leverage multiplexing when your remote peer support either HTTP/2, or HTTP/3.

The only thing you will ever have to do to get started is to specify ``multiplexed=True`` from
within your ``Session`` constructor.

Any ``Response`` returned by get, post, put, etc... will be a lazy instance of ``Response``.

.. note::

An important note about using ``Session(multiplexed=True)`` is that, in order to be efficient
and actually leverage its perks, you will have to issue multiple concurrent request before
actually trying to access any ``Response`` methods or attributes.

**Example A)** Emitting concurrent requests and loading them via `Session.gather()`::

from niquests import Session
from time import time

s = Session(multiplexed=True)

before = time()
responses = []

responses.append(
s.get("https://pie.dev/delay/3")
)

responses.append(
s.get("https://pie.dev/delay/1")
)

s.gather()

print(f"waited {time() - before} second(s)") # will print 3s


**Example B)** Emitting concurrent requests and loading them via direct access::

from niquests import Session
from time import time

s = Session(multiplexed=True)

before = time()
responses = []

responses.append(
s.get("https://pie.dev/delay/3")
)

responses.append(
s.get("https://pie.dev/delay/1")
)

# internally call gather with self (Response)
print(responses[0].status_code) # 200! :! Hidden call to s.gather(responses[0])
print(responses[1].status_code) # 200!

print(f"waited {time() - before} second(s)") # will print 3s

The possible algorithms are actually nearly limitless, and you may arrange/write you own scheduling technics!

Async session
-------------

You may have a program that require ``awaitable`` HTTP request. You are in luck as **Niquests** ships with
an implementation of ``Session`` that support **async**.

All known methods remain the same at the sole difference that it return a coroutine.

.. note:: The underlying main library **urllib3.future** does not support native async but is thread safe. This is why we choose to implement / backport `sync_to_async` from Django that use a ThreadPool under the carpet.

Here is an example::

from niquests import AsyncSession
import asyncio
from time import time

async def emit() -> None:
responses = []

async with AsyncSession() as s: # it also work well using multiplexed=True
responses.append(await s.get("https://pie.dev/get"))
responses.append(await s.get("https://pie.dev/delay/3"))

await s.gather()

print(responses)

async def main() -> None:
foo = asyncio.create_task(emit())
bar = asyncio.create_task(emit())
await foo
await bar

if __name__ == "__main__":
before = time()
asyncio.run(main())
print(time() - before) # 3s!

.. warning:: For the time being **Niquests** only support **asyncio** as the backend library for async. Contributions are welcomed if you want it to be compatible with **anyio** for example.

.. note:: Shortcut functions `get`, `post`, ..., from the top-level package does not support async.

-----------------------

Ready for more? Check out the :ref:`advanced <advanced>` section.
Loading

0 comments on commit 17678b8

Please sign in to comment.