feat: commit processing and mass calcs
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
→ processes next-gen revit commit → logs any objects missing identified attributes → computes mass for objects with valid attributes
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
SPECKLE_TOKEN="mytoken"
|
|
||||||
SPECKLE_SERVER_URL="http://127.0.0.1:3000"
|
|
||||||
SPECKLE_PROJECT_ID=""
|
|
||||||
SPECKLE_AUTOMATION_ID=""
|
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+5
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="enabledOnReformat" value="true" />
|
||||||
|
<option name="enabledOnSave" value="true" />
|
||||||
|
<option name="pathToExecutable" value="C:\Users\stein\pipx\venvs\poetry\Scripts\black.exe" />
|
||||||
|
<option name="sdkName" value="Poetry (Embodied-Carbon-Calculator)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (Embodied-Carbon-Calculator)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Embodied-Carbon-Calculator.iml" filepath="$PROJECT_DIR$/.idea/Embodied-Carbon-Calculator.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,100 +1 @@
|
|||||||
# Speckle Automate function template - Python
|
insert cool readme 😎 ...
|
||||||
|
|
||||||
This template repository is for a Speckle Automate function written in Python
|
|
||||||
using the [specklepy](https://pypi.org/project/specklepy/) SDK to interact with Speckle data.
|
|
||||||
|
|
||||||
This template contains the full scaffolding required to publish a function to the Automate environment.
|
|
||||||
It also has some sane defaults for development environment setups.
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
1. Use this template repository to create a new repository in your own / organization's profile.
|
|
||||||
|
|
||||||
Register the function
|
|
||||||
|
|
||||||
### Add new dependencies
|
|
||||||
|
|
||||||
To add new Python package dependencies to the project, use the following:
|
|
||||||
`$ poetry add pandas`
|
|
||||||
|
|
||||||
### Change launch variables
|
|
||||||
|
|
||||||
Describe how the launch.json should be edited.
|
|
||||||
|
|
||||||
### Github Codespaces
|
|
||||||
|
|
||||||
Create a new repo from this template, and use the create new code.
|
|
||||||
|
|
||||||
### Using this Speckle Function
|
|
||||||
|
|
||||||
1. [Create](https://automate.speckle.dev/) a new Speckle Automation.
|
|
||||||
1. Select your Speckle Project and Speckle Model.
|
|
||||||
1. Select the deployed Speckle Function.
|
|
||||||
1. Enter a phrase to use in the comment.
|
|
||||||
1. Click `Create Automation`.
|
|
||||||
|
|
||||||
## Getting Started with Creating Your Own Speckle Function
|
|
||||||
|
|
||||||
1. [Register](https://automate.speckle.dev/) your Function with [Speckle Automate](https://automate.speckle.dev/) and select the Python template.
|
|
||||||
1. A new repository will be created in your GitHub account.
|
|
||||||
1. Make changes to your Function in `main.py`. See below for the Developer Requirements and instructions on how to test.
|
|
||||||
1. To create a new version of your Function, create a new [GitHub release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) in your repository.
|
|
||||||
|
|
||||||
## Developer Requirements
|
|
||||||
|
|
||||||
1. Install the following:
|
|
||||||
- [Python 3](https://www.python.org/downloads/)
|
|
||||||
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
|
|
||||||
1. 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`.
|
|
||||||
|
|
||||||
### Building and running the Docker Container Image
|
|
||||||
|
|
||||||
Running and testing your code on your 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
|
|
||||||
|
|
||||||
The GitHub Action packages your code into the format required by Speckle Automate. This is done by building a Docker Image, which Speckle Automate runs. You can attempt to build the Docker Image locally to test the building process.
|
|
||||||
|
|
||||||
To build the Docker Container Image, you must have [Docker](https://docs.docker.com/get-docker/) installed.
|
|
||||||
|
|
||||||
Once you have Docker running on your local machine:
|
|
||||||
|
|
||||||
1. Open a terminal
|
|
||||||
1. Navigate to the directory in which you cloned this repository
|
|
||||||
1. Run the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -f ./Dockerfile -t speckle_automate_python_example .
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Running the Docker Container Image
|
|
||||||
|
|
||||||
Once the GitHub Action has built the image, 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 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 we built earlier. `speckle_automate_python_example` is the name of the Docker Container Image. The `--rm` flag tells Docker to remove the container after it has finished running, freeing up space on your machine.
|
|
||||||
|
|
||||||
The line `python -u main.py run` is the command run inside the Docker Container Image. The rest of the command is the arguments 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 the Automation creator can set. Here, they are blank, but you can add your parameters to test your function.
|
|
||||||
- `yourSpeckleServerAuthenticationToken`—the authentication token for the Speckle Server that the Automation can connect to. This is required 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.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"speckleToken": "YOUR SPEKCLE TOKEN",
|
|
||||||
"functionInputs": {
|
|
||||||
"whisperMessage": "you are doing something weird",
|
|
||||||
"forbiddenSpeckleType": "wall"
|
|
||||||
},
|
|
||||||
"automationRunData": {
|
|
||||||
"project_id": "project id",
|
|
||||||
"speckle_server_url": "https://latest.speckle.systems",
|
|
||||||
"automation_id": "automation id",
|
|
||||||
"automation_run_id": "automation run id",
|
|
||||||
"function_run_id": "function run id",
|
|
||||||
"triggers": [
|
|
||||||
{
|
|
||||||
"payload": { "modelId": "model id", "versionId": "version id" },
|
|
||||||
"triggerType": "versionCreation"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
"""Helper module for a simple speckle object tree flattening."""
|
|
||||||
|
|
||||||
from collections.abc import Iterable
|
|
||||||
|
|
||||||
from specklepy.objects import Base
|
|
||||||
|
|
||||||
|
|
||||||
def flatten_base(base: Base) -> Iterable[Base]:
|
|
||||||
"""Flatten a base object into an iterable of bases.
|
|
||||||
|
|
||||||
This function recursively traverses the `elements` or `@elements` attribute of the
|
|
||||||
base object, yielding each nested base object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base (Base): The base object to flatten.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
Base: Each nested base object in the hierarchy.
|
|
||||||
"""
|
|
||||||
# Attempt to get the elements attribute, fallback to @elements if necessary
|
|
||||||
elements = getattr(base, "elements", getattr(base, "@elements", None))
|
|
||||||
|
|
||||||
if elements is not None:
|
|
||||||
for element in elements:
|
|
||||||
yield from flatten_base(element)
|
|
||||||
|
|
||||||
yield base
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
"""This module contains the function's business logic.
|
|
||||||
|
|
||||||
Use the automation_context module to wrap your function in an Automate context helper.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pydantic import Field, SecretStr
|
from pydantic import Field, SecretStr
|
||||||
from speckle_automate import (
|
from speckle_automate import (
|
||||||
AutomateBase,
|
AutomateBase,
|
||||||
@@ -10,17 +5,10 @@ from speckle_automate import (
|
|||||||
execute_automate_function,
|
execute_automate_function,
|
||||||
)
|
)
|
||||||
|
|
||||||
from flatten import flatten_base
|
from src.processors.commit_processory import CommitProcessor
|
||||||
|
|
||||||
|
|
||||||
class FunctionInputs(AutomateBase):
|
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/
|
|
||||||
"""
|
|
||||||
|
|
||||||
# An example of how to use secret values.
|
# An example of how to use secret values.
|
||||||
whisper_message: SecretStr = Field(title="This is a secret message")
|
whisper_message: SecretStr = Field(title="This is a secret message")
|
||||||
forbidden_speckle_type: str = Field(
|
forbidden_speckle_type: str = Field(
|
||||||
@@ -36,68 +24,39 @@ def automate_function(
|
|||||||
automate_context: AutomationContext,
|
automate_context: AutomationContext,
|
||||||
function_inputs: FunctionInputs,
|
function_inputs: FunctionInputs,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""This is an example Speckle Automate function.
|
# TODO: Add method to automation_context for sourceApplication
|
||||||
|
version_id = automate_context.automation_run_data.triggers[0].payload.version_id
|
||||||
|
commit_root = automate_context.speckle_client.commit.get(
|
||||||
|
automate_context.automation_run_data.project_id, version_id
|
||||||
|
)
|
||||||
|
|
||||||
Args:
|
# ℹ️ sourceApplication value for v2: AppName + Version => Revit2024, Revit2023 etc.
|
||||||
automate_context: A context-helper object that carries relevant information
|
# ℹ️ sourceApplication value for v3: slug => revit
|
||||||
about the runtime context of this function.
|
# ⚠️ We're just working with v3 data - adapt commit_processor for v2 data structure if you want
|
||||||
It gives access to the Speckle project data that triggered this run.
|
# ⚠️ Alternatively, write a model factory that injects the correct CommitProcessor()
|
||||||
It also has convenient methods for attaching result data to the Speckle model.
|
if commit_root.sourceApplication != "revit":
|
||||||
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()
|
|
||||||
|
|
||||||
objects_with_forbidden_speckle_type = [
|
|
||||||
b
|
|
||||||
for b in flatten_base(version_root_object)
|
|
||||||
if b.speckle_type == function_inputs.forbidden_speckle_type
|
|
||||||
]
|
|
||||||
count = len(objects_with_forbidden_speckle_type)
|
|
||||||
|
|
||||||
if count > 0:
|
|
||||||
# This is how a run is marked with a failure cause.
|
|
||||||
automate_context.attach_error_to_objects(
|
|
||||||
category="Forbidden speckle_type"
|
|
||||||
f" ({function_inputs.forbidden_speckle_type})",
|
|
||||||
object_ids=[o.id for o in objects_with_forbidden_speckle_type if o.id],
|
|
||||||
message="This project should not contain the type: "
|
|
||||||
f"{function_inputs.forbidden_speckle_type}",
|
|
||||||
)
|
|
||||||
automate_context.mark_run_failed(
|
automate_context.mark_run_failed(
|
||||||
"Automation failed: "
|
f"Automation built for v3 Revit commits. These are commits with a "
|
||||||
f"Found {count} object that have one of the forbidden speckle types: "
|
f"case-sensitive sourceApplication == 'revit', not {commit_root.sourceApplication})"
|
||||||
f"{function_inputs.forbidden_speckle_type}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set the automation context view to the original model/version view
|
# Process elements
|
||||||
# to show the offending objects.
|
model_root = automate_context.receive_version() # TODO: This is a waste!
|
||||||
automate_context.set_context_view()
|
processor = CommitProcessor()
|
||||||
|
processor.process_elements(model_root)
|
||||||
|
|
||||||
else:
|
compliance_summary = processor.logger.get_summary()
|
||||||
automate_context.mark_run_success("No forbidden types found.")
|
for missing_property, elements in compliance_summary.items():
|
||||||
|
automate_context.attach_warning_to_objects(
|
||||||
|
category="Missing Revit Material Property",
|
||||||
|
object_ids=elements,
|
||||||
|
message=f"Missing {missing_property} on the object, preventing mass calculation. "
|
||||||
|
f"Update Revit object to contain the necessary properties if element is critical. ",
|
||||||
|
)
|
||||||
|
|
||||||
# If the function generates file results, this is how it can be
|
# TODO: create_new_version_in_project
|
||||||
# attached to the Speckle project/model
|
automate_context.mark_run_success("Under development.")
|
||||||
# automate_context.store_file_result("./report.pdf")
|
|
||||||
|
|
||||||
|
|
||||||
def automate_function_without_inputs(automate_context: AutomationContext) -> None:
|
|
||||||
"""A function example without inputs.
|
|
||||||
|
|
||||||
If your function does not need any input variables,
|
|
||||||
besides what the automation context provides,
|
|
||||||
the inputs argument can be omitted.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# make sure to call the function with the executor
|
|
||||||
if __name__ == "__main__":
|
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)
|
execute_automate_function(automate_function, FunctionInputs)
|
||||||
|
|
||||||
# If the function has no arguments, the executor can handle it like so
|
|
||||||
# execute_automate_function(automate_function_without_inputs)
|
|
||||||
|
|||||||
Generated
+108
-1
@@ -42,6 +42,17 @@ files = [
|
|||||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "astroid"
|
||||||
|
version = "3.3.8"
|
||||||
|
description = "An abstract syntax tree for Python with inference support."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9.0"
|
||||||
|
files = [
|
||||||
|
{file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"},
|
||||||
|
{file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "23.2.0"
|
version = "23.2.0"
|
||||||
@@ -283,6 +294,21 @@ wrapt = ">=1.10,<2"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"]
|
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dill"
|
||||||
|
version = "0.3.9"
|
||||||
|
description = "serialize all of Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"},
|
||||||
|
{file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
graph = ["objgraph (>=1.7.2)"]
|
||||||
|
profile = ["gprof2dot (>=2022.7.29)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gql"
|
name = "gql"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
@@ -406,6 +432,32 @@ files = [
|
|||||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "6.0.0"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9.0"
|
||||||
|
files = [
|
||||||
|
{file = "isort-6.0.0-py3-none-any.whl", hash = "sha256:567954102bb47bb12e0fae62606570faacddd441e45683968c8d1734fb1af892"},
|
||||||
|
{file = "isort-6.0.0.tar.gz", hash = "sha256:75d9d8a1438a9432a7d7b54f2d3b45cad9a4a0fdba43617d9873379704a8bdf1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colors = ["colorama"]
|
||||||
|
plugins = ["setuptools"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "McCabe checker, plugin for flake8"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
@@ -882,6 +934,33 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0
|
|||||||
toml = ["tomli (>=2.0.1)"]
|
toml = ["tomli (>=2.0.1)"]
|
||||||
yaml = ["pyyaml (>=6.0.1)"]
|
yaml = ["pyyaml (>=6.0.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pylint"
|
||||||
|
version = "3.3.4"
|
||||||
|
description = "python code static checker"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9.0"
|
||||||
|
files = [
|
||||||
|
{file = "pylint-3.3.4-py3-none-any.whl", hash = "sha256:289e6a1eb27b453b08436478391a48cd53bb0efb824873f949e709350f3de018"},
|
||||||
|
{file = "pylint-3.3.4.tar.gz", hash = "sha256:74ae7a38b177e69a9b525d0794bd8183820bfa7eb68cc1bee6e8ed22a42be4ce"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
astroid = ">=3.3.8,<=3.4.0-dev0"
|
||||||
|
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||||
|
dill = [
|
||||||
|
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
|
||||||
|
{version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
||||||
|
]
|
||||||
|
isort = ">=4.2.5,<5.13.0 || >5.13.0,<7"
|
||||||
|
mccabe = ">=0.6,<0.8"
|
||||||
|
platformdirs = ">=2.2.0"
|
||||||
|
tomlkit = ">=0.10.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
spelling = ["pyenchant (>=3.2,<4.0)"]
|
||||||
|
testutils = ["gitpython (>3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.4.4"
|
version = "7.4.4"
|
||||||
@@ -1019,6 +1098,34 @@ files = [
|
|||||||
{file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"},
|
{file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structlog"
|
||||||
|
version = "25.1.0"
|
||||||
|
description = "Structured Logging for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "structlog-25.1.0-py3-none-any.whl", hash = "sha256:843fe4f254540329f380812cbe612e1af5ec5b8172205ae634679cd35a6d6321"},
|
||||||
|
{file = "structlog-25.1.0.tar.gz", hash = "sha256:2ef2a572e0e27f09664965d31a576afe64e46ac6084ef5cec3c2b8cd6e4e3ad3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"]
|
||||||
|
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"]
|
||||||
|
tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]
|
||||||
|
typing = ["mypy (>=1.4)", "rich", "twisted"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomlkit"
|
||||||
|
version = "0.13.2"
|
||||||
|
description = "Style preserving TOML library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"},
|
||||||
|
{file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.12.2"
|
version = "4.12.2"
|
||||||
@@ -1386,4 +1493,4 @@ propcache = ">=0.2.0"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "016af7c16ab48a41f716ccec40a277fee96b42bb15dc790ac1f6b2c0fc9920a4"
|
content-hash = "1251201031b01adfe8b5f59d340deb29175d98c2d372a34dd1f883df16dec333"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ package-mode = false
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
specklepy = "^2.21.0"
|
specklepy = "^2.21.0"
|
||||||
|
pylint = "^3.3.4"
|
||||||
|
structlog = "^25.1.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^23.3.0"
|
black = "^23.3.0"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import Dict
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
class MassAggregator:
|
||||||
|
def __init__(self):
|
||||||
|
self.totals = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
|
||||||
|
|
||||||
|
def add_mass(self, mass: float, level: str, type: str, material: str) -> None:
|
||||||
|
if mass <= 1e-6:
|
||||||
|
return
|
||||||
|
self.totals[level][type][material] += mass
|
||||||
|
|
||||||
|
def get_totals(self) -> Dict:
|
||||||
|
return {
|
||||||
|
"by_level": {
|
||||||
|
level: {
|
||||||
|
"total": sum(
|
||||||
|
sum(material_masses.values())
|
||||||
|
for material_masses in types.values()
|
||||||
|
),
|
||||||
|
"by_type": {
|
||||||
|
type_name: {
|
||||||
|
"total": sum(material_masses.values()),
|
||||||
|
"by_material": material_masses,
|
||||||
|
}
|
||||||
|
for type_name, material_masses in types.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for level, types in self.totals.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import structlog
|
||||||
|
from typing import Dict, DefaultDict
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class ComplianceLogger:
|
||||||
|
def __init__(self):
|
||||||
|
self.missing_properties: DefaultDict[str, set] = defaultdict(set)
|
||||||
|
|
||||||
|
def log_missing_properties(self, object_id: str, missing_property: str) -> None:
|
||||||
|
# Log to our collection for automation results
|
||||||
|
self.missing_properties[missing_property].add(object_id)
|
||||||
|
|
||||||
|
# Still log individual cases for dev
|
||||||
|
logger.warn(
|
||||||
|
"non_compliant_element",
|
||||||
|
object_id=object_id,
|
||||||
|
property=missing_property,
|
||||||
|
message=f"Missing: '{missing_property}' on object {object_id}. No computation on "
|
||||||
|
f"for this object possible. Skipped.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_summary(self) -> Dict[str, list]:
|
||||||
|
return {
|
||||||
|
prop: list(elements) for prop, elements in self.missing_properties.items()
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from src.aggregators.carbon_totals import MassAggregator
|
||||||
|
from src.logging.compliance_logger import ComplianceLogger
|
||||||
|
from src.utils.constants import * # wildcard is a little dangerous
|
||||||
|
|
||||||
|
|
||||||
|
class CommitProcessor:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = ComplianceLogger()
|
||||||
|
self.mass_aggregator = MassAggregator()
|
||||||
|
|
||||||
|
def process_elements(
|
||||||
|
self, model: "Base"
|
||||||
|
) -> None: # No return needed, we're modifying in-place
|
||||||
|
levels = getattr(model, ELEMENTS, None)
|
||||||
|
if not levels: # First nesting => levels
|
||||||
|
raise ValueError("Invalid commit: missing elements at the model root.")
|
||||||
|
|
||||||
|
for level in levels:
|
||||||
|
type_groups = getattr(level, ELEMENTS, None)
|
||||||
|
if not type_groups:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid level structure: missing elements in {getattr(level,NAME, '!Missing name attribute!')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for type_group in type_groups:
|
||||||
|
revit_objects = getattr(type_group, ELEMENTS, None)
|
||||||
|
if not revit_objects:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid type structure: missing elements in "
|
||||||
|
f"{getattr(type_group, NAME, '!Missing name attribute!')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
level_name = getattr(level, NAME, None)
|
||||||
|
type_name = getattr(type_group, NAME, None)
|
||||||
|
if level_name is None or type_name is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Every object should be on a level and be of a type."
|
||||||
|
)
|
||||||
|
|
||||||
|
for revit_object in revit_objects:
|
||||||
|
self.process_element(
|
||||||
|
level=level_name, type_name=type_name, revit_object=revit_object
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_element(
|
||||||
|
self, level: str, type_name: str, revit_object: Dict[str, Any]
|
||||||
|
) -> None: # Mutating in-place
|
||||||
|
elements = getattr(revit_object, ELEMENTS, None)
|
||||||
|
if not elements:
|
||||||
|
self.logger.log_missing_properties(revit_object[ID], ELEMENTS)
|
||||||
|
|
||||||
|
for element in elements:
|
||||||
|
properties = getattr(element, PROPERTIES, None)
|
||||||
|
if not properties:
|
||||||
|
self.logger.log_missing_properties(
|
||||||
|
revit_object[ID], PROPERTIES
|
||||||
|
) # 🤔 revit_object/element?
|
||||||
|
return
|
||||||
|
|
||||||
|
material_quantities = properties.get(MATERIAL_QUANTITIES, None)
|
||||||
|
if not material_quantities:
|
||||||
|
self.logger.log_missing_properties(
|
||||||
|
revit_object[ID], MATERIAL_QUANTITIES
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for material_name, material_data in material_quantities.items():
|
||||||
|
if VOLUME not in material_data:
|
||||||
|
self.logger.log_missing_properties(revit_object[ID], VOLUME)
|
||||||
|
return
|
||||||
|
|
||||||
|
if STRUCTURAL_ASSET not in material_data:
|
||||||
|
self.logger.log_missing_properties(
|
||||||
|
revit_object[ID], STRUCTURAL_ASSET
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ⚠️ This should never hit. No STRUCTURAL_ASSET → no DENSITY
|
||||||
|
if DENSITY not in material_data:
|
||||||
|
self.logger.log_missing_properties(revit_object[ID], DENSITY)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Dict structure for numerical properties(e.g.)
|
||||||
|
# {"name" : "volume", "value" : 100, "units" : "Cubic metres"}
|
||||||
|
# 🤫 Shouldn't change.
|
||||||
|
volume = material_data[VOLUME][VALUE]
|
||||||
|
density = material_data[DENSITY][VALUE]
|
||||||
|
mass = volume * density
|
||||||
|
|
||||||
|
material_data[MASS] = {
|
||||||
|
NAME: MASS,
|
||||||
|
VALUE: mass,
|
||||||
|
UNITS: material_data[DENSITY][UNITS].split()[0],
|
||||||
|
# TODO: 🫣 Units string operation is super sketchy.
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mass_aggregator.add_mass(
|
||||||
|
mass, level, type_name, material_data[STRUCTURAL_ASSET]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ❗ We've validated everything. If the computation fails, there's a bug.
|
||||||
|
# 🤾 Throw.
|
||||||
|
except (ValueError, TypeError, KeyError) as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Computation failed for {material_name} despite having required properties: {str(e)}"
|
||||||
|
) from e
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
REQUIRED_PROPERTIES = ["volume", "density", "structuralAsset"]
|
||||||
|
|
||||||
|
# Keys
|
||||||
|
DENSITY = "density"
|
||||||
|
ELEMENTS = "elements"
|
||||||
|
ID = "id"
|
||||||
|
MASS = "mass"
|
||||||
|
MATERIAL_QUANTITIES = "Material Quantities"
|
||||||
|
NAME = "name"
|
||||||
|
PROPERTIES = "properties"
|
||||||
|
SOURCE_APPLICATION = "sourceApplication"
|
||||||
|
STRUCTURAL_ASSET = "structuralAsset"
|
||||||
|
UNITS = "units"
|
||||||
|
VALUE = "value"
|
||||||
|
VOLUME = "volume"
|
||||||
Reference in New Issue
Block a user