add function implementation

This commit is contained in:
Gergő Jedlicska
2023-08-25 15:25:22 +02:00
parent e2914169e8
commit bdb54cde50
5 changed files with 313 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
name: 'build and deploy Speckle functions'
on:
push:
branches:
- main
tags:
- '*'
jobs:
publish-automate-function-version: # make sure the action works on a clean machine without building
env:
FUNCTION_SCHEMA_FILE_NAME: functionSchema.json
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.4.0
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
version: 1.3.2
virtualenvs-create: false
virtualenvs-in-project: false
installer-parallel: true
- name: Restore dependencies
run: poetry install --no-root
- name: Extract functionInputSchema
id: extract_schema
run: |
python schema_generation.py ${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}
- name: Speckle Automate Function - Build and Publish
uses: specklesystems/speckle-automate-github-composite-action@0.4.2
with:
speckle_automate_url: 'https://automate.speckle.dev'
speckle_token: ${{ secrets.SPECKLE_FUNCTION_TOKEN }}
speckle_function_id: ${{ secrets.SPECKLE_FUNCTION_ID }}
speckle_function_input_schema_file_path: ${{ env.FUNCTION_SCHEMA_FILE_NAME }}
speckle_function_command: 'python main.py'
+1
View File
@@ -0,0 +1 @@
example speckle automate function with a jupyter notebook
+207
View File
@@ -0,0 +1,207 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Speckle Automate notebook example\n",
"\n",
"This is an example jupyter notebook, that will be executed by speckle automate\n",
"\n",
"## Inputs\n",
"\n",
"The first two block block are defining two input data structures, `SpeckleProjectData` and `FunctionInputs`.\n",
"The project data class is following the data schema of Automate, you shouldn't modify it.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"from pydantic import BaseModel, ConfigDict\n",
"from stringcase import camelcase\n",
"\n",
"\n",
"class SpeckleProjectData(BaseModel):\n",
" \"\"\"\n",
" Only modify this to follow upstream changes in automate!\n",
"\n",
" Values of the project / model that triggered the run of this function.\n",
" \"\"\"\n",
"\n",
" project_id: str\n",
" model_id: str\n",
" version_id: str\n",
" speckle_server_url: str\n",
"\n",
" model_config = ConfigDict(alias_generator=camelcase, protected_namespaces=())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Parameters\n",
"\n",
"These values are supplied to the notebook, when its being run by automate.\n",
"\n",
"WANING: You really shouldn't modify this block, unless its following upstream changes from automate.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"tags": [
"parameters"
]
},
"outputs": [],
"source": [
"speckle_project_data = \"\"\n",
"function_inputs = \"\"\n",
"token_env_var = \"\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Function inputs\n",
"\n",
"The `FunctionInputs` class defines the schema for the values, this function requires to run.\n",
"These values will be provided by the users of the function.\n",
"\n",
"Automate uses the Json Schema, generated from the class, to validate user provided values.\n",
"\n",
"The schema block is tagget with the `function_input` tag, do not remove that!\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"tags": [
"function_inputs"
]
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\"description\": \"User defined inputs to the function.\", \"properties\": {\"speckleType\": {\"title\": \"Speckletype\", \"type\": \"string\"}}, \"required\": [\"speckleType\"], \"title\": \"FunctionInputs\", \"type\": \"object\"}\n"
]
}
],
"source": [
"from pydantic import BaseModel, ConfigDict\n",
"from stringcase import camelcase\n",
"import json\n",
"\n",
"\n",
"class FunctionInputs(BaseModel):\n",
" \"\"\"User defined inputs to the function.\"\"\"\n",
"\n",
" speckle_type: str\n",
"\n",
" model_config = ConfigDict(alias_generator=camelcase)\n",
"\n",
"\n",
"print(json.dumps(FunctionInputs.model_json_schema()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Function logic\n",
"\n",
"in this block we're defining the actual business logic of the function.\n",
"\n",
"By all means modify this block but do keep the function signature compatible with the executing main function.\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"from specklepy.api.client import SpeckleClient\n",
"from specklepy.objects import Base\n",
"from specklepy.transports.memory import MemoryTransport\n",
"from specklepy.transports.server import ServerTransport\n",
"from specklepy.api.operations import receive\n",
"from typing import Iterable\n",
"\n",
"\n",
"def flatten_base(base: Base) -> Iterable[Base]:\n",
" if hasattr(base, \"elements\"):\n",
" for element in base.elements:\n",
" yield from flatten_base(element)\n",
" yield base\n",
"\n",
"\n",
"def automate_function(\n",
" project_data: SpeckleProjectData,\n",
" function_inputs: FunctionInputs,\n",
" speckle_token: str,\n",
"):\n",
" client = SpeckleClient(project_data.speckle_server_url)\n",
" client.authenticate_with_token(speckle_token)\n",
" commit = client.commit.get(project_data.project_id, project_data.version_id)\n",
" branch = client.branch.get(project_data.project_id, project_data.model_id, 1)\n",
"\n",
" memory_transport = MemoryTransport()\n",
" server_transport = ServerTransport(project_data.project_id, client)\n",
" base = receive(commit.referencedObject, server_transport, memory_transport)\n",
"\n",
" matching_types = [\n",
" b for b in flatten_base(base) if b.speckle_type == function_inputs.speckle_type\n",
" ]"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"\n",
"def main():\n",
" inputs = FunctionInputs.model_validate_json(function_inputs)\n",
" project_data = SpeckleProjectData.model_validate_json(speckle_project_data)\n",
" speckle_token = os.environ[token_env_var]\n",
" print(project_data)\n",
" print(inputs)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}
+24
View File
@@ -0,0 +1,24 @@
import os
import sys
import papermill as pm
if __name__ == "__main__":
_, speckle_project_data, function_inputs, speckle_token = sys.argv
# we need to pass in the token via an env var, the params cell is executed
# automatically exposing the token in the output file
token_env_var = "SPECKLE_TOKEN"
os.environ[token_env_var] = speckle_token
output_file = "function.output.ipynb"
pm.execute_notebook(
"function.ipynb",
output_file,
parameters={
"speckle_project_data": speckle_project_data,
"function_inputs": function_inputs,
"token_env_var": token_env_var,
},
)
+42
View File
@@ -0,0 +1,42 @@
"""
This module is used to create the JSON schema from the FunctionInputs class defined in the notebook.
WARNING: you probably should not be modifying this unless you know what you are doing.
"""
import json
from pathlib import Path
import papermill as pm
def save_schema_to_file(file_path: str):
"""
Executes the notebook without valid input values.
Its fine, since we're only interested in the result of the `function_inputs` cell.
"""
notebook_path = "function.ipynb"
run_result = pm.execute_notebook(notebook_path, "-")
# find the cell by its tag
function_inputs_cells = [
c for c in run_result.cells if "function_inputs" in c.metadata.tags
]
# should have only found 1
if len(function_inputs_cells) != 1:
msg = f"Expected 1 cell to have the tag `function_inputs`, found {len(function_inputs_cells)}"
raise ValueError(msg)
# parse and dump the schema to make sure its a valid json object
schema = json.dumps(json.loads(function_inputs_cells[0].outputs[0].text))
Path(file_path).write_text(schema)
if __name__ == "__main__":
import sys
_, file_path = sys.argv
save_schema_to_file(file_path)