joegasewicz / flask-file-upload

Easy file uploads for Flask.
MIT License
154 stars 15 forks source link

Initalizing file_upload with subclassed SQLAlchemy instance? #97

Closed GhastlyParadox closed 4 years ago

GhastlyParadox commented 4 years ago

Hi Joe,

First, thank you for developing this. Provided I can get it working in my project, it'd be perfect for my needs. I'm working on an image manager feature for an existing Flask application with a rather large/complex MySQL database setup that serves as a REST API. While the database itself is a beast, for this project I'm really only dealing with a single table that has no relationships.

The database had been set up using raw SQLAlchemy, but I reconfigured it using Flask-SQLAlchemy, in hopes to get it working with flask-file-upload. So far though, I'm having trouble initializing it, getting the following error:

Traceback (most recent call last): File "/Users/aglane/Box Sync/Git/flora-DEV/herbflask/__init__.py", line 1, in <module> from application import create_app File "/Users/aglane/Box Sync/Git/flora-DEV/herbflask/application/__init__.py", line 32, in <module> from .db.miflora import MIFloraDB File "/Users/aglane/Box Sync/Git/flora-DEV/herbflask/application/db/miflora.py", line 1113, in <module> class ImagesDev(MIFLORA_BASE): File "/Users/aglane/Box Sync/Git/flora-DEV/env/lib/python3.7/site-packages/flask_file_upload/model.py", line 50, in __new__ new_cols_list, filenames_list = _ModelUtils.get_attr_from_model(instance, new_cols, filenames, db) File "/Users/aglane/Box Sync/Git/flora-DEV/env/lib/python3.7/site-packages/flask_file_upload/_model_utils.py", line 123, in get_attr_from_model new_cols.append(_ModelUtils.columns_dict(attr, db)) File "/Users/aglane/Box Sync/Git/flora-DEV/env/lib/python3.7/site-packages/flask_file_upload/_model_utils.py", line 90, in columns_dict create_col File "/Users/aglane/Box Sync/Git/flora-DEV/env/lib/python3.7/site-packages/flask_file_upload/_model_utils.py", line 36, in create_keys col_dict[key] = fn(key, key) File "/Users/aglane/Box Sync/Git/flora-DEV/env/lib/python3.7/site-packages/flask_file_upload/_model_utils.py", line 86, in create_col return db.Column(db.String(str_len), key=key, name=name) AttributeError: 'NoneType' object has no attribute 'Column'

It seems as though the database isn't being recognized as an SQLAlchemy instance?

I'm wondering whether this may be because I've subclassed the SQLAlchemy instance with a MIFloraDB class, which handles all the query logic for the REST API - e.g.

from flask_sqlalchemy import SQLAlchemy

class MIFloraDB(SQLAlchemy): ...

Which I then import into my application (app factory) to create the SQLAlchemy instance:

flora_db = MIFloraDB()

def create_app():

with app.app_context():

flora_db.init_app(app)

file_upload.init_app(app, flora_db)

return app

Everything else seems to be configured properly, so that's my best guess at this point, but any thoughts/suggestions very much appreciated!

GhastlyParadox commented 4 years ago

I managed to get it running by simply adding a constructor with a super function for inheritance - e.g.

class MIFloraDB(SQLAlchemy): def __init__(self): super(MIFloraDB, self).__init__()

Now to dive in and start working with it. Thanks again.

GhastlyParadox commented 4 years ago

Ugh, spoke too soon - forgot I'd commented out the file_upload.Model decorator. Still getting the same error.

joegasewicz commented 4 years ago

@GhastlyParadox Hi Thanks for the message, i'm currently at work but as soon as I get off i'll look into this issue for you and get a fix out asap. Thank you Joe

GhastlyParadox commented 4 years ago

Hi Joe, just getting back to work after a long weekend, and saw you replied. Many thanks!

joegasewicz commented 4 years ago

Hi @GhastlyParadox , it looks like the db is not initiated. The problem is that Flask-Sqlalchemy calls to a __get__ descriptor which initiates the SqlAlchemy session object. So we have to delay this call. If you're not using Flask-Sqlalchemy then this library currently will not be able to create a session. If this is the case, then keep this issue open & i can provide the fix for this.

To confirm this, can you run this please:

print(db.__dict__)

And post the output here, thank you

GhastlyParadox commented 4 years ago

Hi Joe,

I am using Flask-Sqlalchemy now, but my models are currently set up using a raw declarative_base - e.g.MIFLORA_BASE = declarative_base()

But the db instance itself is Flask-Sqlalchemy. I actually wondered whether I needed to be using Flask-Sqlalchemy db.Model as well, for the full Flask-Sqlalchemy configuration, in order to get this working. So I tried that -- it took some restructuring to avoid circular imports -- but in the end, once I got everything working with db.Model, I got the same result/error. So I went back to just using declarative_base(), as it's a cleaner set up.

That said, my current db.__dict__ output is pasted below. Thanks so much for looking into this!

- 2020-08-12 14:35:30,940 - application - INFO - {'use_native_unicode': True, 'Query': <class 'flask_sqlalchemy.BaseQuery'>, 'session': <sqlalchemy.orm.scoping.scoped_session object at 0x10691da90>, 'Model': <class 'sqlalchemy.ext.declarative.api.Model'>, '_engine_lock': <unlocked _thread.lock object at 0x10690ed20>, 'app': None, '_engine_options': {}, 'ARRAY': <class 'sqlalchemy.sql.sqltypes.ARRAY'>, 'BIGINT': <class 'sqlalchemy.sql.sqltypes.BIGINT'>, 'BINARY': <class 'sqlalchemy.sql.sqltypes.BINARY'>, 'BLANK_SCHEMA': symbol('blank_schema'), 'BLOB': <class 'sqlalchemy.sql.sqltypes.BLOB'>, 'BOOLEAN': <class 'sqlalchemy.sql.sqltypes.BOOLEAN'>, 'BigInteger': <class 'sqlalchemy.sql.sqltypes.BigInteger'>, 'Binary': <class 'sqlalchemy.sql.sqltypes.Binary'>, 'Boolean': <class 'sqlalchemy.sql.sqltypes.Boolean'>, 'CHAR': <class 'sqlalchemy.sql.sqltypes.CHAR'>, 'CLOB': <class 'sqlalchemy.sql.sqltypes.CLOB'>, 'CheckConstraint': <class 'sqlalchemy.sql.schema.CheckConstraint'>, 'Column': <class 'sqlalchemy.sql.schema.Column'>, 'ColumnDefault': <class 'sqlalchemy.sql.schema.ColumnDefault'>, 'Constraint': <class 'sqlalchemy.sql.schema.Constraint'>, 'DATE': <class 'sqlalchemy.sql.sqltypes.DATE'>, 'DATETIME': <class 'sqlalchemy.sql.sqltypes.DATETIME'>, 'DDL': <class 'sqlalchemy.sql.ddl.DDL'>, 'DECIMAL': <class 'sqlalchemy.sql.sqltypes.DECIMAL'>, 'Date': <class 'sqlalchemy.sql.sqltypes.Date'>, 'DateTime': <class 'sqlalchemy.sql.sqltypes.DateTime'>, 'DefaultClause': <class 'sqlalchemy.sql.schema.DefaultClause'>, 'Enum': <class 'sqlalchemy.sql.sqltypes.Enum'>, 'FLOAT': <class 'sqlalchemy.sql.sqltypes.FLOAT'>, 'FetchedValue': <class 'sqlalchemy.sql.schema.FetchedValue'>, 'Float': <class 'sqlalchemy.sql.sqltypes.Float'>, 'ForeignKey': <class 'sqlalchemy.sql.schema.ForeignKey'>, 'ForeignKeyConstraint': <class 'sqlalchemy.sql.schema.ForeignKeyConstraint'>, 'INT': <class 'sqlalchemy.sql.sqltypes.INTEGER'>, 'INTEGER': <class 'sqlalchemy.sql.sqltypes.INTEGER'>, 'Index': <class 'sqlalchemy.sql.schema.Index'>, 'Integer': <class 'sqlalchemy.sql.sqltypes.Integer'>, 'Interval': <class 'sqlalchemy.sql.sqltypes.Interval'>, 'JSON': <class 'sqlalchemy.sql.sqltypes.JSON'>, 'LargeBinary': <class 'sqlalchemy.sql.sqltypes.LargeBinary'>, 'MetaData': <class 'sqlalchemy.sql.schema.MetaData'>, 'NCHAR': <class 'sqlalchemy.sql.sqltypes.NCHAR'>, 'NUMERIC': <class 'sqlalchemy.sql.sqltypes.NUMERIC'>, 'NVARCHAR': <class 'sqlalchemy.sql.sqltypes.NVARCHAR'>, 'Numeric': <class 'sqlalchemy.sql.sqltypes.Numeric'>, 'PassiveDefault': <class 'sqlalchemy.sql.schema.PassiveDefault'>, 'PickleType': <class 'sqlalchemy.sql.sqltypes.PickleType'>, 'PrimaryKeyConstraint': <class 'sqlalchemy.sql.schema.PrimaryKeyConstraint'>, 'REAL': <class 'sqlalchemy.sql.sqltypes.REAL'>, 'SMALLINT': <class 'sqlalchemy.sql.sqltypes.SMALLINT'>, 'Sequence': <class 'sqlalchemy.sql.schema.Sequence'>, 'SmallInteger': <class 'sqlalchemy.sql.sqltypes.SmallInteger'>, 'String': <class 'sqlalchemy.sql.sqltypes.String'>, 'TEXT': <class 'sqlalchemy.sql.sqltypes.TEXT'>, 'TIME': <class 'sqlalchemy.sql.sqltypes.TIME'>, 'TIMESTAMP': <class 'sqlalchemy.sql.sqltypes.TIMESTAMP'>, 'Table': <function _make_table.<locals>._make_table at 0x10692b5f0>, 'Text': <class 'sqlalchemy.sql.sqltypes.Text'>, 'ThreadLocalMetaData': <class 'sqlalchemy.sql.schema.ThreadLocalMetaData'>, 'Time': <class 'sqlalchemy.sql.sqltypes.Time'>, 'TypeDecorator': <class 'sqlalchemy.sql.type_api.TypeDecorator'>, 'Unicode': <class 'sqlalchemy.sql.sqltypes.Unicode'>, 'UnicodeText': <class 'sqlalchemy.sql.sqltypes.UnicodeText'>, 'UniqueConstraint': <class 'sqlalchemy.sql.schema.UniqueConstraint'>, 'VARBINARY': <class 'sqlalchemy.sql.sqltypes.VARBINARY'>, 'VARCHAR': <class 'sqlalchemy.sql.sqltypes.VARCHAR'>, 'alias': <function alias at 0x1063950e0>, 'all_': <function all_ at 0x106391440>, 'and_': <function and_ at 0x106391e60>, 'any_': <function any_ at 0x106391950>, 'asc': <function asc at 0x10648bef0>, 'between': <function between at 0x1062fbdd0>, 'bindparam': <function bindparam at 0x106395830>, 'case': <function case at 0x10647a5f0>, 'cast': <function cast at 0x10647ab00>, 'collate': <function collate at 0x1062f5170>, 'column': <function column at 0x10639a440>, 'delete': <function delete at 0x106490050>, 'desc': <function desc at 0x10648d0e0>, 'distinct': <function distinct at 0x10648d290>, 'engine_from_config': <function engine_from_config at 0x1064c05f0>, 'except_': <function except_ at 0x10648b0e0>, 'except_all': <function except_all at 0x10648b290>, 'exists': <function exists at 0x10648b9e0>, 'extract': <function extract at 0x10647add0>, 'false': <function false at 0x10648d710>, 'func': <sqlalchemy.sql.functions._FunctionGenerator object at 0x1063994d0>, 'funcfilter': <function funcfilter at 0x1064900e0>, 'insert': <function insert at 0x10648dd40>, 'inspect': <function inspect at 0x1061cc830>, 'intersect': <function intersect at 0x10648b440>, 'intersect_all': <function intersect_all at 0x10648b560>, 'join': <function join at 0x10648da70>, 'lateral': <function lateral at 0x1063954d0>, 'literal': <function literal at 0x10630e0e0>, 'literal_column': <function literal_column at 0x10630eb00>, 'modifier': <sqlalchemy.sql.functions._FunctionGenerator object at 0x10646c310>, 'not_': <function not_ at 0x10630ea70>, 'null': <function null at 0x10648d8c0>, 'nullsfirst': <function nullsfirst at 0x10648bb90>, 'nullslast': <function nullslast at 0x10648bd40>, 'or_': <function or_ at 0x106395680>, 'outerjoin': <function outerjoin at 0x10648dc20>, 'outparam': <function outparam at 0x10630e9e0>, 'over': <function over at 0x10639a050>, 'select': <function select at 0x1063959e0>, 'subquery': <function subquery at 0x106361dd0>, 'table': <function table at 0x106395ef0>, 'tablesample': <function tablesample at 0x106395200>, 'text': <function text at 0x106395c20>, 'true': <function true at 0x10648d560>, 'tuple_': <function tuple_ at 0x10647aef0>, 'type_coerce': <function type_coerce at 0x10648d3b0>, 'union': <function union at 0x10648b710>, 'union_all': <function union_all at 0x10648b8c0>, 'update': <function update at 0x10648de60>, 'within_group': <function within_group at 0x10639a200>, 'AliasOption': <class 'sqlalchemy.orm.query.AliasOption'>, 'AttributeExtension': <class 'sqlalchemy.orm.deprecated_interfaces.AttributeExtension'>, 'Bundle': <class 'sqlalchemy.orm.query.Bundle'>, 'ColumnProperty': <class 'sqlalchemy.orm.properties.ColumnProperty'>, 'ComparableProperty': <class 'sqlalchemy.orm.descriptor_props.ComparableProperty'>, 'CompositeProperty': <class 'sqlalchemy.orm.descriptor_props.CompositeProperty'>, 'EXT_CONTINUE': symbol('EXT_CONTINUE'), 'EXT_SKIP': symbol('EXT_SKIP'), 'EXT_STOP': symbol('EXT_STOP'), 'Load': <class 'sqlalchemy.orm.strategy_options.Load'>, 'Mapper': <class 'sqlalchemy.orm.mapper.Mapper'>, 'MapperExtension': <class 'sqlalchemy.orm.deprecated_interfaces.MapperExtension'>, 'PropComparator': <class 'sqlalchemy.orm.interfaces.PropComparator'>, 'RelationshipProperty': <class 'sqlalchemy.orm.relationships.RelationshipProperty'>, 'Session': <class 'sqlalchemy.orm.session.Session'>, 'SessionExtension': <class 'sqlalchemy.orm.deprecated_interfaces.SessionExtension'>, 'SynonymProperty': <class 'sqlalchemy.orm.descriptor_props.SynonymProperty'>, 'aliased': <function aliased at 0x1066db290>, 'backref': <function backref at 0x1067f8e60>, 'class_mapper': <function class_mapper at 0x10665fd40>, 'clear_mappers': <function clear_mappers at 0x1067b03b0>, 'close_all_sessions': <function close_all_sessions at 0x1067ed5f0>, 'column_property': <function column_property at 0x1067f8ef0>, 'comparable_property': <function comparable_property at 0x1067b0560>, 'compile_mappers': <function compile_mappers at 0x1067b08c0>, 'composite': <function composite at 0x1067b00e0>, 'configure_mappers': <function configure_mappers at 0x1066f79e0>, 'contains_alias': <function contains_alias at 0x1067b0b00>, 'contains_eager': <function contains_eager at 0x1066e1e60>, 'defaultload': <function defaultload at 0x1066e6440>, 'defer': <function defer at 0x1066e6560>, 'deferred': <function deferred at 0x1067b0050>, 'dynamic_loader': <function dynamic_loader at 0x10692b830>, 'eagerload': <function eagerload at 0x1067b0710>, 'eagerload_all': <function eagerload_all at 0x1067b0950>, 'foreign': <function foreign at 0x1067a4710>, 'immediateload': <function immediateload at 0x1066dcef0>, 'joinedload': <function joinedload at 0x1066dc0e0>, 'joinedload_all': <function joinedload_all at 0x1066dc4d0>, 'lazyload': <function lazyload at 0x1066dcb00>, 'lazyload_all': <function lazyload_all at 0x1066e60e0>, 'load_only': <function load_only at 0x1066e1f80>, 'make_transient': <function make_transient at 0x1067f7d40>, 'make_transient_to_detached': <function make_transient_to_detached at 0x1067f7dd0>, 'mapper': <function mapper at 0x1067b0320>, 'noload': <function noload at 0x1066e6200>, 'object_mapper': <function object_mapper at 0x10665f7a0>, 'object_session': <function object_session at 0x1067f7e60>, 'polymorphic_union': <function polymorphic_union at 0x1066db170>, 'public_factory': <function public_factory at 0x106250ef0>, 'query_expression': <function query_expression at 0x1067b0170>, 'raiseload': <function raiseload at 0x1066e6320>, 'reconstructor': <function reconstructor at 0x1066f7a70>, 'relation': <function relation at 0x10692b710>, 'relationship': <function relationship at 0x10692b680>, 'remote': <function remote at 0x1066fe710>, 'scoped_session': <class 'sqlalchemy.orm.scoping.scoped_session'>, 'selectin_polymorphic': <function selectin_polymorphic at 0x1066e69e0>, 'selectinload': <function selectinload at 0x1066dc710>, 'selectinload_all': <function selectinload_all at 0x1066dccb0>, 'sessionmaker': <class 'sqlalchemy.orm.session.sessionmaker'>, 'subqueryload': <function subqueryload at 0x1066dc320>, 'subqueryload_all': <function subqueryload_all at 0x1066dc8c0>, 'synonym': <function synonym at 0x1067b04d0>, 'undefer': <function undefer at 0x1066e6680>, 'undefer_group': <function undefer_group at 0x1066e67a0>, 'validates': <function validates at 0x1066f7b00>, 'was_deleted': <function was_deleted at 0x1066dd5f0>, 'with_expression': <function with_expression at 0x1066e68c0>, 'with_parent': <function with_parent at 0x1066dd4d0>, 'with_polymorphic': <function with_polymorphic at 0x1066dbe60>, 'event': <module 'sqlalchemy.event' from '/Users/aglane/Box Sync/Git/flora-DEV/env/lib/python3.7/site-packages/sqlalchemy/event/__init__.py'>}

joegasewicz commented 4 years ago

Sorry for the delay @GhastlyParadox i'm going to pick this issue up today...

joegasewicz commented 4 years ago

@GhastlyParadox

But the db instance itself is Flask-Sqlalchemy. I actually wondered whether I needed to be using Flask-Sqlalchemy db.Model

Yes, you must pass db.Model to any class that is decorated with @file_upload.Model.

I'm going to provide details and any required fixes to port FFU to be able to work with vanilla SqlAlchemy as well. This issue will be related to this new feature so please keep this open until the fix & or documentation is updated... ill pick this up tomorrow morning, thanks. Joe

joegasewicz commented 4 years ago

this is being fixed in #99