Pyscript - new integration for easy and powerful Python scripting

Exception in <file.wiki.search_wikipedia> line 6: summary = task.executor(wikipedia.summary, kwargs=funcArgs) ^ TypeError: summary() got an unexpected keyword argument 'kwargs'

Thanks for your persistence. Still not working :frowning: I feel very confident that this will work if somehow we can get the variables passed.

Sorry it took me a bit longer, but I got it working!

@service(supports_response="optional")
def search_wikipedia(searchterm=None, return_response=True):
    """yaml
    name: Search Wikipedia
    description: hello_world service example using pyscript.
    fields:
        searchterm:
            description: What to search for?
            example: Madonna
            required: true
            selector:
                text:
    """
    funcArgs = {"sentences": 2, "auto_suggest": False}
    summary = task.executor(wikipedia.summary, searchterm, **funcArgs)
    response_variable = { "summary": summary }
    return response_variable

I am not fully sure why query cannot be in the **kwargs argument of task.executor honestly. But calling this service from Home Assistant returned a summary of Madonna for me.

1 Like

You are the champion of champions my friend! I cannot thank you enough. This opens so many possibilities for me and the users of my project. We will be able to extend this to other APIs for grabbing data.

If interested, check out the project wiki and/or Discord:

Itā€™s still in beta but really close to release.

1 Like

Anyone still seeing an error with state.persist ?

Cross-posting from another thread: State.persist in pyscript not working - #8 by danhiking

Basically: state.persist('pyscript.foo', default_value=None) still results in NameError: name 'pyscript.foo' is not defined when I try to access pyscript.foo.

Any idea what might have changed, or what precondition is required for the state.persist mechanism to work?

@Slalamander Iā€™m back to bother you one more time. Iā€™m wanting to pull data via API and am not sure how to proceed.

import http.client
@service(supports_response="optional")
def get_dadjoke(return_response=True):
    """yaml
    name: Get Dad joke
    description: Gets a random dad joke from api
    """
    conn = http.client.HTTPSConnection("dad-jokes-by-api-ninjas.p.rapidapi.com")

    headers = {
        'x-rapidapi-key': "get your api key at rapidapi",
        'x-rapidapi-host': "dad-jokes-by-api-ninjas.p.rapidapi.com"
    }

    conn.request("GET", "/v1/dadjokes", headers=headers)

    res = conn.getresponse()
    response_variable = res.read()
    return response_variable

Iā€™m basing this on a sample script from rapidapi which works in regular python (I have a valid api key) and trying to mix in what you gave me for the wikipedia part.

Iā€™m a bit confused on the task.executor portion. I know just a bit about python so excuse me. Do I need to use the task.executor for all lines that use that ā€˜connā€™ variable or certain ones, or none? Seems like a pretty simple script but not so much for me. I think once I get one of these working Iā€™ll be able to follow suit for others and this will open up a lot of things. Thanks for your time and help!

The task.executor is there for functions that block the event loop. Iā€™m not fully sure how the methods for conn function, but I think only conn.request should be run in the executor, perhaps though also getresponse.

However, I think it may be easier to use the requests module. I believe that is included by default in Home Assistant installations. The code would become something like the following I think.

import requests
@service(supports_response="optional")
def get_dadjoke(return_response=True):
    """yaml
    name: Get Dad joke
    description: Gets a random dad joke from api
    """
    headers = {
        'x-rapidapi-key': "get your api key at rapidapi",
        'x-rapidapi-host': "dad-jokes-by-api-ninjas.p.rapidapi.com"
    }

    r = task.executor(requests.get, url, headers=headers)

    response_variable = r.json()
    return response_variable

If you need to pass an authentication argument, you can add auth=auth.

I defined url like this:

url = "https://dad-jokes-by-api-ninjas.p.rapidapi.com/v1/dadjokes"

ran the script and got this:

websocket_api script: Error executing script. Error for call_service at pos 1: Failed to process the returned service response data, expected a dictionary, but got <class 'NoneType'>
websocket_api script: Error executing script. Error for call_service at pos 1: Failed to process the returned service response data, expected a dictionary, but got <class 'list'>

I wish I was better at troubleshooting. Is this failing to connect and therefore is returning the dictionary error because the response is empty?

EDIT By the way, I tried changing from https to http and got this:

message: Please use HTTPS protocol

as my response so it appears to at least be contacting the server. Wondering if it is getting those headers or not. Again, Iā€™m a novice here. Thanks again for the help.

EDIT2 This is an example of what the python script (not pyscript) outputs for reference:

[{"joke": "What do you put on a lonely grilled cheese sandwich? Provolone, but only if you have it\u2019s parmesan."}]

Ah thatā€™s an easy fix actually, itā€™s just a dict put into a list, and Home Assistant only accepts dicts as response variables. I didnā€™t notice the json() function from requests always returns a list with json data (See the docs here). Assuming resp is the output of the python script, you should be able to return it to Home Assistant as such:

resp = r.json()
return resp[0]

This assumes youā€™re only getting one joke at a time. If itā€™s possible to get more jokes, you could also do the following:

resp = r.json()
jokes = {"jokes": resp}
return jokes

This way, the data under "jokes" will always be a list, so youā€™d have to extract each joke individually in Home Assistant, even if it is just the one. I donā€™t know of the top of my head how to do that in Jinja2, so let me know if you run into trouble with that.

EDIT:
Come to think of it, your first error shows a Nonetype returning. Iā€™m not fully sure what the case is with that. It may be a good idea to check for statuscodes, in case you get errors. See here.

if r.status_code == requests.codes.ok:
    ##Code here with how you want to return the response
else:
   resp = {"error": r.status_code, "data": r.json()}
   return resp

(You donā€™t need to return errors, but it may be useful while debugging).

Once again, thank you for this. I will be able to use this method to pull lots of different data from api. You were right, the fix was very easy and I guess the error I provided may have included something from a previous run.

This is all that was needed:

resp = r.json()
return resp[0]

So I thought Iā€™d give myself a little more practice and modify the wiki search to use a different method:

import requests
@service(supports_response="optional")
def search_wikipedia_new(searchterm=None, return_response=True):
    """yaml
    name: Search Wikipedia
    description: hello_world service example using pyscript.
    fields:
        searchterm:
            description: What to search for?
            example: Madonna
            required: true
            selector:
                text:
    """
    url = "https://en.wikipedia.org/api/rest_v1/page/summary/Madonna?redirect=true"
    r = task.executor(requests.get, url)
  
    if r.status_code == requests.codes.ok:
        response_variable = r.json()
        return response_variable[0]
    else:
        response_variable = {"error": r.status_code, "data": r.json()}
        return response_variable

I feel like Iā€™m not changing much short of the url and not using the headers but I am once again getting the error:

Failed to process the returned service response data, expected a dictionary, but got <class 'NoneType'>.

Is there an easy way to view what is being received? Itā€™s super hard to troubleshoot this without being able to somehow get data out short of the return variable route.

As a test I wrote this in regular python and it works as expected:

import requests

url = "https://en.wikipedia.org/api/rest_v1/page/summary/Madonna?redirect=true"
r = requests.get(url)

if r.status_code == requests.codes.ok:
    response_variable = r.json()
    print (response_variable)
else:
    response_variable = {"error": r.status_code, "data": r.json()}
    print (response_variable)

Again, thank you in teaching me this stuff. It will pay off for sure just frustrating with the errors at the moment.

Debugging pyscripts is a real pain yeah :sweat_smile: I tend to use print statements myself and look them up in the log. However they accumulate and different log messages tend to be filed under the same header in the Home Assistant logs :sweat_smile:

You could try setting up the Jupyter Notebook environment, as that allows real time code execution I believe, so itā€™d be easier to check the print statements in the notebook. However I havenā€™t used it much, so I canā€™t really help much with setting it up. I believe the pyscript docs explain it quite a bit tho!

Those print statements sounds like my kind of troubleshooting as Iā€™m definitely familiar with that approach. I didnā€™t realize that was an option. Iā€™m guessing I need to set logging to a certain level to see that?

I tried the Jupyter Notebook approach but itā€™s not easy with my environment. Iā€™m running on Docker and it is a struggle for me to figure out how to get that in place. I did set up a VM and was able to install JN but now having issues getting that going. Struggles.

Jep, see Reference ā€” hacs-pyscript 1.5.0 documentation

You can also use log.warning("something happened") which should probably log it in HA without needing additional configuration (I believe warning is the default level for integrations). For the print statement, the loglevel should be debug.

import requests
@service(supports_response="optional")
def search_wikipedia_new(searchterm=None, return_response=True):
    """yaml
    name: Search Wikipedia New
    description: hello_world service example using pyscript.
    fields:
        searchterm:
            description: What to search for?
            example: Madonna
            required: true
            selector:
                text:
    """
    url = "https://en.wikipedia.org/api/rest_v1/page/summary/" + searchterm.replace(" ","_") + "?redirect=true"
    r = task.executor(requests.get, url)
  
    if r.status_code == requests.codes.ok:
        wiki_data = r.json()
        title = wiki_data['title']
        thumbnail = wiki_data['thumbnail']['source']
        extract= wiki_data['extract']
        response_variable = {"title": title,"thumbnail": thumbnail, "extract": extract}
        return response_variable
    else:
        response_variable = {"error": r.status_code, "data": r.json()}
        return response_variable

This is the push that I needed! Managed to figure it out once I could ā€˜seeā€™ what was going on. Again, thank you @Slalamander ! Hopefully this is all that Iā€™ll need to really hit the ground running.

1 Like

I use log.info('Here is some string to help with debugging') in the .py file and then use the Log Viewer add-on to view them. Itā€™s instant and works well for me.

Out of curiosity, where do you see the print statements in HA?

P.S. PyScript has been a game changer for me. I struggle coding in YAML. So THANK YOU to the developers and contributes to this project!

I run Home Assistant container, so I canā€™t use any add-ons.

The print statements show up in the full logs, which you can access via your-url/config/logs and then scroll down to where it says ā€œload full logsā€. Since itā€™s all the logs itā€™s not as clear though as the base log view.
Also it requires reloading every time you print something new, so I think the add-on is a much better option for anyone that has it available :slight_smile:

Ah that makes sense. Iā€™m currently using HA in a VM on an Unraid server.

Youā€™re absolutely right. I was reading the PyScript documentation and I learned that print(str) is the same as log.debug(str).

@Slalamander and everyone. I am wanting to use pyscript to create temporary timers instead of creating a bunch of helpers. I am writing to see if this is even a good idea.

Hereā€™s what I would like to do:

  • create a temporary timer with a given duration in seconds and also have a name
  • the timer would run down or a time based on the duration would be set
  • on expiring or that time arriving pyscript would create an event that would state the timer has expired and provide the name given when created

That would give basic functionality. Extended functionality would include these:

  • ability to cancel the timer
  • ability to delete the timer before expiring
  • ability to report the remaining time on query

Of these, the most important would be time remaining. I understand that these extended functionality could be difficult to accomplish. Basic functionality would at least get things going though.

Any thoughts?

I am not sure how well itā€™d work, but my best bet would be using asyncio.sleep to set the timers. Iā€™m not sure how you would be able to report on the time remaining though, without setting up a sensor or something.

Anyhow, what Iā€™d attempt would be making a service that creates a new asyncio task. Determine the time it should finish at, and put that in a global variable or a home assistant sensor keeping track of set timers (Like, a dict with task names and their end times). Tasks can be cancelled (And thus deleted) relatively simply, though in that case there is not much difference between cancelling and deleting. The tasks can all have a callback that publishes the event.

I havenā€™t combined asyncio with pyscript, however it should be able to play nicely with it since Home Assistant is also asynchronous (Itā€™s also why pyscript requires some functions to called using the executor statement).

Scrolling up I found this reply: Pyscript - new integration for easy and powerful Python scripting - #317 by Slalamander

Which is basically what youā€™re trying to do now, but with names, if Iā€™m correct right? If so, you can put the event.fire either in a seperate function, or add a field to the function right now that allows you to set the name, and incorporate that in the event.

Idk if I already linked this website, but I really like it. Itā€™s clear and has a ton of examples: Start Here - Super Fast Python
I think going through step 2 of the asyncio stuff should be enough to build the timers (provided my idea actually works)

Thanks for your continued help. Iā€™ve been so tied up with the project and doing a million different things I forgot I had even brought this up and that you had replied.

I read through the two sections you mentioned and have a better understanding. Really well written. Thanks for the link.

So this brings me back to your script. I can call the starttimer with duration and I believe it is running as expected. The issue comes in when it is time to fire the event. I am getting this error:

Exception in <file.saltimer.sal_timer> line 7: event.fire('viewassist_timer_expired', action=action, id=id, duration=duration) ^ TypeError: a coroutine was expected, got None

From what I am reading from the link you mentioned, doesnā€™t this been to be awaited as well?