Flask Rest Api error SQLAlchemyAutoSchema

250 Views Asked by At

i have deployed my flask rest api app on cloud run, using this dockerfile:

# Python image to use.
FROM python:3.10-alpine

# Set the working directory to /app
WORKDIR /app

# copy the requirements file used for dependencies
#COPY requirements.txt .
# Copy the rest of the working directory contents into the container at /app
COPY . .

#RUN export PYTHONPATH=/usr/bin/python
# Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt 
#RUN pip install -r requirements.txt 

# Run app.py when the container launches
#ENTRYPOINT ["python", "app.py"]
CMD gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app

When i try to call one of rest endpoint of app, i receive this error in my log:

"Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/gunicorn/arbiter.py", line 589, in spawn_worker
    worker.init_process()
  File "/usr/local/lib/python3.10/site-packages/gunicorn/workers/gthread.py", line 92, in init_process
    super().init_process()
  File "/usr/local/lib/python3.10/site-packages/gunicorn/workers/base.py", line 134, in init_process
    self.load_wsgi()
  File "/usr/local/lib/python3.10/site-packages/gunicorn/workers/base.py", line 146, in load_wsgi
    self.wsgi = self.app.wsgi()
  File "/usr/local/lib/python3.10/site-packages/gunicorn/app/base.py", line 67, in wsgi
    self.callable = self.load()
  File "/usr/local/lib/python3.10/site-packages/gunicorn/app/wsgiapp.py", line 58, in load
    return self.load_wsgiapp()
  File "/usr/local/lib/python3.10/site-packages/gunicorn/app/wsgiapp.py", line 48, in load_wsgiapp
    return util.import_app(self.app_uri)
  File "/usr/local/lib/python3.10/site-packages/gunicorn/util.py", line 359, in import_app
    mod = importlib.import_module(module)
  File "/usr/local/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/app/app.py", line 12, in <module>
    from resources.user import UserRegister, UserLogin, User, TokenRefresh, UserLogout
  File "/app/resources/user.py", line 13, in <module>
    from schemas.user import UserSchema
  File "/app/schemas/user.py", line 7, in <module>
    class UserSchema(ma.SQLAlchemyAutoSchema):
  File "/usr/local/lib/python3.10/site-packages/marshmallow/schema.py", line 121, in __new__
    klass._declared_fields = mcs.get_declared_fields(
  File "/usr/local/lib/python3.10/site-packages/marshmallow_sqlalchemy/schema.py", line 91, in get_declared_fields
    fields.update(mcs.get_declared_sqla_fields(fields, converter, opts, dict_cls))
  File "/usr/local/lib/python3.10/site-packages/marshmallow_sqlalchemy/schema.py", line 130, in get_declared_sqla_fields
    converter.fields_for_model(
  File "/usr/local/lib/python3.10/site-packages/marshmallow_sqlalchemy/convert.py", line 154, in fields_for_model
    field = base_fields.get(key) or self.property2field(prop)
  File "/usr/local/lib/python3.10/site-packages/marshmallow_sqlalchemy/convert.py", line 193, in property2field
    field_class = field_class or self._get_field_class_for_property(prop)
  File "/usr/local/lib/python3.10/site-packages/marshmallow_sqlalchemy/convert.py", line 275, in _get_field_class_for_property
    column = _base_column(prop.columns[0])
  File "/usr/local/lib/python3.10/site-packages/sqlalchemy/util/langhelpers.py", line 1329, in __getattr__
    return self._fallback_getattr(key)
  File "/usr/local/lib/python3.10/site-packages/sqlalchemy/util/langhelpers.py", line 1298, in _fallback_getattr
    raise AttributeError(key)
AttributeError: columns"

This is my app.py file:

import os
from flask import Flask, jsonify
from flask_restful import Api
from flask_jwt_extended import JWTManager
from marshmallow import ValidationError

from datetime import timedelta

from db import db, getconn
from ma import ma
from blocklist import BLOCKLIST
from resources.user import UserRegister, UserLogin, User, TokenRefresh, UserLogout
from resources.confirmation import Confirmation, ConfirmationByUser

app = Flask(__name__)

### Cloud Sql Google Parameter ###
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql+pg8000://"
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
    "creator" : getconn,
    "max_overflow": 2,
    "pool_timeout": 30,
    "pool_size": 5,
    "pool_recycle": 1800
}
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

#app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URL")
#app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True


#app.secret_key = os.environ.get(
#    "APP_SECRET_KEY"
#)  # could do app.config['JWT_SECRET_KEY'] if we prefer

### Flask JWT Configuration Key ###
app.config["JWT_SECRET_KEY"] = os.environ["JWT_KEY"]
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)

db.init_app(app)
ma.init_app(app)
api = Api(app)





@app.errorhandler(ValidationError)
def handle_marshmallow_validation(err):
    return jsonify(err.messages), 400


jwt = JWTManager(app)


# This method will check if a token is blocklisted, and will be called automatically when blocklist is enabled
@jwt.token_in_blocklist_loader
def check_if_token_in_blocklist(jwt_header, jwt_payload):
    return jwt_payload["jti"] in BLOCKLIST

@app.route("/api")
def user_route():
    return "Welcome user API !"


api.add_resource(UserRegister, "/register")
api.add_resource(User, "/user/<int:user_id>")
api.add_resource(UserLogin, "/login")
api.add_resource(TokenRefresh, "/refresh")
api.add_resource(UserLogout, "/logout")
api.add_resource(Confirmation, "/user_confirm/<string:confirmation_id>")
api.add_resource(ConfirmationByUser, "/confirmation/user/<int:user_id>")

if __name__ == "__main__":
    #app.run(port=5000, debug=True)
    server_port = os.environ.get('PORT', '8082')
    app.run(debug=True, port=server_port, host='0.0.0.0')

This is the requirements.txt file:

Flask==2.2.2
#Flask==2.1.3
requests==2.28.1
flask-sqlalchemy
ptvsd==4.3.2 # Required for debugging.
gunicorn
flask-smorest
python-dotenv
marshmallow
cloud-sql-python-connector
pg8000
flask-jwt-extended
passlib
datetime
uuid
requests
flask_restful
flask_marshmallow
marshmallow-sqlalchemy
Flask-SQLAlchemy

This is my user.py schema:

from marshmallow import pre_dump

from ma import ma
from models.user import UserModel


class UserSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = UserModel
        load_instance = True
        load_only = ("password",)
        dump_only = ("id", "confirmation")

        @pre_dump
        def _pre_dump(self, user: UserModel, **kwargs):
            user.confirmation = [user.most_recent_confirmation]
            return user

And this is my user model:

from requests import Response
from flask import request, url_for

from db import db
from libs.mailgun import Mailgun
from models.confirmation import ConfirmationModel


class UserModel(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), nullable=False, unique=True)
    password = db.Column(db.String(80), nullable=False)
    email = db.Column(db.String(80), nullable=False, unique=True)
    created = db.Column(db.String(80), nullable=False, unique=True)
    uuid_user = db.Column(db.String(200), nullable=False, unique=True)

    confirmation = db.relationship(
        "ConfirmationModel", lazy="dynamic", cascade="all, delete-orphan"
    )

    @property
    def most_recent_confirmation(self) -> "ConfirmationModel":
        # ordered by expiration time (in descending order)
        return self.confirmation.order_by(db.desc(ConfirmationModel.expire_at)).first()

    @classmethod
    def find_by_username(cls, username: str) -> "UserModel":
        return cls.query.filter_by(username=username).first()

    @classmethod
    def find_by_email(cls, email: str) -> "UserModel":
        return cls.query.filter_by(email=email).first()

    @classmethod
    def find_by_id(cls, _id: int) -> "UserModel":
        return cls.query.filter_by(id=_id).first()

    def send_confirmation_email(self) -> Response:
        subject = "Registration Confirmation"
        link = request.url_root[:-1] + url_for(
            "confirmation", confirmation_id=self.most_recent_confirmation.id
        )
        text = f"Please click the link to confirm your registration: {link}"
        html = f"<html>Please click the link to confirm your registration: <a href={link}>link</a></html>"
        return Mailgun.send_email([self.email], subject, text, html)

    def save_to_db(self) -> None:
        db.session.add(self)
        db.session.commit()

    def delete_from_db(self) -> None:
        db.session.delete(self)
        db.session.commit()

Previously the app was on python 3.7 and worked fine, but the requirements.txt was different because it didn't need the modules for GCP and Cloud Sql connection. Any idea how to fix this error and what is the cause? Thanks

1

There are 1 best solutions below

0
Jérôme On

Looks like a marshmallow-sqlalchemy vs. sqlalchemy 2.0 issue.

I released a new marshmallow-sqlalchemy last week with sqlalchemy 2.0 support.

Please try with marshmallow-sqlalchemy.

If it doesn't work, edit your question to add a pip freeze.