Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
anpe03 opened this issue Jan 3, 2025 · 7 comments
Closed

Comments

@anpe03
Copy link

anpe03 commented Jan 3, 2025

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.

@craigbarratt
Copy link
Member

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

@anpe03
Copy link
Author

anpe03 commented Jan 4, 2025

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

@anpe03
Copy link
Author

anpe03 commented Jan 4, 2025

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

@dmamelin
Copy link
Contributor

dmamelin commented Jan 5, 2025

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.

@dmamelin
Copy link
Contributor

dmamelin commented Jan 5, 2025

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.

@dmamelin
Copy link
Contributor

dmamelin commented Jan 5, 2025

#643 the same

@craigbarratt
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants