소스 검색

feat: improve Flask app structure and logging

tanlie 6 일 전
부모
커밋
6ae20abe2f
8개의 변경된 파일170개의 추가작업 그리고 3개의 파일을 삭제
  1. 13 0
      app/app.py
  2. 5 0
      app/exceptions/__init__.py
  3. 46 0
      app/exceptions/api_exception.py
  4. 36 0
      app/exceptions/common_error.py
  5. 5 0
      app/validators/__init__.py
  6. 23 0
      app/validators/base.py
  7. 27 1
      ginger.py
  8. 15 2
      requirements.txt

+ 13 - 0
app/app.py

@@ -32,4 +32,17 @@ def create_app(env: str = "default"):
         pass
 
     register_blueprints(app)
+
+    # Register a global handler to convert APIException instances into JSON responses
+    try:
+        from app.exceptions.api_exception import APIException
+
+        @app.errorhandler(APIException)
+        def handle_api_exception(error):
+            # HTTPException.get_response builds a Response object using get_body/get_headers
+            response = error.get_response()
+            return response
+    except Exception:
+        pass
+
     return app

+ 5 - 0
app/exceptions/__init__.py

@@ -0,0 +1,5 @@
+"""
+__init__.py -
+auther: tanlie
+date: 2026/8/23
+"""

+ 46 - 0
app/exceptions/api_exception.py

@@ -0,0 +1,46 @@
+"""
+api_exception -
+auther: tanlie
+date: 2026/8/23
+"""
+
+from flask import request, json
+from werkzeug.exceptions import HTTPException
+
+
+class APIException(HTTPException):
+    code = 500
+    msg = 'sorry, we made a mistake (* ̄︶ ̄)!'
+    error_code = 999
+
+    def __init__(self, msg=None, code=None, error_code=None, headers=None):
+        if code:
+            self.code = code
+        if error_code:
+            self.error_code = error_code
+        if msg:
+            self.msg = msg
+        super(APIException, self).__init__(msg, None)
+
+    def get_body(self, environ=None, scope=None, *args, **kwargs):
+        body = dict(
+            msg=self.msg,
+            error_code=self.error_code,
+            request=request.method + ' ' + self.get_url_no_param()
+        )
+        text = json.dumps(body)
+        return text
+
+    def get_headers(self, environ=None, scope=None, *args, **kwargs):
+        """Get a list of headers.
+
+        Werkzeug/Flask may pass ``environ`` and ``scope`` positional arguments in
+        newer versions, so accept them for compatibility.
+        """
+        return [("Content-Type", "application/json")]
+
+    @staticmethod
+    def get_url_no_param():
+        full_path = str(request.full_path)
+        main_path = full_path.split('?')
+        return main_path[0]

+ 36 - 0
app/exceptions/common_error.py

@@ -0,0 +1,36 @@
+"""
+common_error -
+auther: tanlie
+date: 2026/8/23
+"""
+from app.exceptions.api_exception import APIException
+
+
+class ServerError(APIException):
+    code = 500
+    msg = 'sorry, we made a mistake (* ̄︶ ̄)!'
+    error_code = 999
+
+
+class ParameterException(APIException):
+    code = 400
+    msg = 'invalid parameter'
+    error_code = 1000
+
+
+class NotFound(APIException):
+    code = 404
+    msg = 'the resource are not found O__O...'
+    error_code = 1001
+
+
+class AuthFailed(APIException):
+    code = 401
+    error_code = 1005
+    msg = 'authorization failed'
+
+
+class Forbidden(APIException):
+    code = 403
+    error_code = 1004
+    msg = 'forbidden, not in scope'

+ 5 - 0
app/validators/__init__.py

@@ -0,0 +1,5 @@
+"""
+__init__.py -
+auther: tanlie
+date: 2026/8/23
+"""

+ 23 - 0
app/validators/base.py

@@ -0,0 +1,23 @@
+"""
+base -
+auther: tanlie
+date: 2026/8/23
+"""
+from flask import request
+from wtforms import Form
+
+from app.exceptions.common_error import ParameterException
+
+
+class BaseForm(Form):
+    def __init__(self):
+        data = request.get_json(silent=True)
+        args = request.args.to_dict()
+        super(BaseForm, self).__init__(data=data, **args)
+
+    def validate_for_api(self):
+        valid = super(BaseForm, self).validate()
+        if not valid:
+            # form errors
+            raise ParameterException(msg=self.errors)
+        return self

+ 27 - 1
ginger.py

@@ -1,8 +1,34 @@
 """Application entrypoint."""
+from werkzeug.exceptions import HTTPException
 
 from app.app import create_app
+from app.exceptions.api_exception import APIException
+from app.exceptions.common_error import ServerError
 
 app = create_app("development")
 
+
+@app.errorhandler(Exception)
+def framework_error(e):
+    """Central error handler: always return a Flask Response instance.
+
+    Avoid returning exception objects directly (Flask expects a response).
+    """
+    if isinstance(e, APIException):
+        return e.get_response()
+
+    if isinstance(e, HTTPException):
+        code = e.code
+        msg = e.description
+        error_code = 1007
+        return APIException(msg, code, error_code).get_response()
+
+    # Other exceptions: convert to ServerError in production, re-raise in debug
+    if not app.config.get("DEBUG"):
+        return ServerError().get_response()
+    else:
+        raise e
+
+
 if __name__ == "__main__":
-    app.run(host="0.0.0.0", port=5000, debug=True)
+    app.run(host="0.0.0.0", port=5000, debug=True)

+ 15 - 2
requirements.txt

@@ -1,3 +1,16 @@
+blinker==1.9.0
+click==8.4.2
+colorama==0.4.6
 Flask==3.1.3
-python-dotenv
-pytest
+Flask-WTF==1.3.0
+iniconfig==2.3.0
+itsdangerous==2.2.0
+Jinja2==3.1.6
+MarkupSafe==3.0.3
+packaging==26.3
+pluggy==1.6.0
+Pygments==2.21.0
+pytest==9.1.1
+python-dotenv==1.2.3
+Werkzeug==3.1.8
+WTForms==3.2.2