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
Managerprovides a single client for multiple requests, managing bothhttpx.Clientandhttpx.AsyncClientinstances. Managercan store one synchronous and one asynchronous client, ensuring only one of each type is available at any time.- To link the
Managerto requests, create aRouterinstance that defines a base URL for all endpoints.
Managing Clients with Manager¶
- Setting Clients: Assign clients to the
Managerwhen 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
Senseiincludes aRateLimitclass 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.