From fff2389a7bbb4d505a01aecdaf2aa86e19f3ca43 Mon Sep 17 00:00:00 2001 From: Jack Hadrill Date: Sun, 13 Sep 2020 02:30:38 +0100 Subject: [PATCH] Initial stubbed API and ORM --- .gitignore | 145 +++++++++++++++++++++++++++++++++- requirements.txt | 3 + setup.py | 28 +++++++ vcinema/__init__.py | 0 vcinema/app.py | 30 +++++++ vcinema/dto/__init__.py | 0 vcinema/dto/room.py | 9 +++ vcinema/dto/user.py | 9 +++ vcinema/models/__init__.py | 0 vcinema/models/room.py | 8 ++ vcinema/models/user.py | 8 ++ vcinema/resources/__init__.py | 0 vcinema/resources/rooms.py | 73 +++++++++++++++++ vcinema/resources/users.py | 41 ++++++++++ vcinema/static/.gitkeep | 0 vcinema/templates/.gitkeep | 0 16 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 vcinema/__init__.py create mode 100644 vcinema/app.py create mode 100644 vcinema/dto/__init__.py create mode 100644 vcinema/dto/room.py create mode 100644 vcinema/dto/user.py create mode 100644 vcinema/models/__init__.py create mode 100644 vcinema/models/room.py create mode 100644 vcinema/models/user.py create mode 100644 vcinema/resources/__init__.py create mode 100644 vcinema/resources/rooms.py create mode 100644 vcinema/resources/users.py create mode 100644 vcinema/static/.gitkeep create mode 100644 vcinema/templates/.gitkeep diff --git a/.gitignore b/.gitignore index 7d9eab5..678270a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ .LSOverride # Icon must end with two \r -Icon +Icon + # Thumbnails ._* @@ -78,3 +79,145 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +## Python + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +.vscode/ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b57c177 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask>=1.1.2 +Flask-RESTful>=0.3.8 +Flask-SQLAlchemy>=2.4.4 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..46eb4de --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +from setuptools import setup + + +with open("requirements.txt") as f: + requirements = f.readlines() + + +setup( + name="vcinema", + version="0.1", + description="", + author="Butlersaurus", + packages=[ + "vcinema", + "vcinema.dto", + "vcinema.models", + "vcinema.resources", + ], + entry_points={ + "console_scripts" : [ + "vcinema = vcinema.app:main" + ] + }, + install_requires=requirements, + test_suite="nose.collector" +) diff --git a/vcinema/__init__.py b/vcinema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vcinema/app.py b/vcinema/app.py new file mode 100644 index 0000000..fe6075f --- /dev/null +++ b/vcinema/app.py @@ -0,0 +1,30 @@ +from flask import Flask +from flask_restful import Api +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" +db = SQLAlchemy(app) +api = Api(app) + + +def init_db(): + from vcinema.models.room import Room + from vcinema.models.user import User + db.create_all() + +def init_resources(): + from vcinema.resources.rooms import RoomApi, RoomsApi + from vcinema.resources.users import UsersApi + api.add_resource(RoomApi, "/rooms/") + api.add_resource(RoomsApi, "/rooms") + api.add_resource(UsersApi, "/users") + # api.add_resource(UserAPI, "/users/") + +def main(): + init_db() + init_resources() + app.run(debug=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/vcinema/dto/__init__.py b/vcinema/dto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vcinema/dto/room.py b/vcinema/dto/room.py new file mode 100644 index 0000000..297cfb4 --- /dev/null +++ b/vcinema/dto/room.py @@ -0,0 +1,9 @@ +class RoomDto: + @staticmethod + def to_dict(room): + return { + "id": room.id, + "name": room.name, + "source": room.source, + "users": [{"id": user.id, "name": user.name} for user in room.users] + } diff --git a/vcinema/dto/user.py b/vcinema/dto/user.py new file mode 100644 index 0000000..d7ce9ac --- /dev/null +++ b/vcinema/dto/user.py @@ -0,0 +1,9 @@ +class UserDto: + @staticmethod + def to_dict(user): + return { + "id": user.id, + "name": user.name, + "token": user.token, + "room": user.room.id + } diff --git a/vcinema/models/__init__.py b/vcinema/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vcinema/models/room.py b/vcinema/models/room.py new file mode 100644 index 0000000..f5d7fca --- /dev/null +++ b/vcinema/models/room.py @@ -0,0 +1,8 @@ +from vcinema.app import db + + +class Room(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(20), unique=True, nullable=False) + source = db.Column(db.String(200), unique=True, nullable=False) + users = db.relationship("User", backref="room", cascade="all, delete-orphan") diff --git a/vcinema/models/user.py b/vcinema/models/user.py new file mode 100644 index 0000000..d283176 --- /dev/null +++ b/vcinema/models/user.py @@ -0,0 +1,8 @@ +from vcinema.app import db + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(20), unique=True, nullable=False) + token = db.Column(db.String(36), unique=True, nullable=False) + room_id = db.Column(db.Integer, db.ForeignKey("room.id"), nullable=False) diff --git a/vcinema/resources/__init__.py b/vcinema/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vcinema/resources/rooms.py b/vcinema/resources/rooms.py new file mode 100644 index 0000000..2f752cd --- /dev/null +++ b/vcinema/resources/rooms.py @@ -0,0 +1,73 @@ +from flask_restful import abort, request, Resource +from sqlalchemy.exc import DatabaseError + +from vcinema.app import db +from vcinema.models.room import Room +from vcinema.dto.room import RoomDto + + +class RoomsApi(Resource): + def get(self): + try: + rooms = Room.query.all() + except DatabaseError as e: + abort(500, status="error", message="An error occured whilst querying the database", exception=str(e)) + + return [RoomDto.to_dict(room) for room in rooms], 200 + + def post(self): + req = request.get_json(force=True) + name = req.get("name") + source = req.get("source") + + if not name or not source: + abort(400, status="error", message="Insufficient parameters specified") + + try: + room = Room(name=name, source=source) + db.session.add(room) + db.session.commit() + except DatabaseError as e: + abort(500, status="error", message="An error occured whilst writing to the database", exception=str(e)) + + return RoomDto.to_dict(room), 201 + + +class RoomApi(Resource): + def get(self, id): + try: + id = int(id) + except ValueError: + abort(400, status="error", message="ID must be an integer") + + try: + room = Room.query.filter_by(id=id).first() + except DatabaseError as e: + abort(500, status="error", message="An error occured whilst querying the database", exception=str(e)) + + if not room: + abort(404, status="error", message=f"Room with ID {id} not found") + + return RoomDto.to_dict(room), 200 + + def delete(self, id): + try: + id = int(id) + except ValueError: + abort(400, status="error", message="ID must be an integer") + + try: + room = Room.query.filter_by(id=id).first() + except DatabaseError as e: + abort(500, status="error", message="An error occured whilst querying the database", exception=str(e)) + + if not room: + abort(404, status="error", message=f"Room with ID {id} not found") + + try: + db.session.delete(room) + db.session.commit() + except DatabaseError as e: + abort(500, status="error", message="An error occured whilst deleting a record from the database", exception=str(e)) + + return "", 204 \ No newline at end of file diff --git a/vcinema/resources/users.py b/vcinema/resources/users.py new file mode 100644 index 0000000..f9e2155 --- /dev/null +++ b/vcinema/resources/users.py @@ -0,0 +1,41 @@ +from uuid import uuid4 + +from flask_restful import abort, request, Resource +from sqlalchemy.exc import DatabaseError + +from vcinema.app import db +from vcinema.models.room import Room +from vcinema.models.user import User +from vcinema.dto.user import UserDto + + +class UsersApi(Resource): + def post(self): + req = request.get_json(force=True) + name = req.get("name") + room_id = req.get("room-id") + + if not name or not room_id: + abort(400, status="error", message="Insufficient parameters specified") + + try: + room_id = int(room_id) + except ValueError: + abort(400, status="error", message="Room ID must be an integer") + + try: + room = Room.query.filter_by(id=room_id).first() + except DatabaseError as e: + abort(500, status="error", message="An error occured whilst querying the database", exception=str(e)) + + if not room: + abort(400, status="error", message=f"Room with ID {room_id} not found") + + try: + user = User(name=name, token=str(uuid4()), room=room) + db.session.add(user) + db.session.commit() + except DatabaseError as e: + abort(500, status="error", message="An error occured whilst writing to the database", exception=str(e)) + + return UserDto.to_dict(user), 201 \ No newline at end of file diff --git a/vcinema/static/.gitkeep b/vcinema/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vcinema/templates/.gitkeep b/vcinema/templates/.gitkeep new file mode 100644 index 0000000..e69de29