diff --git a/.gitignore b/.gitignore index f253fc31f..243c83924 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ __pycache__ examples/sqla-inline/static examples/file/files examples/forms/files -examples/appengine/lib .DS_Store .idea/ *.sqlite @@ -27,4 +26,4 @@ env *.egg .eggs .tox/ -.env \ No newline at end of file +.env diff --git a/doc/introduction.rst b/doc/introduction.rst index 8b4276abc..b651d8376 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -27,7 +27,6 @@ geoalchemy As with SQLAlchemy, but adding support for geographi mongoengine Supports the Flask-Mongoengine library pymongo Supports the PyMongo library peewee Supports the peewee library -appengine Supports Google's appengine library s3 Supports file admin using AWS S3 azure-blob-storage Supports file admin using Azure blob store images Allows working with image data diff --git a/examples/appengine/app.py b/examples/appengine/app.py deleted file mode 100644 index 48b2c7636..000000000 --- a/examples/appengine/app.py +++ /dev/null @@ -1,51 +0,0 @@ - -from flask import Flask -import flask_admin -from flask_admin.contrib import appengine -from google.appengine.ext import db -from google.appengine.ext import ndb - -# Create application -app = Flask(__name__) - -# Create dummy secrey key so we can use sessions -app.config['SECRET_KEY'] = '123456790' - -admin = flask_admin.Admin(app, name="Admin") - -# Flask views -@app.route('/') -def index(): - return 'Click me to get to Admin!' - -class Car(db.Model): - owner = db.StringProperty() - make_model = db.StringProperty() - indexed_int = db.IntegerProperty() - unindexed_int = db.IntegerProperty(indexed=False) - -class Tire(db.Model): - car = db.ReferenceProperty(Car) - position = db.StringProperty() - -class House(ndb.Model): - address = db.StringProperty() - json_property = ndb.JsonProperty() - indexed_int = ndb.IntegerProperty() - unindexed_int = ndb.IntegerProperty(indexed=False) - text_property = ndb.TextProperty() - datetime_property = ndb.DateTimeProperty() - -class Room(ndb.Model): - house = ndb.KeyProperty(kind=House) - name = ndb.StringProperty() - -admin.add_view(appengine.ModelView(Car)) -admin.add_view(appengine.ModelView(Tire)) -admin.add_view(appengine.ModelView(House)) -admin.add_view(appengine.ModelView(Room)) - -if __name__ == '__main__': - - # Start app - app.run(debug=True) diff --git a/examples/appengine/app.yaml b/examples/appengine/app.yaml deleted file mode 100644 index 3c4e3d080..000000000 --- a/examples/appengine/app.yaml +++ /dev/null @@ -1,8 +0,0 @@ -runtime: python27 -threadsafe: true -api_version: 1 -module: default - -handlers: -- url: /admin.* - script: app.app diff --git a/examples/appengine/appengine_config.py b/examples/appengine/appengine_config.py deleted file mode 100644 index edbd09015..000000000 --- a/examples/appengine/appengine_config.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file gets imported as part of running dev_appserver.py -import sys -sys.path = ['lib'] + sys.path diff --git a/examples/appengine/run.sh b/examples/appengine/run.sh deleted file mode 100755 index 451b62b79..000000000 --- a/examples/appengine/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -BASEDIR=$(dirname $0) -# Install wtforms-admin to our lib/ directory, using our local source tree -pip install -t $BASEDIR/lib/ $BASEDIR/../.. wtforms_appengine -# Now run our server -dev_appserver.py $BASEDIR/app.yaml diff --git a/flask_admin/contrib/appengine/__init__.py b/flask_admin/contrib/appengine/__init__.py deleted file mode 100644 index 4da149665..000000000 --- a/flask_admin/contrib/appengine/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# flake8: noqa -try: - import wtforms_appengine -except ImportError: - raise Exception( - 'Could not import `wtforms-appengine`. ' - 'Enable `appengine` integration by installing `flask-admin[appengine]`' - ) - -from .view import ModelView diff --git a/flask_admin/contrib/appengine/fields.py b/flask_admin/contrib/appengine/fields.py deleted file mode 100644 index e75213f74..000000000 --- a/flask_admin/contrib/appengine/fields.py +++ /dev/null @@ -1,18 +0,0 @@ -from wtforms.fields import StringField -from google.appengine.ext import ndb - -import decimal - - -class GeoPtPropertyField(StringField): - def process_formdata(self, valuelist): - if valuelist: - try: - lat, lon = valuelist[0].split(',') - self.data = ndb.GeoPt( - decimal.Decimal(lat.strip()), - decimal.Decimal(lon.strip()) - ) - - except (decimal.InvalidOperation, ValueError): - raise ValueError('Not a valid coordinate location') diff --git a/flask_admin/contrib/appengine/form.py b/flask_admin/contrib/appengine/form.py deleted file mode 100644 index fa5f490a7..000000000 --- a/flask_admin/contrib/appengine/form.py +++ /dev/null @@ -1,10 +0,0 @@ -from wtforms_appengine.ndb import ModelConverter -from .fields import GeoPtPropertyField -from flask_admin.model.form import converts - - -class AdminModelConverter(ModelConverter): - @converts('GeoPt') - def convert_GeoPtProperty(self, model, prop, kwargs): - """Returns a form field for a ``ndb.GeoPtProperty``.""" - return GeoPtPropertyField(**kwargs) diff --git a/flask_admin/contrib/appengine/view.py b/flask_admin/contrib/appengine/view.py deleted file mode 100644 index 99fac7519..000000000 --- a/flask_admin/contrib/appengine/view.py +++ /dev/null @@ -1,235 +0,0 @@ -import logging - -from flask_admin.model import BaseModelView -from wtforms_appengine import db as wt_db -from wtforms_appengine import ndb as wt_ndb - -from google.appengine.ext import db -from google.appengine.ext import ndb - -from flask_wtf import Form -from flask_admin.model.form import create_editable_list_form -from .form import AdminModelConverter - - -class NdbModelView(BaseModelView): - """ - AppEngine NDB model scaffolding. - """ - - def get_pk_value(self, model): - return model.key.urlsafe() - - def scaffold_list_columns(self): - return sorted([k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, ndb.Property)]) - - def scaffold_sortable_columns(self): - return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, ndb.Property) and v._indexed] - - def init_search(self): - return None - - def is_valid_filter(self): - pass - - def scaffold_filters(self): - # TODO: implement - pass - - form_args = None - - model_form_converter = AdminModelConverter - """ - Model form conversion class. Use this to implement custom field conversion logic. - - For example:: - - class MyModelConverter(AdminModelConverter): - pass - - - class MyAdminView(ModelView): - model_form_converter = MyModelConverter - """ - - def scaffold_form(self): - form_class = wt_ndb.model_form( - self.model(), - base_class=Form, - only=self.form_columns, - exclude=self.form_excluded_columns, - field_args=self.form_args, - converter=self.model_form_converter(), - ) - return form_class - - def scaffold_list_form(self, widget=None, validators=None): - form_class = wt_ndb.model_form( - self.model(), - base_class=Form, - only=self.column_editable_list, - field_args=self.form_args, - converter=self.model_form_converter(), - ) - result = create_editable_list_form(Form, form_class, widget) - return result - - def get_list(self, page, sort_field, sort_desc, search, filters, - page_size=None): - # TODO: implement filters (don't think search can work here) - - q = self.model.query() - - if sort_field: - order_field = getattr(self.model, sort_field) - if sort_desc: - order_field = -order_field - q = q.order(order_field) - - if page_size is None: - page_size = self.page_size - - results = q.fetch(page_size, offset=page * page_size) - - return q.count(), results - - def get_one(self, urlsafe_key): - return ndb.Key(urlsafe=urlsafe_key).get() - - def create_model(self, form): - try: - model = self.model() - form.populate_obj(model) - model.put() - except Exception as ex: - if not self.handle_view_exception(ex): - # flash(gettext('Failed to create record. %(error)s', - # error=ex), 'error') - logging.exception('Failed to create record.') - return False - else: - self.after_model_change(form, model, True) - - return model - - def update_model(self, form, model): - try: - form.populate_obj(model) - model.put() - except Exception as ex: - if not self.handle_view_exception(ex): - # flash(gettext('Failed to update record. %(error)s', - # error=ex), 'error') - logging.exception('Failed to update record.') - return False - else: - self.after_model_change(form, model, False) - - return True - - def delete_model(self, model): - try: - model.key.delete() - except Exception as ex: - if not self.handle_view_exception(ex): - # flash(gettext('Failed to delete record. %(error)s', - # error=ex), - # 'error') - logging.exception('Failed to delete record.') - return False - else: - self.after_model_delete(model) - - return True - - -class DbModelView(BaseModelView): - """ - AppEngine DB model scaffolding. - """ - - def get_pk_value(self, model): - return str(model.key()) - - def scaffold_list_columns(self): - return sorted([k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property)]) - - def scaffold_sortable_columns(self): - # We use getattr() because ReferenceProperty does not specify a 'indexed' field - return [k for (k, v) in self.model.__dict__.iteritems() - if isinstance(v, db.Property) and getattr(v, 'indexed', None)] - - def init_search(self): - return None - - def is_valid_filter(self): - pass - - def scaffold_filters(self): - # TODO: implement - pass - - def scaffold_form(self): - return wt_db.model_form(self.model()) - - def get_list(self, page, sort_field, sort_desc, search, filters): - # TODO: implement filters (don't think search can work here) - - q = self.model.all() - - if sort_field: - if sort_desc: - sort_field = "-" + sort_field - q.order(sort_field) - - results = q.fetch(self.page_size, offset=page * self.page_size) - return q.count(), results - - def get_one(self, encoded_key): - return db.get(db.Key(encoded=encoded_key)) - - def create_model(self, form): - try: - model = self.model() - form.populate_obj(model) - model.put() - return model - except Exception as ex: - if not self.handle_view_exception(ex): - # flash(gettext('Failed to create record. %(error)s', - # error=ex), 'error') - logging.exception('Failed to create record.') - return False - - def update_model(self, form, model): - try: - form.populate_obj(model) - model.put() - return True - except Exception as ex: - if not self.handle_view_exception(ex): - # flash(gettext('Failed to update record. %(error)s', - # error=ex), 'error') - logging.exception('Failed to update record.') - return False - - def delete_model(self, model): - try: - model.delete() - return True - except Exception as ex: - if not self.handle_view_exception(ex): - # flash(gettext('Failed to delete record. %(error)s', - # error=ex), - # 'error') - logging.exception('Failed to delete record.') - return False - - -def ModelView(model): - if issubclass(model, ndb.Model): - return NdbModelView(model) - elif issubclass(model, db.Model): - return DbModelView(model) - else: - raise ValueError("Unsupported model: %s" % model) diff --git a/pyproject.toml b/pyproject.toml index b7307c3c7..5fde81f01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,11 +54,6 @@ peewee = [ "peewee>3", "wtf-peewee>3" ] -appengine = [ - "Flask-Admin[sqlalchemy]", - "appengine-python-standard", - "wtforms-appengine" -] s3 = ["boto"] # TODO: migrate to boto3 azure-blob-storage = ["azure-storage-blob<=3"] # TODO: update to v12+ images = ["pillow>=3.3.2"] @@ -76,7 +71,6 @@ all = [ "Flask-Admin[pymongo]", "Flask-Admin[peewee]", - "Flask-Admin[appengine]", "Flask-Admin[s3]", "Flask-Admin[azure-blob-storage]", "Flask-Admin[images]", @@ -182,7 +176,6 @@ module = [ "flask_babel", "flask_mongoengine.*", "flask_wtf", - "google.appengine.ext", "gridfs", "marker", "mongoengine.*", @@ -193,7 +186,6 @@ module = [ "sqlalchemy_utils", "tablib", "wtforms.*", - "wtforms_appengine.*", "wtfpeewee.*", ] ignore_missing_imports = true diff --git a/requirements/dev.txt b/requirements/dev.txt index e170cfa97..68232ade3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -186,7 +186,7 @@ pyproject-api==1.7.1 # via tox pyright==1.1.373 # via -r typing.txt -pytest==8.3.1 +pytest==8.3.2 # via # -r docs.txt # -r tests.in @@ -302,7 +302,7 @@ typing-extensions==4.12.2 # astroid # mypy # pylint -urllib3==1.26.19 +urllib3==2.2.2 # via # -r docs.txt # -r typing.txt diff --git a/requirements/docs.in b/requirements/docs.in index 74211d150..652d61aee 100644 --- a/requirements/docs.in +++ b/requirements/docs.in @@ -1,4 +1,3 @@ --c tests-constraints.txt -r tests.in pallets-sphinx-themes diff --git a/requirements/docs.txt b/requirements/docs.txt index a3b4a52fd..df2c324bf 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -69,7 +69,7 @@ pygments==2.18.0 # via sphinx pylint==3.2.6 # via -r tests.in -pytest==8.3.1 +pytest==8.3.2 # via # -r tests.in # pytest-cov @@ -113,9 +113,7 @@ typing-extensions==4.12.2 # via # astroid # pylint -urllib3==1.26.19 - # via - # -c tests-constraints.txt - # requests +urllib3==2.2.2 + # via requests zipp==3.19.2 # via importlib-metadata diff --git a/requirements/tests-constraints.txt b/requirements/tests-constraints.txt deleted file mode 100644 index d4db0ea1e..000000000 --- a/requirements/tests-constraints.txt +++ /dev/null @@ -1 +0,0 @@ -urllib3<2 # remove when appengine-python-standard supports urllib3>=2 diff --git a/requirements/tests.txt b/requirements/tests.txt index c8fb53bec..3f70eb261 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -48,7 +48,7 @@ pyflakes==3.2.0 # via flake8 pylint==3.2.6 # via -r tests.in -pytest==8.3.1 +pytest==8.3.2 # via # -r tests.in # pytest-cov diff --git a/requirements/typing.in b/requirements/typing.in index f17e08219..982982adf 100644 --- a/requirements/typing.in +++ b/requirements/typing.in @@ -1,4 +1,3 @@ --c tests-constraints.txt -r tests.in mypy diff --git a/requirements/typing.txt b/requirements/typing.txt index 50154bae2..95d06b2ff 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -60,7 +60,7 @@ pylint==3.2.6 # via -r tests.in pyright==1.1.373 # via -r typing.in -pytest==8.3.1 +pytest==8.3.2 # via # -r tests.in # -r typing.in @@ -106,7 +106,5 @@ typing-extensions==4.12.2 # astroid # mypy # pylint -urllib3==1.26.19 - # via - # -c tests-constraints.txt - # requests +urllib3==2.2.2 + # via requests diff --git a/tox.ini b/tox.ini index 5290141e2..21494a982 100644 --- a/tox.ini +++ b/tox.ini @@ -20,9 +20,7 @@ use_frozen_constraints = true setenv = SQLALCHEMY_SILENCE_UBER_WARNING = 1 AZURE_STORAGE_CONNECTION_STRING = DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1; -deps = - -c requirements/tests-constraints.txt - -r requirements/tests.in +deps = -r requirements/tests.in commands_pre = noflaskbabel: pip uninstall -y flask-babel flaskmongoengine: pip install Flask==2.1.3 Werkzeug==2.3.8 flask-sqlalchemy<3 @@ -36,17 +34,13 @@ skip_install = true commands = pre-commit run --all-files [testenv:typing] -deps = - -c requirements/tests-constraints.txt - -r requirements/typing.txt +deps = -r requirements/typing.txt commands = mypy --python-version 3.8 mypy --python-version 3.12 [testenv:docs] -deps = - -c requirements/tests-constraints.txt - -r requirements/docs.txt +deps = -r requirements/docs.txt # commands = sphinx-build -E -W -b dirhtml doc doc/_build/dirhtml # TODO: Switch to the above command when docs have been migrated to use the Pallets theme. commands = sphinx-build -b html -d build/doctrees doc build/html