diff --git a/docker-compose.yml b/docker-compose.yml index df450cc..0da2e5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,13 @@ services: ports: - "5000:5000" volumes: - - ./server:/movie_name/server + - .:/movie_name + environment: + MOVIE_DB_HOST: mysql_db + MOVIE_DB_USER: CINEMA_USER + MOVIE_DB_PASSWORD: CINEMA_PASSWORD + MOVIE_DB_NAME: CINEMA_DB + PYTHONPATH: /movie_name depends_on: - mysql @@ -36,9 +42,9 @@ services: MYSQL_USER: CINEMA_USER MYSQL_PASSWORD: CINEMA_PASSWORD ports: - - "3307:3306" + - "3306:3306" volumes: - mysql_data:/var/lib/mysql volumes: - mysql_data: \ No newline at end of file + mysql_data: diff --git a/server/api/.env b/server/api/.env index daa1d14..e07104f 100644 --- a/server/api/.env +++ b/server/api/.env @@ -1,6 +1,2 @@ MOVIE_API_HOST=0.0.0.0 MOVIE_API_PORT=5000 -MOVIE_DB_HOST=mysql_db -MOVIE_DB_USER=CINEMA_USER -MOVIE_DB_PASSWORD=CINEMA_PASSWORD -MOVIE_DB_NAME=CINEMA_DB diff --git a/server/api/app.py b/server/api/app.py index fa78479..03ff1f8 100755 --- a/server/api/app.py +++ b/server/api/app.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 """ Starts the Flask web app """ import os -from flask import Flask from dotenv import load_dotenv +from flask import Flask, jsonify +from models import storage +from api.views import app_views load_dotenv() app = Flask(__name__) app.url_map.strict_slashes = False +app.register_blueprint(app_views) HOST = "0.0.0.0" PORT = 5000 @@ -18,6 +21,18 @@ def volume(): return "Testing volumes: -Zidane Square headed-" +@app.teardown_appcontext +def teardown_db(exception): + """ Closes the storage session """ + storage.close() + + +@app.errorhandler(404) +def page_not_found(e): + """ Handles the 404 error """ + return jsonify({"error": "Not found"}), 404 + + if __name__ == "__main__": if os.getenv("MOVIE_API_HOST"): HOST = os.getenv("MOVIE_API_HOST") diff --git a/server/api/views/__init__.py b/server/api/views/__init__.py new file mode 100755 index 0000000..fb6bd29 --- /dev/null +++ b/server/api/views/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/python3 +""" Contains the flask blueprint """ +from flask import Blueprint + +app_views = Blueprint('app_views', __name__, url_prefix="/api") + +from api.views.users import * # nopep8 +from api.views.index import * # nopep8 diff --git a/server/api/views/index.py b/server/api/views/index.py new file mode 100755 index 0000000..80c13d4 --- /dev/null +++ b/server/api/views/index.py @@ -0,0 +1,23 @@ +#!/usr/bin/python3 +"""Metrics Routes""" +from api.views import app_views + + +@app_views.route('/status') +def status(): + """Return the API status all wrapped in a json object""" + return {"status": "OK"}, 200 + + +@app_views.route('stats') +def stats(): + """Return the count of all classes""" + from models.user import User + from models.mood import Mood + from models.recommendation import Recommendation + from models import storage + + return {"Users": storage.count(User), + "Moods": storage.count(Mood), + "Recommendations": storage.count(Recommendation) + } diff --git a/server/flask.dockerfile b/server/flask.dockerfile index 5d3729a..824a45b 100644 --- a/server/flask.dockerfile +++ b/server/flask.dockerfile @@ -1,11 +1,23 @@ FROM python:3.8-slim +RUN apt-get update && apt-get upgrade -y +RUN apt-get install -y pkg-config +RUN apt-get install -y default-libmysqlclient-dev +RUN apt-get install -y build-essential + WORKDIR /movie_name COPY . /movie_name -RUN pip3 install flask==2.1.0 werkzeug==2.1.1 flask-cors==4.0.1 sqlalchemy==1.4.22 python-dotenv +RUN pip3 install flask==2.1.0 werkzeug==2.1.1 flask-cors==4.0.1 sqlalchemy==1.4.22 mysqlclient==2.2.4 python-dotenv + +RUN apt-get clean && rm -rf /var/lib/apt/lists/* EXPOSE 5000 -CMD ["python3", "-m", "server.api.app"] \ No newline at end of file +COPY wait-for-mysql.sh /usr/local/bin/wait-for-mysql.sh +RUN chmod +x /usr/local/bin/wait-for-mysql.sh + +WORKDIR /movie_name/server + +CMD ["wait-for-mysql.sh", "python3", "-m", "api.app"] diff --git a/server/models/__init__.py b/server/models/__init__.py new file mode 100755 index 0000000..68c40d2 --- /dev/null +++ b/server/models/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/python3 +"""Initialize the models package.""" + +from models.engine.db_storage import DBStorage + + +storage = DBStorage() +storage.reload() diff --git a/server/models/base_model.py b/server/models/base_model.py new file mode 100755 index 0000000..99df5a6 --- /dev/null +++ b/server/models/base_model.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Contains class BaseModel +""" + +from datetime import datetime +import models +from sqlalchemy import Column, String, DateTime +from sqlalchemy.ext.declarative import declarative_base +import uuid + +time = "%Y-%m-%dT%H:%M:%S.%f" +Base = declarative_base() + + +class BaseModel: + """The BaseModel class from which future classes will be derived""" + id = Column(String(60), primary_key=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow) + + def __init__(self, *args, **kwargs): + """Initialization of the base model""" + if kwargs: + for key, value in kwargs.items(): + if key != "__class__": + setattr(self, key, value) + if kwargs.get("created_at", None) and type(self.created_at) is str: + self.created_at = datetime.strptime(kwargs["created_at"], time) + else: + self.created_at = datetime.utcnow() + if kwargs.get("updated_at", None) and type(self.updated_at) is str: + self.updated_at = datetime.strptime(kwargs["updated_at"], time) + else: + self.updated_at = datetime.utcnow() + if kwargs.get("id", None) is None: + self.id = str(uuid.uuid4()) + else: + self.id = str(uuid.uuid4()) + self.created_at = datetime.utcnow() + self.updated_at = self.created_at + + def __str__(self): + """String representation of the BaseModel class""" + return "[{:s}] ({:s}) {}".format(self.__class__.__name__, self.id, + self.__dict__) + + def __repr__(self): + """String representation of the BaseModel class""" + return "[{:s}] ({:s}) {}".format(self.__class__.__name__, self.id, + self.__dict__) + + def save(self): + """Updates the attribute 'updated_at' with the current datetime""" + self.updated_at = datetime.utcnow() + models.storage.new(self) + models.storage.save() + + def to_dict(self): + """Returns a dictionary containing all keys/values of the instance""" + new_dict = self.__dict__.copy() + if "created_at" in new_dict: + new_dict["created_at"] = new_dict["created_at"].strftime(time) + if "updated_at" in new_dict: + new_dict["updated_at"] = new_dict["updated_at"].strftime(time) + new_dict["__class__"] = self.__class__.__name__ + if "_sa_instance_state" in new_dict: + del new_dict["_sa_instance_state"] + if "password" in new_dict: + del new_dict["password"] + return new_dict + + def delete(self): + """Delete the current instance from the storage""" + models.storage.delete(self) diff --git a/server/models/engine/__init__.py b/server/models/engine/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/server/models/engine/db_storage.py b/server/models/engine/db_storage.py new file mode 100755 index 0000000..d40d358 --- /dev/null +++ b/server/models/engine/db_storage.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Contains the class DBStorage +""" + +from models.base_model import BaseModel, Base +from models.mood import Mood +from models.recommendation import Recommendation +from models.user import User +from os import getenv +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker + +classes = {"Mood": Mood, "Recommendation": Recommendation, "User": User} + + +class DBStorage: + """Interaacts with the MySQL database""" + __engine = None + __session = None + + def __init__(self): + """Instantiate a DBStorage object""" + MOVIE_DB_USER = getenv('MOVIE_DB_USER') + MOVIE_DB_PASSWORD = getenv('MOVIE_DB_PASSWORD') + MOVIE_DB_HOST = getenv('MOVIE_DB_HOST') + MOVIE_DB_NAME = getenv('MOVIE_DB_NAME') + self.__engine = create_engine('mysql+mysqldb://{}:{}@{}/{}'. + format(MOVIE_DB_USER, + MOVIE_DB_PASSWORD, + MOVIE_DB_HOST, + MOVIE_DB_NAME)) + + def all(self, cls=None): + """query on the current database session""" + new_dict = {} + for clss in classes: + if cls is None or cls is classes[clss] or cls is clss: + objs = self.__session.query(classes[clss]).all() + for obj in objs: + key = obj.__class__.__name__ + '.' + obj.id + new_dict[key] = obj + return (new_dict) + + def new(self, obj): + """Add the object to the current database session""" + self.__session.add(obj) + + def save(self): + """Commit all changes of the current database session""" + self.__session.commit() + + def delete(self, obj=None): + """Delete from the current database session obj if not None""" + if obj is not None: + self.__session.delete(obj) + + def reload(self): + """Reloads data from the database""" + Base.metadata.create_all(self.__engine) + sess_factory = sessionmaker(bind=self.__engine, expire_on_commit=False) + Session = scoped_session(sess_factory) + self.__session = Session + + def close(self): + """Call remove() method on the private session attribute""" + self.__session.remove() + + def get(self, cls, id): + """Returns the object based on the class and its ID, + or None if not found""" + objs = self.all(cls).values() + for obj in objs: + if obj.id == id: + return obj + return None + + def count(self, cls=None): + """ + Returns the number of objects in storage matching the given class. + If no class is passed, returns the count of all objects in storage. + """ + return len(self.all(cls).keys()) diff --git a/server/models/mood.py b/server/models/mood.py new file mode 100755 index 0000000..db72302 --- /dev/null +++ b/server/models/mood.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Holds class Mood""" +from models.base_model import BaseModel, Base +from sqlalchemy import Column, String +from sqlalchemy.orm import relationship + + +class Mood(BaseModel, Base): + """Representation of a user """ + __tablename__ = 'moods' + mood = Column(String(24), nullable=False) + recommendations = relationship("Recommendation", + back_populates="mood", + cascade="all, delete-orphan") + + def __init__(self, *args, **kwargs): + """Initializes user""" + super().__init__(*args, **kwargs) diff --git a/server/models/recommendation.py b/server/models/recommendation.py new file mode 100755 index 0000000..af9ea85 --- /dev/null +++ b/server/models/recommendation.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Holds class Recommendation""" +from models.base_model import BaseModel, Base +from sqlalchemy import Column, String, Boolean, ForeignKey +from sqlalchemy.orm import relationship + + +class Recommendation(BaseModel, Base): + """Representation of a vault """ + __tablename__ = 'Recommendation' + user_id = Column(String(128), + ForeignKey('users.id', ondelete='CASCADE'), + nullable=False) + mood_id = Column(String(128), + ForeignKey('moods.id', ondelete='CASCADE'), + nullable=False) + weather = Column(String(128), nullable=False) + recommendaton = Column(String(128), nullable=True) + seen = Column(Boolean, nullable=False) + like = Column(Boolean, nullable=False) + mood = relationship("Mood", back_populates="recommendations") + user = relationship("User", back_populates="recommendations",) + + def __init__(self, *args, **kwargs): + """initializes vault""" + super().__init__(*args, **kwargs) diff --git a/server/models/user.py b/server/models/user.py new file mode 100755 index 0000000..54708ae --- /dev/null +++ b/server/models/user.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Holds class User""" +from models.base_model import BaseModel, Base +from sqlalchemy import Column, String +from sqlalchemy.orm import relationship +import hashlib + + +class User(BaseModel, Base): + """Representation of a user """ + __tablename__ = 'users' + email = Column(String(128), nullable=False) + password = Column(String(128), nullable=False) + first_name = Column(String(128), nullable=False) + last_name = Column(String(128), nullable=False) + recent_mood = Column(String(24), nullable=False) + recommendations = relationship("Recommendation", + back_populates="user", + cascade="all, delete-orphan") + + def __init__(self, *args, **kwargs): + """Initializes user""" + kwargs["password"] = hashlib.sha256( + kwargs["password"].encode()).hexdigest() + super().__init__(*args, **kwargs) diff --git a/wait-for-mysql.sh b/wait-for-mysql.sh new file mode 100644 index 0000000..b7fb0a1 --- /dev/null +++ b/wait-for-mysql.sh @@ -0,0 +1,3 @@ +#!/bin/sh +sleep 10 +exec "$@"