Skip to content

Instantly share code, notes, and snippets.

@Thomascountz
Last active October 25, 2023 13:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Thomascountz/eb1aa85bc08f6c66622053b58980dcbc to your computer and use it in GitHub Desktop.
Save Thomascountz/eb1aa85bc08f6c66622053b58980dcbc to your computer and use it in GitHub Desktop.
AsyncHttpClient is an asyncio-based MicroPython library, designed for asynchronous, concurrent HTTP/HTTPS requests to avoid blocking the execution of other coroutines.

AsyncHttpClient

In the MicroPython ecosystem, the uasyncio library provides the asynchronous I/O framework for developing concurrent applications, but it lacks a native asynchronous HTTP client that can make non-blocking HTTP/HTTPS requests.

Here, I'm sharing my attempt at implementing an HTTP client built specifically for MicroPython that leverages uasyncio to enable non-blocking requests.

Features

  1. Asynchronous and Concurrent: AsyncHttpClient is built upon uasyncio, the asyncio library for MicroPython, allowing multiple HTTP/HTTPS requests to be made concurrently without blocking the execution of other coroutines.

  2. HTTP/HTTPS Support: AsyncHttpClient can handle both HTTP and HTTPS requests, providing flexibility in communicating with different servers.

  3. Timeout and Retries: It supports custom settings for connection and response timeouts, as well as the number of retries in case of request failure.

  4. Robust Error Handling: AsyncHttpClient is designed to robustly handle potential errors and exceptions that might occur during network programming.

  5. User Agent: It includes a User-Agent in the request headers to identify the client.

  6. JSON Parsing: AsyncHttpClient automatically parses JSON responses if the content type is application/json.

Usage

The GET method can be called asynchronously with a URL. The method returns the body of the response as a string or a dictionary if the response is in JSON format.

Example 1: Basic Usage

This is a basic example where we simply fetch a web page:

# Import the necessary modules
import asyncio
from async_http_client import AsyncHttpClient

# Define an asynchronous function to fetch a web page
async def fetch_page():
    # Create an AsyncHttpClient instance
    client = AsyncHttpClient()
    
    # Use the client to send a GET request to a web page
    response = await client.get('https://example.com')
    
    # Print the response
    print(response)

# Run the fetch_page function
asyncio.run(fetch_page())

Example 2: Robust Usage

This example shows a more robust usage where we handle exceptions:

# Import the necessary modules
import asyncio
from async_http_client import AsyncHttpClient

# Define an asynchronous function to fetch a web page
async def fetch_page():
    # Create an AsyncHttpClient instance with custom timeout and retries
    client = AsyncHttpClient(timeout=10, retries=5)
    
    # Use the client to send a GET request to a web page
    try:
        response = await client.get('https://example.com')
        print(response)
    except Exception as e:
        print(f'Failed to fetch page: {e}')

# Run the fetch_page function
asyncio.run(fetch_page())

Example 3: Realistic Usage

This is a realistic example where we use the client to fetch weather data and update a shared state:

# Import the necessary modules
import asyncio
from async_http_client import AsyncHttpClient

# Define a global variable to hold the current temperature
current_temp = None

# Define an asynchronous function to fetch the weather
async def fetch_weather():
    # Create an AsyncHttpClient instance
    client = AsyncHttpClient()
    
    # Fetch the weather data every 10 minutes
    while True:
        try:
            # Send a GET request to the weather API
            response = await client.get('https://api.weatherapi.com/v1/current.json?key=YOUR_KEY&q=YOUR_LOCATION')

            # Update the current temperature
            global current_temp
            current_temp = response['current']['temp_c']

        except Exception as e:
            print(f'Failed to fetch weather: {e}')

        # Wait for 10 minutes before fetching the weather again
        await asyncio.sleep(600)

# Define an asynchronous function to display the current temperature
async def display_temperature():
    while True:
        # Print the current temperature
        print(current_temp)

        # Wait for 1 second before updating the display
        await asyncio.sleep(1)

# Run the fetch_weather and display_temperature functions concurrently
asyncio.run(asyncio.gather(fetch_weather(), display_temperature()))

In this example, the fetch_weather function sends a GET request to a weather API every 10 minutes and updates the current_temp global variable. The display_temperature function displays the current temperature every second. Both functions run concurrently, so the display is updated regularly even while the weather data is being fetched. This is an example of how you might use the AsyncHttpClient class to fetch data from an API and update a shared state in a real-world application.

Changelog

v0.1

  • Initial implementation
  • Introduce get(url) method
  • Untested
# v0.1
import usocket as socket
import ussl
import uasyncio as asyncio
import ujson as json
from utime import ticks_ms, ticks_diff
class AsyncHttpClient:
"""
A class used to represent an Asynchronous HTTP Client
...
Attributes
----------
timeout : int
The maximum time to wait for a response before timing out (default is 5)
retries : int
The maximum number of retries if the request fails (default is 3)
Methods
-------
get(url):
Sends a GET request to the specified url
"""
def __init__(self, timeout=5, retries=3):
"""
Parameters
----------
timeout : int, optional
The maximum time to wait for a response before timing out (default is 5)
retries : int, optional
The maximum number of retries if the request fails (default is 3)
"""
self.timeout = timeout
self.retries = retries
async def _timeout_manager(self, t):
"""Checks if the operation has timed out.
Parameters
----------
t : int
The time when the operation started
Returns
-------
bool
True if the operation has timed out, False otherwise.
"""
if ticks_diff(ticks_ms(), t) > self.timeout * 1000:
return True
return False
async def _connect(self, addr):
"""Opens a non-blocking socket to the specified address.
Parameters
----------
addr : tuple
The address of the host to connect to, as a (host, port) tuple
Returns
-------
socket
The socket object connected to the host.
Raises
------
OSError
If the connection times out
"""
s = socket.socket()
s.setblocking(False)
t = ticks_ms()
while True:
try:
s.connect(addr)
break
except OSError:
pass
await asyncio.sleep(0)
if await self._timeout_manager(t):
raise OSError('Connection timeout')
return s
async def get(self, url):
"""Sends a GET request to the specified url.
The request is sent over a non-blocking SSL socket. The function waits
for the response and returns the body as a string or a JSON object.
Parameters
----------
url : str
The URL to send the GET request to
Returns
-------
str or dict
The body of the response. If the response is JSON, a dictionary is returned.
Raises
------
OSError
If the connection or response times out, or if the connection fails after retries
"""
_, _, host, path = url.split('/', 3)
addr = socket.getaddrinfo(host, 443)[0][-1]
for _ in range(self.retries):
try:
s = await self._connect(addr)
break
except OSError:
pass
else:
raise OSError('Failed to connect after {} retries'.format(self.retries))
s = ussl.wrap_socket(s)
request = 'GET /{} HTTP/1.1\r\nHost: {}\r\nUser-Agent: ESP32\r\nConnection: close\r\n\r\n'.format(path, host)
while True:
try:
s.write(request)
break
except OSError:
pass
await asyncio.sleep(0)
response = b''
t = ticks_ms()
while True:
try:
data = s.read(100)
if data:
response += data
else:
break
except OSError:
pass
await asyncio.sleep(0)
if await self._timeout_manager(t):
s.close()
raise OSError('Response timeout')
s.close()
header, body = response.split(b'\r\n\r\n', 1)
body = body.decode('utf-8')
if 'application/json' in header:
body = json.loads(body)
return body
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment