diff --git a/.flaskenv b/.flaskenv index c6dc85b..102e98f 100644 --- a/.flaskenv +++ b/.flaskenv @@ -3,4 +3,12 @@ FLASK_APP="entrance:create_app()" FLASK_ENV=development FLASK_DEBUG=1 FLASK_RUN_HOST = 127.0.0.1 -FLASK_RUN_PORT = 5000 \ No newline at end of file +FLASK_RUN_PORT = 5000 + +# MySql配置信息 +MYSQL_HOST=127.0.0.1 +# MYSQL_HOST=dbserver +MYSQL_PORT=3306 +MYSQL_DATABASE=next +MYSQL_USERNAME=root +MYSQL_PASSWORD=root \ No newline at end of file diff --git a/common/model.py b/common/model.py new file mode 100644 index 0000000..827adf7 --- /dev/null +++ b/common/model.py @@ -0,0 +1,79 @@ +import datetime + +from entrance.extensions import db + + +class BaseModel(db.Model): + __abstract__ = True + + # insert and update + def save(self): + db.session.add(self) + db.session.commit() + + # delete + def delete(self): + db.session.delete(self) + db.session.commit() + + +class CommonModel(BaseModel): + __abstract__ = True + id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='用户ID') + create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间') + update_time = db.Column(db.DateTime, onupdate=datetime.datetime.now, comment='更新时间') + + +class CurdModel(BaseModel): + __abstract__ = True + id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='用户ID') + create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间') + update_time = db.Column(db.DateTime, onupdate=datetime.datetime.now, comment='更新时间') + delete_at = db.Column(db.DateTime, comment='删除时间') + + +Column = db.Column + +TypeDecorator = db.TypeDecorator +INT = db.INT +CHAR = db.CHAR +VARCHAR = db.VARCHAR +NCHAR = db.NCHAR +NVARCHAR = db.NVARCHAR +TEXT = db.TEXT +Text = db.Text +FLOAT = db.FLOAT +NUMERIC = db.NUMERIC +REAL = db.REAL +DECIMAL = db.DECIMAL +TIMESTAMP = db.TIMESTAMP +DATETIME = db.DATETIME +CLOB = db.CLOB +BLOB = db.BLOB +BINARY = db.BINARY +VARBINARY = db.VARBINARY +BOOLEAN = db.BOOLEAN +BIGINT = db.BIGINT +SMALLINT = db.SMALLINT +INTEGER = db.INTEGER +DATE = db.DATE +TIME = db.TIME +TupleType = db.TupleType +String = db.String +Integer = db.Integer +SmallInteger = db.SmallInteger +BigInteger = db.BigInteger +Numeric = db.Numeric +Float = db.Float +DateTime = db.DateTime +Date = db.Date +Time = db.Time +LargeBinary = db.LargeBinary +Boolean = db.Boolean +Unicode = db.Unicode +UnicodeText = db.UnicodeText +PickleType = db.PickleType +Interval = db.Interval +Enum = db.Enum +ARRAY = db.ARRAY +JSON = db.JSON \ No newline at end of file diff --git a/common/view.py b/common/view.py index 4271e22..555b105 100644 --- a/common/view.py +++ b/common/view.py @@ -1 +1,79 @@ -from flask_restx import Resource, Namespace +from flask import Blueprint as FlaskBlueprint +from flask.views import MethodViewType, MethodView + + +class Blueprint(FlaskBlueprint): + + # Order in which the methods are presented in the spec + HTTP_METHODS = ["OPTIONS", "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"] + + DEFAULT_LOCATION_CONTENT_TYPE_MAPPING = { + "json": "application/json", + "form": "application/x-www-form-urlencoded", + "files": "multipart/form-data", + } + + + + def __init__(self, *args, **kwargs): + + self.description = kwargs.pop("description", "") + + super().__init__(*args, **kwargs) + + self._endpoints = [] + + def add_url_rule( + self, + rule, + endpoint=None, + view_func=None, + provide_automatic_options=None, + *, + parameters=None, + tags=None, + **options, + ): + + if view_func is None: + raise TypeError("view_func must be provided") + + if endpoint is None: + endpoint = view_func.__name__ + + # Ensure endpoint name is unique + # - to avoid a name clash when registering a MethodView + # - to use it as a key internally in endpoint -> doc mapping + if endpoint in self._endpoints: + endpoint = f"{endpoint}_{len(self._endpoints)}" + self._endpoints.append(endpoint) + + if isinstance(view_func, MethodViewType): + func = view_func.as_view(endpoint) + else: + func = view_func + + # Add URL rule in Flask and store endpoint documentation + super().add_url_rule(rule, endpoint, func, **options) + + def route(self, rule, *, parameters=None, tags=None, **options): + """Decorator to register view function in application and documentation + + Calls :meth:`add_url_rule `. + """ + + def decorator(func): + endpoint = options.pop("endpoint", None) + self.add_url_rule( + rule, endpoint, func, parameters=parameters, tags=tags, **options + ) + return func + + return decorator + + def register_child_bp(self, bp_lists): + for bp in bp_lists: + self.register_blueprint(bp) + +class View(MethodView): + pass \ No newline at end of file diff --git a/entrance/__init__.py b/entrance/__init__.py index e3470b0..2de3153 100644 --- a/entrance/__init__.py +++ b/entrance/__init__.py @@ -1,11 +1,15 @@ from flask import Flask -from entrance.extend.restx import sys -# from entrance.extensions import init_extensions +from entrance import config +from entrance.bps import sys, register_parent_bp + + +from entrance.extensions import init_extensions def create_app(): app = Flask(__name__) - app.register_blueprint(sys) - # init_extensions(app) + app.config.from_pyfile('config.py') + register_parent_bp(app) + init_extensions(app) return app diff --git a/entrance/bps.py b/entrance/bps.py new file mode 100644 index 0000000..73451e4 --- /dev/null +++ b/entrance/bps.py @@ -0,0 +1,19 @@ +from flask import Flask +from typing import List +from common.view import Blueprint +from modules.sys.view.user import user + +sys = Blueprint('sys', __name__, url_prefix='/sys') +sys_lists: List[Blueprint] = [ + user +] +sys.register_child_bp(sys_lists) + +parent_lists: List[Blueprint] = [ + sys +] + + +def register_parent_bp(app: Flask): + for bp in parent_lists: + app.register_blueprint(bp) diff --git a/entrance/config.py b/entrance/config.py new file mode 100644 index 0000000..574305c --- /dev/null +++ b/entrance/config.py @@ -0,0 +1,15 @@ +import os + +# mysql 配置 +MYSQL_USERNAME: str = os.getenv('MYSQL_USERNAME') or "root" +MYSQL_PASSWORD: str = os.getenv('MYSQL_PASSWORD') or "123456" +MYSQL_HOST: str = os.getenv('MYSQL_HOST') or "127.0.0.1" +MYSQL_PORT: int = int(os.getenv('MYSQL_PORT') or 3306) +MYSQL_DATABASE: str = os.getenv('MYSQL_DATABASE') or "PearAdminFlask" + +# mysql 数据库的配置信息 +SQLALCHEMY_DATABASE_URI: str = f"mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}" + +SQLALCHEMY_TRACK_MODIFICATIONS = True +SQLALCHEMY_ECHO = False +SQLALCHEMY_POOL_RECYCLE = 8 diff --git a/entrance/extend/migrate.py b/entrance/extend/migrate.py new file mode 100644 index 0000000..3042ff7 --- /dev/null +++ b/entrance/extend/migrate.py @@ -0,0 +1,6 @@ +from flask_migrate import Migrate + +from entrance.extend.orm import db +from entrance.models import model_lists +migrate = Migrate(db=db, directory='entrance/migrations') + diff --git a/entrance/extend/orm.py b/entrance/extend/orm.py new file mode 100644 index 0000000..8158f3b --- /dev/null +++ b/entrance/extend/orm.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db: SQLAlchemy = SQLAlchemy() diff --git a/entrance/extend/restx.py b/entrance/extend/restx.py deleted file mode 100644 index c283b4f..0000000 --- a/entrance/extend/restx.py +++ /dev/null @@ -1,12 +0,0 @@ -from flask import Blueprint -from flask_restx import Api - -from entrance.namespaces import namespaces - -sys = Blueprint('sys', __name__, url_prefix='/sys') - -api = Api(sys, version='1.0', title='Pear Admin Flask', - description='admin based on flask and layui', - ) -for i in namespaces: - api.add_namespace(i) diff --git a/entrance/extensions.py b/entrance/extensions.py index 0413586..ec3b8d5 100644 --- a/entrance/extensions.py +++ b/entrance/extensions.py @@ -1,8 +1,9 @@ from flask import Flask -from flask_restx import Api -from flask import Blueprint -from entrance.namespaces import namespaces + +from entrance.extend.orm import db +from entrance.extend.migrate import migrate def init_extensions(app: Flask): - pass + db.init_app(app) + migrate.init_app(app) \ No newline at end of file diff --git a/entrance/migrations/README b/entrance/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/entrance/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/entrance/migrations/alembic.ini b/entrance/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/entrance/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/entrance/migrations/env.py b/entrance/migrations/env.py new file mode 100644 index 0000000..68feded --- /dev/null +++ b/entrance/migrations/env.py @@ -0,0 +1,91 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.get_engine().url).replace( + '%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = current_app.extensions['migrate'].db.get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/entrance/migrations/script.py.mako b/entrance/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/entrance/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/entrance/migrations/versions/74133ba99af7_.py b/entrance/migrations/versions/74133ba99af7_.py new file mode 100644 index 0000000..dec7795 --- /dev/null +++ b/entrance/migrations/versions/74133ba99af7_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 74133ba99af7 +Revises: +Create Date: 2022-01-22 18:50:29.097473 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '74133ba99af7' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('admin_user', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='用户ID'), + sa.Column('username', sa.String(length=20), nullable=True, comment='用户名'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('admin_user') + # ### end Alembic commands ### diff --git a/entrance/models.py b/entrance/models.py new file mode 100644 index 0000000..dcf7259 --- /dev/null +++ b/entrance/models.py @@ -0,0 +1,5 @@ +from modules.sys.models import user + +model_lists = { + user +} diff --git a/entrance/namespaces.py b/entrance/namespaces.py deleted file mode 100644 index 5cecb25..0000000 --- a/entrance/namespaces.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import List -from flask_restx import Namespace -from modules.sys.user import userns - -namespaces: List[Namespace] = [ - userns, -] diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/sys/models/user.py b/modules/sys/models/user.py new file mode 100644 index 0000000..d75d34e --- /dev/null +++ b/modules/sys/models/user.py @@ -0,0 +1,15 @@ +from common.model import BaseModel, Column, String, Integer +from pydantic import BaseModel as PModel + + +class User(BaseModel): + __tablename__ = 'admin_user' + id = Column(Integer, primary_key=True, autoincrement=True, comment='用户ID') + username = Column(String(20), comment='用户名') + + +class Userschema(PModel): + id: int + username: str + class Config: + orm_mode = True diff --git a/modules/sys/user.py b/modules/sys/user.py deleted file mode 100644 index df241ea..0000000 --- a/modules/sys/user.py +++ /dev/null @@ -1,13 +0,0 @@ -from common.view import Resource,Namespace - -userns = Namespace('aa', ordered=True) - - -@userns.route('/a') -class MyResource(Resource): - def get(self): - return {} - - @userns.response(403, 'Not Authorized') - def post(self): - userns.abort(403) diff --git a/modules/sys/view/user.py b/modules/sys/view/user.py new file mode 100644 index 0000000..453e307 --- /dev/null +++ b/modules/sys/view/user.py @@ -0,0 +1,19 @@ +from typing import List + +from common.view import Blueprint,View +from flask import jsonify + +from pydantic import BaseModel, ValidationError, constr, parse_obj_as + +from modules.sys.models.user import User, Userschema + +user = Blueprint('sys', __name__, url_prefix='/user') + + +@user.route('/') +class UserView(View): + def get(self): + u = User.query.all() + # m = parse_obj_as(List[User], u) + + return '1' \ No newline at end of file diff --git a/require/min.txt b/require/min.txt index 926f1b0..17664ac 100644 --- a/require/min.txt +++ b/require/min.txt @@ -1,3 +1,5 @@ -i https://pypi.tuna.tsinghua.edu.cn/simple Flask==2.0.2 -flask-restx==0.5.1 \ No newline at end of file +flask-restx==0.5.1 +pymysql==1.0.2 +Flask-Migrate==3.1.0 \ No newline at end of file