Commit ef44a098 authored by Andrew Hrdy's avatar Andrew Hrdy

Merge branch 'rewrite2' of git.gmu.edu:srct/where into rewrite2

parents cfc1d4b8 1ac13070
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = where/alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///db.sqlite3
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return "<h1>Hello</h1>"
if __name__ == '__main__':
app.run()
import enum
class FieldType(enum.Enum):
STRING = 'string'
FLOAT = 'float'
INTEGER = 'integer'
BOOLEAN = 'boolean'
ENUM = 'enum'
import sqlalchemy
from sqlalchemy.schema import Column
from sqlalchemy import String, DateTime, Numeric, ARRAY, Table, ForeignKey, Enum, Integer
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship, validates
from sqlalchemy.orm.session import Session as BaseSession
from .field_types import FieldType
# TODO declarative_base
class Base(object):
id = Column(UUID, primary_key=True, default=lambda: str(uuid.uuid4()))
class Session(SessionManager):
engine = sqlalchemy.create_engine(config['sqlalchemy.url'])
# This table maintains the field <-> ObjectType links.
type_links = Table('type_links', Base.metadata,
Column('type_id', UUID, ForeignKey('objecttype.id')),
Column('field_id' UUID, ForeignKey('field.id')))
class MapObject(Base):
__tablename__ = 'mapobject' #TODO fix for py3/new sa
name = Column(String, nullable=False)
type_id = Column(UUID, ForeignKey('objecttype.id')) # TODO fix foreignkeyconstraint
type = relationship('ObjectType')
lat = Column(Numeric(precision=10, scale=15, asdecimal=False), nullable=False)#TODO this doesn't work
lon = Column(Numeric(precision=10, scale=15, asdecimal=False), nullable=False)
data = Column(JSONB)
@validates('data')
def validate_data(self, _, data):
if data is None:
return
fields = type.fields
for key in data:
for field in fields:
if field.name == key:
break
else:
raise ValueError('extra data must be a registered field')
field.validate_data(data[key])
return data
class ObjectType(Base):
"""
Represent a schema
"""
__tablename__ = 'objecttype' #TODO py3
name = Column(String, nullable=False, unique=True)
fields = relationship('Field', secondary=type_links, backref='types')
class Field(Base):
"""
Represent a field that can be on an ObjectType schema
"""
__tablename__ = 'field' # TODO this don't work in py3?
name = Column(String, nullable=False, unique=True)
type = Column(Enum(FieldType), nullable=False)
unit = Column(String)
values = Column(ARRAY(String)) # enum values - TODO change to comma-separated list if sqlite don't support?
# types: List[ObjectType]
def validate_data(self, data):
"""
Verify that data is the correct type for this Field.
"""
if self.type is FieldType.BOOLEAN and isinstance(data, bool):
return
if self.type is FieldType.FLOAT and isinstance(data, float) or isinstance(data, int):
return
if self.type is FieldType.INTEGER and isinstance(data, int):
return
if self.type is FieldType.STRING and isinstance(data, str):
return
if self.type is FieldType.ENUM and isinstance(data, str):
if data in self.values:
return
print(type(data))
print(self.type)
raise ValueError('Invalid input "{}" for field {}'.format(data, self.name))
......@@ -2,7 +2,7 @@
"id": 24,
"name": null,
"floor": 3,
"category": "water_fountain",
"category": 1,
"parent": 1,
"lat": 77.374695589,
"lon": -44.89345483,
......
{
"id": 0,
"slug": "water_fountain",
"id": 1,
"name": "Water Fountain",
"icon": "https://karel.pw/water.png",
"attributes": {
"coldness": {
"name": "Coldness",
"type": "rating",
"icon": "https://karel.pw/water.png"
"type": "RATING",
"slug": "coldness"
},
"bottle_filler": {
"type": "bool",
"name": "Has Bottle Filler"
"type": "BOOLEAN",
"name": "Has Bottle Filler",
"slug": "bottle_filler"
}
}
}
Generic single-database configuration.
\ No newline at end of file
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
import sys, os
sys.path.append(os.getcwd())
from where.model.sa import Base
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
"""empty message
Revision ID: 01aa045b98a6
Revises: 26f2853e67de
Create Date: 2020-02-16 20:19:56.497610
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '01aa045b98a6'
down_revision = '26f2853e67de'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
"""empty message
Revision ID: 09ab4264e119
Revises: 01aa045b98a6
Create Date: 2020-02-16 20:24:57.916333
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '09ab4264e119'
down_revision = '01aa045b98a6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
"""empty message
Revision ID: 26f2853e67de
Revises:
Create Date: 2020-02-16 19:33:26.275339
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '26f2853e67de'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('category',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('icon', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('field',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('slug', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('type', sa.Enum('STRING', 'FLOAT', 'INTEGER', 'BOOLEAN', 'RATING', name='fieldtype'), nullable=False),
sa.Column('category_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['category_id'], ['category.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('point',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('lat', sa.Float(), nullable=False),
sa.Column('lon', sa.Float(), nullable=False),
sa.Column('attributes', sa.JSON(), nullable=False),
sa.Column('category_id', sa.Integer(), nullable=False),
sa.Column('parent_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['category_id'], ['category.id'], ),
sa.ForeignKeyConstraint(['parent_id'], ['point.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('point')
op.drop_table('field')
op.drop_table('category')
# ### end Alembic commands ###
from flask import Flask, redirect, jsonify, abort
from where.model.field_types import FieldType
from where.model.sa import Category, Point, Field, session_context
app = Flask(__name__)
@app.route('/')
def index():
return """
<head>
</head>
<body>
<h1>W H E R E</h1>
<p>This is the WHERE project.</p>
<a href='/test_data'>Click here to nuke the database and make it all be test data.</a>
</body>
"""
@app.route('/test_data')
def test_data():
with session_context() as session:
# session = Session()
session.query(Point).delete()
session.query(Field).delete()
session.query(Category).delete()
# Water Fountain, the class.
wf = Category()
wf.name = "Water Fountain"
wf.icon = "https://karel.pw/water.png"
session.add(wf)
session.commit()
# coldness
cd = Field()
cd.name = "Coldness"
cd.slug = "coldness"
cd.type = FieldType.RATING
cd.category_id = wf.id
# filler
fl = Field()
fl.slug = "bottle_filler"
fl.name = "Has Bottle Filler"
fl.type = FieldType.BOOLEAN
fl.category_id = wf.id
session.add(cd)
session.add(fl)
session.commit()
# an actual instance!
fn = Point()
fn.name = None
fn.lat = 38.829791
fn.lon = -77.307043
# fn.category_id = wf.id
fn.category = wf
fn.parent_id = None
fn.attributes = {
"coldness": {
"num_reviews": 32,
"average_rating": 0.5
},
"bottle_filler": {
"value": True
}
}
session.add(fn)
session.commit()
return redirect('/')
@app.route('/category/<id>')
def get_category(id):
with session_context() as session:
result = session.query(Category).filter_by(id=id).first()
if result:
return jsonify(result.as_json())
else:
abort(404)
@app.route('/point/<id>')
def get_point(id):
with session_context() as session:
result = session.query(Point).filter_by(id=id).first()
if result:
return jsonify(result.as_json())
else:
abort(404)
if __name__ == '__main__':
app.run()
import enum
class FieldType(enum.Enum):
STRING = {'value': str}
FLOAT = {'value': float}
INTEGER = {'value': int}
BOOLEAN = {'value': bool}
RATING = {'num_reviews': int, 'average_rating': float}
def validate(self, data):
"""
Given an instance of a value, verfiy that the instance satisfies the
schema for this primitive type. e.g. if FieldType.STRING.validate was
called, it would make sure that the `data` parameter looked like
{
"value": "some string"
}
This method throws a ValueError if the passed value doesn't conform to the
schema.
:param data: the instance of this primitive to validate.
:raises ValueError: if the passed data is incorrect
"""
if type(data) is not dict:
raise ValueError("YA DONE GOOFED. NEED A DICT.")
for field in self.value:
if field not in data:
raise ValueError(f"Fields of type {self.name} need a {field} field")
if type(data[field]) is not self.value[field]:
raise ValueError(f"Expecting field of type {self.value[field]}, got {type(data[field])}")
if len(data) != len(self.value):
raise ValueError(f"Too many fields for field of type {self.name}")
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine('sqlite:///db.sqlite3') # TODO configurable
Session = sessionmaker(bind=engine)
from contextlib import contextmanager
from sqlalchemy import String, ForeignKey, Enum, Integer, Float, JSON
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import relationship, validates
from sqlalchemy.schema import Column
from .field_types import FieldType
from .meta import Session
@contextmanager
def session_context():
session = Session()
try:
yield session
session.commit()
except BaseException:
session.rollback()
raise
finally:
session.close()
@as_declarative()
class Base(object):
pass
class Point(Base):
"""
Represents actual instances of any and all points on the map.
"""
__tablename__ = 'point'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=True)
lat = Column(Float, nullable=False)
lon = Column(Float, nullable=False)
attributes = Column(JSON, nullable=False)
# Relationships
category_id = Column(Integer, ForeignKey('category.id'), nullable=False)
category = relationship('Category')
parent_id = Column(Integer, ForeignKey('point.id'), nullable=True)
parent = relationship('Point', remote_side=[id])
children = relationship('Point')
@validates('attributes')
def validate_data(self, _, data):
if data is None:
return
fields = self.category.fields
for key in data:
# Find Field object that corresponds to this key
for field in fields:
if field.slug