custom-components/pyscript

object str can't be used in 'await' expression in PyScript Async Functions

Closed this issue · 7 comments

I am encountering a persistent error in PyScript when trying to perform an asynchronous HTTP request using aiohttp. Despite adhering to the asyncio guidelines and leveraging Home Assistant’s async_get_clientsession, the following error occurs consistently. This error arises regardless of the specific HTTP client method (response.text(), response.json(), or others), suggesting a deeper issue with how PyScript interprets awaitable objects.
1. The same code works flawlessly in a standalone Python environment.
2. Using task.executor to offload synchronous requests calls also fails due to PyScript restrictions.
3. The issue persists across various attempts to simplify the logic.

The issue seems to stem from PyScript misinterpreting certain objects (e.g., str or dict) as awaitable. This is not typical behavior for asyncio or aiohttp and may indicate a PyScript-specific bug.

Please include your code. Try removing the awaits in your pyscript code.

import aiohttp

Base URLs

AUTH_URL = "https://auth.quandify.com/"
BASE_URL = "https://api.quandify.com"

Replace with your actual credentials

ACCOUNT_ID = # Your account ID
PASSWORD = # Your password
ORGANIZATION_ID = # Your organization ID

Time range (example: Unix timestamps)

FROM_TIMESTAMP = 1700000000
TO_TIMESTAMP = 1700600000

async def authenticate(account_id, password):
"""Authenticate with the Quandify API and retrieve an ID token."""
log.info("Starting authentication...")
payload = {"account_id": account_id, "password": password}
headers = {"Content-Type": "application/json"}

async with aiohttp.ClientSession() as session:
    try:
        log.info("Sending authentication request...")
        response = await session.post(AUTH_URL, json=payload, headers=headers)
        log.info(f"Authentication response status: {response.status}")

        # Validate HTTP status
        if response.status != 200:
            error_message = await response.text()
            log.error(f"Authentication failed with error: {error_message}")
            return None

        # Parse the response JSON
        data = await response.json()
        log.info(f"Authentication response data: {data}")

        # Validate response structure
        if isinstance(data, dict) and "id_token" in data:
            return data["id_token"]
        else:
            log.error(f"Invalid authentication response structure: {data}")
            return None

    except Exception as e:
        log.error(f"Exception during authentication: {e}")
        return None

@service
async def authenticate_quandify(account_id=None, password=None):
"""PyScript service to perform authentication and save the token."""
log.info("Starting authenticate_quandify service...")

# Use provided credentials or defaults
account_id = account_id or ACCOUNT_ID
password = password or PASSWORD

# Call the authentication function
id_token = await authenticate(account_id, password)
if id_token:
    log.info(f"Authentication successful. Token: {id_token}")
    state.set("sensor.quandify_auth_token", id_token, {"friendly_name": "Quandify Auth Token"})
else:
    log.error("Authentication service failed. No token received.")

async def get_aggregated_data(id_token, organization_id, from_ts, to_ts):
"""Fetch aggregated consumption data (totalVolume)."""
log.info("Fetching aggregated data...")
url = f"{BASE_URL}/organization/{organization_id}/nodes/detailed-consumption"
params = {"from": from_ts, "to": to_ts, "truncate": "day"}
headers = {"Authorization": f"Bearer {id_token}", "Content-Type": "application/json"}

async with aiohttp.ClientSession() as session:
    try:
        response = await session.get(url, headers=headers, params=params)
        log.info(f"Aggregated data response status: {response.status}")

        # Validate HTTP status
        if response.status != 200:
            error_message = await response.text()
            log.error(f"Failed to fetch aggregated data: {error_message}")
            return None

        # Parse the response JSON
        data = await response.json()
        log.info(f"Aggregated data response: {data}")

        # Validate response structure
        if isinstance(data, dict) and "aggregate" in data:
            return data["aggregate"]["total"]["totalVolume"]
        else:
            log.error(f"Unexpected data structure: {data}")
            return None

    except Exception as e:
        log.error(f"Exception during data fetch: {e}")
        return None

@service
async def fetch_quandify_data():
"""Fetch and log the total consumption volume."""
log.info("Starting fetch_quandify_data service...")

try:
    # Step 1: Retrieve the authentication token from the state
    token_entity = state.get("sensor.quandify_auth_token")
    if not token_entity:
        log.error("No auth token found. Run authenticate_quandify first.")
        return

    id_token = token_entity

    # Step 2: Fetch the aggregated data
    total_volume = await get_aggregated_data(id_token, ORGANIZATION_ID, FROM_TIMESTAMP, TO_TIMESTAMP)
    if total_volume is not None:
        log.info(f"Total Volume: {total_volume}")
        state.set("sensor.total_volume", total_volume, {"unit_of_measurement": "m³", "friendly_name": "Total Volume"})
    else:
        log.error("Failed to fetch total volume.")

except Exception as e:
    log.error(f"Unhandled exception in fetch_quandify_data: {e}")

Gives error log:
This error originated from a custom integration.

Logger: custom_components.pyscript.file.Quandify.authenticate_quandify
Source: custom_components/pyscript/eval.py:1982
integration: Pyscript Python scripting (documentation, issues)
First occurred: 8:34:46 PM (15 occurrences)
Last logged: 8:52:31 PM

Error during authentication: object dict can't be used in 'await' expression
Authentication service failed.
Authentication service failed. No token received.
Unhandled exception during authentication: object dict can't be used in 'await' expression
Exception during authentication: object dict can't be used in 'await' expression

import aiohttp

Base URL for authentication

AUTH_URL = "https://auth.quandify.com/"

Replace with your actual credentials

ACCOUNT_ID = # Your account ID
PASSWORD = # Your password

@service
async def authenticate_quandify_debug():
"""Debug authentication service to pinpoint 'await' misuse."""
log.info("Starting debug authentication service...")

# Prepare payload and headers
payload = {"account_id": ACCOUNT_ID, "password": PASSWORD}
headers = {"Content-Type": "application/json"}

try:
    # Log the payload and headers
    log.info(f"Payload: {payload}")
    log.info(f"Headers: {headers}")

    # Send the POST request
    async with aiohttp.ClientSession() as session:
        log.info("Sending POST request to authenticate...")
        response = await session.post(AUTH_URL, json=payload, headers=headers)

        # Log response status
        log.info(f"Response status: {response.status}")

        # Check response status
        if response.status != 200:
            error_message = await response.text()
            log.error(f"Authentication failed: {error_message}")
            return

        # Attempt to parse JSON response
        log.info("Attempting to parse JSON response...")
        raw_data = await response.text()
        log.info(f"Raw response text: {raw_data}")
        data = await response.json()

        # Log the parsed data and its type
        log.info(f"Parsed response data: {data} (type: {type(data)})")

        # Validate response structure
        if not isinstance(data, dict):
            log.error(f"Unexpected response type: {type(data)}")
            return
        if "id_token" not in data:
            log.error(f"Missing 'id_token' in response: {data}")
            return

        # Log and save the token
        id_token = data["id_token"]
        log.info(f"Authentication successful. Token: {id_token}")
        state.set("sensor.quandify_auth_token", id_token, {"friendly_name": "Quandify Auth Token"})

except Exception as e:
    # Log the full exception
    log.error(f"Exception during authentication: {e}")

This error originated from a custom integration.

Logger: custom_components.pyscript.file.Quandify.authenticate_quandify_debug
Source: custom_components/pyscript/eval.py:1982
integration: Pyscript Python scripting (documentation, issues)
First occurred: 8:58:59 PM (1 occurrences)
Last logged: 8:58:59 PM

Exception during authentication: object str can't be used in 'await' expression

Simple code to reproduce:

def aio_test():
    async with aiohttp.ClientSession() as session:
        async with session.get("https://google.com") as response:
            if response.status == 200:
                return await response.text()

Removing await resolves the problem.

The issue is in AstEval.ast_await:

async def ast_await(self, arg):
    """Evaluate await expr."""
    coro = await self.aeval(arg.value)
    if coro:
        return await coro
    return None

In my local setup, I fixed it like this, and everything works perfectly:

async def ast_await(self, arg):
    """Evaluate await expr."""
    return await self.aeval(arg.value)

@craigbarratt, can AstEval.aeval actually return a coroutine?
If it can, this might be correct:

async def ast_await(self, arg):
    """Evaluate await expr."""
    val = await self.aeval(arg.value)
    if asyncio.iscoroutinefunction(val):
        return await val
    else:
        return val

No PR since I’m unsure which is correct.

#643 the same

This shoud be fixed with #688 (thanks!), and a subsequent commit 8ee7926.