Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| affecf10d9 | |||
| 121758dd3c | |||
| c599bb57a5 | |||
| 666eb7df7e | |||
| f077320638 | |||
| 1f230df1e8 | |||
| 60e2d33ee2 | |||
| 1e3105e918 | |||
| c1b8ffacc1 | |||
| 4ab447f9ec | |||
| 94fefe56ca | |||
| ce4ee1cd46 | |||
| 75cf2a4ea7 | |||
| f4053cd413 |
@@ -1 +0,0 @@
|
||||
SPECKLE_TOKEN=mytoken
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user