diff --git a/app/config/development.default.py b/app/config/development.default.py index d132cc9d4157d8719a5422f633ed84a36c7eb667..e72d5aa0f7c4ed05794e1738ae554dbf6a720c3a 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -16,9 +16,9 @@ SERVICE_CONFIG = { 'app_token': 'x-blocpower-app-token', 'app_secret': 'x-blocpower-app-secret'}, 'urls': { - 'app': 'http://staging.app.s.blocpower.us/', - 'document': 'http://dev.document.s.blocpower.io/', - 'user': 'http://staging.user.s.blocpower.us/'} + 'app': 'http://dev.appservice.blocpower.io/', + 'user': 'http://dev.userservice.blocpower.io/', + 'document': 'http://dev.document.s.blocpower.io/'} } # AppService diff --git a/app/lib/database.py b/app/lib/database.py index e33d8008986f23e520c8bf5633e7198e8b7b7e90..23bea8bd4efd9f432b758b4570b53efa4ac0f761 100644 --- a/app/lib/database.py +++ b/app/lib/database.py @@ -22,7 +22,7 @@ def register(app): def commit(): """A wrapper for db.session.commit(). - + This rolls back the database session after a failed commit so that the app can continue to request future commits on the same thread. """ @@ -31,3 +31,62 @@ def commit(): except Exception as e: db.session.rollback() raise e + +class ProcTable: + """ProcTable represents a simplified class of SQLAlchemy db.model""" + + def __init__(self, name, *columns): + self.name = name + self.columns = [column for column in columns] + + def get_columns(self): + """List all columns""" + return [column.key for column in self.columns] + +class ProcColumn: + """ProcColumn represents a column similar to SQLAlchemy column""" + + def __init__(self, key): + self.key = key + +def proc(model, method, limit=None, offset=None, **kwargs): + """ + Run stored procedure + + Args: + model (class): The class of the db table + method (str): Method name for stored procedure + kwargs: Arguments for stored proc + Returns: + list: Results of the query + """ + + params = "" + cols = ','.join(str(i) for i in model.__table__.get_columns()) + for key, value in kwargs.items(): + params += "in_{} := '{}', ".format(key, value) + params = params[:-2] # remove last comma and space + + query = "select {} from {}.{}({})".format( + cols, + model.__table_args__['schema'], + method, + params + ) + if limit: + query += ' limit {}'.format(limit) + if offset: + query += ' offset {}'.format(offset) + + try: + results = db.session.execute(query) + db.session.commit() + + data = [] + for row in results: + data.append(model(**dict(zip(row.keys(), row)))) + + except Exception as err: + raise err + + return data diff --git a/app/models/base.py b/app/models/base.py index a40ccd872a654cd90bd58ad94469a45278f37d31..e6a44c446c36b6f03856e21a92aa0f1175084d80 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -1,16 +1,72 @@ -from ..lib.database import db -from ..lib.service import services +import arrow +import decimal +import uuid + from werkzeug.exceptions import InternalServerError, BadRequest import base64 import json +from sqlalchemy.sql import func +from sqlalchemy.ext.declarative import declared_attr + +from geoalchemy2 import Geometry +from geoalchemy2.elements import WKBElement, WKTElement + +from ..lib.database import db +from ..lib import geography +from . import columns +from ..lib.service import services + + +class BaseModel: + """A base mixin for all models.""" + def get_dictionary(self): + """Return a dictionary representation of the model. + + By default, this just shows every field on the model. + """ + d = {} + for column in self.__table__.columns: + key = column.key + value = getattr(self, key) + + # Custom rendering can go here or in a custom JSON parser. + if isinstance(value, arrow.Arrow): + value = value.isoformat() + elif isinstance(value, uuid.UUID): + value = str(value) + elif isinstance(value, decimal.Decimal): + # If we cast decimals to floats, we end up with rounding + # errors. Better to render them out to strings. + value = str(value) + elif isinstance(value, (WKBElement, WKTElement)): + value = db.session.scalar(value.ST_AsText()) + + d[key] = value + return d + + +class Model(BaseModel): + """A base mixin for SQLAlchemy model.""" + @declared_attr + def __tablename__(cls): + """Automatically set the database table name to the class name + lower-cased. + """ + return cls.__name__.lower() + + __table_args__ = {"schema": "public"} + + def __str__(self): + """Provide a sane default for model string representation.""" + return '<{} (id={})>'.format(self.__class__.__name__, self.id) + + id = db.Column(db.Integer, primary_key=True) -class Model: @staticmethod def run_proc(method, schema, columns=None, limit=None, offset=None, **kwargs): """ Run stored proc - Args: method (str) - Method name for stored procedure columns (list) - DB Columns @@ -56,15 +112,15 @@ class Model: encoded_string_data = encoded_byte_data.decode('utf-8') folder_path = "/Buildings/{}_{}/{}".format(building_id, - address, - folder_name) + address, + folder_name) post_data = { - "path": folder_path, - "data": "data:csv/plain;charset=utf-8;base64,{}".format(encoded_string_data), - "building_id": building_id, - "tags": tag, - "name": file_name - } + "path": folder_path, + "data": "data:csv/plain;charset=utf-8;base64,{}".format(encoded_string_data), + "building_id": building_id, + "tags": tag, + "name": file_name + } # Call the generic method in the base class return services.document.post('', '/document/', data=json.dumps(post_data)) @@ -85,3 +141,31 @@ class Model: else: raise InternalServerError("Failed to get documents from document service: {} {}".format(response.status_code, response.reason)) return document_list + + +class Tracked(object): + """A mixin to include tracking datetime fields.""" + created = db.Column(columns.Arrow, default=func.now()) + updated = db.Column(columns.Arrow, onupdate=func.now()) + + +class Address(object): + """An address (with street number, city, state, zip, ...). This does not + necessarily correlate with a physical location as defined in the + Location base model. + """ + street_address = db.Column(db.Unicode(255), nullable=False) + city = db.Column(db.Unicode(255), nullable=False) + county = db.Column(db.Unicode(255)) + state = db.Column( + db.Enum(*geography.states, name='geo_states'), + nullable=False) + country = db.Column( + db.Enum(*geography.countries, name='geo_countries'), + nullable=False) + postal_code = db.Column(db.Unicode(10)) + + +class Location(object): + """A physical location (with latitude, longitude, and elevation).""" + point = db.Column(Geometry('POINT'), nullable=False)