From 5098cce88c484e7e3e055e96a28c2456e8b18c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Fri, 23 May 2025 12:17:13 +0200 Subject: [PATCH 1/5] improve docs --- docs/assets/connect_function.png | Bin 0 -> 13779 bytes docs/assets/create_codespace.png | Bin 0 -> 22331 bytes docs/assets/display_function_credentials.png | Bin 0 -> 18132 bytes docs/assets/portal-user-menu.png | Bin 0 -> 10192 bytes docs/assets/private_repo.png | Bin 0 -> 18081 bytes docs/assets/use_template.png | Bin 0 -> 12008 bytes docs/examples/enforce_field_rules.md | 99 +++++ docs/examples/field_calculation.md | 107 ++++++ docs/examples/index.md | 4 + docs/examples/workflows.md | 75 ++++ docs/getting_started.md | 154 +++++--- docs/index.md | 87 ++++- docs/key_concepts.md | 18 +- mkdocs.yml | 21 +- poetry.lock | 364 ++++++++++++++++++- pyproject.toml | 5 + 16 files changed, 869 insertions(+), 65 deletions(-) create mode 100644 docs/assets/connect_function.png create mode 100644 docs/assets/create_codespace.png create mode 100644 docs/assets/display_function_credentials.png create mode 100644 docs/assets/portal-user-menu.png create mode 100644 docs/assets/private_repo.png create mode 100644 docs/assets/use_template.png create mode 100644 docs/examples/enforce_field_rules.md create mode 100644 docs/examples/field_calculation.md create mode 100644 docs/examples/index.md create mode 100644 docs/examples/workflows.md diff --git a/docs/assets/connect_function.png b/docs/assets/connect_function.png new file mode 100644 index 0000000000000000000000000000000000000000..6438f9c4f5b4f23c714ab3ac1b0a25470e3525d9 GIT binary patch literal 13779 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 Ph6_+t)KaJgSw#F7PVJu? literal 0 HcmV?d00001 diff --git a/docs/assets/display_function_credentials.png b/docs/assets/display_function_credentials.png new file mode 100644 index 0000000000000000000000000000000000000000..d659150a7408877704cc310dd81481e0f7ebacd7 GIT binary patch literal 18132 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 literal 0 HcmV?d00001 diff --git a/docs/assets/portal-user-menu.png b/docs/assets/portal-user-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..9033d2bd164af452e15399fcb27a6e3a457392ad GIT binary patch literal 10192 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 literal 0 HcmV?d00001 diff --git a/docs/assets/private_repo.png b/docs/assets/private_repo.png new file mode 100644 index 0000000000000000000000000000000000000000..e08d46254814528e872f8f1a4332a6644bead9b1 GIT binary patch literal 18081 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 list[str]: + """ + Returns a list of classification property codes for a given object ID. + """ + + graphql_url = str(metadata.db_service_url).rstrip("/") + "/graphql/v1" + query = f"""{{ + object_property_values(ref_object_id: "{cdb_object_id}") {{ + property_code + }} + }} + """ + response = requests.post( + graphql_url, + headers={"Authorization": f"Bearer {metadata.service_token}"}, + json={"query": query}, + ) + response.raise_for_status() + data = response.json() + return [ + item["property_code"] + for item in data["data"]["object_property_values"] + ] + + +def parts_need_classification( + metadata: MetaData, + event: PartReleaseCheckEvent , + service: Service, +): + """ + Parts need to be classified before they can be released. + """ + + for part in event.data.parts: + # the event contains a list of parts that are about to be released + # for each part fetch the classification property codes and check if they are empty + property_codes = fetch_part_classification_property_codes(part.cdb_object_id, metadata) + if not property_codes: + return AbortAndShowErrorAction( + message=f"The part '{part.eng_benennung or part.benennung}' is missing classification data." + ) + +``` diff --git a/docs/examples/field_calculation.md b/docs/examples/field_calculation.md new file mode 100644 index 0000000..78235a9 --- /dev/null +++ b/docs/examples/field_calculation.md @@ -0,0 +1,107 @@ +# Field calculation + +The data sheet editor in CIM Database Cloud already allows you to define some basic [field calculations](https://saas-docs.contact-cloud.com/2025.13.1-en/admin/admin-contact_cloud/saas_admin/app_setup_data_edit_field_calc){:target="_blank"} to fill out fields automatically. + +However, the Python expressions available in the datasheet editor are limited. Functions allow for much more freedom in defining your field calculations, and allow you to do things like *"fetching external data"* or *"referencing other objects"*. + +Field calculations with Functions utilize the "FieldCalulationEvent", e.g. [PartFieldCalculationEvent](../reference/events.md#partfieldcalculationevent), which expect the response to contain a `DataResponse` with a dictionary containing the fields that should be updated. + +```python +return DataResponse(data={somefield="new value"}) +``` + + +## Custom part number for external parts + +This example shows you the basics of calculating fields with Functions and how to use the `service` parameter to generate a fresh number. + +The example Function checks if the part is an *"External"* part and generates a custom part number for it. + +```python +from csfunctions import DataResponse +from csfunctions.events import PartFieldCalculationEvent +from csfunctions.metadata import MetaData +from csfunctions.service import Service + + +def calculate_part_number(metadata: MetaData, event: PartFieldCalculationEvent, service: Service): + """ + Example Function. + This function is triggered when a part fields should be calculated. + For "External" parts we want to set the part number as "E-000123". + All other parts should keep the standard part number. + + """ + if event.data.action != "create": + # part number can only be set when the part is created + return + + # match "External Single Part" or "External Assembly" + if event.data.part.t_kategorie_name_en.startswith("External"): + + # generate a new number using the service + new_number = service.generator.get_number("external_part_number") + + # new_number is an integer, so we need to convert it to a string + # and pad it with leading zeros to 6 digits + new_part_number = str(new_number).zfill(6) + + # then add the prefix "E-" to the number + new_part_number = "E-" + new_part_number + + # finally we return the new part number (teilenummer) + return DataResponse(data={"teilenummer": new_part_number}) +``` + +!!! tip + You can check `event.data.action` to decide for which operations (*copy*,*create*,*index* and *modify*) you want your field calculation to return a new value. + Some fields, like part number (*teilenummer*) can only be set during the initial creation. + +## Translate a field with DeepL + +Inside Functions you can fetch data from external systems and fill out fields based on that data. This is something that would not be possible with the field calculations in the datasheet editor. You could use this for example to fetch new part numbers from an ERP system. + +This example uses the API from [DeepL](https://www.deepl.com) to translate a field from German to English. The example uses the additional attributes 1 and 2 on parts, but you can of course change that to any attributes that fit your use-case. + +```python +import os +from csfunctions import DataResponse +from csfunctions.events import PartFieldCalculationEvent +import requests + +# set the DEEPL_API_KEY during deployment like this: +# cfc env deploy --environment-variables "DEEPL_API_KEY=" +DEEPL_API_KEY = os.getenv("DEEPL_API_KEY") + +def part_field_calculation(metadata, event: PartFieldCalculationEvent, service): + if event.data.action != "create": + # only translate on creation + return + + if event.data.part.cssaas_frame_add_attr_1: + translated_text = translate_text( + event.data.part.cssaas_frame_add_attr_1, "EN", "DE") + return DataResponse(data={"cssaas_frame_add_attr_2": translated_text}) + +def translate_text(text, target_lang, source_lang=None): + url = "https://api-free.deepl.com/v2/translate" + data = { + "auth_key": DEEPL_API_KEY, + "text": text, + "target_lang": target_lang.upper() + } + if source_lang: + data["source_lang"] = source_lang.upper() + + response = requests.post(url, data=data) + response.raise_for_status() + return response.json()["translations"][0]["text"] + +``` + +!!! note + This example requires a DeepL API key to function. Adding secrets like API keys to your code is a bad practice, which is why the example fetches the API key from an environment variable. + + You can set environment variables during deployment of your Function to the CIM Database Cloud Functions infrastructure like this: + + `cfc env deploy --environment-variables "DEEPL_API_KEY="` diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 0000000..48399a6 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,4 @@ +# Examples + +This section contains example Functions that you can copy and adapt to your specific use case. +Remember to [register the Function](../getting_started.md#register-the-function) in the `environment.yaml` after copying it into your code base. diff --git a/docs/examples/workflows.md b/docs/examples/workflows.md new file mode 100644 index 0000000..ce6b500 --- /dev/null +++ b/docs/examples/workflows.md @@ -0,0 +1,75 @@ +# Working with workflows + +Functions can interact with workflows. You can trigger Functions from within workflows using the [Trigger Webhook](https://saas-docs.contact-cloud.com/latest-en/admin/admin-contact_cloud/saas_admin/webhooks_workflow){:target="_blank"} task and you can even start new workflows by using the [StartWorkflowAction](../reference/actions.md#startworkflowaction)! + + +## Start workflow on EC status change + +This example shows you how to start a workflow template in response to an engineering change status change. + +!!! note + Starting workflows in response to engineering change status changes is already possible in CIM Database Cloud without the use of Functions. However Functions allow you to dynamically select different templates and fill out task parameters, based on the nature of the change. + +This example uses a very simple template, containing just an *information task*. If an engineering change contains external parts, users with the *External Part Manager* Role should be notified of the planned change during the evaluation phase. + +You can easily adapt this example to your use-case, by adding additional tasks to the template or changing the conditions under which the workflow should be started. + + +```python +from csfunctions.actions.start_workflow import ( + StartWorkflowAction, + Subject, + TaskConfiguration, +) +from csfunctions.events import EngineeringChangeStatusChangedEvent +from csfunctions import MetaData + +# change these to match your template and roles!!! +TEMPLATE_ID = "PT00000002" +INFORMATION_TASK_ID = "T00000008" +INFORM_ROLE = "External Part Manager" + + +def start_workflow_on_ec_status_change( + metadata: MetaData, event: EngineeringChangeStatusChangedEvent, service +): + if event.data.engineering_change.status != 30: + # only start the workflow if the status changed to 30 (Evaluation) + return + + # check if the ec contains external parts + if not any( + part.t_kategorie_name_en.startswith("External") + for part in event.data.engineering_change.planned_changes_parts + ): + # no external parts, so we don't need to start the workflow + return + + return StartWorkflowAction( + template_id=TEMPLATE_ID, + title=f"Information about EC {event.data.engineering_change.cdb_ec_id}", + # attach the engineering change to the workflow + global_briefcase_object_ids=[ + event.data.engineering_change.cdb_object_id], + task_configurations=[ + TaskConfiguration( + task_id=INFORMATION_TASK_ID, + description="A an engineering change containing external parts moved to the evaluation phase.", + recipients=[ + Subject( + subject_type="Common Role", + subject_id=INFORM_ROLE, + ) + ], + ) + + ], + ) +``` + +!!! note + To sucessfully execute this example you need to: + + - Create a workflow template with an information task and adjust the `TEMPLATE_ID` and `INFORMATION_TASK_ID` to match them + + - Create and assign an "External Part Manager" role to a user diff --git a/docs/getting_started.md b/docs/getting_started.md index 20bec75..37dcc0c 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -1,9 +1,31 @@ -## Installation +This guide will help you get started building your first Function and deploying it to CIM Database Cloud. -Install using pip: -``` sh -pip install contactsoftware-functions -``` +## Setting up your Codespace + +The first step to developing your own Functions is setting up a development environment. To make this simple, we recommend using GitHub Codespaces, which is a remote development environment that you can access through your browser. No local setup required! + +!!! note + + If you are an experienced developer and wish to set up a development environment on your own machine, you can skip Codespaces and install the SDK in a local Python environment using `pip install contactsoftware-functions`. + +To get started, head to the template repository for Functions: [https://github.com/cslab/functions-template-python](https://github.com/cslab/functions-template-python){:target="_blank"} + +- You need a (free) account on GitHub. +- Copy the repository by clicking the "Use this template" button on the top right and select "Create a new repository". + + ![Use template button](assets/use_template.png) + +- Make sure your new repository is set to private! + + ![Private repository](assets/private_repo.png) + +- In your new repository, create a development container by clicking on the green "Code" button and selecting "Create codespace on main". + + ![Create codespace](assets/create_codespace.png) + + This will take a few minutes, and you will see a new tab open in your browser with a development container running. + +After completing these steps, you will have a development environment with all required tools already installed! ## Build your first Function @@ -13,109 +35,133 @@ A minimal Function implementation consists of three files: - `environment.yaml` describes the environment and the Functions contained in it - `requirements.txt` contains the dependencies of your Functions (usually only contactsoftware-functions) -- `mymodule.py` a Python file containing the code of your Functions (feel free to pick a different name) +- `mymodule.py` is a Python file containing the code of your Functions (feel free to pick a different name) Here is the complete structure: ``` bash - my_example_environment/ + src/ ├── environment.yaml ├── mymodule.py └── requirements.txt ``` -### Function Code -Start by writing the code for your first Function. As an example we will write a Function that sends released Documents to an ERP system. - -``` python title="mymodule.py" - -from csfunctions import MetaData, Service -from csfunctions.events import DocumentReleaseEvent +If you are using the Codespaces template repository, you will find that it already contains the required file structure—including a small example Function. -def send_doc_to_erp(metadata: MetaData, event: DocumentReleaseEvent, service: Service): - ... -``` +### Function Code +Start by writing the code for your first Function. As a first example, you will write a Function that prevents you from creating documents with the title "Test". +If you are interested in more complex (and realistic) examples, check out the [Examples](examples/index.md) section of this documentation. -While you don't have to use type annotations, it is highly recommended because it enables autocomplete in your IDE and helps you spot mistakes faster. -For our example we only need the [DocumentReleasedEvent](reference/events.md/#documentreleasedevent). It contains a list of documents that were released. Typically this will only be a single document, however it is best practices to iterate over all of the documents. +In this example, you will use the [DocumentCreateCheckEvent](reference/events.md/#documentcreatecheckevent). It contains a list of documents that are about to be created. Typically, this will only be a single document; however, it is best practice to iterate over all of the documents. ``` python title="mymodule.py" -import requests -import json - from csfunctions import MetaData, Service -from csfunctions.events import DocumentReleasedEvent +from csfunctions.events import DocumentCreateCheckEvent +from csfunctions.actions import AbortAndShowErrorAction -def send_doc_to_erp(metadata: MetaData, event: DocumentReleasedEvent, service: Service): +def prevent_test_document(metadata: MetaData, event: DocumentCreateCheckEvent, service: Service): # iterate over the documents contained in the event for document in event.data.documents: - # create the payload for our (fictional ERP system) - payload = json.dumps({ - "document_number": document.z_nummer, - "document_index": document.z_index, - "document_title": document.titel - }) - res = requests.post("https://example.com", data=payload) - if res.status_code != 200: - return ValueError(f"Failed to upload document to ERP. Got response code {res.status_code}") + # for each document, check if the title starts with "Test" + if document.titel.startswith("Test"): + # abort and show an error message to the user + return AbortAndShowErrorAction(message="Test documents are not allowed.") + # if no documents match the condition the Function doesn't need to do anything + return ``` -Here we send a payload, containing a few attributes from the released document, to [example.com](https://example.com). This is just for illustration purposes! -Please refer to the documentation of your ERP system on how the request needs to be formatted and which endpoint and credentials to use. +!!! tip + Using type annotations is not required in Python, but it is highly recommended, as it allows your code editor to give you better autocomplete recommendations and helps you spot mistakes faster. ### Register the Function -The Function needs to be registered in the `environment.yaml`: +The Function needs to be registered in the `environment.yaml` config file. If you are using the Codespaces template, the config file already contains a reference to the example Function. You can just add another entry for your new Function below: ``` yaml title="environment.yaml" runtime: python3.10 version: v1 functions: - - name: send_doc_to_erp - entrypoint: mymodule.send_doc_to_erp + # this is the example Function from the template + - name: example + entrypoint: example_module.example_doc_release_check + # just add your new Function below like this: + - name: prevent_test_document + entrypoint: mymodule.prevent_test_document ``` -You can add as many functions to the list as you like. The function `name` can be picked freely and doesn't have to match the name of your Python method (although it is recommended that it does). The name will be used to identify the Function in you CIM Database Cloud instance. The `entrypoint` needs to be the import path of your Python function. - +You can add as many functions to the list as you like. The function `name` can be chosen freely and doesn't have to match the name of your Python method (although it is recommended that it does). The name will be used to identify the Function in your CIM Database Cloud instance. The `entrypoint` needs to be the import path of your Python function. ### Dependencies -Lastly define your codes dependencies in the `requirements.txt`: +Lastly, define your code's dependencies in the `requirements.txt`: ``` python title="requirements.txt" contactsoftware-functions ``` -contactsoftware-functions will always need to be in the requirements.txt unless you register your own main_entrypoint (see [Python runtime](reference/runtime.md)). +`contactsoftware-functions` will always need to be in the requirements.txt unless you register your own main_entrypoint (see [Python runtime](reference/runtime.md)). +The Codespaces template already includes the required dependencies. -### Deploy the Code -To deploy the Code you first need to install the [contactsoftware-functions-client](https://pypi.org/project/contactsoftware-functions-client/) and retrieve developer credentials in the CONTACT Portal. -Install client: +## Test your code -```bash -pip install contactsoftware-functions-client -``` +To test if your Function works as intended, you could now proceed to upload the code to CIM Database Cloud (as described in the next section). However, to speed up development, it is recommended to use the development server that is built into **csfunctions** to run and test the code in your local development environment. + +Head over to the [development server documentation](development_server.md) to find out how to run your Functions locally. Once you are happy with your Function's code, proceed to the next section of this guide to learn how to deploy your code to the CIM Database Cloud serverless infrastructure. + +## Deploy the code + +To deploy your code to CIM Database Cloud, you first need to retrieve your Functions developer credentials. This requires you to have the **Functions Developer** role in the CONTACT Portal. The role can be assigned to you by your organization's administrator in the CONTACT Portal. -Login: +- Go to the CONTACT Portal [https://portal.contact-cloud.com](https://portal.contact-cloud.com){:target="_blank"} and log in. +- Open the menu in the top right corner and click on your name. + ![Portal User Menu](assets/portal-user-menu.png) +- In the context menu of your user, click on "Display credentials for functions development". + ![Display credentials](assets/display_function_credentials.png) + + +You can then use the [Functions client](https://pypi.org/project/contactsoftware-functions-client/){:target="_blank"} to upload your Function code to CIM Database Cloud. If you are using Codespaces, the Functions client is already installed. Otherwise, you can install it with `pipx install contactsoftware-functions-client`. + + +Log in with the credentials you retrieved from the CONTACT Portal: ```bash cfc login ``` -Create a new environment: +Create a new [Function Environment](key_concepts.md#function-environments): ```bash cfc env create myenv ``` -Upload code into new environment: +Upload code into the new environment: ```bash cfc env deploy myenv ``` -### Test the Function -To test your Function you need to connect the Function to an event in your CIM Database Cloud instance. -Please refer to the Webhooks CIM Database Cloud documentation on how to do that. +!!! warning + Uploading code into an environment will overwrite the existing code in the environment. + +## Connect the Function + +The final step is to connect your Function to an event in your CIM Database Cloud instance. You need to have the **Administrator** role in the instance to do this. + +To connect your Function, open the Application Setup in your CIM Database Cloud instance and create a new Function: + +![Create Function Dialog](assets/connect_function.png) + +In the dialog, select the event that should trigger your Function. For this example, choose *document_create_check*. + +Find your environment's name in the *Environment* drop-down menu, then select your Function. The Function's name will match the name you used when you [registered the Function](#register-the-function) in the `environment.yaml` file. + +!!! tip + + If you can't find your environment or Function in the drop-down menu, make sure the Function was deployed successfully to CIM Database Cloud. Try closing and reopening the dialog to refresh the list of environments. + + +!!! note + + CIM Database Cloud instances can only see Functions uploaded by users from the organization the environment belongs to. If you were invited to a CIM Database Cloud instance from a different organization, you will not be able to connect your Functions to that instance. diff --git a/docs/index.md b/docs/index.md index d6d3d4c..4a2290d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,16 +1,97 @@ -## About Functions +# Functions-SDK for Python + This SDK provides the **csfunctions** library for developing Functions with Python. -Functions are deeply integrated in the CIM Database Cloud Webhooks technology. They are designed to work seamlessly together. The goal is to allow implementing custom business logic in a CIM Database Cloud SaaS application without leaving the CONTACT Cloud and without the need to create and maintain a separate infrastructure. +Functions are deeply integrated in the [CIM Database Cloud](https://www.cim-database-cloud.com){:target="_blank"} Webhooks technology. They are designed to work seamlessly together. The goal is to allow implementing custom business logic in a CIM Database Cloud SaaS application without leaving the CONTACT Cloud and without the need to create and maintain a separate infrastructure. ## Requirements Python 3.10+ -csfunctions is build with [Pydantic 2](https://docs.pydantic.dev/latest/) +csfunctions is build with [Pydantic 2](https://docs.pydantic.dev/latest/){:target="_blank"} ## Installation Install using pip: ``` sh pip install contactsoftware-functions ``` +## Usage +### Build the Function + +Folder content of a minimal example for a Function implementation: + +``` bash + my_example_functions/ + ├── environment.yaml + ├── mymodule.py + └── requirements.txt +``` + + +Code for a Function: + +``` python title="mymodule.py" +import requests +import json + +from csfunctions import MetaData, Service +from csfunctions.events import DocumentReleaseEvent + +def send_doc_to_erp(metadata: MetaData, event: DocumentReleaseEvent, service: Service): + # iterate over the documents contained in the event + for document in event.data.documents: + # create the payload for our (fictional ERP system) + payload = json.dumps({ + "document_number": document.z_nummer, + "document_index": document.z_index, + "document_title": document.titel + }) + res = requests.post("https://example.com", data=payload) + if res.status_code != 200: + return ValueError(f"Failed to upload document to ERP. Got response code {res.status_code}") + +``` + +Environment file to define runtime and Function entrypoints: + +``` yaml title="environment.yaml" +runtime: python3.10 +version: v1 +functions: + - name: send_doc_to_erp + entrypoint: mymodule.send_doc_to_erp +``` + + +Define requirements: + +``` python title="requirements.txt" +contactsoftware-functions +``` + +### Deploy the Code +To deploy the Code you first need to install the [contactsoftware-functions-client](https://pypi.org/project/contactsoftware-functions-client/){:target="_blank"} and retrieve developer credentials in the CONTACT Portal. + +Install client: + +```bash +pip install contactsoftware-functions-client +``` + +Login: + +```bash +cfc login +``` + +Create a new environment: + +```bash +cfc env create myenv +``` + +Upload code into new environment: + +```bash +cfc env deploy myenv +``` diff --git a/docs/key_concepts.md b/docs/key_concepts.md index c30ba45..6877baf 100644 --- a/docs/key_concepts.md +++ b/docs/key_concepts.md @@ -1,17 +1,27 @@ ## Webhooks Webhooks in __CIM Database Cloud__ can be used to call HTTP endpoints with context-related metadata for certain events (e.g. document release). -Further information about webhooks can be found in the CIM Database Cloud documentation. +Further information about webhooks can be found in the [CIM Database Cloud documentation](https://saas-docs.contact-cloud.com/2025.14.0-en/admin/admin-contact_cloud/saas_admin/webhooks){:target="_blank"}. ## Functions -Webhooks can also call **Functions**, which allow execution of custom code in the CIM Database Cloud serverless infrastructure. This allows customers to enhance their CIM Database Cloud experience by implementing their own business logic. +Events in CIM Database Cloud can also trigger **Functions**, which represent user defined code that is executed in the CIM Database Cloud serverless infrastructure. This allows customers to extend the functionality of CIM Database Cloud with custom business logic.
![Overview schema](assets/functions-overview.png)
-## Environments -Functions are grouped into **environments**, which are the (Docker) container the code runs in. An environment contains a runtime for its specific programming language, the Function code and a configuration file describing the environment. +## Function Environments + +Functions are grouped into **Function Environments**, which are the container the code runs in. An environment contains a runtime for its specific programming language, the Function code and a configuration file describing the environment. If the Functions in an environment have not been executed in a while, the environment will become "cold" and the next start of a Function will take a bit longer. Therefore it is recommended to place all your Functions in the same environment. + +Function environments are created with the [Functions client](https://pypi.org/project/contactsoftware-functions-client/){:target="_blank"}: + +```bash +pipx install contactsoftware-functions-client + +cfc login +cfc env create myenvironment +``` diff --git a/mkdocs.yml b/mkdocs.yml index 78586a5..6006b33 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,24 +17,39 @@ theme: name: Switch to light mode logo: assets/branding_web_app_icon.png favicon: assets/branding_web_favicon.ico + features: + - content.code.copy + - navigation.indexes extra_css: - stylesheets/extra.css repo_url: https://github.com/cslab/functions-sdk-python markdown_extensions: - pymdownx.highlight: anchor_linenums: true - - pymdownx.superfences - attr_list - md_in_html - toc + - admonition + - pymdownx.details + - pymdownx.superfences +plugins: + - search + - link-marker + + nav: - - Home: index.md + - Intro: index.md - Key concepts: key_concepts.md - Getting started: getting_started.md - - Development server: development_server.md - Reference: - reference/events.md - reference/objects.md - reference/actions.md + - development_server.md - Python runtime: reference/runtime.md + - Examples: + - examples/index.md + - examples/enforce_field_rules.md + - examples/field_calculation.md + - examples/workflows.md - Release notes: release_notes.md diff --git a/poetry.lock b/poetry.lock index 70c92fd..cb93489 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,38 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] + +[[package]] +name = "backrefs" +version = "5.8" +description = "A wrapper around re and regex that adds additional back references." +optional = false +python-versions = ">=3.9" +files = [ + {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, + {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, + {file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"}, + {file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"}, + {file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"}, + {file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"}, +] + +[package.extras] +extras = ["regex"] + [[package]] name = "certifi" version = "2025.1.31" @@ -123,6 +155,20 @@ files = [ {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] +[[package]] +name = "click" +version = "8.2.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +files = [ + {file = "click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c"}, + {file = "click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -148,6 +194,23 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + [[package]] name = "idna" version = "3.10" @@ -173,6 +236,38 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markdown" +version = "3.8" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.9" +files = [ + {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, + {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, +] + +[package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + [[package]] name = "markupsafe" version = "3.0.2" @@ -243,6 +338,118 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-link-marker" +version = "0.1.3" +description = "MkDocs plugin for marking external or mail links in your documentation." +optional = false +python-versions = ">=3.4" +files = [ + {file = "mkdocs-link-marker-0.1.3.tar.gz", hash = "sha256:6d8760c819a376650675f02c7de82f5fbc0a992e7ed3239955cc01021a1af779"}, + {file = "mkdocs_link_marker-0.1.3-py3-none-any.whl", hash = "sha256:798a6bfbb059ce49a81f724e84306bec8f1d5241aace89e54ae7d9088beeab5b"}, +] + +[package.dependencies] +jinja2 = "*" +mkdocs = ">=1.0" + +[[package]] +name = "mkdocs-material" +version = "9.6.14" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b"}, + {file = "mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +backrefs = ">=5.7.post1,<6.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.1,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + [[package]] name = "packaging" version = "24.2" @@ -254,6 +461,48 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -401,6 +650,38 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.15" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f"}, + {file = "pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.19.1)"] + [[package]] name = "pytest" version = "8.3.5" @@ -423,6 +704,20 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pyyaml" version = "6.0.2" @@ -485,6 +780,20 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "requests" version = "2.32.3" @@ -523,6 +832,17 @@ requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "tomli" version = "2.2.1" @@ -606,6 +926,48 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "werkzeug" version = "3.1.3" @@ -626,4 +988,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "d4a6b08e6492e15e68b80664e161bea0954afcdbf07835046729cf3899049d70" +content-hash = "ea4efaa8f5c19e40b84dbe5b646b7b9ed44a45a0e1ec1e0c0e20b675f1bdb154" diff --git a/pyproject.toml b/pyproject.toml index f6847e3..c29badf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,11 @@ pytest = "^8.3.4" requests-mock = "^1.12.1" +[tool.poetry.group.dev.dependencies] +mkdocs = "^1.6.1" +mkdocs-material = "^9.6.14" +mkdocs-link-marker = "^0.1.3" + [tool.ruff] line-length = 120 From 83f56b442bfbfdf11a6bdb53704b9daca058d751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Fri, 23 May 2025 12:37:03 +0200 Subject: [PATCH 2/5] fix grammar mistakes --- README.md | 2 +- docs/development_server.md | 23 +++++++-------- docs/examples/enforce_field_rules.md | 29 ++++++++++--------- docs/examples/field_calculation.md | 42 ++++++++++++---------------- docs/examples/workflows.md | 33 ++++++++++------------ docs/index.md | 26 ++++++++--------- 6 files changed, 73 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 454d4b0..ed202e2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Functions are deeply integrated in the CIM Database Cloud Webhooks technology. T Python 3.10+ -csfunctions is build with [Pydantic 2](https://docs.pydantic.dev/latest/) +csfunctions is built with [Pydantic 2](https://docs.pydantic.dev/latest/) ## Installation Install using pip: diff --git a/docs/development_server.md b/docs/development_server.md index 805bd47..d1eafff 100644 --- a/docs/development_server.md +++ b/docs/development_server.md @@ -1,6 +1,8 @@ +# Development Server + The Functions SDK includes a development server that allows you to run your Functions in your development environment. The server reads Functions from the `environment.yaml` file and makes them available via HTTP endpoints. You can then connect these Functions to your CIM Database Cloud instance using webhooks. -This speeds up the development of Functions, because you can instantly test your changes, without deploying them to the cloud infrastructure first. +This speeds up the development of Functions because you can instantly test your changes without deploying them to the cloud infrastructure first. ## Starting the Server @@ -16,7 +18,7 @@ You can set the port of the server using the `--port` flag (default is 8000), or python -m csfunctions.devserver --port 8080 ``` -You can set the directory containing the `environment.yaml` file using the `--dir` flag (by default the current working directory is used) or by setting the `CON_DEV_DIR` environment variable: +You can set the directory containing the `environment.yaml` file using the `--dir` flag (by default, the current working directory is used) or by setting the `CON_DEV_DIR` environment variable: ```bash python -m csfunctions.devserver --dir ./my_functions @@ -32,7 +34,7 @@ python -m csfunctions.devserver --secret my_secret The development server will automatically restart if you make changes to your Functions code or to the `environment.yaml` file. -## Exposing the server +## Exposing the Server To enable your CIM Database Cloud instance to send webhook requests to your Functions, you need to make the server accessible from the internet. Here are several ways to do this: @@ -50,8 +52,7 @@ If you are developing Functions locally, you can use services like [ngrok](https Please refer to the documentation of the specific service for instructions on how to do this. - -## Create a webhook in CIM Database Cloud +## Create a Webhook in CIM Database Cloud To test your Functions locally, create a webhook in your CIM Database Cloud instance and point it to your development server. @@ -59,17 +60,17 @@ The webhook URL should combine your development server URL with the Function nam `https:///` -For example the `example` function would be available at: - -```https://mycodespace-5g7grjgvrv9h4jrx-8000.app.github.dev/example``` +For example, the `example` function would be available at: +``` +https://mycodespace-5g7grjgvrv9h4jrx-8000.app.github.dev/example +``` -Make sure to set the webhooks event to the correct event you want to test with your Function. +Make sure to set the webhook's event to the correct event you want to test with your Function. For more detailed information on how to create a webhook in CIM Database Cloud, please refer to the [CIM Database Cloud documentation](https://saas-docs.contact-cloud.com/2025.7.0-en/admin/admin-contact_cloud/saas_admin/webhooks). - -## Securing the development server +## Securing the Development Server Since the development server is exposed to the outside world, you should secure it to prevent unauthorized access. diff --git a/docs/examples/enforce_field_rules.md b/docs/examples/enforce_field_rules.md index 977695e..1b8f676 100644 --- a/docs/examples/enforce_field_rules.md +++ b/docs/examples/enforce_field_rules.md @@ -1,10 +1,10 @@ -Functions can be used to validate user input and thus ensure that fields on e.g. parts or documents are filled out correctly. +Functions can be used to validate user input and ensure that fields on, for example, parts or documents are filled out correctly. ### Required field based on Part category -This example shows how you can enforce parts of category *"Single Part"* to have a material assigned to them. +This example shows how you can enforce that parts in the category *"Single Part"* must have a material assigned. -The example Function can be connected to the [PartCreateCheckEvent](../reference/events.md#partcreatecheckevent) and [PartModifyCheckEvent](../reference/events.md#partmodifycheckevent) and will return an [AbortAndShowErrorAction](../reference/actions.md#abortandshowerroraction) to abort the creation or modification of the part if the condition is not met. +The example Function can be connected to the [PartCreateCheckEvent](../reference/events.md#partcreatecheckevent) and [PartModifyCheckEvent](../reference/events.md#partmodifycheckevent). It will return an [AbortAndShowErrorAction](../reference/actions.md#abortandshowerroraction) to abort the creation or modification of the part if the condition is not met. ```python from csfunctions import MetaData, Service @@ -21,33 +21,32 @@ def single_part_needs_material( service: Service, ): """ - If a part of category ' Single Part' is created, a material has to be assigned. + If a part of category 'Single Part' is created or modified, a material must be assigned. This should be checked when the part is created or modified. """ for part in event.data.parts: - # the event contains a list of parts that are about to be created + # The event contains a list of parts that are about to be created or modified if part.t_kategorie_name_en == "Single Part" and not part.material_object_id: return AbortAndShowErrorAction( - message="A material has to be assigned to a part of category 'Single Part'." + message="A material must be assigned to a part of category 'Single Part'." ) ``` ### Require parts to be classified before release -Classification is a powerful tool for organizing your parts, however the best tool only works if users use it. -With this example Function you can require parts to be classified before they can be released. +Classification is a powerful tool for organizing your parts. However, even the best tool is only effective if users actually use it. +With this example Function, you can require that parts are classified before they can be released. -This Function should be connected to the [PartReleaseCheckEvent](../reference/events.md#partreleasecheckevent) and will return an [AbortAndShowErrorAction](../reference/actions.md#abortandshowerroraction) to prevent the release, if classification data is missing. +This Function should be connected to the [PartReleaseCheckEvent](../reference/events.md#partreleasecheckevent) and will return an [AbortAndShowErrorAction](../reference/actions.md#abortandshowerroraction) to prevent the release if classification data is missing. -The example code shows you how to fetch classification data for parts from the [CIM Database Cloud GraphQL API](https://saas-docs.contact-cloud.com/latest-en/admin/admin-contact_cloud/saas_admin/webhooks_graphql){:target="_blank"}. The Function then checks if any classification data is present, however you can easily expand this to check for specific classes. +The example code demonstrates how to fetch classification data for parts from the [CIM Database Cloud GraphQL API](https://saas-docs.contact-cloud.com/latest-en/admin/admin-contact_cloud/saas_admin/webhooks_graphql){:target="_blank"}. The Function then checks if any classification data is present, but you can easily expand this to check for specific classes. ```python from csfunctions import MetaData, Service from csfunctions.actions import AbortAndShowErrorAction from csfunctions.events import ( - PartReleaseCheckEvent, ) import requests @@ -80,16 +79,16 @@ def fetch_part_classification_property_codes(cdb_object_id: str, metadata: MetaD def parts_need_classification( metadata: MetaData, - event: PartReleaseCheckEvent , + event: PartReleaseCheckEvent, service: Service, ): """ - Parts need to be classified before they can be released. + Parts must be classified before they can be released. """ for part in event.data.parts: - # the event contains a list of parts that are about to be released - # for each part fetch the classification property codes and check if they are empty + # The event contains a list of parts that are about to be released + # For each part, fetch the classification property codes and check if they are empty property_codes = fetch_part_classification_property_codes(part.cdb_object_id, metadata) if not property_codes: return AbortAndShowErrorAction( diff --git a/docs/examples/field_calculation.md b/docs/examples/field_calculation.md index 78235a9..ea7d773 100644 --- a/docs/examples/field_calculation.md +++ b/docs/examples/field_calculation.md @@ -1,19 +1,19 @@ # Field calculation -The data sheet editor in CIM Database Cloud already allows you to define some basic [field calculations](https://saas-docs.contact-cloud.com/2025.13.1-en/admin/admin-contact_cloud/saas_admin/app_setup_data_edit_field_calc){:target="_blank"} to fill out fields automatically. +The datasheet editor in CIM Database Cloud already allows you to define some basic [field calculations](https://saas-docs.contact-cloud.com/2025.13.1-en/admin/admin-contact_cloud/saas_admin/app_setup_data_edit_field_calc){:target="_blank"} to fill out fields automatically. -However, the Python expressions available in the datasheet editor are limited. Functions allow for much more freedom in defining your field calculations, and allow you to do things like *"fetching external data"* or *"referencing other objects"*. +However, the Python expressions available in the datasheet editor are limited. Functions allow for much more flexibility in defining your field calculations, enabling you to do things like *fetching external data* or *referencing other objects*. -Field calculations with Functions utilize the "FieldCalulationEvent", e.g. [PartFieldCalculationEvent](../reference/events.md#partfieldcalculationevent), which expect the response to contain a `DataResponse` with a dictionary containing the fields that should be updated. +Field calculations with Functions utilize the `FieldCalculationEvent`, e.g. [PartFieldCalculationEvent](../reference/events.md#partfieldcalculationevent), which expects the response to contain a `DataResponse` with a dictionary of the fields that should be updated. ```python -return DataResponse(data={somefield="new value"}) +return DataResponse(data={"somefield": "new value"}) ``` ## Custom part number for external parts -This example shows you the basics of calculating fields with Functions and how to use the `service` parameter to generate a fresh number. +This example shows you the basics of calculating fields with Functions and how to use the `service` parameter to generate a new number. The example Function checks if the part is an *"External"* part and generates a custom part number for it. @@ -23,45 +23,39 @@ from csfunctions.events import PartFieldCalculationEvent from csfunctions.metadata import MetaData from csfunctions.service import Service - def calculate_part_number(metadata: MetaData, event: PartFieldCalculationEvent, service: Service): """ Example Function. - This function is triggered when a part fields should be calculated. - For "External" parts we want to set the part number as "E-000123". + This function is triggered when a part field should be calculated. + For "External" parts, we want to set the part number as "E-000123". All other parts should keep the standard part number. - """ if event.data.action != "create": - # part number can only be set when the part is created + # Part number can only be set when the part is created return - # match "External Single Part" or "External Assembly" + # Match "External Single Part" or "External Assembly" if event.data.part.t_kategorie_name_en.startswith("External"): - - # generate a new number using the service + # Generate a new number using the service new_number = service.generator.get_number("external_part_number") - # new_number is an integer, so we need to convert it to a string # and pad it with leading zeros to 6 digits new_part_number = str(new_number).zfill(6) - - # then add the prefix "E-" to the number + # Add the prefix "E-" to the number new_part_number = "E-" + new_part_number - - # finally we return the new part number (teilenummer) + # Return the new part number (teilenummer) return DataResponse(data={"teilenummer": new_part_number}) ``` !!! tip - You can check `event.data.action` to decide for which operations (*copy*,*create*,*index* and *modify*) you want your field calculation to return a new value. - Some fields, like part number (*teilenummer*) can only be set during the initial creation. + You can check `event.data.action` to decide for which operations (*copy*, *create*, *index*, and *modify*) you want your field calculation to return a new value. + Some fields, like part number (*teilenummer*), can only be set during the initial creation. ## Translate a field with DeepL -Inside Functions you can fetch data from external systems and fill out fields based on that data. This is something that would not be possible with the field calculations in the datasheet editor. You could use this for example to fetch new part numbers from an ERP system. +Inside Functions, you can fetch data from external systems and fill out fields based on that data. This is something that would not be possible with the field calculations in the datasheet editor. For example, you could use this to fetch new part numbers from an ERP system. -This example uses the API from [DeepL](https://www.deepl.com) to translate a field from German to English. The example uses the additional attributes 1 and 2 on parts, but you can of course change that to any attributes that fit your use-case. +This example uses the API from [DeepL](https://www.deepl.com) to translate a field from German to English. The example uses the additional attributes 1 and 2 on parts, but you can of course change that to any attributes that fit your use case. ```python import os @@ -69,13 +63,13 @@ from csfunctions import DataResponse from csfunctions.events import PartFieldCalculationEvent import requests -# set the DEEPL_API_KEY during deployment like this: +# Set the DEEPL_API_KEY during deployment like this: # cfc env deploy --environment-variables "DEEPL_API_KEY=" DEEPL_API_KEY = os.getenv("DEEPL_API_KEY") def part_field_calculation(metadata, event: PartFieldCalculationEvent, service): if event.data.action != "create": - # only translate on creation + # Only translate on creation return if event.data.part.cssaas_frame_add_attr_1: diff --git a/docs/examples/workflows.md b/docs/examples/workflows.md index ce6b500..b215971 100644 --- a/docs/examples/workflows.md +++ b/docs/examples/workflows.md @@ -1,19 +1,18 @@ # Working with workflows -Functions can interact with workflows. You can trigger Functions from within workflows using the [Trigger Webhook](https://saas-docs.contact-cloud.com/latest-en/admin/admin-contact_cloud/saas_admin/webhooks_workflow){:target="_blank"} task and you can even start new workflows by using the [StartWorkflowAction](../reference/actions.md#startworkflowaction)! +Functions can interact with workflows. You can trigger Functions from within workflows using the [Trigger Webhook](https://saas-docs.contact-cloud.com/latest-en/admin/admin-contact_cloud/saas_admin/webhooks_workflow){:target="_blank"} task, and you can even start new workflows by using the [StartWorkflowAction](../reference/actions.md#startworkflowaction)! -## Start workflow on EC status change +## Start a workflow on EC status change -This example shows you how to start a workflow template in response to an engineering change status change. +This example shows how to start a workflow template in response to an engineering change status change. !!! note - Starting workflows in response to engineering change status changes is already possible in CIM Database Cloud without the use of Functions. However Functions allow you to dynamically select different templates and fill out task parameters, based on the nature of the change. + Starting workflows in response to engineering change status changes is already possible in CIM Database Cloud without the use of Functions. However, Functions allow you to dynamically select different templates and fill out task parameters based on the nature of the change. -This example uses a very simple template, containing just an *information task*. If an engineering change contains external parts, users with the *External Part Manager* Role should be notified of the planned change during the evaluation phase. - -You can easily adapt this example to your use-case, by adding additional tasks to the template or changing the conditions under which the workflow should be started. +This example uses a very simple template containing just an *information task*. If an engineering change contains external parts, users with the *External Part Manager* role should be notified of the planned change during the evaluation phase. +You can easily adapt this example to your use case by adding additional tasks to the template or changing the conditions under which the workflow should be started. ```python from csfunctions.actions.start_workflow import ( @@ -24,7 +23,7 @@ from csfunctions.actions.start_workflow import ( from csfunctions.events import EngineeringChangeStatusChangedEvent from csfunctions import MetaData -# change these to match your template and roles!!! +# Change these to match your template and roles! TEMPLATE_ID = "PT00000002" INFORMATION_TASK_ID = "T00000008" INFORM_ROLE = "External Part Manager" @@ -34,27 +33,27 @@ def start_workflow_on_ec_status_change( metadata: MetaData, event: EngineeringChangeStatusChangedEvent, service ): if event.data.engineering_change.status != 30: - # only start the workflow if the status changed to 30 (Evaluation) + # Only start the workflow if the status changed to 30 (Evaluation) return - # check if the ec contains external parts + # Check if the EC contains external parts if not any( part.t_kategorie_name_en.startswith("External") for part in event.data.engineering_change.planned_changes_parts ): - # no external parts, so we don't need to start the workflow + # No external parts, so we don't need to start the workflow return return StartWorkflowAction( template_id=TEMPLATE_ID, title=f"Information about EC {event.data.engineering_change.cdb_ec_id}", - # attach the engineering change to the workflow + # Attach the engineering change to the workflow global_briefcase_object_ids=[ event.data.engineering_change.cdb_object_id], task_configurations=[ TaskConfiguration( task_id=INFORMATION_TASK_ID, - description="A an engineering change containing external parts moved to the evaluation phase.", + description="An engineering change containing external parts moved to the evaluation phase.", recipients=[ Subject( subject_type="Common Role", @@ -62,14 +61,12 @@ def start_workflow_on_ec_status_change( ) ], ) - ], ) ``` !!! note - To sucessfully execute this example you need to: - - - Create a workflow template with an information task and adjust the `TEMPLATE_ID` and `INFORMATION_TASK_ID` to match them + To successfully execute this example, you need to: - - Create and assign an "External Part Manager" role to a user + - Create a workflow template with an information task and adjust the `TEMPLATE_ID` and `INFORMATION_TASK_ID` to match them. + - Create and assign an "External Part Manager" role to a user. diff --git a/docs/index.md b/docs/index.md index 4a2290d..68d664c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,25 +2,25 @@ This SDK provides the **csfunctions** library for developing Functions with Python. -Functions are deeply integrated in the [CIM Database Cloud](https://www.cim-database-cloud.com){:target="_blank"} Webhooks technology. They are designed to work seamlessly together. The goal is to allow implementing custom business logic in a CIM Database Cloud SaaS application without leaving the CONTACT Cloud and without the need to create and maintain a separate infrastructure. +Functions are deeply integrated with the [CIM Database Cloud](https://www.cim-database-cloud.com){:target="_blank"} Webhooks technology. They are designed to work seamlessly together. The goal is to allow you to implement custom business logic in a CIM Database Cloud SaaS application without leaving CONTACT Cloud and without the need to create and maintain separate infrastructure. ## Requirements Python 3.10+ -csfunctions is build with [Pydantic 2](https://docs.pydantic.dev/latest/){:target="_blank"} +csfunctions is built with [Pydantic 2](https://docs.pydantic.dev/latest/){:target="_blank"}. ## Installation Install using pip: -``` sh +```bash pip install contactsoftware-functions ``` ## Usage ### Build the Function -Folder content of a minimal example for a Function implementation: +Folder contents of a minimal example for a Function implementation: -``` bash +```bash my_example_functions/ ├── environment.yaml ├── mymodule.py @@ -30,7 +30,7 @@ Folder content of a minimal example for a Function implementation: Code for a Function: -``` python title="mymodule.py" +```python title="mymodule.py" import requests import json @@ -38,9 +38,9 @@ from csfunctions import MetaData, Service from csfunctions.events import DocumentReleaseEvent def send_doc_to_erp(metadata: MetaData, event: DocumentReleaseEvent, service: Service): - # iterate over the documents contained in the event + # Iterate over the documents contained in the event for document in event.data.documents: - # create the payload for our (fictional ERP system) + # Create the payload for our (fictional ERP system) payload = json.dumps({ "document_number": document.z_nummer, "document_index": document.z_index, @@ -52,9 +52,9 @@ def send_doc_to_erp(metadata: MetaData, event: DocumentReleaseEvent, service: Se ``` -Environment file to define runtime and Function entrypoints: +Environment file to define runtime and Function entry points: -``` yaml title="environment.yaml" +```yaml title="environment.yaml" runtime: python3.10 version: v1 functions: @@ -65,12 +65,12 @@ functions: Define requirements: -``` python title="requirements.txt" +```python title="requirements.txt" contactsoftware-functions ``` ### Deploy the Code -To deploy the Code you first need to install the [contactsoftware-functions-client](https://pypi.org/project/contactsoftware-functions-client/){:target="_blank"} and retrieve developer credentials in the CONTACT Portal. +To deploy the code, you first need to install the [contactsoftware-functions-client](https://pypi.org/project/contactsoftware-functions-client/){:target="_blank"} and retrieve developer credentials in the CONTACT Portal. Install client: @@ -90,7 +90,7 @@ Create a new environment: cfc env create myenv ``` -Upload code into new environment: +Upload code into the new environment: ```bash cfc env deploy myenv From d391852e46f81058703962473fb969d78d8b2d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Fri, 23 May 2025 12:39:03 +0200 Subject: [PATCH 3/5] add missing target=_blank --- docs/development_server.md | 2 +- docs/examples/field_calculation.md | 2 +- docs/reference/runtime.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/development_server.md b/docs/development_server.md index d1eafff..08a9479 100644 --- a/docs/development_server.md +++ b/docs/development_server.md @@ -48,7 +48,7 @@ You can then copy the URL of the server and use it to connect your Functions to **ngrok and Cloudflare** -If you are developing Functions locally, you can use services like [ngrok](https://ngrok.com/) or [Cloudflare](https://cloudflare.com) to expose your server to the internet. +If you are developing Functions locally, you can use services like [ngrok](https://ngrok.com/){:target="_blank"} or [Cloudflare](https://cloudflare.com){:target="_blank"} to expose your server to the internet. Please refer to the documentation of the specific service for instructions on how to do this. diff --git a/docs/examples/field_calculation.md b/docs/examples/field_calculation.md index ea7d773..cbddd22 100644 --- a/docs/examples/field_calculation.md +++ b/docs/examples/field_calculation.md @@ -55,7 +55,7 @@ def calculate_part_number(metadata: MetaData, event: PartFieldCalculationEvent, Inside Functions, you can fetch data from external systems and fill out fields based on that data. This is something that would not be possible with the field calculations in the datasheet editor. For example, you could use this to fetch new part numbers from an ERP system. -This example uses the API from [DeepL](https://www.deepl.com) to translate a field from German to English. The example uses the additional attributes 1 and 2 on parts, but you can of course change that to any attributes that fit your use case. +This example uses the API from [DeepL](https://www.deepl.com){:target="_blank"} to translate a field from German to English. The example uses the additional attributes 1 and 2 on parts, but you can of course change that to any attributes that fit your use case. ```python import os diff --git a/docs/reference/runtime.md b/docs/reference/runtime.md index 17b86e1..cedbf06 100644 --- a/docs/reference/runtime.md +++ b/docs/reference/runtime.md @@ -38,4 +38,4 @@ The return value of the execute method is the json encoded response payload. ## Payloads -The Request and response payloads are described in the CIM Database Cloud documentation. The [functions-sdk-python](https://github.com/cslab/functions-sdk-python) GitHub repository also contains the complete [JSON-schema files](https://github.com/cslab/functions-sdk-python/tree/main/json_schemas). +The Request and response payloads are described in the CIM Database Cloud documentation. The [functions-sdk-python](https://github.com/cslab/functions-sdk-python){:target="_blank"} GitHub repository also contains the complete [JSON-schema files](https://github.com/cslab/functions-sdk-python/tree/main/json_schemas){:target="_blank"}. From f86e22dd1c7d38fbfe65748ec76e839578d35909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Fri, 23 May 2025 13:04:59 +0200 Subject: [PATCH 4/5] re-order sections --- mkdocs.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 6006b33..182a258 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,18 +38,18 @@ plugins: nav: - - Intro: index.md - - Key concepts: key_concepts.md + - Home: index.md - Getting started: getting_started.md + - Concepts: key_concepts.md + - Examples: + - examples/index.md + - examples/enforce_field_rules.md + - examples/field_calculation.md + - examples/workflows.md - Reference: - reference/events.md - reference/objects.md - reference/actions.md - development_server.md - Python runtime: reference/runtime.md - - Examples: - - examples/index.md - - examples/enforce_field_rules.md - - examples/field_calculation.md - - examples/workflows.md - - Release notes: release_notes.md + - Releases: release_notes.md From 80165cd2a8c0cae4b3070f52174477235b3b7cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Fri, 23 May 2025 13:05:09 +0200 Subject: [PATCH 5/5] remove toc from release note page --- docs/release_notes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release_notes.md b/docs/release_notes.md index 3477f18..0f16cfb 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -1,3 +1,8 @@ +--- +hide: + - toc +--- + ### Version 0.14.0: - Feat: Improve error logging when using the devserver - Feat: Add StartWorkflowAction