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/assets/connect_function.png b/docs/assets/connect_function.png new file mode 100644 index 0000000..6438f9c Binary files /dev/null and b/docs/assets/connect_function.png differ diff --git a/docs/assets/create_codespace.png b/docs/assets/create_codespace.png new file mode 100644 index 0000000..eb8c8d2 Binary files /dev/null and b/docs/assets/create_codespace.png differ diff --git a/docs/assets/display_function_credentials.png b/docs/assets/display_function_credentials.png new file mode 100644 index 0000000..d659150 Binary files /dev/null and b/docs/assets/display_function_credentials.png differ diff --git a/docs/assets/portal-user-menu.png b/docs/assets/portal-user-menu.png new file mode 100644 index 0000000..9033d2b Binary files /dev/null and b/docs/assets/portal-user-menu.png differ diff --git a/docs/assets/private_repo.png b/docs/assets/private_repo.png new file mode 100644 index 0000000..e08d462 Binary files /dev/null and b/docs/assets/private_repo.png differ diff --git a/docs/assets/use_template.png b/docs/assets/use_template.png new file mode 100644 index 0000000..0997e7a Binary files /dev/null and b/docs/assets/use_template.png differ diff --git a/docs/development_server.md b/docs/development_server.md index 805bd47..08a9479 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: @@ -46,12 +48,11 @@ 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. - -## 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 new file mode 100644 index 0000000..1b8f676 --- /dev/null +++ b/docs/examples/enforce_field_rules.md @@ -0,0 +1,98 @@ +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 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). 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 +from csfunctions.actions import AbortAndShowErrorAction +from csfunctions.events import ( + PartCreateCheckEvent, + PartModifyCheckEvent, +) + + +def single_part_needs_material( + metadata: MetaData, + event: PartCreateCheckEvent | PartModifyCheckEvent, + service: Service, +): + """ + 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 or modified + if part.t_kategorie_name_en == "Single Part" and not part.material_object_id: + return AbortAndShowErrorAction( + 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, 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. + +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 + + +def fetch_part_classification_property_codes(cdb_object_id: str, metadata: MetaData) -> 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 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 + 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..cbddd22 --- /dev/null +++ b/docs/examples/field_calculation.md @@ -0,0 +1,101 @@ +# Field calculation + +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 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 `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"}) +``` + + +## 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 new 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 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 + 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) + # Add the prefix "E-" to the number + new_part_number = "E-" + new_part_number + # 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. 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){: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 +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..b215971 --- /dev/null +++ b/docs/examples/workflows.md @@ -0,0 +1,72 @@ +# 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 a workflow on EC 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. + +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="An engineering change containing external parts moved to the evaluation phase.", + recipients=[ + Subject( + subject_type="Common Role", + subject_id=INFORM_ROLE, + ) + ], + ) + ], + ) +``` + +!!! note + To successfully 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..68d664c 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 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/) +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 contents 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 entry points: + +```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 the 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/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"}. 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 diff --git a/mkdocs.yml b/mkdocs.yml index 78586a5..182a258 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 - - Key concepts: key_concepts.md - Getting started: getting_started.md - - Development server: development_server.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 - - Release notes: release_notes.md + - Releases: 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