Python
The Python runtime is in BETA.
The Python runtime allows you to run Python code in AutoKitteh.
The minimal Python version we support is 3.11.
In order to run Python workflow, Python must be installed on the machine running the code - where the ak
command is installed.
ak
will use the first Python it finds in the PATH
.
Example
So you can jump right ahead.
Write the following two files in a directory.
---
version: v1
project:
name: py_simple
connections:
- name: http_trigger
integration: http
triggers:
- name: post
connection: http_trigger
event_type: post
call: simple.py:greet
vars:
- name: USER
value: Garfield
from os import getenv
import json
HOME = getenv('HOME') # From environment
USER = getenv('USER') # From "vars" section in the manifest
def greet(event):
print(f'INFO: simple: HOME: {HOME}')
print(f'INFO: simple: USER: {USER}')
print(f'INFO: simple: event: {event!r}')
body = event['data']['body']
print(f'BODY: {body!r}')
request = json.loads(body)
print(f'REQUEST: {request!r}')
Start AutoKitteh
ak up --mode dev
Deploy (in the directory where you have the above files
ak deploy -m ./autokitteh.yaml -d .
Now you can run the workflow:
curl -i -X POST -d '{"user": "joe", "event": "login"}' http://localhost:9980/http/py_simple/
Any output from Python to standard output (sys.stdout
) or standard error (sys.stderr
) is captured in the session logs.
Which means both print
and calls to the logging
module.
Use the session log
command to view:
ak session log --prints-only
Overview
The Python runtime can receive events from configured connection triggers. It cannot use the connection for outgoing messages, you will need to use a Python library to do that.
Handler Functions
Each Python handler is a function that receives a single parameter called event
.
The event has several fields:
- data: This is the "raw" event sent by the trigger. The content of
data
depends on the trigger's integration.
The event is a dict, but you can also use attribute access (.
) with it.
Both of the following lines are valid:
body = event['data']['body']
body = event.data.body
You cannot set event values with attributes. The following is valid:
event['data']['user'] = 'Garfield'
The following will raise an exception:
event.data.user = 'Garfield'
:::
Module Level Function Calls
Functions that are called during module import are executed as regular functions (and not activities). Make sure that these function are deterministic, otherwise a re-run of your workflow can yield different results.
A deterministic function is a function that always returns the same results if given the same input values.
For example:
from os import getenv
api_key = getenv('API_KEY')
getenv
will not run as activity.
This is intentional and useful for setting up connections and other global state.
Third Party Dependencies
AutoKitteh creates a virtual environment for the system Python.
This will be the first python3
or python
found in your PATH
environment variable.
The virtual environment is created once on the first time you deploy a Python workflow. This will cause the first time you deploy a Python workflow to take a while. Next deployments will be faster.
Once the virtual environment is created, AutoKitteh will use the python executable inside it.
To install packages at the virtual environment, see here.
You can override which Python executable to use by setting the AK_WORKER_PYTHON
environment variable.
If you set this environment variable, AutoKitteh will this this Python without creating a virtual environment.
If you want to create your own virtual environment, see here. To see how you can install other version of Python, see pyenv.
The virtual environment is preloaded with the following packages:
# Integrations
atlassian-python-api ~= 3.41
boto3 ~= 1.34
google-api-python-client ~= 2.122
google-auth-httplib2 ~= 0.2
google-auth-oauthlib ~= 1.2
jira ~= 3.8
openai ~= 1.14
PyGithub ~= 2.2
redis ~= 5.0
slack-sdk ~= 3.27
twilio ~= 9.0
# AutoKitteh SDK
autokitteh ~= 0.2
# General
beautifulsoup4 ~= 4.12
PyYAML ~= 6.0
requests ~= 2.31
See the update list here.
Installing Python Packages
If you want to install other packages, first find out where the virtual environment is.
ak config where
Say the "Data home directory" is /home/ak/.local/share/autokitteh
,
then the virtual environment is at /home/ak/.local/share/autokitteh/venv
.
To install a package (e.g. pandas), run Python from the virtual environment:
/home/ak/.local/share/autokitteh/venv/bin/python -m pip install pandas~=2.2
Local Development
Setting Up Your IDE
If you want to debug your code and get autocompletion, you need to tell your IDE to use the Python from the autokitteh virtual environment. This way, your code will run in the same environment that AutoKitteh uses to run it.
If you don't have AutoKitteh virtual environment, you can create a virtual environment yourself.
Download pyproject.toml to your machine. The in a terminal, run:
python -m virtualenv venv
venv/bin/python -m pip install .[all]
This will create a virtual environment called venv
, point your IDE Python to /path/to/venv/bin/python
.
Visual Studio Code
Copy the interpreter path (see above) to the clipboard.
Open the command pallet and write Python: Select Interpreter
and paste the interpreter path.
For more information, see the official Visual Studio Code documentation.
Also, don't forget to install the autokitteh extension so you'll be able to install and manage workflows.
PyCharm
In Settings
pick your project and then Python Interpreter
.
Click on Add interpreter
on the top right and select Add Local Interpreter
.
Click on System Interpreter
from the list on the left and then enter the interpreter path from autokitteh virtual environment (see above).
For more information, see the official PyCharm documentation.
Isolated Handler Functions
Handler functions are the functions you define in the manifest, they are called by AutoKitteh on new events. Handler function are regular Python functions, and you can debug them like you debug other Python code.
If your handler function is using vars or secrets, you need to set the environment before importing the handler module. See Working with Secrets on how to name your environment variables.
For example, say you have a handler.py
with an HTTP on_event
handler function:
def on_event(event):
print("BODY:", event.data.body.decode())
Then you can run the handler function like this:
from autokitteh import AttrDict
import handler
event = AttrDict({"data": {"body": "hello".encode()}})
handler.on_event(event)
AutoKitteh events are an instances of AttrDict
, this is why we create event
above in such manner.
python -m pip install autokitteh
.
You can see the API documentation here.
The autokitteh
module also contains common operation for connection to various services.
Debugging Workflows
Before you run ak
locally, set up any environment variables for secrets/vars that are required by your workflow.
export TOKEN=s3cr3t
Another option is to use the ak var set
command instead of setting environment variables.
You need to run this command after starting ak
Then run
ak up --mode dev
The easiest way to run a workflow locally is to add an HTTP trigger.
version: v1
project:
name: hello
connections:
- name: http_trigger
integration: http
token: event
triggers:
- name: events
connection: http_trigger
event_type: post
entrypoint: handler.py:on_event
Next, deploy your workflow:
ak deploy --manifest ./autokitteh.yaml --file handler.py
And now you can trigger your code:
curl -d hello http://localhost:9980/http/hello/events
You can view the print output using the ak session log
command:
ak session log --prints-only
Every time you change the handler code, you need to re-deploy the workflow.
Limitations
We try to lift these limitations as we progress, but currently your workflow will fail if you don't follow them.
Function Arguments and Return Values Must Be Pickleable
We use pickle to pass function arguments back to AutoKitteh to run as an activity. See What can be pickled and unpickled? for supported types. Most notably, the following can't be pickled:
- lambda
- dynamically generate functions (notably
os.environ.get
)
import db
def handler(event):
mapper = lambda n: n.lower()
db.apply(mapper) # BAD
import db
def mapper(n):
return n.lower()
def handler(event):
db.apply(mapper) # GOOD
Using the autokitteh.activity
Decorator
The autokitteh.activity
decorator allow you to mark a function so that it'll always run as activity.
This allows you to run function with arguments or return values that are not compatible with pickle
.
The autokitteh
module is installed to the default AutoKitteh virtual environment.
If you provide your own Python by setting the AK_WORKER_PYTHON
environment variable,
you will need to install the autokitteh
module.
python -m pip install autokitteh
.
You can see the API documentation here.
The autokitteh
module also contains common operation for connection to various services.
Say you have the following code in your handler:
import json
from urllib.request import urlopen
def handler(event):
login = event['login']
url = f'https://api.github.com/users/{login}'
with urlopen(url) as fp:
resp = json.load(fp)
print('user name:', resp['name'])
If you try to run this handler it will fail since the result of urlopen
cannot be pickled.
What you can do is move the code into a function marked as activity:
import json
from urllib.request import urlopen
import autokitteh
def handler(event):
login = event['login']
info = user_info(login)
print('user name:', info['name'])
@autokitteh.activity
def user_info(login):
url = f'https://api.github.com/users/{login}'
with urlopen(url) as fp:
resp = json.load(fp)
return resp
All the code in user_info
will run in a single activity.
Since user_info
accepts a str
and returns a dict
it can run as activity.
Async Functions Are Not Supported
Currently we don't support async functions (including await
).
If you have to use async functions, you can try running your own I/O loop, for example:
import asyncio
from threading import Thread
loop = asyncio.new_event_loop()
Thread(target=loop.run_forever, daemon=True).start()
def handler(event):
log_event(event)
@autokitteh.activity
def log_event(event):
coro = async_log_event(event)
fut = asyncio.run_coroutine_threadsafe(coro, loop)
asyncio.wait([fut], timeout=3)
async def async_log_event(event):
await logger.log(event)
How It Works
AutoKitteh will first load your code and instrument every function call that is external to your module. Each instrumented call will be converted to an activity.
Working with Secrets
AutoKitteh has a secret store. These secrets are exposed to Python via the environment.
For example, if you set a token:
$ ak var set --secret --env env_01hyg78h31e3yahn2dt4mf9nzz TOKEN s3cr3t
To get the list of environments (for --env
), use ak env list
.
Then in your python code you can write the following to access it:
from os import getenv
def handler(event):
token = getenv('TOKEN')
if not token:
raise ValueError('missing `TOKEN` environment variable')
# Use token...
If you need a secret in a file (e.g. Google's service account keys),
you can set the secret value to be the file's contents.
Download the file (e.g. to /tmp/credentials.json
), and run these commands:
$ ak var set --secret --env env_01hyg78h31e3yahn2dt4mf9nzz GOOGLE_CREDS_DATA < /tmp/credentials.json
$ shred -u /tmp/credentials.json # Delete local file, optional but recommended
Then in your script you can write the file and set GOOGLE_APPLICATION_CREDENTIALS
environment variable:
import os
def ensure_google_credentials():
env_key = 'GOOGLE_APPLICATION_CREDENTIALS'
if os.getenv(env_key):
return
creds_file = 'credentials.json'
data = os.getenv(env_key)
with open(creds_file, 'w') as out:
out.write(data)
os.putenv(env_key, creds_file)
def commit_handler(event):
ensure_google_credentials()
... # Rest of handler code
Connection Secrets
Connections have their own secrets. You can access them via the environment as well. The environment variable name is the connection name followed by two underscores and the secret name.
For example: Say you defined an HTTP connection called http_trigger
and set its bearer token to s3cr3t
.
You can look at the connection web page (under /connections
) to see the environment variable name under the Vars
section.
In this case, it'll be auth
. So in your workflow code you can write:
token = getenv('http_trigger__auth')
print(f'HTTP token: {token!r}')
Note that the value of token is Bearer s3cr3t
, not s3cr3t
.