Initial commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||
{
|
||||
"name": "Python 3",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||
},
|
||||
|
||||
"remoteEnv": {
|
||||
"SPECKLE_TOKEN": "foobar"
|
||||
},
|
||||
"containerEnv": {
|
||||
"SPECKLE_TOKEN": "asdfasdf"
|
||||
},
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "cp .env.example .env && POETRY_VIRTUALENVS_IN_PROJECT=true poetry install --no-root",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.python",
|
||||
"ms-python.black-formatter",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"mikestead.dotenv"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
SPECKLE_TOKEN=mytoken
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -0,0 +1,38 @@
|
||||
name: 'build and deploy Speckle functions'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
publish-automate-function-version: # make sure the action works on a clean machine without building
|
||||
env:
|
||||
FUNCTION_SCHEMA_FILE_NAME: functionSchema.json
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install and configure Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: 1.3.2
|
||||
virtualenvs-create: false
|
||||
virtualenvs-in-project: false
|
||||
installer-parallel: true
|
||||
- name: Restore dependencies
|
||||
run: poetry install --no-root
|
||||
- name: Extract functionInputSchema
|
||||
id: extract_schema
|
||||
run: |
|
||||
python 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.8.1
|
||||
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'
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,pycharm
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python,pycharm
|
||||
|
||||
function_inputs.json
|
||||
|
||||
**/.env
|
||||
**/.envrc
|
||||
|
||||
**/.tool-versions
|
||||
|
||||
### PyCharm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### PyCharm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
# Azure Toolkit for IntelliJ plugin
|
||||
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||
.idea/**/azureSettings.xml
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,pycharm
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Speckle Automate function",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "main.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"args": ["run", "function_inputs.json"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"camelcase",
|
||||
"pydantic",
|
||||
"stringcase",
|
||||
"typer"
|
||||
],
|
||||
"python.defaultInterpreterPath": ".venv/bin/python"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# We use the official Python 3.11 image as our base image and will add our code to it. For more details, see https://hub.docker.com/_/python
|
||||
FROM python:3.11-slim
|
||||
|
||||
# We install poetry to generate a list of dependencies which will be required by our application
|
||||
RUN pip install poetry
|
||||
|
||||
# We set the working directory to be the /home/speckle directory; all of our files will be copied here.
|
||||
WORKDIR /home/speckle
|
||||
|
||||
# Copy all of our code and assets from the local directory into the /home/speckle directory of the container.
|
||||
# We also ensure that the user 'speckle' owns these files, so it can access them
|
||||
# This assumes that the Dockerfile is in the same directory as the rest of the code
|
||||
COPY . /home/speckle
|
||||
|
||||
# Using poetry, we generate a list of requirements, save them to requirements.txt, and then use pip to install them
|
||||
RUN poetry export --format requirements.txt --output /home/speckle/requirements.txt && pip install --requirement /home/speckle/requirements.txt
|
||||
@@ -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).
|
||||
@@ -0,0 +1,176 @@
|
||||
# Speckle Automate Workshop: Python Function Template
|
||||
|
||||
This repository serves as a starting point for creating Speckle Automate
|
||||
functions in Python for the Automate workshops.
|
||||
It is based on
|
||||
the [Speckle Automate Python Function Template](https://github.com/specklesystems/speckle_automate_python_example)
|
||||
which
|
||||
is the preferred starting point for creating new
|
||||
Speckle Automate functions in Python.
|
||||
|
||||
## Getting started
|
||||
|
||||
This template will be available from the Speckle Automate New Function wizard.
|
||||
Select the Workshop icon to create a new repository from this template.
|
||||
|
||||
By default, the wizard will create a new repository in your GitHub account,
|
||||
and you will be able to start editing the code in the `main.py` file.
|
||||
|
||||
To create a new version of your Function, create a new GitHub release in your
|
||||
repository.
|
||||
This will trigger GitHub Action that builds, tests and deploys your function to
|
||||
Speckle Automate.
|
||||
|
||||
### Managing Dependencies with Poetry
|
||||
|
||||
Poetry simplifies dependency management for Python projects, ensuring consistent
|
||||
environments and hassle-free dependency
|
||||
resolution. Here's why we use Poetry:
|
||||
|
||||
Dependency Resolution: Poetry ensures compatible library versions, preventing
|
||||
conflicts.
|
||||
Virtual Environments: It manages project dependencies in isolated virtual
|
||||
environments.
|
||||
Lockfile: Generates a lockfile (poetry.lock) for reproducible builds.
|
||||
Simplified Installation: Adding dependencies is as easy as poetry add <
|
||||
package-name>.
|
||||
Dependency Isolation: Ensures project dependencies are self-contained and
|
||||
portable.
|
||||
Project Metadata: Manages project configuration in a single `pyproject.toml`
|
||||
file.
|
||||
|
||||
#### Adding Dependencies
|
||||
|
||||
To add new dependencies, use:
|
||||
|
||||
`$ poetry add <package-name>`
|
||||
|
||||
Replace <package-name> with the desired package. Poetry handles the rest,
|
||||
updating project files automatically.
|
||||
|
||||
Note: It's while it is a good practice to combine the use of Poetry with virtual
|
||||
environments to ensure a clean and
|
||||
isolated development environment for your Python projects, other tools like
|
||||
pipenv or venv can also be used. It is not
|
||||
mandatory to use a virtual environment for Speckle Automate functions.
|
||||
|
||||
### Configuring Launch Variables (Visual Studio Code)
|
||||
|
||||
To edit launch variables in Visual Studio Code, follow these steps:
|
||||
|
||||
Open the project in Visual Studio Code.
|
||||
Navigate to the .vscode directory.
|
||||
Open the launch.json file.
|
||||
Edit the configurations as needed.
|
||||
Save the file.
|
||||
These configurations specify how your Python script will be run and debugged
|
||||
within Visual Studio Code.
|
||||
|
||||
### GitHub Codespaces
|
||||
|
||||
Once you have created a clone of this template repo with the Automate wizard,
|
||||
you can use GitHub Codespaces to
|
||||
develop your function in the cloud. In the Codespaces environment, you can edit
|
||||
code, run tests, and debug your
|
||||
function.
|
||||
To open your repository in a Codespace, click the "Code" button in the GitHub UI
|
||||
and select "Open with Codespaces".
|
||||
|
||||
## Using this Speckle Function
|
||||
|
||||
1. [Create](https://automate.speckle.dev/) a new Speckle Automation.
|
||||
2. Select your Speckle Project and Speckle Model.
|
||||
3. Select the Speckle Function you created from this template.
|
||||
4. Enter the requested inputs. For first run this will be a phrase to use in a
|
||||
comment.
|
||||
5. Click `Create Automation`.
|
||||
|
||||
## Developer Requirements
|
||||
|
||||
1. Install the following:
|
||||
- [Python 3](https://www.python.org/downloads/) (>= 3.10)
|
||||
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
|
||||
2. Run `poetry shell && poetry install` to install the required Python packages.
|
||||
|
||||
## Building and Testing
|
||||
|
||||
The code can be tested locally by running `poetry run pytest`. The tests are
|
||||
located in the `tests` directory.
|
||||
The tests also allow for testing the function locally by mocking the Speckle
|
||||
Automate environment or using the specklepy
|
||||
authentication token to connect to a real Speckle Server and use real data.
|
||||
|
||||
### 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.
|
||||
|
||||
#### 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.
|
||||
|
||||
To build the Docker Container Image, you will need to
|
||||
have [Docker](https://docs.docker.com/get-docker/) installed.
|
||||
|
||||
Once you have Docker running on your local machine:
|
||||
|
||||
1. Open a terminal
|
||||
2. Navigate to the directory in which you cloned this repository
|
||||
3. 3.Run the following command:
|
||||
|
||||
```bash
|
||||
docker build -f ./Dockerfile -t speckle_automate_python_example .
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
1. To then run the Docker Container Image, run the following command:
|
||||
|
||||
```bash
|
||||
docker run --rm speckle_automate_python_example \
|
||||
python -u main.py run \
|
||||
'{"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"}' \
|
||||
'{}' \
|
||||
yourSpeckleServerAuthenticationToken
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Learn](https://speckle.guide/dev/python.html) more about `specklepy`, and
|
||||
interacting with Speckle from Python.
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Helper module for a speckle object tree flattening."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import Instance, Transform
|
||||
|
||||
|
||||
def speckle_print(log_string: str = "banana") -> None:
|
||||
|
||||
print("\033[92m" + str(log_string) + "\033[0m")
|
||||
|
||||
|
||||
def flatten_base(base: Base) -> Iterable[Base]:
|
||||
"""Flatten a base object into an iterable of bases."""
|
||||
elements = getattr(base, "elements", getattr(base, "@elements", None))
|
||||
if elements is not None:
|
||||
for element in elements:
|
||||
yield from flatten_base(element)
|
||||
yield base
|
||||
|
||||
|
||||
def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]:
|
||||
"""Take a base and flatten it to an iterable of bases.
|
||||
|
||||
Args:
|
||||
base: The base object to flatten.
|
||||
parent_type: The type of the parent object, if any.
|
||||
|
||||
Yields:
|
||||
Base: A flattened base object.
|
||||
"""
|
||||
if isinstance(base, Base):
|
||||
base["parent_type"] = parent_type
|
||||
|
||||
elements = getattr(base, "elements", getattr(base, "@elements", None))
|
||||
if elements:
|
||||
try:
|
||||
for element in elements:
|
||||
# Recursively yield flattened elements of the child
|
||||
yield from flatten_base_thorough(element, base.speckle_type)
|
||||
except KeyError:
|
||||
pass
|
||||
elif hasattr(base, "@Lines"):
|
||||
categories = base.get_dynamic_member_names()
|
||||
|
||||
# could be old revit
|
||||
try:
|
||||
for category in categories:
|
||||
print(category)
|
||||
if category.startswith("@"):
|
||||
category_object: Base = getattr(base, category)[0]
|
||||
yield from flatten_base_thorough(
|
||||
category_object, category_object.speckle_type
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
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]]]:
|
||||
"""
|
||||
Traverses Speckle object hierarchies to yield `Base` objects and their transformations.
|
||||
Tailored to Speckle's AEC data structures, it covers the newer hierarchical structures
|
||||
with Collections and also with patterns found in older Revit specific data.
|
||||
|
||||
Parameters:
|
||||
- base (Base): The starting point `Base` object for traversal.
|
||||
- inherited_instance_id (str, optional): The inherited identifier for `Base` objects without a unique ID.
|
||||
- transform_list (List[Transform], optional): Accumulated list of transformations from parent to child objects.
|
||||
|
||||
Yields:
|
||||
- tuple: A `Base` object, its identifier, and a list of applicable `Transform` objects or None.
|
||||
|
||||
The id of the `Base` object is either the inherited identifier for a definition from an instance
|
||||
or the one defined in the object.
|
||||
"""
|
||||
# Derive the identifier for the current `Base` object, defaulting to an inherited one if needed.
|
||||
current_id = getattr(base, "id", inherited_instance_id)
|
||||
transform_list = transform_list or []
|
||||
|
||||
if isinstance(base, Instance):
|
||||
# Append transformation data and dive into the definition of `Instance` objects.
|
||||
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:
|
||||
# Initial yield for the current `Base` object.
|
||||
yield base, current_id, transform_list
|
||||
|
||||
# Process 'elements' and '@elements', typical containers for `Base` objects in AEC models.
|
||||
elements_attr = getattr(base, "elements", []) or getattr(base, "@elements", [])
|
||||
for element in elements_attr:
|
||||
if isinstance(element, Base):
|
||||
# Recurse into each `Base` object within 'elements' or '@elements'.
|
||||
yield from extract_base_and_transform(
|
||||
element, current_id, transform_list.copy()
|
||||
)
|
||||
|
||||
# Recursively process '@'-prefixed properties that are Base objects with 'elements'.
|
||||
# This is a common pattern in older Speckle data models, such as those used for Revit commits.
|
||||
for attr_name in dir(base):
|
||||
if attr_name.startswith("@"):
|
||||
attr_value = getattr(base, attr_name)
|
||||
# If the attribute is a Base object containing 'elements', recurse into it.
|
||||
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,18 @@
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def read_rules_from_spreadsheet(url):
|
||||
"""Reads a TSV file from a provided URL and returns a DataFrame.
|
||||
|
||||
Args:
|
||||
url (str): The URL to the TSV file.
|
||||
|
||||
Returns:
|
||||
DataFrame: Pandas DataFrame containing the TSV data.
|
||||
"""
|
||||
try:
|
||||
# Since the output is a TSV, we use `pd.read_csv` with `sep='\t'` to specify tab-separated values.
|
||||
return pd.read_csv(url, sep="\t")
|
||||
except Exception as e:
|
||||
print(f"Failed to read the TSV from the URL: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,70 @@
|
||||
from pydantic import Field
|
||||
from speckle_automate import AutomationContext, AutomateBase
|
||||
from Utilities.helpers import flatten_base
|
||||
import random
|
||||
|
||||
|
||||
class FunctionInputs(AutomateBase):
|
||||
"""These are function author defined values.
|
||||
|
||||
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/
|
||||
"""
|
||||
|
||||
comment_phrase: str = Field(
|
||||
title="Comment Phrase",
|
||||
description="This phrase will be added to a random model element.",
|
||||
)
|
||||
|
||||
|
||||
def automate_function(
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
) -> None:
|
||||
"""This is an example Speckle Automate function.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# the context provides a convenient way, to receive the triggering version
|
||||
version_root_object = automate_context.receive_version()
|
||||
|
||||
flat_list_of_objects = flatten_base(version_root_object)
|
||||
|
||||
# filter the list to only include objects that are displayable.
|
||||
# this is a simple example, that checks if the object has a displayValue
|
||||
displayable_objects = [
|
||||
speckle_object
|
||||
for speckle_object in flat_list_of_objects
|
||||
if (
|
||||
getattr(speckle_object, "displayValue", None)
|
||||
or getattr(speckle_object, "@displayValue", None)
|
||||
)
|
||||
and getattr(speckle_object, "id", None) is not None
|
||||
]
|
||||
|
||||
if len(displayable_objects) == 0:
|
||||
automate_context.mark_run_failed(
|
||||
"Automation failed: No displayable objects found."
|
||||
)
|
||||
|
||||
else:
|
||||
# select a random object from the list
|
||||
random_object = random.choice(displayable_objects)
|
||||
|
||||
automate_context.attach_info_to_objects(
|
||||
category="Selected Object",
|
||||
object_ids=[random_object.id],
|
||||
message=function_inputs.comment_phrase,
|
||||
)
|
||||
|
||||
automate_context.mark_run_success("Added a comment to a random object.")
|
||||
|
||||
# set the automation context view, to the original model / version view
|
||||
automate_context.set_context_view()
|
||||
@@ -0,0 +1,139 @@
|
||||
from pydantic import Field
|
||||
from speckle_automate import AutomationContext, AutomateBase
|
||||
from Utilities.helpers import flatten_base, speckle_print
|
||||
import random
|
||||
|
||||
|
||||
class FunctionInputs(AutomateBase):
|
||||
"""These are function author defined values.
|
||||
|
||||
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/
|
||||
"""
|
||||
|
||||
comment_phrase: str = Field(
|
||||
title="Comment Phrase",
|
||||
description="This phrase will be added to a random model element.",
|
||||
)
|
||||
|
||||
# We now want to specify the number of elements to which the comment phrase will be added.
|
||||
number_of_elements: int = Field(
|
||||
title="Number of Elements",
|
||||
description="The number of elements to which the comment phrase will be added.",
|
||||
)
|
||||
|
||||
|
||||
def automate_function(
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
) -> None:
|
||||
"""This is an example Speckle Automate function.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# the context provides a convenient way, to receive the triggering version
|
||||
version_root_object = automate_context.receive_version()
|
||||
|
||||
flat_list_of_objects = list(flatten_base(version_root_object))
|
||||
|
||||
# filter the list to only include objects that are displayable.
|
||||
# this is a simple example, that checks if the object has a displayValue
|
||||
displayable_objects = [
|
||||
speckle_object
|
||||
for speckle_object in flat_list_of_objects
|
||||
if (
|
||||
getattr(speckle_object, "displayValue", None)
|
||||
or getattr(speckle_object, "@displayValue", None)
|
||||
)
|
||||
and getattr(speckle_object, "id", None) is not None
|
||||
]
|
||||
|
||||
# a better displayable_objects should also include those instance objects that have a definition property
|
||||
# that cross-references to a speckle id, that is in turn displayable, so we need to add those objects to the list
|
||||
displayable_objects += [
|
||||
instance_object
|
||||
for instance_object in flat_list_of_objects
|
||||
if (
|
||||
getattr(instance_object, "definition", None)
|
||||
and (
|
||||
(
|
||||
getattr(
|
||||
getattr(instance_object, "definition"), "displayValue", None
|
||||
)
|
||||
or getattr(
|
||||
getattr(instance_object, "definition"), "@displayValue", None
|
||||
)
|
||||
)
|
||||
and getattr(getattr(instance_object, "definition"), "id", None)
|
||||
is not None
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
if len(displayable_objects) == 0:
|
||||
automate_context.mark_run_failed(
|
||||
"Automation failed: No displayable objects found."
|
||||
)
|
||||
|
||||
else:
|
||||
# select a random object from the list
|
||||
# random_object = random.choice(displayable_objects)
|
||||
|
||||
# instead of a single object we will select a random subset of displayable objects from the provided dataset
|
||||
real_number_of_elements = min(
|
||||
# We cant take more elements than we have
|
||||
function_inputs.number_of_elements,
|
||||
len(displayable_objects),
|
||||
)
|
||||
|
||||
selected_objects = random.sample(
|
||||
displayable_objects,
|
||||
real_number_of_elements,
|
||||
)
|
||||
|
||||
# create a list of object ids for all selected objects
|
||||
selected_object_ids = [obj.id for obj in selected_objects]
|
||||
|
||||
# ACTIONS
|
||||
|
||||
# attach comment phrase to all selected objects
|
||||
# it is possible to attach the same comment phrase to multiple objects
|
||||
# the category "Selected Objects" is used to group the objects in the viewer
|
||||
# grouping results in this way is a clean way to organize the objects in the viewer
|
||||
comment_message = f"{function_inputs.comment_phrase}"
|
||||
automate_context.attach_info_to_objects(
|
||||
category="Selected Objects",
|
||||
object_ids=selected_object_ids,
|
||||
message=comment_message,
|
||||
)
|
||||
|
||||
# attach index as gradient value for all selected objects. this will be used for visualisation purposes
|
||||
# the category "Index Visualisation" is used to group the objects in the viewer
|
||||
gradient_values = {
|
||||
object_id: {"gradientValue": index + 1}
|
||||
for index, object_id in enumerate(selected_object_ids)
|
||||
}
|
||||
|
||||
automate_context.attach_info_to_objects(
|
||||
category="Index Visualisation",
|
||||
metadata={
|
||||
"gradient": True,
|
||||
"gradientValues": gradient_values,
|
||||
},
|
||||
message="Object Indexes",
|
||||
object_ids=selected_object_ids,
|
||||
)
|
||||
|
||||
automate_context.mark_run_success(
|
||||
f"Added comment to {real_number_of_elements} random objects."
|
||||
)
|
||||
|
||||
# set the automation context view, to the original model / version view
|
||||
automate_context.set_context_view()
|
||||
@@ -0,0 +1,134 @@
|
||||
from pydantic import Field
|
||||
from speckle_automate import AutomationContext, AutomateBase
|
||||
from Utilities.helpers import flatten_base, speckle_print
|
||||
|
||||
from Workshop.Exercise_3.rules import RevitRules
|
||||
|
||||
|
||||
class FunctionInputs(AutomateBase):
|
||||
"""These are function author defined values.
|
||||
|
||||
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/
|
||||
"""
|
||||
|
||||
# In this exercise, we will add two new input fields to the FunctionInputs class.
|
||||
category: str = Field(
|
||||
title="Revit Category",
|
||||
description="This is the category objects to check.",
|
||||
)
|
||||
property: str = Field(
|
||||
title="Property Name",
|
||||
description="This is the property to check.",
|
||||
)
|
||||
|
||||
|
||||
def automate_function(
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
) -> None:
|
||||
"""This version of the function will add a check for the new provide 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.
|
||||
"""
|
||||
|
||||
# the context provides a convenient way, to receive the triggering version
|
||||
version_root_object = automate_context.receive_version()
|
||||
|
||||
# We can continue to work with a flattened list of objects.
|
||||
flat_list_of_objects = list(flatten_base(version_root_object))
|
||||
|
||||
# filter to only include objects that are in the specified category
|
||||
in_category_objects = [
|
||||
speckle_object
|
||||
for speckle_object in flat_list_of_objects
|
||||
if RevitRules.is_category(speckle_object, function_inputs.category)
|
||||
]
|
||||
|
||||
# check if the property exists on the objects
|
||||
non_property_objects = [
|
||||
obj
|
||||
for obj in in_category_objects
|
||||
if not RevitRules.has_parameter(obj, function_inputs.property)
|
||||
]
|
||||
|
||||
property_objects = [
|
||||
obj
|
||||
for obj in in_category_objects
|
||||
if RevitRules.has_parameter(obj, function_inputs.property)
|
||||
]
|
||||
|
||||
# property_objects should be those where while the property is present,
|
||||
# is not an empty string or the default value
|
||||
valid_property_objects = [
|
||||
obj
|
||||
for obj in property_objects
|
||||
if RevitRules.get_parameter_value(obj, function_inputs.property)
|
||||
not in ["", "Default", None]
|
||||
]
|
||||
|
||||
for obj in valid_property_objects:
|
||||
speckle_print(RevitRules.get_parameter_value(obj, function_inputs.property))
|
||||
|
||||
# invalid_property_objects property_objects not in valid_property_objects
|
||||
invalid_property_objects = [
|
||||
obj for obj in property_objects if obj not in valid_property_objects
|
||||
]
|
||||
|
||||
# mark all the non-property objects as failed
|
||||
|
||||
(
|
||||
automate_context.attach_error_to_objects(
|
||||
category=f"Missing Property {function_inputs.category} Objects",
|
||||
object_ids=[obj.id for obj in non_property_objects],
|
||||
message=f"This {function_inputs.category} does not have the specified property {function_inputs.property}",
|
||||
)
|
||||
if non_property_objects
|
||||
else None
|
||||
)
|
||||
|
||||
# mark all the invalid property objects as warning
|
||||
(
|
||||
automate_context.attach_warning_to_objects(
|
||||
category=f"Invalid Property {function_inputs.category} Objects",
|
||||
object_ids=[obj.id for obj in invalid_property_objects],
|
||||
message=f"This {function_inputs.category} has the specified property {function_inputs.property} but it is "
|
||||
f"empty or default",
|
||||
)
|
||||
if invalid_property_objects
|
||||
else None
|
||||
)
|
||||
|
||||
# mark all the property objects as successful
|
||||
(
|
||||
automate_context.attach_info_to_objects(
|
||||
category=f"Valid Property {function_inputs.category} Objects",
|
||||
object_ids=[obj.id for obj in property_objects],
|
||||
message=f"This {function_inputs.category} has the specified property {function_inputs.property}",
|
||||
)
|
||||
if property_objects
|
||||
else None
|
||||
)
|
||||
|
||||
if len(non_property_objects) > 0:
|
||||
automate_context.mark_run_failed(
|
||||
"Some objects do not have the specified property."
|
||||
)
|
||||
elif len(invalid_property_objects) > 0:
|
||||
automate_context.mark_run_success(
|
||||
"Some objects have the specified property but it is empty or default.",
|
||||
)
|
||||
|
||||
else:
|
||||
automate_context.mark_run_success(
|
||||
f"All {function_inputs.category} objects have the {function_inputs.property} property."
|
||||
)
|
||||
|
||||
# set the automation context view, to the original model / version view
|
||||
automate_context.set_context_view()
|
||||
@@ -0,0 +1,529 @@
|
||||
from typing import List, Optional, Tuple, Callable, Dict, Any, cast, Union
|
||||
from specklepy.objects.base import Base
|
||||
from Levenshtein import ratio
|
||||
import re
|
||||
|
||||
# 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 `Rules`. We'll also define a set of rules specific to Revit
|
||||
# objects in a class called `RevitRules`.
|
||||
|
||||
|
||||
class Rules:
|
||||
"""
|
||||
A collection of rules for processing properties in Speckle objects.
|
||||
|
||||
Simple rules can be straightforwardly implemented as static methods that
|
||||
return boolean value to be used either as a filter or a condition.
|
||||
These can then be abstracted into returning lambda functions that we can
|
||||
use in our main processing logic. By encapsulating these rules, we can easily
|
||||
extend or modify them in the future.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def try_get_display_value(
|
||||
speckle_object: Base,
|
||||
) -> Optional[List[Base]]:
|
||||
"""Try fetching the display value from a Speckle object.
|
||||
|
||||
This method encapsulates the logic for attempting to retrieve the display value from a Speckle object.
|
||||
It returns a list containing the display values if found, otherwise it returns None.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to extract the display value from.
|
||||
|
||||
Returns:
|
||||
Optional[List[Base]]: A list containing the display values. If no display value is found,
|
||||
returns None.
|
||||
"""
|
||||
# Attempt to get the display value from the speckle_object
|
||||
raw_display_value = getattr(speckle_object, "displayValue", None) or getattr(
|
||||
speckle_object, "@displayValue", None
|
||||
)
|
||||
|
||||
# If no display value found, return None
|
||||
if raw_display_value is None:
|
||||
return None
|
||||
|
||||
# If display value found, filter out non-Base objects
|
||||
display_values = [
|
||||
value for value in raw_display_value if isinstance(value, Base)
|
||||
]
|
||||
|
||||
# If no valid display values found, return None
|
||||
if not display_values:
|
||||
return None
|
||||
|
||||
return display_values
|
||||
|
||||
@staticmethod
|
||||
def is_displayable_object(speckle_object: Base) -> bool:
|
||||
"""
|
||||
Determines if a given Speckle object is displayable.
|
||||
|
||||
This method encapsulates the logic for determining if a Speckle object is displayable.
|
||||
It checks if the speckle_object has a display value and returns True if it does, otherwise it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the object has a display value, False otherwise.
|
||||
"""
|
||||
# Check if the speckle_object has a display value using the try_get_display_value method
|
||||
display_values = Rules.try_get_display_value(speckle_object)
|
||||
if display_values and getattr(speckle_object, "id", None) is not None:
|
||||
return True
|
||||
|
||||
# Check for displayable state via definition, using try_get_display_value on the definition object
|
||||
definition = getattr(speckle_object, "definition", None)
|
||||
if definition:
|
||||
definition_display_values = Rules.try_get_display_value(definition)
|
||||
if (
|
||||
definition_display_values
|
||||
and getattr(definition, "id", None) is not None
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# Below are more speculatively defined rules that could be used in a traversal of flat list parsing
|
||||
|
||||
@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 prop: getattr(prop, "speckle_type", None) == desired_type
|
||||
|
||||
@staticmethod
|
||||
def is_speckle_type(prop: Base, desired_type: str) -> bool:
|
||||
"""
|
||||
Rule: Check if a parameter's speckle_type matches the desired type.
|
||||
"""
|
||||
return getattr(prop, "speckle_type", None) == desired_type
|
||||
|
||||
@staticmethod
|
||||
def has_missing_value(prop: 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 prop.get("value")
|
||||
|
||||
@staticmethod
|
||||
def has_default_value(prop: Dict[str, str], default="Default") -> 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 prop.get("value") == default
|
||||
|
||||
@staticmethod
|
||||
def parameter_exists(prop_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 prop_name in parent_object.get("parameters", {})
|
||||
|
||||
|
||||
def get_displayable_objects(flat_list_of_objects: List[Base]) -> List[Base]:
|
||||
# modify this lambda from before to use the static method from the Checks class
|
||||
return [
|
||||
speckle_object
|
||||
for speckle_object in flat_list_of_objects
|
||||
if Rules.is_displayable_object(speckle_object)
|
||||
and getattr(speckle_object, "id", None)
|
||||
]
|
||||
|
||||
# and the same logic that could be modified to traverse a tree of objects
|
||||
|
||||
|
||||
# Now we're going to define a set of rules that are specific to Revit objects.
|
||||
class RevitRules:
|
||||
@staticmethod
|
||||
def has_parameter(speckle_object: Base, parameter_name: str) -> bool:
|
||||
"""
|
||||
Checks if the speckle_object has a Revit parameter with the given name.
|
||||
|
||||
This method checks if the speckle_object has a parameter with the specified name,
|
||||
considering the following cases:
|
||||
1. The parameter is a named property at the root object level.
|
||||
2. The parameter is stored as a key in the "parameters" dictionary.
|
||||
3. The parameter is stored as a nested dictionary within the "parameters" property,
|
||||
and the parameter name is stored as the value of the "name" property within each nested dictionary.
|
||||
|
||||
If the parameter exists, it returns True; otherwise, it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check for.
|
||||
|
||||
Returns:
|
||||
bool: True if the object has the parameter, False otherwise.
|
||||
"""
|
||||
if hasattr(speckle_object, parameter_name):
|
||||
return True
|
||||
|
||||
parameters = cast(Base, getattr(speckle_object, "parameters", None))
|
||||
|
||||
if parameters is None:
|
||||
return False
|
||||
|
||||
# the parameters object can function like a dict but isn't one.
|
||||
# convert a Base object to a dict
|
||||
parameters_dict = {}
|
||||
|
||||
for parameter_key in parameters.get_dynamic_member_names():
|
||||
parameters_dict[parameter_key] = getattr(parameters, parameter_key, None)
|
||||
|
||||
if parameter_name in parameters_dict:
|
||||
return True
|
||||
|
||||
return any(
|
||||
getattr(param_value, "name", None) == parameter_name
|
||||
for param_value in parameters_dict.values()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_parameter_value(
|
||||
speckle_object: Base,
|
||||
parameter_name: str,
|
||||
default_value: Any = None,
|
||||
) -> Any | None:
|
||||
"""
|
||||
Retrieves the value of the specified Revit parameter from the speckle_object.
|
||||
|
||||
This method checks if the speckle_object has a parameter with the specified name,
|
||||
considering the following cases:
|
||||
1. The parameter is a named property at the root object level.
|
||||
2. The parameter is stored as a key in the "parameters" dictionary.
|
||||
3. The parameter is stored as a nested dictionary within the "parameters" property,
|
||||
and the parameter name is stored as the value of the "name" property within each nested dictionary.
|
||||
|
||||
If the parameter exists and its value is not None or the specified default_value, it returns the value.
|
||||
If the parameter does not exist or its value is None or the specified default_value, it returns None.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to retrieve the parameter value from.
|
||||
parameter_name (str): The name of the parameter to retrieve the value for.
|
||||
default_value: The default value to compare against. If the parameter value matches this value,
|
||||
it will be treated the same as None.
|
||||
|
||||
Returns:
|
||||
The value of the parameter if it exists and is not None or the specified default_value, or None otherwise.
|
||||
"""
|
||||
# Attempt to retrieve the parameter from the root object level
|
||||
value = getattr(speckle_object, parameter_name, None)
|
||||
if value not in [None, default_value]:
|
||||
return value
|
||||
|
||||
# If the "parameters" attribute is a Base object, extract its dynamic members
|
||||
parameters = getattr(speckle_object, "parameters", None)
|
||||
if parameters is None:
|
||||
return None
|
||||
|
||||
# Prepare a dictionary of parameter values from the dynamic members of the parameters attribute
|
||||
parameters_dict = {
|
||||
key: getattr(parameters, key)
|
||||
for key in parameters.get_dynamic_member_names()
|
||||
}
|
||||
|
||||
# Search for a direct match or a nested match in the parameters dictionary
|
||||
param_value = parameters_dict.get(parameter_name)
|
||||
if param_value is not None:
|
||||
if isinstance(param_value, Base):
|
||||
# Extract the nested value from a Base object if available
|
||||
nested_value = getattr(param_value, "value", None)
|
||||
if nested_value not in [None, default_value]:
|
||||
return nested_value
|
||||
elif param_value not in [None, default_value]:
|
||||
return param_value
|
||||
|
||||
# Use a generator to find the first matching 'value' for shared parameters stored in Base objects
|
||||
return next(
|
||||
(
|
||||
getattr(p, "value", None)
|
||||
for p in parameters_dict.values()
|
||||
if isinstance(p, Base) and getattr(p, "name", None) == parameter_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value(
|
||||
speckle_object: Base, parameter_name: str, value_to_match: Any
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter matches the given value.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
value_to_match (Any): The value to match against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value matches the given value, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return parameter_value == value_to_match
|
||||
|
||||
@staticmethod
|
||||
def is_like_parameter_value(
|
||||
speckle_object: Base,
|
||||
parameter_name: str,
|
||||
pattern: str,
|
||||
fuzzy: bool = False,
|
||||
threshold: float = 0.8,
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter matches the given pattern.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
pattern (str): The pattern to match against.
|
||||
fuzzy (bool): If True, performs fuzzy matching using Levenshtein distance.
|
||||
If False (default), performs exact pattern matching using regular expressions.
|
||||
threshold (float): The similarity threshold for fuzzy matching (default: 0.8).
|
||||
Only applicable when fuzzy=True.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value matches the pattern (exact or fuzzy), False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
|
||||
if fuzzy:
|
||||
similarity = ratio(str(parameter_value), pattern)
|
||||
return similarity >= threshold
|
||||
else:
|
||||
return bool(re.match(pattern, str(parameter_value)))
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_greater_than(
|
||||
speckle_object: Base, parameter_name: str, threshold: Union[int, float]
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is greater than the given threshold.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
threshold (Union[int, float]): The threshold value to compare against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is greater than the threshold, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Parameter value must be a number, got {type(parameter_value)}"
|
||||
)
|
||||
return parameter_value > threshold
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_less_than(
|
||||
speckle_object: Base, parameter_name: str, threshold: Union[int, float]
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is less than the given threshold.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
threshold (Union[int, float]): The threshold value to compare against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is less than the threshold, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Parameter value must be a number, got {type(parameter_value)}"
|
||||
)
|
||||
return parameter_value < threshold
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_in_range(
|
||||
speckle_object: Base,
|
||||
parameter_name: str,
|
||||
min_value: Union[int, float],
|
||||
max_value: Union[int, float],
|
||||
inclusive: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter falls within the given range.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
min_value (Union[int, float]): The minimum value of the range.
|
||||
max_value (Union[int, float]): The maximum value of the range.
|
||||
inclusive (bool): If True (default), the range is inclusive (min <= value <= max).
|
||||
If False, the range is exclusive (min < value < max).
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value falls within the range (inclusive), False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Parameter value must be a number, got {type(parameter_value)}"
|
||||
)
|
||||
|
||||
return (
|
||||
min_value <= parameter_value <= max_value
|
||||
if inclusive
|
||||
else min_value < parameter_value < max_value
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_in_list(
|
||||
speckle_object: Base, parameter_name: str, value_list: List[Any]
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is present in the given list of values.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
value_list (List[Any]): The list of values to check against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is found in the list, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return parameter_value in value_list
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_true(speckle_object: Base, parameter_name: str) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is True.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is True, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return parameter_value is True
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_false(speckle_object: Base, parameter_name: str) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is False, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return parameter_value is False
|
||||
|
||||
@staticmethod
|
||||
def has_category(speckle_object: Base) -> bool:
|
||||
"""
|
||||
Checks if the speckle_object has a 'category' parameter.
|
||||
|
||||
This method checks if the speckle_object has a 'category' parameter.
|
||||
If the 'category' parameter exists, it returns True; otherwise, it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the object has the 'category' parameter, False otherwise.
|
||||
"""
|
||||
return RevitRules.has_parameter(speckle_object, "category")
|
||||
|
||||
@staticmethod
|
||||
def is_category(speckle_object: Base, category_input: str) -> bool:
|
||||
"""
|
||||
Checks if the value of the 'category' property matches the given input.
|
||||
|
||||
This method checks if the 'category' property of the speckle_object
|
||||
matches the given category_input. If they match, it returns True;
|
||||
otherwise, it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
category_input (str): The category value to compare against.
|
||||
|
||||
Returns:
|
||||
bool: True if the 'category' property matches the input, False otherwise.
|
||||
"""
|
||||
category_value = RevitRules.get_parameter_value(speckle_object, "category")
|
||||
return category_value == category_input
|
||||
|
||||
@staticmethod
|
||||
def get_category_value(speckle_object: Base) -> str:
|
||||
"""
|
||||
Retrieves the value of the 'category' parameter from the speckle_object.
|
||||
|
||||
This method retrieves the value of the 'category' parameter from the speckle_object.
|
||||
If the 'category' parameter exists and its value is not None, it returns the value.
|
||||
If the 'category' parameter does not exist or its value is None, it returns an empty string.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to retrieve the 'category' parameter value from.
|
||||
|
||||
Returns:
|
||||
str: The value of the 'category' parameter if it exists and is not None, or an empty string otherwise.
|
||||
"""
|
||||
return RevitRules.get_parameter_value(speckle_object, "category")
|
||||
|
||||
|
||||
def filter_objects_by_category(
|
||||
speckle_objects: List[Base], category_input: str
|
||||
) -> Tuple[List[Base], List[Base]]:
|
||||
"""
|
||||
Filters objects by category value and test.
|
||||
|
||||
This function takes a list of Speckle objects, filters out the objects
|
||||
with a matching category value and satisfies the test, and returns
|
||||
both the matching and non-matching objects.
|
||||
|
||||
Args:
|
||||
speckle_objects (List[Base]): The list of Speckle objects to filter.
|
||||
category_input (str): The category value to match against.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Base], List[Base]]: A tuple containing two lists:
|
||||
- The first list contains objects with matching category and test.
|
||||
- The second list contains objects without matching category or test.
|
||||
"""
|
||||
matching_objects = []
|
||||
non_matching_objects = []
|
||||
|
||||
for speckle_object in speckle_objects:
|
||||
if RevitRules.is_category(speckle_object, category_input):
|
||||
matching_objects.append(speckle_object)
|
||||
else:
|
||||
non_matching_objects.append(speckle_object)
|
||||
|
||||
return matching_objects, non_matching_objects
|
||||
@@ -0,0 +1,57 @@
|
||||
from pydantic import Field
|
||||
from speckle_automate import AutomationContext, AutomateBase
|
||||
from specklepy.objects import Base
|
||||
|
||||
from Utilities.helpers import flatten_base, speckle_print
|
||||
from Utilities.spreadsheet import read_rules_from_spreadsheet
|
||||
from Workshop.Exercise_4.rules import apply_rules_to_objects
|
||||
|
||||
|
||||
class FunctionInputs(AutomateBase):
|
||||
"""These are function author defined values.
|
||||
|
||||
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/
|
||||
"""
|
||||
|
||||
# In this exercise, we will move rules to an external source so not to hardcode them.
|
||||
spreadsheet_url: str = Field(
|
||||
title="Spreadsheet URL",
|
||||
description="This is the URL of the spreadsheet to check. It should be a TSV format data source.",
|
||||
)
|
||||
|
||||
|
||||
def automate_function(
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
) -> None:
|
||||
"""This version of the function will add a check for the new provide 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.
|
||||
"""
|
||||
|
||||
# the context provides a convenient way, to receive the triggering version
|
||||
version_root_object = automate_context.receive_version()
|
||||
|
||||
# We can continue to work with a flattened list of objects.
|
||||
flat_list_of_objects = list(flatten_base(version_root_object))
|
||||
|
||||
# read the rules from the spreadsheet
|
||||
rules = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
|
||||
|
||||
# apply the rules to the objects
|
||||
apply_rules_to_objects(flat_list_of_objects, rules, automate_context)
|
||||
|
||||
# set the automation context view, to the original model / version view
|
||||
automate_context.set_context_view()
|
||||
|
||||
# report success
|
||||
automate_context.mark_run_success(
|
||||
f"Successfully applied rules to {len(flat_list_of_objects)} objects."
|
||||
)
|
||||
@@ -0,0 +1,745 @@
|
||||
from typing import List, Optional, Tuple, Any, cast
|
||||
from speckle_automate import AutomationContext, ObjectResultLevel
|
||||
from specklepy.objects.base import Base
|
||||
from Levenshtein import ratio
|
||||
import pandas as pd
|
||||
import re
|
||||
|
||||
from Utilities.helpers import speckle_print
|
||||
|
||||
|
||||
# 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 Rules:
|
||||
"""
|
||||
A collection of rules for processing properties in Speckle objects.
|
||||
|
||||
Simple rules can be straightforwardly implemented as static methods that
|
||||
return boolean value to be used either as a filter or a condition.
|
||||
These can then be abstracted into returning lambda functions that we can
|
||||
use in our main processing logic. By encapsulating these rules, we can easily
|
||||
extend or modify them in the future.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def try_get_display_value(
|
||||
speckle_object: Base,
|
||||
) -> Optional[List[Base]]:
|
||||
"""Try fetching the display value from a Speckle object.
|
||||
|
||||
This method encapsulates the logic for attempting to retrieve the display value from a Speckle object.
|
||||
It returns a list containing the display values if found, otherwise it returns None.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to extract the display value from.
|
||||
|
||||
Returns:
|
||||
Optional[List[Base]]: A list containing the display values. If no display value is found,
|
||||
returns None.
|
||||
"""
|
||||
# Attempt to get the display value from the speckle_object
|
||||
raw_display_value = getattr(speckle_object, "displayValue", None) or getattr(
|
||||
speckle_object, "@displayValue", None
|
||||
)
|
||||
|
||||
# If no display value found, return None
|
||||
if raw_display_value is None:
|
||||
return None
|
||||
|
||||
# If display value found, filter out non-Base objects
|
||||
display_values = [
|
||||
value for value in raw_display_value if isinstance(value, Base)
|
||||
]
|
||||
|
||||
# If no valid display values found, return None
|
||||
if not display_values:
|
||||
return None
|
||||
|
||||
return display_values
|
||||
|
||||
@staticmethod
|
||||
def is_displayable_object(speckle_object: Base) -> bool:
|
||||
"""
|
||||
Determines if a given Speckle object is displayable.
|
||||
|
||||
This method encapsulates the logic for determining if a Speckle object is displayable.
|
||||
It checks if the speckle_object has a display value and returns True if it does, otherwise it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the object has a display value, False otherwise.
|
||||
"""
|
||||
# Check for direct displayable state using try_get_display_value
|
||||
display_values = Rules.try_get_display_value(speckle_object)
|
||||
if display_values and getattr(speckle_object, "id", None) is not None:
|
||||
return True
|
||||
|
||||
# Check for displayable state via definition, using try_get_display_value on the definition object
|
||||
definition = getattr(speckle_object, "definition", None)
|
||||
if definition:
|
||||
definition_display_values = Rules.try_get_display_value(definition)
|
||||
if (
|
||||
definition_display_values
|
||||
and getattr(definition, "id", None) is not None
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_displayable_objects(flat_list_of_objects: List[Base]) -> List[Base]:
|
||||
# modify this lambda from before to use the static method from the Checks class
|
||||
return [
|
||||
speckle_object
|
||||
for speckle_object in flat_list_of_objects
|
||||
if Rules.is_displayable_object(speckle_object)
|
||||
and getattr(speckle_object, "id", None)
|
||||
]
|
||||
|
||||
# and the same logic that could be modified to traverse a tree of objects
|
||||
|
||||
|
||||
def filter_objects_by_category(
|
||||
speckle_objects: List[Base], category_input: str
|
||||
) -> Tuple[List[Base], List[Base]]:
|
||||
"""
|
||||
Filters objects by category value and test.
|
||||
|
||||
This function takes a list of Speckle objects, filters out the objects
|
||||
with a matching category value and satisfies the test, and returns
|
||||
both the matching and non-matching objects.
|
||||
|
||||
Args:
|
||||
speckle_objects (List[Base]): The list of Speckle objects to filter.
|
||||
category_input (str): The category value to match against.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Base], List[Base]]: A tuple containing two lists:
|
||||
- The first list contains objects with matching category and test.
|
||||
- The second list contains objects without matching category or test.
|
||||
"""
|
||||
matching_objects = []
|
||||
non_matching_objects = []
|
||||
|
||||
for obj in speckle_objects:
|
||||
if RevitRules.is_category(obj, category_input):
|
||||
matching_objects.append(obj)
|
||||
else:
|
||||
non_matching_objects.append(obj)
|
||||
|
||||
return matching_objects, non_matching_objects
|
||||
|
||||
|
||||
class RevitRules:
|
||||
@staticmethod
|
||||
def has_parameter(
|
||||
speckle_object: Base, parameter_name: str, *_args, **_kwargs
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the speckle_object has a Revit parameter with the given name.
|
||||
|
||||
This method checks if the speckle_object has a parameter with the specified name,
|
||||
considering the following cases:
|
||||
1. The parameter is a named property at the root object level.
|
||||
2. The parameter is stored as a key in the "parameters" dictionary.
|
||||
3. The parameter is stored as a nested dictionary within the "parameters" property,
|
||||
and the parameter name is stored as the value of the "name" property within each nested dictionary.
|
||||
|
||||
If the parameter exists, it returns True; otherwise, it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check for.
|
||||
*_args: Extra positional arguments which are ignored.
|
||||
**_kwargs: Extra keyword arguments which are ignored.
|
||||
|
||||
Returns:
|
||||
bool: True if the object has the parameter, False otherwise.
|
||||
"""
|
||||
if hasattr(speckle_object, parameter_name):
|
||||
return True
|
||||
|
||||
parameters = cast(Base, getattr(speckle_object, "parameters", None))
|
||||
|
||||
if parameters is None:
|
||||
return False
|
||||
|
||||
# the parameters object can function like a dict but isn't one.
|
||||
# convert a Base object to a dict
|
||||
parameters_dict = {}
|
||||
|
||||
for parameter_key in parameters.get_dynamic_member_names():
|
||||
parameters_dict[parameter_key] = getattr(parameters, parameter_key, None)
|
||||
|
||||
if parameter_name in parameters_dict:
|
||||
return True
|
||||
|
||||
return any(
|
||||
getattr(param_value, "name", None) == parameter_name
|
||||
for param_value in parameters_dict.values()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_parameter_value(
|
||||
speckle_object: Base,
|
||||
parameter_name: str,
|
||||
default_value: Any = None,
|
||||
) -> Any | None:
|
||||
"""
|
||||
Retrieves the value of the specified Revit parameter from the speckle_object.
|
||||
|
||||
This method checks if the speckle_object has a parameter with the specified name,
|
||||
considering the following cases:
|
||||
1. The parameter is a named property at the root object level.
|
||||
2. The parameter is stored as a key in the "parameters" dictionary.
|
||||
3. The parameter is stored as a nested dictionary within the "parameters" property,
|
||||
and the parameter name is stored as the value of the "name" property within each nested dictionary.
|
||||
|
||||
If the parameter exists and its value is not None or the specified default_value, it returns the value.
|
||||
If the parameter does not exist or its value is None or the specified default_value, it returns None.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to retrieve the parameter value from.
|
||||
parameter_name (str): The name of the parameter to retrieve the value for.
|
||||
default_value: The default value to compare against. If the parameter value matches this value,
|
||||
it will be treated the same as None.
|
||||
|
||||
Returns:
|
||||
The value of the parameter if it exists and is not None or the specified default_value, or None otherwise.
|
||||
"""
|
||||
# Attempt to retrieve the parameter from the root object level
|
||||
value = getattr(speckle_object, parameter_name, None)
|
||||
if value not in [None, default_value]:
|
||||
return value
|
||||
|
||||
# If the "parameters" attribute is a Base object, extract its dynamic members
|
||||
parameters = getattr(speckle_object, "parameters", None)
|
||||
if parameters is None:
|
||||
return None
|
||||
|
||||
# Prepare a dictionary of parameter values from the dynamic members of the parameters attribute
|
||||
parameters_dict = {
|
||||
key: getattr(parameters, key)
|
||||
for key in parameters.get_dynamic_member_names()
|
||||
}
|
||||
|
||||
# Search for a direct match or a nested match in the parameters dictionary
|
||||
param_value = parameters_dict.get(parameter_name)
|
||||
if param_value is not None:
|
||||
if isinstance(param_value, Base):
|
||||
# Extract the nested value from a Base object if available
|
||||
nested_value = getattr(param_value, "value", None)
|
||||
if nested_value not in [None, default_value]:
|
||||
return nested_value
|
||||
elif param_value not in [None, default_value]:
|
||||
return param_value
|
||||
|
||||
# Use a generator to find the first matching 'value' for shared parameters stored in Base objects
|
||||
return next(
|
||||
(
|
||||
getattr(p, "value", None)
|
||||
for p in parameters_dict.values()
|
||||
if isinstance(p, Base) and getattr(p, "name", None) == parameter_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
from typing import Any, Union, List
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value(
|
||||
speckle_object: Base, parameter_name: str, value_to_match: Any
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter matches the given value.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
value_to_match (Any): The value to match against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value matches the given value, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return parameter_value == value_to_match
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_like(
|
||||
speckle_object: Base,
|
||||
parameter_name: str,
|
||||
pattern: str,
|
||||
fuzzy: bool = False,
|
||||
threshold: float = 0.8,
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter matches the given pattern.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
pattern (str): The pattern to match against.
|
||||
fuzzy (bool): If True, performs fuzzy matching using Levenshtein distance.
|
||||
If False (default), performs exact pattern matching using regular expressions.
|
||||
threshold (float): The similarity threshold for fuzzy matching (default: 0.8).
|
||||
Only applicable when fuzzy=True.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value matches the pattern (exact or fuzzy), False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
|
||||
if fuzzy:
|
||||
similarity = ratio(str(parameter_value), pattern)
|
||||
return similarity >= threshold
|
||||
else:
|
||||
return bool(re.match(pattern, str(parameter_value)))
|
||||
|
||||
@staticmethod
|
||||
def parse_number_from_string(input_string: str):
|
||||
"""
|
||||
Attempts to parse an integer or float from a given string.
|
||||
|
||||
Args:
|
||||
input_string (str): The string containing the number to be parsed.
|
||||
|
||||
Returns:
|
||||
int or float: The parsed number, or raises ValueError if parsing is not possible.
|
||||
"""
|
||||
try:
|
||||
# First try to convert it to an integer
|
||||
return int(input_string)
|
||||
except ValueError:
|
||||
# If it fails to convert to an integer, try to convert to a float
|
||||
try:
|
||||
return float(input_string)
|
||||
except ValueError:
|
||||
# Raise an error if neither conversion is possible
|
||||
raise ValueError("Input string is not a valid integer or float")
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_greater_than(
|
||||
speckle_object: Base, parameter_name: str, threshold: str
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is greater than the given threshold.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
threshold (Union[int, float]): The threshold value to compare against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is greater than the threshold, False otherwise.
|
||||
"""
|
||||
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
|
||||
if not isinstance(parameter_value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Parameter value must be a number, got {type(parameter_value)}"
|
||||
)
|
||||
return parameter_value > RevitRules.parse_number_from_string(threshold)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_less_than(
|
||||
speckle_object: Base, parameter_name: str, threshold: str
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is less than the given threshold.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
threshold (Union[int, float]): The threshold value to compare against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is less than the threshold, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Parameter value must be a number, got {type(parameter_value)}"
|
||||
)
|
||||
return parameter_value < RevitRules.parse_number_from_string(threshold)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_in_range(
|
||||
speckle_object: Base, parameter_name: str, range: str
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter falls within the given range.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
range (str): The range to check against, in the format "min_value, max_value".
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value falls within the range (inclusive), False otherwise.
|
||||
"""
|
||||
|
||||
min_value, max_value = range.split(",")
|
||||
min_value = RevitRules.parse_number_from_string(min_value)
|
||||
max_value = RevitRules.parse_number_from_string(max_value)
|
||||
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Parameter value must be a number, got {type(parameter_value)}"
|
||||
)
|
||||
|
||||
return min_value <= parameter_value <= max_value
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_in_range_expanded(
|
||||
speckle_object: Base,
|
||||
parameter_name: str,
|
||||
min_value: Union[int, float],
|
||||
max_value: Union[int, float],
|
||||
inclusive: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter falls within the given range.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
min_value (Union[int, float]): The minimum value of the range.
|
||||
max_value (Union[int, float]): The maximum value of the range.
|
||||
inclusive (bool): If True (default), the range is inclusive (min <= value <= max).
|
||||
If False, the range is exclusive (min < value < max).
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value falls within the range (inclusive), False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
if parameter_value is None:
|
||||
return False
|
||||
if not isinstance(parameter_value, (int, float)):
|
||||
raise ValueError(
|
||||
f"Parameter value must be a number, got {type(parameter_value)}"
|
||||
)
|
||||
|
||||
return (
|
||||
min_value <= parameter_value <= max_value
|
||||
if inclusive
|
||||
else min_value < parameter_value < max_value
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_in_list(
|
||||
speckle_object: Base, parameter_name: str, value_list: List[Any]
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is present in the given list of values.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
value_list (List[Any]): The list of values to check against.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is found in the list, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
|
||||
if isinstance(value_list, str):
|
||||
value_list = [value.strip() for value in value_list.split(",")]
|
||||
|
||||
# parameter_value is effectively Any type, so to find its value in the value_list
|
||||
def is_value_in_list(value: Any, my_list: Any) -> bool:
|
||||
# Ensure that my_list is actually a list
|
||||
if isinstance(my_list, list):
|
||||
return value in my_list or str(value) in my_list
|
||||
else:
|
||||
speckle_print(f"Expected a list, got {type(my_list)} instead.")
|
||||
return False
|
||||
|
||||
return is_value_in_list(parameter_value, value_list)
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_true(speckle_object: Base, parameter_name: str) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is True.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is True, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return parameter_value is True
|
||||
|
||||
@staticmethod
|
||||
def is_parameter_value_false(speckle_object: Base, parameter_name: str) -> bool:
|
||||
"""
|
||||
Checks if the value of the specified parameter is False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
parameter_name (str): The name of the parameter to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the parameter value is False, False otherwise.
|
||||
"""
|
||||
parameter_value = RevitRules.get_parameter_value(speckle_object, parameter_name)
|
||||
return parameter_value is False
|
||||
|
||||
@staticmethod
|
||||
def has_category(speckle_object: Base) -> bool:
|
||||
"""
|
||||
Checks if the speckle_object has a 'category' parameter.
|
||||
|
||||
This method checks if the speckle_object has a 'category' parameter.
|
||||
If the 'category' parameter exists, it returns True; otherwise, it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the object has the 'category' parameter, False otherwise.
|
||||
"""
|
||||
return RevitRules.has_parameter(speckle_object, "category")
|
||||
|
||||
@staticmethod
|
||||
def is_category(speckle_object: Base, category_input: str) -> bool:
|
||||
"""
|
||||
Checks if the value of the 'category' property matches the given input.
|
||||
|
||||
This method checks if the 'category' property of the speckle_object
|
||||
matches the given category_input. If they match, it returns True;
|
||||
otherwise, it returns False.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to check.
|
||||
category_input (str): The category value to compare against.
|
||||
|
||||
Returns:
|
||||
bool: True if the 'category' property matches the input, False otherwise.
|
||||
"""
|
||||
category_value = RevitRules.get_parameter_value(speckle_object, "category")
|
||||
return category_value == category_input
|
||||
|
||||
@staticmethod
|
||||
def get_category_value(speckle_object: Base) -> str:
|
||||
"""
|
||||
Retrieves the value of the 'category' parameter from the speckle_object.
|
||||
|
||||
This method retrieves the value of the 'category' parameter from the speckle_object.
|
||||
If the 'category' parameter exists and its value is not None, it returns the value.
|
||||
If the 'category' parameter does not exist or its value is None, it returns an empty string.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to retrieve the 'category' parameter value from.
|
||||
|
||||
Returns:
|
||||
str: The value of the 'category' parameter if it exists and is not None, or an empty string otherwise.
|
||||
"""
|
||||
return RevitRules.get_parameter_value(speckle_object, "category")
|
||||
|
||||
|
||||
# Mapping of input predicates to the corresponding methods in RevitRules
|
||||
input_predicate_mapping = {
|
||||
"exists": "has_parameter",
|
||||
"matches": "is_parameter_value",
|
||||
"greater than": "is_parameter_value_greater_than",
|
||||
"less than": "is_parameter_value_less_than",
|
||||
"in range": "is_parameter_value_in_range",
|
||||
"in list": "is_parameter_value_in_list",
|
||||
"equals": "is_parameter_value",
|
||||
"true": "is_parameter_value_true",
|
||||
"false": "is_parameter_value_false",
|
||||
"is like": "is_parameter_value_like",
|
||||
}
|
||||
|
||||
|
||||
def evaluate_condition(speckle_object: Base, condition: pd.Series) -> bool:
|
||||
"""
|
||||
Given a Speckle object and a condition, evaluates the condition and returns a boolean value.
|
||||
A condition is a pandas Series object with the following keys:
|
||||
- 'Property Name': The name of the property to evaluate.
|
||||
- 'Predicate': The predicate to use for evaluation.
|
||||
- 'Value': The value to compare against.
|
||||
|
||||
Args:
|
||||
speckle_object (Base): The Speckle object to evaluate.
|
||||
condition (pd.Series): The condition to evaluate.
|
||||
|
||||
Returns:
|
||||
bool: The result of the evaluation. True if the condition is met, False otherwise.
|
||||
"""
|
||||
property_name = condition["Property Name"]
|
||||
predicate_key = condition["Predicate"]
|
||||
value = condition["Value"]
|
||||
|
||||
if predicate_key in input_predicate_mapping:
|
||||
method_name = input_predicate_mapping[predicate_key]
|
||||
method = getattr(RevitRules, method_name, None)
|
||||
|
||||
# speckle_print(f"Checking {property_name} {predicate_key} {value}")
|
||||
|
||||
if method:
|
||||
check_answer = method(speckle_object, property_name, value)
|
||||
|
||||
return check_answer
|
||||
return False
|
||||
|
||||
|
||||
def process_rule(
|
||||
speckle_objects: List[Base], rule_group: pd.DataFrame
|
||||
) -> Tuple[List[Base], List[Base]]:
|
||||
"""
|
||||
Processes a set of rules against Speckle objects, returning those that pass and fail.
|
||||
The first rule is used as a filter ('WHERE'), and subsequent rules as conditions ('AND').
|
||||
|
||||
Args:
|
||||
speckle_objects: List of Speckle objects to be processed.
|
||||
rule_group: DataFrame defining the filter and conditions.
|
||||
|
||||
Returns:
|
||||
A tuple of lists containing objects that passed and failed the rule.
|
||||
"""
|
||||
|
||||
# Extract the 'WHERE' condition and subsequent 'AND' conditions
|
||||
filter_condition = rule_group.iloc[0]
|
||||
subsequent_conditions = rule_group.iloc[1:]
|
||||
|
||||
# get the last row of the rule_group and get the Message and Report Severity
|
||||
rule_info = rule_group.iloc[-1]
|
||||
|
||||
# Filter objects based on the 'WHERE' condition
|
||||
filtered_objects = [
|
||||
speckle_object
|
||||
for speckle_object in speckle_objects
|
||||
if evaluate_condition(speckle_object, filter_condition)
|
||||
]
|
||||
|
||||
rule_number = rule_info["Rule Number"]
|
||||
|
||||
speckle_print(
|
||||
f"{ filter_condition['Logic']} {filter_condition['Property Name']} "
|
||||
f"{filter_condition['Predicate']} {filter_condition['Value']}"
|
||||
)
|
||||
|
||||
speckle_print(
|
||||
f"{rule_number}: {len(list(filtered_objects))} objects passed the filter."
|
||||
)
|
||||
|
||||
# Initialize lists for passed and failed objects
|
||||
pass_objects, fail_objects = [], []
|
||||
|
||||
# Evaluate each filtered object against the 'AND' conditions
|
||||
for speckle_object in filtered_objects:
|
||||
if all(
|
||||
evaluate_condition(speckle_object, cond)
|
||||
for _, cond in subsequent_conditions.iterrows()
|
||||
):
|
||||
pass_objects.append(speckle_object)
|
||||
else:
|
||||
fail_objects.append(speckle_object)
|
||||
|
||||
return pass_objects, fail_objects
|
||||
|
||||
|
||||
def apply_rules_to_objects(
|
||||
speckle_objects: List[Base],
|
||||
rules_df: pd.DataFrame,
|
||||
automate_context: AutomationContext,
|
||||
) -> dict[str, Tuple[List[Base], List[Base]]]:
|
||||
"""
|
||||
Applies defined rules to a list of objects and updates the automate context based on the results.
|
||||
|
||||
Args:
|
||||
speckle_objects (List[Base]): The list of objects to which rules are applied.
|
||||
rules_df (pd.DataFrame): The DataFrame containing rule definitions.
|
||||
automate_context (Any): Context manager for attaching rule results.
|
||||
"""
|
||||
grouped_rules = rules_df.groupby("Rule Number")
|
||||
|
||||
grouped_results = {}
|
||||
|
||||
for rule_id, rule_group in grouped_rules:
|
||||
rule_id_str = str(rule_id) # Convert rule_id to string
|
||||
|
||||
# Ensure rule_group has necessary columns
|
||||
if (
|
||||
"Message" not in rule_group.columns
|
||||
or "Report Severity" not in rule_group.columns
|
||||
):
|
||||
continue # Or raise an exception if these columns are mandatory
|
||||
|
||||
pass_objects, fail_objects = process_rule(speckle_objects, rule_group)
|
||||
|
||||
attach_results(
|
||||
pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True
|
||||
)
|
||||
attach_results(
|
||||
fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False
|
||||
)
|
||||
|
||||
grouped_results[rule_id_str] = (pass_objects, fail_objects)
|
||||
|
||||
# return pass_objects, fail_objects for each rule
|
||||
return grouped_results
|
||||
|
||||
|
||||
def attach_results(
|
||||
speckle_objects: List[Base],
|
||||
rule_info: pd.Series,
|
||||
rule_id: str,
|
||||
context: AutomationContext,
|
||||
passed: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Attaches the results of a rule to the objects in the context.
|
||||
|
||||
Args:
|
||||
speckle_objects (List[Base]): The list of objects to which the rule was applied.
|
||||
rule_info (pd.Series): The information about the rule.
|
||||
rule_id (str): The ID of the rule.
|
||||
context (AutomationContext): The context manager for attaching results.
|
||||
passed (bool): Whether the rule passed or failed.
|
||||
"""
|
||||
|
||||
if not speckle_objects:
|
||||
return
|
||||
|
||||
message = f"{rule_info['Message']} - {'Passed' if passed else 'Failed'}"
|
||||
if passed:
|
||||
context.attach_info_to_objects(
|
||||
category=f"Rule {rule_id} Success",
|
||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
||||
message=message,
|
||||
)
|
||||
else:
|
||||
|
||||
speckle_print(rule_info["Report Severity"])
|
||||
|
||||
severity = (
|
||||
ObjectResultLevel.WARNING
|
||||
if rule_info["Report Severity"].capitalize() == "Warning"
|
||||
or rule_info["Report Severity"].capitalize() == "Warn"
|
||||
else ObjectResultLevel.ERROR
|
||||
)
|
||||
context.attach_result_to_objects(
|
||||
category=f"Rule {rule_id} Results",
|
||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
||||
message=message,
|
||||
level=severity,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"speckleToken": "YOUR SPEKCLE TOKEN",
|
||||
"functionInputs": {
|
||||
"whisperMessage": "you are doing something weird",
|
||||
"forbiddenSpeckleType": ""
|
||||
},
|
||||
"automationRunData": {
|
||||
"project_id": "project id",
|
||||
"model_id": "model id",
|
||||
"branch_name": "branch name",
|
||||
"version_id": "version id",
|
||||
"speckle_server_url": "https://latest.speckle.systems",
|
||||
"automation_id": "automation id",
|
||||
"automation_revision_id": "automation revision id",
|
||||
"automation_run_id": "automation run id",
|
||||
"function_id": "function id",
|
||||
"function_name": "function name",
|
||||
"function_logo": null
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
"""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") and base.elements is not None:
|
||||
for element in base["elements"]:
|
||||
yield from flatten_base(element)
|
||||
yield base
|
||||
@@ -0,0 +1,80 @@
|
||||
import random
|
||||
|
||||
from pydantic import Field
|
||||
from speckle_automate import AutomationContext, AutomateBase, execute_automate_function
|
||||
|
||||
from Utilities.helpers import flatten_base
|
||||
|
||||
|
||||
class FunctionInputs(AutomateBase):
|
||||
"""These are function author defined values.
|
||||
|
||||
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/
|
||||
"""
|
||||
|
||||
comment_phrase: str = Field(
|
||||
title="Comment Phrase",
|
||||
description="This phrase will be added to a random model element.",
|
||||
)
|
||||
|
||||
|
||||
def automate_function(
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
) -> None:
|
||||
"""This is an example Speckle Automate function.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# the context provides a convenient way, to receive the triggering version
|
||||
version_root_object = automate_context.receive_version()
|
||||
|
||||
flat_list_of_objects = flatten_base(version_root_object)
|
||||
|
||||
# filter the list to only include objects that are displayable.
|
||||
# this is a simple example, that checks if the object has a displayValue
|
||||
displayable_objects = [
|
||||
speckle_object
|
||||
for speckle_object in flat_list_of_objects
|
||||
if (
|
||||
getattr(speckle_object, "displayValue", None)
|
||||
or getattr(speckle_object, "@displayValue", None)
|
||||
)
|
||||
and getattr(speckle_object, "id", None) is not None
|
||||
]
|
||||
|
||||
if len(displayable_objects) == 0:
|
||||
automate_context.mark_run_failed(
|
||||
"Automation failed: No displayable objects found."
|
||||
)
|
||||
|
||||
else:
|
||||
# select a random object from the list
|
||||
random_object = random.choice(displayable_objects)
|
||||
|
||||
automate_context.attach_info_to_objects(
|
||||
category="Selected Object",
|
||||
object_ids=[random_object.id],
|
||||
message=function_inputs.comment_phrase,
|
||||
)
|
||||
|
||||
automate_context.mark_run_success("Added a comment to a random object.")
|
||||
|
||||
# set the automation context view, to the original model / version view
|
||||
automate_context.set_context_view()
|
||||
|
||||
|
||||
# make sure to call the function with the executor
|
||||
if __name__ == "__main__":
|
||||
# NOTE: always pass in the automate function by its reference, do not invoke it!
|
||||
|
||||
# pass in the function reference with the inputs schema to the executor
|
||||
execute_automate_function(automate_function, FunctionInputs)
|
||||
Generated
+1606
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
[tool.poetry]
|
||||
name = "speckle-automate-py"
|
||||
version = "0.1.0"
|
||||
description = "Template function for BILT Riga Coding workshop"
|
||||
authors = ["Jonathon Broughton <jonathon@speckle.systems>"]
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
specklepy = "2.18.3"
|
||||
python-levenshtein = "^0.25.1"
|
||||
more-itertools = "^10.2.0"
|
||||
pandas = "^2.2.2"
|
||||
python-dotenv = "^1.0.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.0.0"
|
||||
mypy = "^1.3.0"
|
||||
ruff = "^0.4.0"
|
||||
pytest = "^8.0.0"
|
||||
# specklepy = { path = "../specklepy", develop = true }
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"UP", # pyupgrade
|
||||
"D", # pydocstyle
|
||||
"I", # isort
|
||||
]
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
convention = "google"
|
||||
@@ -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
|
||||
@@ -0,0 +1,146 @@
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from gql import gql
|
||||
from speckle_automate import (
|
||||
AutomationContext,
|
||||
AutomationRunData,
|
||||
AutomationStatus,
|
||||
run_function,
|
||||
)
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
from Workshop.Exercise_1.automate_function import FunctionInputs, automate_function
|
||||
|
||||
|
||||
def crypto_random_string(length: int) -> str:
|
||||
"""Generate a semi crypto random string of a given length."""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def register_new_automation(
|
||||
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(
|
||||
"""
|
||||
mutation CreateAutomation(
|
||||
$projectId: String!
|
||||
$modelId: String!
|
||||
$automationName: String!
|
||||
$automationId: String!
|
||||
$automationRevisionId: String!
|
||||
) {
|
||||
automationMutations {
|
||||
create(
|
||||
input: {
|
||||
projectId: $projectId
|
||||
modelId: $modelId
|
||||
automationName: $automationName
|
||||
automationId: $automationId
|
||||
automationRevisionId: $automationRevisionId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
params = {
|
||||
"projectId": project_id,
|
||||
"modelId": model_id,
|
||||
"automationName": automation_name,
|
||||
"automationId": automation_id,
|
||||
"automationRevisionId": automation_revision_id,
|
||||
}
|
||||
speckle_client.httpclient.execute(query, params)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def speckle_server_url() -> 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")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
|
||||
"""Initialize a SpeckleClient for testing."""
|
||||
test_client = SpeckleClient(
|
||||
speckle_server_url, speckle_server_url.startswith("https")
|
||||
)
|
||||
test_client.authenticate_with_token(speckle_token)
|
||||
return test_client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
# 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
|
||||
project_id = "ce0f229748"
|
||||
model_id = "5e44c03b0b"
|
||||
|
||||
function_name = "BILT Exercise 1"
|
||||
|
||||
automation_id = crypto_random_string(10)
|
||||
automation_name = "Local Test"
|
||||
automation_revision_id = crypto_random_string(10)
|
||||
|
||||
register_new_automation(
|
||||
project_id,
|
||||
model_id,
|
||||
test_client,
|
||||
automation_id,
|
||||
automation_name,
|
||||
automation_revision_id,
|
||||
)
|
||||
|
||||
fake_run_data = AutomationRunData(
|
||||
project_id=project_id,
|
||||
model_id=model_id,
|
||||
branch_name="exercise 1",
|
||||
version_id="136fe819e0",
|
||||
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=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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def context(fake_automation_run_data: AutomationRunData, speckle_token: str):
|
||||
return AutomationContext.initialize(fake_automation_run_data, speckle_token)
|
||||
|
||||
|
||||
def test_function_run(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(commentPhrase="Tested Locally")
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
@@ -0,0 +1,143 @@
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from gql import gql
|
||||
from speckle_automate import (
|
||||
AutomationContext,
|
||||
AutomationRunData,
|
||||
AutomationStatus,
|
||||
run_function,
|
||||
)
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
from Workshop.Exercise_2.automate_function import FunctionInputs, automate_function
|
||||
|
||||
|
||||
def crypto_random_string(length: int) -> str:
|
||||
"""Generate a semi crypto random string of a given length."""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def register_new_automation(
|
||||
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(
|
||||
"""
|
||||
mutation CreateAutomation(
|
||||
$projectId: String!
|
||||
$modelId: String!
|
||||
$automationName: String!
|
||||
$automationId: String!
|
||||
$automationRevisionId: String!
|
||||
) {
|
||||
automationMutations {
|
||||
create(
|
||||
input: {
|
||||
projectId: $projectId
|
||||
modelId: $modelId
|
||||
automationName: $automationName
|
||||
automationId: $automationId
|
||||
automationRevisionId: $automationRevisionId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
params = {
|
||||
"projectId": project_id,
|
||||
"modelId": model_id,
|
||||
"automationName": automation_name,
|
||||
"automationId": automation_id,
|
||||
"automationRevisionId": automation_revision_id,
|
||||
}
|
||||
speckle_client.httpclient.execute(query, params)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def speckle_server_url() -> 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")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
|
||||
"""Initialize a SpeckleClient for testing."""
|
||||
test_client = SpeckleClient(
|
||||
speckle_server_url, speckle_server_url.startswith("https")
|
||||
)
|
||||
test_client.authenticate_with_token(speckle_token)
|
||||
return test_client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
# 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
|
||||
project_id = "ce0f229748"
|
||||
model_id = "7658a4f311"
|
||||
|
||||
function_name = "Local Test"
|
||||
|
||||
automation_id = crypto_random_string(10)
|
||||
automation_name = "BILT Exercise 2"
|
||||
automation_revision_id = crypto_random_string(10)
|
||||
|
||||
register_new_automation(
|
||||
project_id,
|
||||
model_id,
|
||||
test_client,
|
||||
automation_id,
|
||||
automation_name,
|
||||
automation_revision_id,
|
||||
)
|
||||
|
||||
fake_run_data = AutomationRunData(
|
||||
project_id=project_id,
|
||||
model_id=model_id,
|
||||
branch_name="exercise 2",
|
||||
version_id="bbfa404f93",
|
||||
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=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(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(commentPhrase="Tested Locally", numberOfElements=10000),
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
@@ -0,0 +1,143 @@
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from gql import gql
|
||||
from speckle_automate import (
|
||||
AutomationContext,
|
||||
AutomationRunData,
|
||||
AutomationStatus,
|
||||
run_function,
|
||||
)
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
from Workshop.Exercise_3.automate_function import FunctionInputs, automate_function
|
||||
|
||||
|
||||
def crypto_random_string(length: int) -> str:
|
||||
"""Generate a semi crypto random string of a given length."""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def register_new_automation(
|
||||
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(
|
||||
"""
|
||||
mutation CreateAutomation(
|
||||
$projectId: String!
|
||||
$modelId: String!
|
||||
$automationName: String!
|
||||
$automationId: String!
|
||||
$automationRevisionId: String!
|
||||
) {
|
||||
automationMutations {
|
||||
create(
|
||||
input: {
|
||||
projectId: $projectId
|
||||
modelId: $modelId
|
||||
automationName: $automationName
|
||||
automationId: $automationId
|
||||
automationRevisionId: $automationRevisionId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
params = {
|
||||
"projectId": project_id,
|
||||
"modelId": model_id,
|
||||
"automationName": automation_name,
|
||||
"automationId": automation_id,
|
||||
"automationRevisionId": automation_revision_id,
|
||||
}
|
||||
speckle_client.httpclient.execute(query, params)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def speckle_server_url() -> 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")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
|
||||
"""Initialize a SpeckleClient for testing."""
|
||||
test_client = SpeckleClient(
|
||||
speckle_server_url, speckle_server_url.startswith("https")
|
||||
)
|
||||
test_client.authenticate_with_token(speckle_token)
|
||||
return test_client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
# 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
|
||||
project_id = "ce0f229748"
|
||||
model_id = "71fc3bfac6"
|
||||
|
||||
function_name = "Local Test"
|
||||
|
||||
automation_id = crypto_random_string(10)
|
||||
automation_name = "BILT Exercise 3"
|
||||
automation_revision_id = crypto_random_string(10)
|
||||
|
||||
register_new_automation(
|
||||
project_id,
|
||||
model_id,
|
||||
test_client,
|
||||
automation_id,
|
||||
automation_name,
|
||||
automation_revision_id,
|
||||
)
|
||||
|
||||
fake_run_data = AutomationRunData(
|
||||
project_id=project_id,
|
||||
model_id=model_id,
|
||||
branch_name="exercise 3",
|
||||
version_id="227572481f",
|
||||
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=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(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(category="Doors", property="SPECKLE_Classification"),
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
@@ -0,0 +1,149 @@
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from gql import gql
|
||||
from speckle_automate import (
|
||||
AutomationContext,
|
||||
AutomationRunData,
|
||||
AutomationStatus,
|
||||
run_function,
|
||||
)
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
from Utilities.helpers import speckle_print
|
||||
from Workshop.Exercise_4.automate_function import FunctionInputs, automate_function
|
||||
|
||||
|
||||
def crypto_random_string(length: int) -> str:
|
||||
"""Generate a semi crypto random string of a given length."""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def register_new_automation(
|
||||
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(
|
||||
"""
|
||||
mutation CreateAutomation(
|
||||
$projectId: String!
|
||||
$modelId: String!
|
||||
$automationName: String!
|
||||
$automationId: String!
|
||||
$automationRevisionId: String!
|
||||
) {
|
||||
automationMutations {
|
||||
create(
|
||||
input: {
|
||||
projectId: $projectId
|
||||
modelId: $modelId
|
||||
automationName: $automationName
|
||||
automationId: $automationId
|
||||
automationRevisionId: $automationRevisionId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
params = {
|
||||
"projectId": project_id,
|
||||
"modelId": model_id,
|
||||
"automationName": automation_name,
|
||||
"automationId": automation_id,
|
||||
"automationRevisionId": automation_revision_id,
|
||||
}
|
||||
speckle_client.httpclient.execute(query, params)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def speckle_server_url() -> 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")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
|
||||
"""Initialize a SpeckleClient for testing."""
|
||||
test_client = SpeckleClient(
|
||||
speckle_server_url, speckle_server_url.startswith("https")
|
||||
)
|
||||
test_client.authenticate_with_token(speckle_token)
|
||||
return test_client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
# 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
|
||||
project_id = "ce0f229748"
|
||||
model_id = "6dadd92d7f"
|
||||
|
||||
function_name = "Local Test"
|
||||
|
||||
automation_id = crypto_random_string(10)
|
||||
automation_name = "BILT Exercise 4"
|
||||
automation_revision_id = crypto_random_string(10)
|
||||
|
||||
register_new_automation(
|
||||
project_id,
|
||||
model_id,
|
||||
test_client,
|
||||
automation_id,
|
||||
automation_name,
|
||||
automation_revision_id,
|
||||
)
|
||||
|
||||
fake_run_data = AutomationRunData(
|
||||
project_id=project_id,
|
||||
model_id=model_id,
|
||||
branch_name="exercise 4",
|
||||
version_id="8fe89b87d9",
|
||||
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=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(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)
|
||||
|
||||
default_url: str = (
|
||||
"https://docs.google.com/spreadsheets/d/e/2PACX-1vSFmjLfqxPKXJHg-wEs1cp_nJEJJhESGVTLCvWLG_"
|
||||
"IgIuRZ4CmMDCSceOYFvuo8IqcmT4sj9qPiLfCx/pub?gid=0&single=true&output=tsv"
|
||||
)
|
||||
|
||||
automate_sdk = run_function(
|
||||
context,
|
||||
automate_function,
|
||||
FunctionInputs(spreadsheet_url=default_url),
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from gql import gql
|
||||
from speckle_automate import (
|
||||
AutomationContext,
|
||||
AutomationRunData,
|
||||
AutomationStatus,
|
||||
run_function,
|
||||
)
|
||||
from specklepy.api.client import SpeckleClient
|
||||
|
||||
from main import FunctionInputs, automate_function
|
||||
|
||||
|
||||
def crypto_random_string(length: int) -> str:
|
||||
"""Generate a semi crypto random string of a given length."""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def register_new_automation(
|
||||
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(
|
||||
"""
|
||||
mutation CreateAutomation(
|
||||
$projectId: String!
|
||||
$modelId: String!
|
||||
$automationName: String!
|
||||
$automationId: String!
|
||||
$automationRevisionId: String!
|
||||
) {
|
||||
automationMutations {
|
||||
create(
|
||||
input: {
|
||||
projectId: $projectId
|
||||
modelId: $modelId
|
||||
automationName: $automationName
|
||||
automationId: $automationId
|
||||
automationRevisionId: $automationRevisionId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
params = {
|
||||
"projectId": project_id,
|
||||
"modelId": model_id,
|
||||
"automationName": automation_name,
|
||||
"automationId": automation_id,
|
||||
"automationRevisionId": automation_revision_id,
|
||||
}
|
||||
speckle_client.httpclient.execute(query, params)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def speckle_server_url() -> 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")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
|
||||
"""Initialize a SpeckleClient for testing."""
|
||||
test_client = SpeckleClient(
|
||||
speckle_server_url, speckle_server_url.startswith("https")
|
||||
)
|
||||
test_client.authenticate_with_token(speckle_token)
|
||||
return test_client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
# 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
|
||||
project_id = "ce0f229748"
|
||||
model_id = "634a39fa45"
|
||||
|
||||
function_name = "BILT Riga Workshop"
|
||||
|
||||
automation_id = crypto_random_string(10)
|
||||
automation_name = "Local Test"
|
||||
automation_revision_id = crypto_random_string(10)
|
||||
|
||||
register_new_automation(
|
||||
project_id,
|
||||
model_id,
|
||||
test_client,
|
||||
automation_id,
|
||||
automation_name,
|
||||
automation_revision_id,
|
||||
)
|
||||
|
||||
fake_run_data = AutomationRunData(
|
||||
project_id=project_id,
|
||||
model_id=model_id,
|
||||
branch_name="basic",
|
||||
version_id="df0f86a3fd",
|
||||
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=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(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(commentPhrase="Test Locally"),
|
||||
# FunctionInputs(
|
||||
# tolerance=0.1, tolerance_unit="mm", static_model_name="simple beams"
|
||||
# ),
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
Reference in New Issue
Block a user