Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
name: continuous deployment

on:
pull_request:
branches:
- develop
- main
push:
branches:
- develop
- main
release:
types:
- published

jobs:

# deploy docs for develop and main branches
# deploy docs if not release
deploy-docs:
if: github.event_name != 'release'
runs-on: ubuntu-latest
steps:
# setup, checkout pull_request.head.ref for repo-vis
Expand Down Expand Up @@ -56,10 +55,9 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/_build/html

# deploy distribution if a new release and tag are created
# deploy distribution if release
deploy-dist:
needs: deploy-docs
if: startsWith(github.ref, 'refs/tags')
if: github.event_name == 'release'
runs-on: ubuntu-latest
steps:
# setup
Expand Down Expand Up @@ -88,3 +86,41 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}

# deploy development distribution with changes to develop
deploy-dev-dist:
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: setup-python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: install-poetry
uses: snok/install-poetry@v1
with:
version: 1.4.0
virtualenvs-in-project: false
virtualenvs-path: ~/.virtualenvs
- name: install dependencies
run: poetry install --no-root --with=dev
- name: increment dev version
env:
PYPI_URL: https://pypi.org
run: poetry run python scripts/bump_dev_version.py
- name: build dist
run: poetry build
- name: publish dev distribution to Test PyPI
id: test-pypi
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
skip_existing: true
- name: publish distribution to PyPI
if: steps.test-pypi.outcome == 'success'
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
16 changes: 11 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.10', '3.11', '3.12']
steps:
- name: checkout
uses: actions/checkout@v4
Expand All @@ -62,12 +62,14 @@ jobs:
virtualenvs-path: ~/.virtualenvs
- name: poetry install
run: poetry install --all-extras
- name: install pytest-xdist for parallel tests
run: poetry run pip install pytest-xdist
- name: lint
run: poetry run flake8 buildingmotif
- name: type check
run: poetry run mypy --ignore-missing-imports
- name: unit tests
run: poetry run pytest tests/unit --cov=./ --cov-report=xml
run: poetry run pytest -n auto tests/unit --cov=./ --cov-report=xml
- name: build tests
run: poetry build

Expand Down Expand Up @@ -101,8 +103,10 @@ jobs:
virtualenvs-path: ~/.virtualenvs
- name: poetry install
run: poetry install --all-extras
- name: install pytest-xdist for parallel tests
run: poetry run pip install pytest-xdist
- name: integration tests
run: poetry run pytest tests/integration
run: poetry run pytest -n auto tests/integration
- name: bacnet tests
run: |
cd tests/integration/fixtures/bacnet
Expand Down Expand Up @@ -139,8 +143,10 @@ jobs:
virtualenvs-path: ~/.virtualenvs
- name: poetry install
run: poetry install --all-extras
- name: install pytest-xdist for parallel tests
run: poetry run pip install pytest-xdist
- name: library tests
run: poetry run pytest tests/library
run: poetry run pytest -n auto tests/library

coverage:
needs: testing
Expand All @@ -149,4 +155,4 @@ jobs:
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
files: ./coverage.xml
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repos:
- id: black
entry: poetry run black
- repo: https://github.com/pycqa/flake8
rev: 5.0.0
rev: 7.0.0
hooks:
- id: flake8
entry: poetry run flake8 buildingmotif
Expand Down
8 changes: 3 additions & 5 deletions buildingmotif/api/views/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from flask import Blueprint, current_app, jsonify
from flask_api import status
from rdflib import URIRef
from sqlalchemy.orm.exc import NoResultFound

from buildingmotif.api.serializers.library import serialize
from buildingmotif.database.errors import LibraryNotFound
from buildingmotif.dataclasses.shape_collection import ShapeCollection

blueprint = Blueprint("libraries", __name__)
Expand Down Expand Up @@ -81,9 +81,7 @@ def get_library(library_id: int) -> flask.Response:
"""
try:
db_lib = current_app.building_motif.table_connection.get_db_library(library_id)
except NoResultFound:
return {
"message": f"No library with id {library_id}"
}, status.HTTP_404_NOT_FOUND
except LibraryNotFound:
return {"message": f"ID: {library_id}"}, status.HTTP_404_NOT_FOUND

return jsonify(serialize(db_lib)), status.HTTP_200_OK
52 changes: 30 additions & 22 deletions buildingmotif/api/views/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
from flask_api import status
from rdflib import Graph, URIRef
from rdflib.plugins.parsers.notation3 import BadSyntax
from sqlalchemy.orm.exc import NoResultFound

from buildingmotif.api.serializers.model import serialize
from buildingmotif.database.errors import (
LibraryNotFound,
ModelNotFound,
ShapeCollectionNotFound,
)
from buildingmotif.dataclasses import Library, Model, ShapeCollection

blueprint = Blueprint("models", __name__)
Expand Down Expand Up @@ -34,8 +38,8 @@ def get_model(models_id: int) -> flask.Response:
"""
try:
model = current_app.building_motif.table_connection.get_db_model(models_id)
except NoResultFound:
return {"message": f"No model with id {models_id}"}, status.HTTP_404_NOT_FOUND
except ModelNotFound:
return {"message": f"ID: {models_id}"}, status.HTTP_404_NOT_FOUND

return jsonify(serialize(model)), status.HTTP_200_OK

Expand All @@ -51,8 +55,8 @@ def get_model_graph(models_id: int) -> Graph:
"""
try:
model = Model.load(models_id)
except NoResultFound:
return {"message": f"No model with id {models_id}"}, status.HTTP_404_NOT_FOUND
except ModelNotFound:
return {"message": f"ID: {models_id}"}, status.HTTP_404_NOT_FOUND

g = Graph() + model.graph

Expand All @@ -70,8 +74,8 @@ def get_target_nodes(models_id: int) -> Graph:
"""
try:
model = Model.load(models_id)
except NoResultFound:
return {"message": f"No model with id {models_id}"}, status.HTTP_404_NOT_FOUND
except ModelNotFound:
return {"message": f"ID: {models_id}"}, status.HTTP_404_NOT_FOUND

result = model.graph.query(
"""
Expand Down Expand Up @@ -132,8 +136,8 @@ def update_model_graph(models_id: int) -> flask.Response:
"""
try:
model = Model.load(models_id)
except NoResultFound:
return {"message": f"No model with id {models_id}"}, status.HTTP_404_NOT_FOUND
except ModelNotFound:
return {"message": f"ID: {models_id}"}, status.HTTP_404_NOT_FOUND

if request.content_type != "application/xml":
return {
Expand All @@ -160,12 +164,16 @@ def validate_model(models_id: int) -> flask.Response:
# get model
try:
model = Model.load(models_id)
except NoResultFound:
return {"message": f"No model with id {models_id}"}, status.HTTP_404_NOT_FOUND
except ModelNotFound:
return {"message": f"ID: {models_id}"}, status.HTTP_404_NOT_FOUND

# we will read the shape collections from the input
shape_collections = []

# no body provided -- default to model manifest and default SHACL engine
# get shacl_engine from the query params, defaults to the current engine
shacl_engine = request.args.get("shacl_engine", None)

# no body provided -- default to model manifest
if request.content_length is None:
shape_collections = [model.get_manifest()]
else:
Expand All @@ -182,23 +190,23 @@ def validate_model(models_id: int) -> flask.Response:

if body is not None and not isinstance(body, dict):
return {"message": "body is not dict"}, status.HTTP_400_BAD_REQUEST
shape_collections = []
body = body if body is not None else {}
nonexistent_libraries = []
for library_id in body.get("library_ids", []):
try:
shape_collection = Library.load(library_id).get_shape_collection()
shape_collections.append(shape_collection)
except NoResultFound:
except LibraryNotFound:
nonexistent_libraries.append(library_id)
if len(nonexistent_libraries) > 0:
return {
"message": f"Libraries with ids {nonexistent_libraries} do not exist"
}, status.HTTP_400_BAD_REQUEST

# if shape_collections is empty, model.validate will default
# to the model's manifest
vaildation_context = model.validate(shape_collections)
# if shape_collections is empty, model.validate will default to the model's manifest
vaildation_context = model.validate(
shape_collections, error_on_missing_imports=False, shacl_engine=shacl_engine
)

return {
"message": vaildation_context.report_string,
Expand All @@ -215,8 +223,8 @@ def validate_shape(models_id: int) -> flask.Response:
# get model
try:
model = Model.load(models_id)
except NoResultFound:
return {"message": f"No model with id {models_id}"}, status.HTTP_404_NOT_FOUND
except ModelNotFound:
return {"message": f"ID: {models_id}"}, status.HTTP_404_NOT_FOUND

# get body
if request.content_type != "application/json":
Expand All @@ -239,7 +247,7 @@ def validate_shape(models_id: int) -> flask.Response:
try:
shape_collection = ShapeCollection.load(shape_collection_id)
shape_collections.append(shape_collection)
except NoResultFound:
except ShapeCollectionNotFound:
nonexistent_shape_collections.append(shape_collection_id)
if len(nonexistent_shape_collections) > 0:
return {
Expand All @@ -255,8 +263,8 @@ def validate_shape(models_id: int) -> flask.Response:
target_class = URIRef(body.get("target_class"))

# test
conformance = model.test_model_against_shapes(
shape_collections=shape_collections,
compiled = model.compile(shape_collections)
conformance = compiled.validate_model_against_shapes(
shapes_to_test=shape_uris,
target_class=target_class,
)
Expand Down
Loading
Loading