Managing Requests
The base of Sensei HTTP requests is the httpx
library.
When Sensei makes requests it useshttpx.Client
(or httpx.AsyncClient
) object.
You, too, might use these objects, and you don't suspect it.
Tip
If you don't know, why httpx
is better than the requests
library, you should read
HTTP Requests/Introducing httpx
When you make a simple request, like the following:
import httpx
response = httpx.get('https://example-api.com', params={'page': 1})
print(response.json())
httpx
follows this algorithm:
sequenceDiagram
participant httpx.get(...)
participant httpx.Client
participant API
httpx.get(...)->>httpx.Client: Open Client
httpx.Client->>API: Make Request
API-->>httpx.Client: Response
httpx.Client-->>httpx.get(...): Wrap in httpx.Response
httpx.get(...)->>httpx.Client: Close Client
If you have used requests
, it does the same. But httpx.Client
corresponds to requests.Session
.
Technical Details
Here is the implementation of the get
function in requests
and httpx
.
Here most of the arguments are omitted and replaced with ...
def request(
method: str,
url: URL | str,
*,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
...
) -> Response:
with Client(...) as client:
return client.request(
method=method,
url=url,
params=params,
headers=headers,
...
)
def get(
url: URL | str,
*,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
...
) -> Response:
return request(
"GET",
url,
params=params,
headers=headers,
...
)
def request(method, url, **kwargs):
with sessions.Session() as session:
return session.request(method=method, url=url, **kwargs)
def get(url, params=None, **kwargs):
return request("get", url, params=params, **kwargs)
If you make dozens of requests, like this code:
import httpx
urls = [...]
params_list = [{...}, ...]
for url, params in zip(urls, params_list):
response = httpx.get(url, params=params)
print(response.json())
httpx
will open the client for each request. It slows your application. The better
solution is to use a single client instance. You can close it whenever you want.
import httpx
urls = [...]
params_list = [{...}, ...]
with httpx.Client() as client:
for url, params in zip(urls, params_list):
response = client.get(url, params=params)
print(response.json())
In the example above httpx.Client
is closed after the last statement inside with
block. You can close it
manually, calling the close
method.
import httpx
urls = [...]
params_list = [{...}, ...]
client = httpx.Client()
for url, params in zip(urls, params_list):
response = client.get(url, params=params)
print(response.json())
client.close()
Furthermore, a client can be used for advanced request configuration. You can read
the article from the httpx
documentation, to learn more about httpx.Client
.
When you call routed functions, Sensei makes the same: Open client → Make request → Close client.
How you can use your client so that you will close whenever you want? Let's introduce Manager
Manager¶
Manager
serves as a bridge between the application and Sensei, to dynamically provide a client for routed function calls.
It separately stores httpx.AsyncClient
and httpx.Client
.
To use Manager
you need to create it and pass it to the router.
Example
from sensei import Manager, Router, Client
manager = Manager()
router = Router('httpx://example-api.com', manager=manager)
@router.get('/users/{id_}')
def get_user(id_: int) -> User:
pass
with Client(base_url=router.base_url) as client:
manager.set(client)
user = get_user(1)
print(user)
manager.pop()
You can import httpx.Client
from sensei
or httpx
. They are the same classes.
from sensei import Client
from httpx import Client
Let's explore common actions.
Setting¶
You must know, that Manager
can store only one instance of a client of each type (one httpx.AsyncClient
and one httpx.Client
)
There are two ways to set client.
from sensei import Manager, Router, Client, AsyncClient
base_url = 'httpx://example-api.com'
client = Client(base_url=base_url)
aclient = AsyncClient(base_url=base_url)
manager = Manager(sync_client=client, async_client=aclient)
router = Router(base_url, manager=manager)
from sensei import Manager, Router, Client, AsyncClient
manager = Manager()
router = Router('httpx://example-api.com', manager=manager)
client = Client(base_url=router.base_url)
aclient = AsyncClient(base_url=router.base_url)
manager.set(client)
manager.set(aclient)
Warning
Client's base URL and router's base URL must be equal
ValueError
from sensei import Client, Manager, Router
client = Client(base_url='https://order-api.com')
manager = Manager(client)
router = Router(host='https://user-api.com', manager=manager)
@router.get('/users/{id_}')
def get_user(id_: int) -> User:
pass
print(get_user(1))
Retrieving¶
There are two ways to retrieve a client.
This returns a client without removing it from Manager
. If required=True
(default is True
) in the Manager
constructor, the error
will be thrown if the client is not set.
manager = Manager()
manager = Manager(sync_client=client, async_client=aclient)
client = manager.get(is_async=False)
aclient = manager.get(is_async=True)
print(client, aclient)
This return client and removes it from Manager
manager = Manager()
manager = Manager(sync_client=client, async_client=aclient)
client = manager.pop(is_async=False)
aclient = manager.pop(is_async=True)
print(client, aclient)
Is empty¶
You can check whether a client is empty:
manager = Manager()
manager = Manager(sync_client=client)
manager.pop()
print(manager.empty()) # Output: True
Rate Limiting¶
Many APIs enforce rate limits to control how frequently clients can make
requests. You can add automatic waiting between requests, based on the period and the maximum number of requests allowed per this
period. This is achieved through a RateLimit
instance
This code is equivalent to 5 requests per second.
from sensei import RateLimit, Router
calls, period = 5, 1
rate_limit = RateLimit(calls, period)
router = Router('https://example-api.com', rate_limit=rate_limit)
The RateLimit
class implements a token bucket rate-limiting
system. Tokens are added at a fixed rate, and each request uses one token.
If tokens run out, Sensei waits until new tokens are available, preventing rate-limit violations.
If a token was consumed, the new one will appear in period / calls
seconds. That is 5 requests per second is equivalent
1 token per 1/5 seconds.
In the following example, the code will be paused for 1 second after each request:
from sensei import RateLimit, Router
calls, period = 1, 1
rate_limit = RateLimit(calls, period)
router = Router('https://example-api.com', rate_limit=rate_limit)
@router.get('/users/{id_}')
def get_user(id_: int) -> User:
pass
for i in range(5):
get_user(i) # (1)!
- Here code will be paused for 1 second after each iteration
If you want to use another rate-limiting system, you can implement the IRateLimit
interface and use it like before.
Namely, you need to implement the following two methods.
from sensei.types import IRateLimit
class CustomLimit(IRateLimit):
async def async_wait_for_slot(self) -> None:
...
def wait_for_slot(self) -> None:
...
Setting Port¶
If you connect to some local API, that allows configuring port, you can make a dynamic URL with {port}
placeholder.
In addition, you can change port
attribute in Router
.
Here is an example:
from sensei import Router
router = Router(host='https://local-api.com:{port}/api/v2', port=3000)
print(router.base_url) # Output: https://local-api.com:3000/api/v2
router.port = 4000
print(router.base_url) # Output: https://local-api.com:4000/api/v2
If {port}
placeholder is not provided, the port will be appended to the end of the URL
from sensei import Router
router = Router(host='https://local-api.com', port=3000)
print(router.base_url) # Output: https://local-api.com:3000
Recap¶
Here’s a recap of working with httpx
clients for efficient HTTP request management:
Sensei’s Manager
and Routing System¶
- The
Manager
provides a single client for multiple requests, managing bothhttpx.Client
andhttpx.AsyncClient
instances. Manager
can store one synchronous and one asynchronous client, ensuring only one of each type is available at any time.- To link the
Manager
to requests, create aRouter
instance that defines a base URL for all endpoints.
Managing Clients with Manager
¶
- Setting Clients: Assign clients to the
Manager
when created or later with.set()
. - Retrieving Clients: Use
.get()
to access clients without removal or.pop()
to retrieve and remove the client fromManager
. - Checking Clients: Use
.empty()
to check if there are no clients inManager
.
Rate Limiting with RateLimit
¶
- APIs often have rate limits, and
Sensei
includes aRateLimit
class to enforce these. - Set calls per second or minute to prevent exceeding rate limits.
- Implement custom rate-limiting by subclassing
IRateLimit
.
Configuring Ports Dynamically¶
- Specify a
{port}
placeholder in the base URL to dynamically adjust the API port as needed withRouter
.
By efficiently managing HTTP clients and rate limits, Sensei
optimizes API interactions, reducing latency and improving performance.