14 Commits

Author SHA1 Message Date
dependabot[bot] affecf10d9 Bump actions/setup-python from 4 to 5 (#1)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 09:12:26 +00:00
Jonathon Broughton 121758dd3c SDK Update 2024-01-09 09:12:01 +00:00
Jonathon Broughton c599bb57a5 Create LICENSE 2023-11-14 02:52:51 +00:00
Jonathon Broughton 666eb7df7e Memory and CPU boost
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2023-11-13 13:16:42 +00:00
Jonathon Broughton f077320638 demo ready 2023-11-13 12:22:09 +00:00
Jonathon Broughton 1f230df1e8 Caveat in README.md
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2023-11-12 23:25:37 +00:00
Jonathon Broughton 60e2d33ee2 Code Completion 2023-11-12 23:21:27 +00:00
Jonathon Broughton 1e3105e918 Refactor 2023-11-12 19:46:42 +00:00
Jonathon Broughton c1b8ffacc1 Docker caching enabled 2023-11-12 19:40:42 +00:00
Jonathon Broughton 4ab447f9ec Function Inputs (last one + 1 + 11) 2023-11-12 19:30:36 +00:00
Jonathon Broughton 94fefe56ca Function Inputs (last one + 1 + 1)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2023-11-12 17:32:07 +00:00
Jonathon Broughton ce4ee1cd46 Function Inputs (last one + 1)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2023-11-12 17:26:54 +00:00
Jonathon Broughton 75cf2a4ea7 Function Inputs (last one)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2023-11-12 17:11:02 +00:00
Jonathon Broughton f4053cd413 Function Inputs (last one) 2023-11-12 17:06:42 +00:00
19 changed files with 1507 additions and 568 deletions
-1
View File
@@ -1 +0,0 @@
SPECKLE_TOKEN=mytoken
+4 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install and configure Poetry
@@ -29,10 +29,12 @@ jobs:
run: |
python main.py generate_schema ${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}
- name: Speckle Automate Function - Build and Publish
uses: specklesystems/speckle-automate-github-composite-action@0.7.2
uses: specklesystems/speckle-automate-github-composite-action@0.7.4
with:
speckle_automate_url: ${{ env.SPECKLE_AUTOMATE_URL || 'https://automate.speckle.dev' }}
speckle_token: ${{ secrets.SPECKLE_FUNCTION_TOKEN }}
speckle_function_id: ${{ secrets.SPECKLE_FUNCTION_ID }}
speckle_function_input_schema_file_path: ${{ env.FUNCTION_SCHEMA_FILE_NAME }}
speckle_function_command: 'python -u main.py run'
speckle_function_recommended_cpu_m: 4000
speckle_function_recommended_memory_mi: 4000
+208
View File
@@ -0,0 +1,208 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 AEC Systems
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
NOTICE: Unless otherwise described, the code in this repository is
licensed under the license above. Some modules, extensions or code herein
might be otherwise licensed. This is indicated either in the root of the
containing folder under a different license file, or in the respective
file's header. If you have any questions, don't hesitate to get in touch
with us via [email](mailto:hello@speckle.systems).
+48 -19
View File
@@ -3,53 +3,70 @@
# Speckle Automate Function: Data Standards Checker with IDS and bsDD
## Overview
This repository contains the Data Standards Checker function for Speckle Automate, designed to validate AEC models against the Information Delivery Specification (IDS) and BuildingSMART Data Dictionary (bsDD) standards. It showcases the ability of Speckle to ensure that models adhere to these established data standards.
This repository contains the Data Standards Checker function for Speckle Automate, designed to validate AEC models
against the Information Delivery Specification (IDS) and BuildingSMART Data Dictionary (bsDD) standards. It showcases
the ability of Speckle to ensure that models adhere to these established data standards.
## ⚠️ Disclaimer: Conceptual Demonstration Only
**IMPORTANT: This function is a conceptual demonstration and not a functional implementation. It is intended to exhibit the possibilities of aligning AEC models with IDS and bsDD standards within Speckle Automate.**
**IMPORTANT**: This function is a conceptual demonstration and not a functional implementation. It is intended to
exhibit the possibilities of aligning AEC models with IDS and bsDD standards within Speckle Automate. **For
demonstration purposes, it currently checks a single category, property, and value.**
## Functionality
- **IDS and bsDD Compliance:** Validates models against IDS requirements and bsDD standards.
- **Automated Standard Checking:** Demonstrates the potential for automated compliance checks.
- **Model Data Alignment:** Ensures model data aligns with the specified standards for consistency and accuracy.
- **Reporting and Insights:** Generates reports detailing compliance and areas requiring attention.
### How It Works
The function analyzes AEC models in Speckle, comparing their elements and metadata against the requirements set by IDS and the classifications and properties defined in bsDD.
The function analyzes AEC models in Speckle, comparing their elements and metadata against the requirements set by IDS
and the classifications and properties defined in bsDD.
### Potential Use Cases
- **Quality Assurance:** Ensures model data quality and standard adherence.
- **Regulatory Compliance:** Assists in meeting industry-specific compliance requirements.
- **Data Integrity:** Maintains the integrity of model data throughout the project lifecycle.
## Getting Started
1. **Clone the Repository**: Set up this repository in your local or cloud environment.
2. **Install Dependencies**: Follow the instructions to install necessary dependencies.
3. **Configure and Run**: Set up your Speckle server connection and run the function for conceptual testing.
## Contributing
Contributions in the form of ideas, discussions, or potential enhancements are welcome. Please open issues or pull requests for any suggestions.
Contributions in the form of ideas, discussions, or potential enhancements are welcome. Please open issues or pull
requests for any suggestions.
## Contact
For more information or to provide feedback, please contact [Contact Information].
---
**Note:** This repository is intended for demonstration and discussion around standard compliance in Speckle Automate using IDS and bsDD.
**Note:** This repository is intended for demonstration and discussion around standard compliance in Speckle Automate
using IDS and bsDD.
## Using this Speckle Function
1. **Create a New Speckle Automation**: Set up in the Speckle dashboard.
2. **Configure the Function**: Choose the "Basic Clash Analysis" function.
3. **Run and Review**: Execute the function and review the clash reports.
1. [Register](https://automate.speckle.dev/) your Function with [Speckle Automate](https://automate.speckle.dev/) and select the Python template.
1. [Register](https://automate.speckle.dev/) your Function with [Speckle Automate](https://automate.speckle.dev/) and
select the Python template.
1. A new repository will be created in your GitHub account.
1. Make changes to your Function in `main.py`. See below for the Developer Requirements, and instructions on how to test.
1. To create a new version of your Function, create a new [GitHub release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) in your repository.
1. Make changes to your Function in `main.py`. See below for the Developer Requirements, and instructions on how to
test.
1. To create a new version of your Function, create a
new [GitHub release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository)
in your repository.
## Developer Requirements
@@ -64,11 +81,15 @@ The code can be tested locally by running `poetry run pytest`.
### Building and running the Docker Container Image
Running and testing your code on your own machine is a great way to develop your Function; the following instructions are a bit more in-depth and only required if you are having issues with your Function in GitHub Actions or on Speckle Automate.
Running and testing your code on your own machine is a great way to develop your Function; the following instructions
are a bit more in-depth and only required if you are having issues with your Function in GitHub Actions or on Speckle
Automate.
#### Building the Docker Container Image
Your code is packaged by the GitHub Action into the format required by Speckle Automate. This is done by building a Docker Image, which is then run by Speckle Automate. You can attempt to build the Docker Image yourself to test the building process locally.
Your code is packaged by the GitHub Action into the format required by Speckle Automate. This is done by building a
Docker Image, which is then run by Speckle Automate. You can attempt to build the Docker Image yourself to test the
building process locally.
To build the Docker Container Image, you will need to have [Docker](https://docs.docker.com/get-docker/) installed.
@@ -84,7 +105,9 @@ Once you have Docker running on your local machine:
#### Running the Docker Container Image
Once the image has been built by the GitHub Action, it is sent to Speckle Automate. When Speckle Automate runs your Function as part of an Automation, it will run the Docker Container Image. You can test that your Docker Container Image runs correctly by running it locally.
Once the image has been built by the GitHub Action, it is sent to Speckle Automate. When Speckle Automate runs your
Function as part of an Automation, it will run the Docker Container Image. You can test that your Docker Container Image
runs correctly by running it locally.
1. To then run the Docker Container Image, run the following command:
@@ -98,13 +121,19 @@ Once the image has been built by the GitHub Action, it is sent to Speckle Automa
Let's explain this in more detail:
`docker run --rm speckle_automate_python_example` tells Docker to run the Docker Container Image that we built earlier. `speckle_automate_python_example` is the name of the Docker Container Image that we built earlier. The `--rm` flag tells docker to remove the container after it has finished running, this frees up space on your machine.
`docker run --rm speckle_automate_python_example` tells Docker to run the Docker Container Image that we built
earlier. `speckle_automate_python_example` is the name of the Docker Container Image that we built earlier. The `--rm`
flag tells docker to remove the container after it has finished running, this frees up space on your machine.
The line `python -u main.py run` is the command that is run inside the Docker Container Image. The rest of the command is the arguments that are passed to the command. The arguments are:
The line `python -u main.py run` is the command that is run inside the Docker Container Image. The rest of the command
is the arguments that are passed to the command. The arguments are:
- `'{"projectId": "1234", "modelId": "1234", "branchName": "myBranch", "versionId": "1234", "speckleServerUrl": "https://speckle.xyz", "automationId": "1234", "automationRevisionId": "1234", "automationRunId": "1234", "functionId": "1234", "functionName": "my function", "functionLogo": "base64EncodedPng"}'` - the metadata that describes the automation and the function.
- `{}` - the input parameters for the function that the Automation creator is able to set. Here they are blank, but you can add your own parameters to test your function.
- `yourSpeckleServerAuthenticationToken` - the authentication token for the Speckle Server that the Automation can connect to. This is required to be able to interact with the Speckle Server, for example to get data from the Model.
- `'{"projectId": "1234", "modelId": "1234", "branchName": "myBranch", "versionId": "1234", "speckleServerUrl": "https://speckle.xyz", "automationId": "1234", "automationRevisionId": "1234", "automationRunId": "1234", "functionId": "1234", "functionName": "my function", "functionLogo": "base64EncodedPng"}'` -
the metadata that describes the automation and the function.
- `{}` - the input parameters for the function that the Automation creator is able to set. Here they are blank, but you
can add your own parameters to test your function.
- `yourSpeckleServerAuthenticationToken` - the authentication token for the Speckle Server that the Automation can
connect to. This is required to be able to interact with the Speckle Server, for example to get data from the Model.
## Resources
+44
View File
@@ -0,0 +1,44 @@
from abc import ABC, abstractmethod
from collections import defaultdict
from typing import Dict, List
from speckle_automate import AutomationContext
# Base class for defining actions to be taken on parameters in Speckle data.
class ParameterAction(ABC):
"""
A base class for creating actions that can be applied to parameters in
Speckle objects. This abstract class outlines the structure and mandates
the implementation of specific methods in derived classes.
"""
def __init__(self) -> None:
# Dictionary for tracking affected parameters. Key: parent object's ID,
# Value: list of affected parameter names.
self.affected_parameters: Dict[str, List[str]] = defaultdict(list)
@abstractmethod
def apply(self, parameter: Dict[str, str], parent_object: Dict[str, str]) -> None:
"""
Applies the specific logic of the action to a parameter.
Args:
parameter: The parameter to which the action is applied.
parent_object: The object that holds the parameter.
"""
pass
@abstractmethod
def report(self, automate_context: AutomationContext) -> None:
"""
Generates a report based on the results of applying the action.
Args:
automate_context: The context in which the automation is executed,
providing mechanisms for attaching results to the Speckle model.
"""
pass
# Further specific action classes can be defined here, inheriting from
# ParameterAction and implementing the abstract methods 'apply' and 'report'.
+137
View File
@@ -0,0 +1,137 @@
# Required imports
from typing import Callable, Dict, Union
from specklepy.objects import Base
# We're going to define a set of rules that will allow us to filter and
# process parameters in our Speckle objects. These rules will be encapsulated
# in a class called `ParameterRules`.
class BaseObjectRules:
"""
A collection of rules for processing parameters in Speckle objects.
This class provides static methods that return lambda functions. These
lambda functions serve as filters or conditions we can use in our main
processing logic. By encapsulating these rules, we can easily extend
or modify them in the future.
"""
@staticmethod
def speckle_type_rule(desired_type: str) -> Callable[[Base], bool]:
"""
Rule: Check if a parameter's speckle_type matches the desired type.
"""
return (
lambda parameter: getattr(parameter, "speckle_type", None) == desired_type
)
@staticmethod
def forbidden_prefix_rule(given_prefix: str) -> Callable[[Base], bool]:
"""
Rule: check if a parameter's name starts with a given prefix.
This is a simple check, but there could be more complex naming rules for parameters of
different types. For example, a rule that checks if a parameter's name starts with a given string
exists particularly within IFC where parameters are often prefixed with "Ifc" or "Pset".
"""
return lambda parameter: parameter.name.startswith(given_prefix)
# This example Automate function is for prefixed parameter removal. Additional example rules below follow the same
# pattern, but with different logic. In some instances there is a strong coupling between the action and the checking
# logic, and in others there is a looser coupling. Which is why I have defined the actions separately from the
# checking logic.
@staticmethod
def has_missing_value(parameter: Union[Base, Dict[str, str]]) -> bool:
"""
Rule: Missing Value Check.
The AEC industry often requires all parameters to have meaningful values.
This rule checks if a parameter is missing its value, potentially indicating
an oversight during data entry or transfer.
"""
return not getattr(parameter, "value")
@staticmethod
def has_default_value(parameter: Dict[str, str]) -> bool:
"""
Rule: Default Value Check.
Default values can sometimes creep into final datasets due to software defaults.
This rule identifies parameters that still have their default values, helping
to highlight areas where real, meaningful values need to be provided.
"""
return parameter.get("value") == "Default"
@staticmethod
def parameter_exists(parameter_name: str, parent_object: Dict[str, str]) -> bool:
"""
Rule: Parameter Existence Check.
For certain critical parameters, their mere presence (or lack thereof) is vital.
This rule verifies if a specific parameter exists within an object, allowing
teams to ensure that key data points are always present.
"""
return parameter_name in parent_object.get("parameters", {})
@staticmethod
def is_category(category: str) -> Callable[[Base], bool]:
"""
Rule: Category Check.
This rule checks if a parameter's category matches the desired category.
"""
return lambda parameter: getattr(parameter, "category", None) == category
@staticmethod
def parameter_name_is(parameter_name: str) -> Callable[[Base], bool]:
"""
Rule: Parameter Name Check.
This rule checks if a parameter's name matches the desired name.
"""
return (
lambda parameter: getattr(parameter, "name") is not None
and parameter.name == parameter_name
)
@staticmethod
def parameter_value_startswith(prefix: str) -> Callable[[Base], bool]:
"""
Rule: Parameter Name Starts With.
This rule checks if a parameter's name starts with a given prefix.
"""
return lambda parameter: parameter.name.startswith(prefix)
@staticmethod
def is_revit_parameter(parameter: Union[Base, Dict[str, str]]):
"""
Checks if a parameter is a Revit parameter.
This function checks if a parameter is a Revit parameter by checking if it
has a 'category' property.
"""
return (
getattr(parameter, "speckle_type", None)
== "Objects.BuiltElements.Revit.Parameter"
)
@staticmethod
def evaluate_parameter(parameter, function_inputs):
"""Evaluates a parameter and returns its evaluation state."""
if not BaseObjectRules.is_revit_parameter(parameter):
return None
if BaseObjectRules.has_missing_value(parameter):
return "missing"
value = getattr(parameter, "value", None)
if value is not None and isinstance(value, str) and value.startswith(function_inputs.single_rule):
return "passing"
else:
return "invalid"
+49
View File
@@ -0,0 +1,49 @@
from specklepy.objects.graph_traversal.traversal import TraversalRule, GraphTraversal
def get_data_traversal_rules() -> GraphTraversal:
"""
Generates traversal rules for navigating Speckle data structures.
This function defines and returns traversal rules tailored for Speckle data.
These rules are used to navigate and extract specific data properties
within complex Speckle data hierarchies.
It defines two main rules:
1. `display_value_rule`:
- Targets objects that have properties named either "displayValue" or
"@displayValue".
- Specifically focuses on objects with a 'speckle_type' containing
"Geometry".
- For such objects, the function looks to traverse their 'elements'
or '@elements' properties.
2. `default_rule`:
- A more general rule that applies to all objects.
- It aims to traverse all member names of an object while avoiding
deprecated members (a potential enhancement for the future).
Returns:
GraphTraversal: A GraphTraversal instance initialized with the
defined rules.
"""
display_value_property_aliases = {"displayValue", "@displayValue"}
elements_property_aliases = {"elements", "@elements"}
display_value_rule = TraversalRule(
[
lambda o: any(
getattr(o, alias, None) for alias in display_value_property_aliases
),
lambda o: "Geometry" in o.speckle_type,
],
lambda o: elements_property_aliases,
)
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(),
)
return GraphTraversal([display_value_rule, default_rule])
View File
+90
View File
@@ -0,0 +1,90 @@
"""
This helper module provides functions for flattening Speckle object trees and
extracting base objects along with their transformations. It's designed for AEC
professionals working with complex Speckle data structures.
"""
from collections.abc import Iterable
from typing import Optional, Tuple, List
from specklepy.objects import Base
from specklepy.objects.other import Instance, Transform
def flatten_base(base: Base, parent_type: str = None) -> Iterable[Base]:
"""
Flattens a Speckle object tree into an iterable of base objects.
Args:
base: The base object to flatten.
parent_type: The type of the parent object, if applicable.
Yields:
Base: A flattened base object, making complex hierarchies linear.
"""
if isinstance(base, Base):
base["parent_type"] = parent_type
# Handle collections of elements in the base object
if hasattr(base, "elements") and base.elements:
try:
for element in base.elements:
yield from flatten_base(element, base.speckle_type)
except KeyError:
pass
# Handle older Revit-specific patterns with '@Lines'
elif hasattr(base, "@Lines"):
categories = base.get_dynamic_member_names()
for category in categories:
if category.startswith("@"):
category_object: Base = getattr(base, category)[0]
yield from flatten_base(category_object, category_object.speckle_type)
else:
yield base
def extract_base_and_transform(
base: Base,
inherited_instance_id: Optional[str] = None,
transform_list: Optional[List[Transform]] = None,
) -> Tuple[Base, str, Optional[List[Transform]]]:
"""
Extracts `Base` objects and their transformations from Speckle data.
Args:
base: The starting point `Base` object for traversal.
inherited_instance_id: Inherited ID for objects without a unique one.
transform_list: List of transformations from parent to child objects.
Yields:
tuple: A `Base` object, its identifier, and applicable transforms.
"""
current_id = getattr(base, "id", inherited_instance_id)
transform_list = transform_list or []
if isinstance(base, Instance):
if base.transform:
transform_list.append(base.transform)
if base.definition:
yield from extract_base_and_transform(
base.definition, current_id, transform_list.copy()
)
else:
yield base, current_id, transform_list
# Process 'elements' and '@elements' in the base object
elements_attr = getattr(base, "elements", []) or getattr(base, "@elements", [])
for element in elements_attr:
if isinstance(element, Base):
yield from extract_base_and_transform(
element, current_id, transform_list.copy()
)
# Process '@'-prefixed properties in older Speckle data models
for attr_name in dir(base):
if attr_name.startswith("@"):
attr_value = getattr(base, attr_name)
if isinstance(attr_value, Base) and hasattr(attr_value, "elements"):
yield from extract_base_and_transform(
attr_value, current_id, transform_list.copy()
)
+32
View File
@@ -0,0 +1,32 @@
from Rules.checks import BaseObjectRules
# Function to get type and family based on conditions
def get_type_and_family(obj):
if getattr(obj, "speckle_type", None) == "Objects.Other.Revit.RevitInstance" and hasattr(obj, "definition"):
return getattr(obj.definition, "type", "Unknown"), getattr(obj.definition, "family", "Unknown")
return getattr(obj, "type", "Unknown"), getattr(obj, "family", "Unknown")
# Function to create object info
def create_object_info(obj, type_, family):
return {
"name": getattr(obj, "name", "Unknown"),
"type": type_,
"family": family,
"id": getattr(obj, "id", "Unknown"),
}
# Function to process parameters
def process_parameters(current_object, function_inputs):
parameters = getattr(current_object, "parameters", None)
if not parameters:
return
parameter_name_is = BaseObjectRules.parameter_name_is(function_inputs.single_property)
for parameter_key in getattr(parameters, 'get_dynamic_member_names', lambda: [])():
parameter = parameters[parameter_key]
if parameter_name_is(parameter):
return BaseObjectRules.evaluate_parameter(parameter, function_inputs)
+161
View File
@@ -0,0 +1,161 @@
import json
from typing import List, Dict
from fpdf import FPDF # To install: `pip install fpdf2`
def save_html_report(data: str, filename: str) -> None:
"""
Saves HTML content as a file, handy for viewing in web browsers.
Args:
data (str): HTML content.
filename (str): File path to save HTML content.
"""
with open(filename, "w") as file:
file.write(data)
def save_json_report(
data: Dict[str, List[Dict[str, str]]],
filename: str,
single_category: str,
single_property: str,
single_value: str,
) -> None:
"""
Saves data as JSON. Ideal for data exchange or further processing.
Args:
data: The structured data to save.
filename: Where to save the JSON file.
single_category: Assessment category.
single_property: Assessment criteria.
single_value: Assessment value rule.
"""
report_data = {
"Assessment Criteria": {
"Category": single_category,
"Property": single_property,
"Value": single_value,
},
"Results": data,
}
with open(filename, "w") as file:
json.dump(report_data, file, indent=4)
def generate_pdf_report(
data: Dict[str, List[Dict[str, str]]],
filename: str,
single_category: str,
single_property: str,
single_value: str,
) -> None:
"""
Generates a PDF report. Suitable for official documentation.
Args:
data: Data to be included in the report.
filename: PDF file to save.
single_category: Assessment category.
single_property: Assessment criteria.
single_value: Assessment value rule.
"""
pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", size=12)
pdf.cell(200, 10, txt="Report", ln=True, align="C")
criteria_info = f"Criteria: {single_category} - {single_property} - {single_value}"
pdf.cell(200, 10, txt=criteria_info, ln=True)
pdf.cell(200, 10, txt="Name | Type | Family | ID | Status", ln=True)
for status, objects in data.items():
for obj in objects:
obj_info = f"{obj['name']} | {obj['type']} | {obj['family']} | {obj['id']} | {status}"
pdf.cell(200, 10, txt=obj_info, ln=True)
pdf.output(filename)
def generate_html_report(
data: Dict[str, List[Dict[str, str]]],
single_category: str,
single_property: str,
single_value: str,
) -> str:
"""
Generates HTML content for the report. Easily styled and readable.
Args:
data: The data to display.
single_category: Assessment category.
single_property: Assessment criteria.
single_value: Assessment value rule.
"""
html_content = "<html><head><title>Report</title></head><body>"
criteria_header = (
f"<h1>Report: {single_category} - {single_property} - {single_value}</h1>"
)
html_content += criteria_header
html_content += "<table border='1'>"
html_content += (
"<tr><th>Name</th><th>Type</th><th>Family</th><th>ID</th><th>Status</th></tr>"
)
for status, objects in data.items():
for obj in objects:
row = (
f"<tr><td>{obj['name']}</td><td>{obj['type']}</td>"
f"<td>{obj['family']}</td><td>{obj['id']}</td><td>{status}</td></tr>"
)
html_content += row
html_content += "</table></body></html>"
return html_content
def generate_report(
assessed_objects: Dict[str, List[Dict[str, str]]],
report_format: str,
single_category: str,
single_property: str,
single_value: str,
) -> str:
"""
Main function to orchestrate report generation in various formats.
Args:
assessed_objects: Categorized assessment data.
report_format: The format to generate ('HTML', 'JSON', 'PDF').
single_category: Assessment category.
single_property: Assessment criteria.
single_value: Assessment value rule.
"""
report_filename = f"report.{report_format.lower()}"
if report_format == "HTML":
html_report = generate_html_report(
assessed_objects, single_category, single_property, single_value
)
save_html_report(html_report, report_filename)
elif report_format == "JSON":
save_json_report(
assessed_objects,
report_filename,
single_category,
single_property,
single_value,
)
elif report_format == "PDF":
generate_pdf_report(
assessed_objects,
report_filename,
single_category,
single_property,
single_value,
)
else:
raise ValueError("Unsupported report format")
return report_filename
-13
View File
@@ -1,13 +0,0 @@
"""Helper module for a simple speckle object tree flattening."""
from collections.abc import Iterable
from specklepy.objects import Base
def flatten_base(base: Base) -> Iterable[Base]:
"""Take a base and flatten it to an iterable of bases."""
if hasattr(base, "elements"):
for element in base["elements"]:
yield from flatten_base(element)
yield base
+145 -56
View File
@@ -1,6 +1,7 @@
"""This module contains the business logic of the function.
Use the automation_context module to wrap your function in an Autamate context helper
"""
This module contains the business logic for a Speckle Automate function.
It demonstrates how to define input models, traverse and process data,
and generate reports based on user-specified criteria.
"""
from enum import Enum
@@ -11,23 +12,45 @@ from speckle_automate import (
execute_automate_function,
)
from flatten import flatten_base
from Rules.checks import BaseObjectRules
from Rules.traversal import get_data_traversal_rules
from Utilities.helpers import process_parameters, get_type_and_family, create_object_info
from Utilities.report import generate_report
class ThresholdMode(Enum):
ERROR = 'ERROR'
WARN = 'WARN'
INFO = 'INFO'
"""
ThresholdMode: Defines different modes for reporting thresholds.
"""
ERROR = "ERROR"
WARN = "WARN"
INFO = "INFO"
class Format(Enum):
"""
Format: Enum for defining report formats.
"""
PDF = "PDF"
HTML = "HTML"
JSON = "JSON"
def create_one_of_enum(enum_cls):
return [
{"const": item.value, "title": item.name}
for item in enum_cls
]
"""
Helper function to create a JSON schema from an Enum class.
This is used for generating user input forms in the UI.
"""
return [{"const": item.value, "title": item.name} for item in enum_cls]
class FunctionInputs(AutomateBase):
"""These are function author defined values.
"""
FunctionInputs: Defines user inputs for the automation function.
The structure is based on Pydantic models for data validation.
Automate will make sure to supply them matching the types specified here.
Please use the pydantic model schema to define your inputs:
https://docs.pydantic.dev/latest/usage/models/
"""
@@ -37,22 +60,43 @@ class FunctionInputs(AutomateBase):
title="IDS XML File",
description="URL or content of the IDS XML file defining project standards. e.g. https://example.com/project_standards/ids.xml",
json_schema_extra={
"readOnly": True
"readOnly": True,
"label": "https://example.com/project_standards/ids.xml",
},
)
bsdd_sheets: str = Field(
"https://example.com/project_standards/bsdd.json",
title="bsDD Sheet Identifier(s)",
description="Identifier or URL for the bsDD sheet relevant to the project. e.g. https://example.com/project_standards/bsdd.json",
json_schema_extra={
"readOnly": True
}
"readOnly": True,
"label": "https://example.com/project_standards/bsdd.json",
},
)
report_format: str = Field(
default="PDF",
single_category: str = Field(
default="Windows",
title="Single Category for Demo",
description="For demonstration purposes only this is a single category. e.g. Windows.",
)
single_property: str = Field(
default="OmniClass Number",
title="Single Property for Demo",
description="For demonstration purposes only this is a single property. e.g. OmniClass Number.",
)
single_rule: str = Field(
default="23.30.20",
title="Rule for Demo",
description="For demonstration purposes only this is a single value for that property. e.g. Prefixed 23.30.20. ",
)
report_format: Format = Field(
default=Format.PDF,
title="Report Format",
description="Preferred format for the compliance report."
description="Preferred format for the compliance report. e.g. PDF, HTML, JSON.",
json_schema_extra={
"oneOf": create_one_of_enum(Format),
},
)
threshold_mode: ThresholdMode = Field(
default=ThresholdMode.ERROR,
@@ -60,59 +104,104 @@ class FunctionInputs(AutomateBase):
description="Set the threshold mode for reporting results: ERROR, WARN, or INFO.",
json_schema_extra={
"oneOf": create_one_of_enum(ThresholdMode),
}
},
)
def automate_function(
automate_context: AutomationContext,
function_inputs: FunctionInputs,
automate_context: AutomationContext,
function_inputs: FunctionInputs,
) -> None:
"""This is an example Speckle Automate function.
"""
The core logic of the Speckle Automate function.
Processes Speckle data and generates a report based on user inputs.
Args:
automate_context: A context helper object, that carries relevant information
about the runtime context of this function.
It gives access to the Speckle project data, that triggered this run.
It also has convenience methods attach result data to the Speckle model.
function_inputs: An instance object matching the defined schema.
automate_context: Context object with data and methods for the run.
function_inputs: User-defined input values.
"""
# the context provides a convenient way, to receive the triggering version
version_root_object = automate_context.receive_version()
objects_with_forbidden_speckle_type = [
b
for b in flatten_base(version_root_object)
if b.speckle_type == function_inputs.forbidden_speckle_type
]
count = len(objects_with_forbidden_speckle_type)
# Traverse the received Speckle data.
speckle_data = get_data_traversal_rules()
traversal_contexts_collection = speckle_data.traverse(version_root_object)
if count > 0:
# this is how a run is marked with a failure cause
automate_context.attach_error_to_objects(
category="Forbidden speckle_type"
" ({function_inputs.forbidden_speckle_type})",
object_ids=[o.id for o in objects_with_forbidden_speckle_type if o.id],
message="This project should not contain the type: "
f"{function_inputs.forbidden_speckle_type}",
)
automate_context.mark_run_failed(
"Automation failed: "
f"Found {count} object that have one of the forbidden speckle types: "
f"{function_inputs.forbidden_speckle_type}"
# Assuming each object has properties: name, type, and id
assessed_objects = {"missing": [], "invalid": [], "passing": []}
# Main loop for checking parameters
for context in traversal_contexts_collection:
current_object = context.current
is_category = BaseObjectRules.is_category(function_inputs.single_category)
if is_category(current_object) and hasattr(current_object, "parameters"):
assessment = process_parameters(current_object, function_inputs)
if assessment:
type_, family = get_type_and_family(current_object)
object_info = create_object_info(current_object, type_, family)
assessed_objects[assessment].append(object_info)
# Attach errors or info to objects based on their parameter evaluation state
for state, objects in assessed_objects.items():
ids = [obj["id"] for obj in objects if "id" in obj and obj["id"]]
if not ids:
continue
# Construct a detailed message for each object
detailed_messages = [
f"{obj['name']} (Type: {obj['type']}, ID: {obj['id']})"
for obj in objects
if "id" in obj and obj["id"]
]
# Combine messages into a single string
combined_message = (
f"Found {len(objects)} objects with {state} parameters: "
+ "; ".join(detailed_messages)
)
# set the automation context view, to the original model / version view
# to show the offending objects
automate_context.set_context_view()
if state in ["missing", "invalid"]:
automate_context.attach_error_to_objects(
category=state.capitalize(), object_ids=ids, message=combined_message
)
else: # 'valid'
automate_context.attach_info_to_objects(
category=state.capitalize(), object_ids=ids, message=combined_message
)
# Generate and attach the report
report_format = (
function_inputs.report_format.value
) # Accessing the value of the Enum
report_file = generate_report(
assessed_objects,
report_format,
function_inputs.single_category,
function_inputs.single_property,
function_inputs.single_rule,
)
automate_context.store_file_result(report_file)
print("Report file: ", report_file)
# Determine overall automation success or failure
if assessed_objects["missing"] or assessed_objects["invalid"]:
total_objects = len(assessed_objects["missing"]) + len(assessed_objects["invalid"]) + len(
assessed_objects["passing"])
pass_rate = len(assessed_objects["passing"]) / total_objects * 100
invalid_rate = len(assessed_objects["invalid"]) / total_objects * 100
missing_rate = len(assessed_objects["missing"]) / total_objects * 100
success_rating_message = f"Pass rate: {pass_rate:.2f}%, Invalid rate: {invalid_rate:.2f}%, Missing rate: {missing_rate:.2f}%"
print(success_rating_message)
automate_context.mark_run_failed("Automation failed due to parameter issues. " + success_rating_message)
else:
automate_context.mark_run_success("No forbidden types found.")
# if the function generates file results, this is how it can be
# attached to the Speckle project / model
# automate_context.store_file_result("./report.pdf")
automate_context.mark_run_success("All parameters are valid.")
# make sure to call the function with the executor
Generated
+466 -430
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,13 +1,14 @@
[tool.poetry]
name = "speckle-automate-py"
version = "0.1.0"
version = "0.1.3"
description = "Example function for Speckle Automate using specklepy"
authors = ["Gergő Jedlicska <gergo@jedlicska.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
specklepy = "2.17.11"
specklepy = "2.17.17"
fpdf = "^1.7.2"
[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
+45
View File
@@ -110,6 +110,8 @@ charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "4.0"
deprecated==1.2.14 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c \
--hash=sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3
fpdf==1.7.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:125840783289e7d12552b1e86ab692c37322e7a65b96a99e0ea86cca041b6779
gql[requests,websockets]==3.4.1 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:11dc5d8715a827f2c2899593439a4f36449db4f0eafa5b1ea63948f8a2f8c545 \
--hash=sha256:315624ca0f4d571ef149d455033ebd35e45c1a13f18a059596aeddcea99135cf
@@ -611,3 +613,46 @@ yarl==1.9.2 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7 \
--hash=sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78 \
--hash=sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7
yarl~=1.9.2
gql~=3.4.1
backoff~=2.2.1
multidict~=6.0.4
requests~=2.31.0
websockets~=10.4
pytest~=7.4.2
h11~=0.14.0
pip~=23.3.1
attrs~=23.1.0
wheel~=0.40.0
Pillow~=10.1.0
docutils~=0.20.1
sphinx~=7.0.1
Jinja2~=3.1.2
fpdf~=1.7.2
mypy~=1.6.1
filelock~=3.12.2
idna~=3.4
sniffio~=1.3.0
black~=23.10.0
platformdirs~=3.11.0
packaging~=23.2
pathspec~=0.11.2
click~=8.1.5
httpcore~=1.0.2
Pygments~=2.15.1
certifi~=2023.7.22
setuptools~=65.5.0
pluggy~=1.3.0
Deprecated~=1.2.14
iniconfig~=2.0.0
numpy~=1.25.2
anyio~=4.0.0
pydantic~=2.4.2
urllib3~=1.26.18
specklepy~=2.17.11
stringcase~=1.2.0
ujson~=5.8.0
wrapt~=1.16.0
httpx~=0.25.1
python-dotenv~=1.0.0
View File
+24
View File
@@ -0,0 +1,24 @@
import os
from dotenv import load_dotenv
def pytest_configure(config):
load_dotenv(dotenv_path=".env")
token_var = "SPECKLE_TOKEN"
server_var = "SPECKLE_SERVER_URL"
token = os.getenv(token_var)
server = os.getenv(server_var)
if not token:
raise ValueError(f"Cannot run tests without a {token_var} environment variable")
if not server:
raise ValueError(
f"Cannot run tests without a {server_var} environment variable"
)
# Set the token as an attribute on the config object
config.SPECKLE_TOKEN = token
config.SPECKLE_SERVER_URL = server
+51 -45
View File
@@ -1,19 +1,18 @@
"""Run integration tests with a speckle server."""
import os
import secrets
import string
import pytest
from gql import gql
from speckle_automate import (
AutomationContext,
AutomationRunData,
AutomationStatus,
run_function,
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport
from main import FunctionInputs, automate_function
@@ -25,12 +24,12 @@ def crypto_random_string(length: int) -> str:
def register_new_automation(
project_id: str,
model_id: str,
speckle_client: SpeckleClient,
automation_id: str,
automation_name: str,
automation_revision_id: str,
project_id: str,
model_id: str,
speckle_client: SpeckleClient,
automation_id: str,
automation_name: str,
automation_revision_id: str,
):
"""Register a new automation in the speckle server."""
query = gql(
@@ -67,19 +66,14 @@ def register_new_automation(
@pytest.fixture()
def speckle_token() -> str:
"""Provide a speckle token for the test suite."""
env_var = "SPECKLE_TOKEN"
token = os.getenv(env_var)
if not token:
raise ValueError(f"Cannot run tests without a {env_var} environment variable")
return token
def speckle_token(request) -> str:
return request.config.SPECKLE_TOKEN
@pytest.fixture()
def speckle_server_url() -> str:
def speckle_server_url(request) -> str:
"""Provide a speckle server url for the test suite, default to localhost."""
return os.getenv("SPECKLE_SERVER_URL", "http://127.0.0.1:3000")
return request.config.SPECKLE_SERVER_URL
@pytest.fixture()
@@ -101,23 +95,18 @@ def test_object() -> Base:
@pytest.fixture()
def automation_run_data(
test_object: Base, test_client: SpeckleClient, speckle_server_url: str
) -> AutomationRunData:
"""Set up an automation context for testing."""
project_id = test_client.stream.create("Automate function e2e test")
branch_name = "main"
# fixture to mock the AutomationRunData that would be generated by a full Automation Run
def fake_automation_run_data(request, test_client: SpeckleClient) -> AutomationRunData:
SERVER_URL = request.config.SPECKLE_SERVER_URL
TOKEN = request.config.SPECKLE_TOKEN
model = test_client.branch.get(project_id, branch_name, commits_limit=1)
model_id: str = model.id
project_id = "4f064f09e6"
model_id = "180a044971"
root_obj_id = operations.send(
test_object, [ServerTransport(project_id, test_client)]
)
version_id = test_client.commit.create(project_id, root_obj_id)
function_name = "Automate Density Check"
automation_name = crypto_random_string(10)
automation_id = crypto_random_string(10)
automation_name = "Local Test Automation"
automation_revision_id = crypto_random_string(10)
register_new_automation(
@@ -129,30 +118,47 @@ def automation_run_data(
automation_revision_id,
)
automation_run_id = crypto_random_string(10)
function_id = crypto_random_string(10)
function_revision = crypto_random_string(10)
return AutomationRunData(
fake_run_data = AutomationRunData(
project_id=project_id,
model_id=model_id,
branch_name=branch_name,
version_id=version_id,
speckle_server_url=speckle_server_url,
branch_name="main",
version_id="2729513a2d",
speckle_server_url=SERVER_URL,
# These ids would be available with a valid registered Automation definition.
automation_id=automation_id,
automation_revision_id=automation_revision_id,
automation_run_id=automation_run_id,
function_id=function_id,
function_revision=function_revision,
automation_run_id=crypto_random_string(12),
# These ids would be available with a valid registered Function definition. Can also be faked.
function_id="12345",
function_name=function_name,
function_logo=None,
)
return fake_run_data
def test_function_run(automation_run_data: AutomationRunData, speckle_token: str):
def test_function_run_fail(fake_automation_run_data: AutomationRunData, speckle_token: str):
"""Run an integration test for the automate function."""
context = AutomationContext.initialize(fake_automation_run_data, speckle_token)
automate_sdk = run_function(
context,
automate_function,
automation_run_data,
speckle_token,
FunctionInputs(forbidden_speckle_type="Base"),
FunctionInputs(single_category="Windows", single_property="OmniClass Number", single_rule="23.30.20.00",
report_format="JSON")
)
assert automate_sdk.run_status == AutomationStatus.FAILED
def test_function_run_pass(fake_automation_run_data: AutomationRunData, speckle_token: str):
"""Run an integration test for the automate function."""
context = AutomationContext.initialize(fake_automation_run_data, speckle_token)
automate_sdk = run_function(
context,
automate_function,
FunctionInputs(single_category="Windows", single_property="OmniClass Number", single_rule="23.30.20")
)
assert automate_sdk.run_status != AutomationStatus.FAILED