Skip to content

Commit

Permalink
Merge pull request #230 from NREL/ci-cleanup
Browse files Browse the repository at this point in the history
fix merge confict mistakes from CI cleanup
  • Loading branch information
MatthewSteen authored Mar 22, 2023
2 parents dbd76f0 + 3ef60a1 commit d9a7dca
Show file tree
Hide file tree
Showing 27 changed files with 1,591 additions and 713 deletions.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POSTGRES_DB=buildingmotif
POSTGRES_USER=buildingmotif
POSTGRES_PASSWORD=password
34 changes: 34 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.10"
# You can also specify other tool versions:
# nodejs: "19"
# rust: "1.64"
# golang: "1.19"
jobs:
pre_build:
# Generate the Sphinx configuration for this Jupyter Book so it builds.
- "pip install -U jupyter-book"
- "jupyter-book config sphinx docs/"

# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py

# If using Sphinx, optionally build your docs in additional formats such as PDF
# formats:
# - pdf

# Optionally declare the Python requirements required to build your docs
# python:
# install:
# - requirements: docs/requirements.txt
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.8

# Copy project
WORKDIR /buildingmotif
COPY ./buildingmotif ./buildingmotif

# Install Dependices
RUN pip install poetry && poetry config virtualenvs.create false
COPY ./poetry.lock .
COPY ./pyproject.toml .
COPY ./README.md .
RUN poetry install --no-dev

COPY ./libraries ./libraries
COPY ./configs.py ./configs.py
COPY ./migrations ./migrations
COPY ./alembic.ini ./alembic.ini
42 changes: 0 additions & 42 deletions README.md

This file was deleted.

8 changes: 8 additions & 0 deletions buildingmotif-app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:18.10.0

WORKDIR /buildingmotif-app
COPY . .

RUN npm install -g @angular/cli

CMD ng serve --host 0.0.0.0
2 changes: 1 addition & 1 deletion buildingmotif/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ def create_app(DB_URI):
import configs as building_motif_configs # type: ignore # isort:skip

app = create_app(building_motif_configs.DB_URI)
app.run(debug=True, threaded=False)
app.run(debug=True, host="0.0.0.0", threaded=False)
168 changes: 168 additions & 0 deletions buildingmotif/bin/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import argparse
import logging
import shutil
import sys
from os import getenv
from pathlib import Path

from buildingmotif import BuildingMOTIF
from buildingmotif.dataclasses import Library

cli = argparse.ArgumentParser(
prog="buildingmotif", description="CLI Interface for common BuildingMOTIF tasks"
)
subparsers = cli.add_subparsers(dest="subcommand")
subcommands = {}
log = logging.getLogger()
log.setLevel(logging.INFO)

ONTOLOGY_FILE_SUFFIXES = ["ttl", "n3", "ntriples", "xml"]


# borrowing some ideas from https://gist.github.com/mivade/384c2c41c3a29c637cb6c603d4197f9f
def arg(*argnames, **kwargs):
"""Helper for defining arguments on subcommands"""
return argnames, kwargs


def subcommand(*subparser_args, parent=subparsers):
"""Decorates a function and makes it available as a subcommand"""

def decorator(func):
parser = parent.add_parser(func.__name__, description=func.__doc__)
for args, kwargs in subparser_args:
parser.add_argument(*args, **kwargs)
parser.set_defaults(func=func)
subcommands[func] = parser

return decorator


def get_db_uri(args) -> str:
"""
Fetches the db uri from args, or prints the usage
for the corresponding subcommand
"""
db_uri = args.db
if db_uri is not None:
return db_uri
db_uri = getenv("DB_URI")
if db_uri is not None:
return db_uri
try:
import configs as building_motif_configs
except ImportError:
print("No DB URI could be found")
print("No configs.py file found")
subcommands[args.func].print_help()
sys.exit(1)
db_uri = building_motif_configs.DB_URI
if db_uri is not None:
return db_uri
print("No DB URI could be found")
subcommands[args.func].print_help()
sys.exit(1)


@subcommand(
arg(
"-d",
"--db",
help="Database URI of the BuildingMOTIF installation. "
'Defaults to $DB_URI and then contents of "config.py"',
),
arg(
"--dir",
help="Path to a local directory containing the library",
nargs="+",
),
arg(
"-o",
"--ont",
help="Remote URL or local file path to an RDF ontology",
nargs="+",
),
arg(
"-l",
"--libraries",
help="Filename of the libraries YAML file specifying what "
"should be loaded into BuildingMOTIF",
default="libraries.yml",
nargs="+",
dest="library_manifest_file",
),
)
def load(args):
"""
Loads libraries from (1) local directories (--dir),
(2) local or remote ontology files (--ont)
(3) library spec file (--libraries): the provided YML file into the
BuildingMOTIF instance at $DB_URI or whatever is in 'configs.py'.
Use 'get_default_libraries_yml' for the format of the expected libraries.yml file
"""
db_uri = get_db_uri(args)
bm = BuildingMOTIF(db_uri)
bm.setup_tables()
for directory in args.dir or []:
Library.load(directory=directory)
for ont in args.ont or []:
Library.load(ontology_graph=ont)
for library_manifest_file in args.library_manifest_file or []:
manifest_path = Path(library_manifest_file)
log.info(f"Loading buildingmotif libraries listed in {manifest_path}")
Library.load_from_libraries_yml(str(manifest_path))
bm.session.commit()


@subcommand()
def get_default_libraries_yml(_args):
"""
Creates a default 'libraries.default.yml' file in the current directory
that can be edited and used with buildingmotif
"""
default_file = (
Path(__file__).resolve().parents[1] / "resources" / "libraries.default.yml"
)
shutil.copyfile(default_file, "libraries.default.yml")
print("libraries.default.yml created in the current directory")


@subcommand(
arg(
"-b",
"--bind",
help="Address on which to bind the API server",
default="localhost",
),
arg(
"-p", "--port", help="Listening port for the API server", type=int, default=5000
),
arg(
"-d",
"--db",
help="Database URI of the BuildingMOTIF installation. "
'Defaults to $DB_URI and then contents of "config.py"',
),
)
def serve(args):
"""
Serves the BuildingMOTIF API on the indicated host:port
"""
from buildingmotif.api.app import create_app

db_uri = get_db_uri(args)
webapp = create_app(db_uri)
webapp.run(host=args.host, port=args.port, threaded=False)


def app():
args = cli.parse_args()
if args.subcommand is None:
cli.print_help()
else:
args.func(args)


# entrypoint is actually defined in pyproject.toml; this is here for convenience/testing
if __name__ == "__main__":
app()
9 changes: 6 additions & 3 deletions buildingmotif/database/tables.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import Dict, List

from sqlalchemy import JSON, Column, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy import Column, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, declarative_base, relationship

# from sqlalchemy.dialects.postgresql import JSON
from buildingmotif.database.utils import JSONType

Base = declarative_base()


Expand Down Expand Up @@ -59,7 +62,7 @@ class DepsAssociation(Base):
dependant_id: Mapped[int] = Column(ForeignKey("template.id"))
dependee_id: Mapped[int] = Column(ForeignKey("template.id"))
# args are a mapping of dependee args to dependant args
args: Mapped[Dict[str, str]] = Column(JSON)
args: Mapped[Dict[str, str]] = Column(JSONType) # type: ignore

__table_args__ = (
UniqueConstraint(
Expand All @@ -79,7 +82,7 @@ class DBTemplate(Base):
id: Mapped[int] = Column(Integer, primary_key=True)
name: Mapped[str] = Column(String(), nullable=False)
body_id: Mapped[str] = Column(String())
optional_args: Mapped[List[str]] = Column(JSON)
optional_args: Mapped[List[str]] = Column(JSONType) # type: ignore

library_id: Mapped[int] = Column(Integer, ForeignKey("library.id"), nullable=False)
library: Mapped[DBLibrary] = relationship("DBLibrary", back_populates="templates")
Expand Down
24 changes: 24 additions & 0 deletions buildingmotif/database/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
import json

import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB


class JSONType(sa.types.TypeDecorator):
"""Custom JSON type that uses JSONB on Postgres and JSON on other dialects.
This allows us to use our custom JSON serialization below *and* have the
database enforce uniqueness on JSON-encoded dictionaries
"""

impl = sa.JSON
hashable = False
cache_ok = True

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def load_dialect_impl(self, dialect):
if dialect.name == "postgresql":
# Use the native JSON type.
return dialect.type_descriptor(JSONB())
else:
return dialect.type_descriptor(sa.JSON())


# the custom ser/de handlers are to allow the database to ensure
# uniqueness of dependency bindings (see https://github.com/NREL/BuildingMOTIF/pull/113)
Expand Down
Loading

0 comments on commit d9a7dca

Please sign in to comment.