fastapi-scaff 0.1.0__py3-none-any.whl → 0.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,48 +1,78 @@
1
1
  {
2
- ".gitignore": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n# Usually these files are written by a python script from a template\n# before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n# For a library or package, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n# However, in case of collaboration, if having platform-specific dependencies or dependencies\n# having no cross-platform support, pipenv may install dependencies that don't work, or not\n# install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n#.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# Append\n.idea\n.vscode\n*.sqlite\n",
2
+ ".dockerignore": "# Python \u5b57\u8282\u7801\u548c\u7f13\u5b58\u6587\u4ef6\n__pycache__/\n#*.py[cod]\n#*.so\n.Python\n*.pth\n\n# \u865a\u62df\u73af\u5883\u6587\u4ef6\nvenv/\nenv/\n.venv/\n\n# \u65e5\u5fd7\u6587\u4ef6\nlogs/\n*.log\n\n# \u7248\u672c\u63a7\u5236\u7cfb\u7edf\u6587\u4ef6\n.git/\n.gitignore\n.gitattributes\n\n# IDE \u548c\u7f16\u8f91\u5668\u914d\u7f6e\u6587\u4ef6\n.vscode/\n.idea/\n*.swp\n*.swo\n\n# \u6d4b\u8bd5\u6587\u4ef6\ntests/\ntest/\ntesting/\n.tox/\n.coverage\n.pytest_cache/\n\n# \u6784\u5efa\u4ea7\u7269\u548c\u5206\u53d1\u6587\u4ef6\nbuild/\ndist/\n*.egg-info/\n\n# \u6587\u6863\u6587\u4ef6\nREADME.md\nLICENSE\ndocs/\n\n# \u64cd\u4f5c\u7cfb\u7edf\u751f\u6210\u7684\u6587\u4ef6\n.DS_Store\nThumbs.db\n\n# \u5176\u4ed6\n.dockerignore\nDockerfile\nDockerfile-slim\ndocker-compose.yml\ndocker-compose.yaml\ndocker-compose.swarm.yml\ndocker-compose.swarm.yaml\nbuild.sh\nconfig/.env\n*.sqlite3\n*.sqlite\n*.db\ncelerybeat-schedule*\n",
3
+ ".gitignore": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n# Usually these files are written by a python script from a template\n# before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n# For a library or package, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n# However, in case of collaboration, if having platform-specific dependencies or dependencies\n# having no cross-platform support, pipenv may install dependencies that don't work, or not\n# install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n#.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# Append\n.idea\n.vscode\n*.sqlite3\n*.sqlite\n*.db\ncelerybeat-schedule*\n",
4
+ ".python-version": "3.12\n",
5
+ "build.sh": "#!/bin/bash\n\n# =================== Configuration Variables ===================\nIMAGE_NAME=\"fastapi-scaff\"\nIMAGE_DEFAULT_TAG=\"latest\"\n\n# Dockerfile and build context\nDOCKERFILE_PATH=\"./Dockerfile\"\nDOCKERFILE_CONTEXT=\"./\"\n\n# ================ User Input for Tag ==================\necho \"Current image name: $IMAGE_NAME\"\nread -p \"Please enter the image tag to build [\u9ed8\u8ba4: $IMAGE_DEFAULT_TAG]: \" IMAGE_TAG_INPUT\n\nif [ -z \"$IMAGE_TAG_INPUT\" ]; then\n IMAGE_TAG=\"$IMAGE_DEFAULT_TAG\"\nelse\n IMAGE_TAG=\"$IMAGE_TAG_INPUT\"\nfi\n\n# ================ Build Image ==================\necho \"=> Building image: $IMAGE_NAME:$IMAGE_TAG using Dockerfile: $DOCKERFILE_PATH\"\n\ndocker build -f \"$DOCKERFILE_PATH\" -t \"$IMAGE_NAME:$IMAGE_TAG\" \"$DOCKERFILE_CONTEXT\"\n\nif [ $? -ne 0 ]; then\n echo \"Docker build failed!\"\n exit 1\nfi\n\necho \"Image built successfully: $IMAGE_NAME:$IMAGE_TAG\"\n",
6
+ "docker-compose.swarm.yaml": "services:\n\n backend:\n# build:\n# context: .\n# dockerfile: Dockerfile\n image: fastapi-scaff:v1.0.0 # \u5f3a\u70c8\u5efa\u8bae\u4e0d\u8981\u7528 :latest\n environment:\n app_env: prod\n volumes:\n - ./config:/backend/config\n - ./logs:/backend/logs\n command: \"uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 3 --log-level info\"\n healthcheck:\n test: [ \"CMD\", \"curl\", \"-f\", \"http://localhost:8000/api/ping\" ]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 60s\n deploy:\n mode: replicated\n replicas: 3\n update_config:\n parallelism: 1\n delay: 10s\n order: start-first # \u5148\u542f\u52a8\u65b0\u526f\u672c\uff0c\u518d\u505c\u6b62\u65e7\u7684\uff08\u96f6\u505c\u673a\uff09\n failure_action: rollback\n monitor: 60s\n restart_policy:\n condition: on-failure\n delay: 5s\n max_attempts: 3\n window: 180s\n resources:\n limits:\n memory: 1G\n cpus: '1.0'\n reservations:\n memory: 256M\n cpus: '0.25'\n networks:\n app-network:\n aliases:\n - backend\n\n nginx:\n image: nginx:1.29.3\n ports:\n - target: 80\n published: 8000\n protocol: tcp\n mode: ingress # ingress | host\n volumes:\n - ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro\n deploy:\n mode: replicated\n replicas: 1\n update_config:\n parallelism: 1\n delay: 10s\n order: start-first\n failure_action: rollback\n monitor: 60s\n restart_policy:\n condition: any\n delay: 5s\n max_attempts: 15\n window: 180s\n resources:\n limits:\n memory: 256M\n cpus: '0.3'\n reservations:\n memory: 64M\n cpus: '0.1'\n networks:\n - app-network\n depends_on: # \u636e\u8bf4 swarm \u6a21\u5f0f\u4e0b\u65e0\u6548\n - backend\n\nnetworks:\n app-network:\n driver: overlay\n attachable: true\n\n\n# =============================================================================\n# \u90e8\u7f72\u6267\u884c\u6b65\u9aa4\uff08\u8bf7\u6309\u987a\u5e8f\u64cd\u4f5c\uff09\n# =============================================================================\n\n# 1\ufe0f\u3002\u3010\u524d\u63d0\u3011\u786e\u4fdd\u5df2\u5728\u76ee\u6807\u673a\u5668\u4e0a\u5b89\u88c5 Docker Engine \u5e76\u542f\u7528 Swarm \u6a21\u5f0f\n# - \u5b89\u88c5 Docker\uff1ahttps://docs.docker.com/engine/install/\n# - \u521d\u59cb\u5316 Swarm\uff08\u4ec5\u9996\u6b21\u6267\u884c\u4e00\u6b21\uff09\uff1a\n# docker swarm init --advertise-addr \u672c\u673aIP\n# - \u5176\u4ed6\u8282\u70b9\u82e5\u52a0\u5165 Swarm \u96c6\u7fa4\uff0c\u53ef\u901a\u8fc7\u4ee5\u4e0b\u547d\u4ee4\u83b7\u53d6\u52a0\u5165\u547d\u4ee4\uff1a\n# - \u83b7\u53d6 manager \u52a0\u5165\u547d\u4ee4\uff1adocker swarm join-token manager\n# - \u83b7\u53d6 worker \u52a0\u5165\u547d\u4ee4\uff1adocker swarm join-token worker\n\n# 2\ufe0f\u3002\u3010\u51c6\u5907\u3011\u5728\u90e8\u7f72\u76ee\u5f55\u4e0b\u521b\u5efa\u5fc5\u8981\u672c\u5730\u76ee\u5f55\uff08\u4e0e volumes \u5bf9\u5e94\uff09\n# mkdir -p ./config ./logs\n# - \u5c06\u9879\u76ee\u914d\u7f6e\u6587\u4ef6\u653e\u5165 ./config/\n# - \u786e\u4fdd ./config/nginx.conf \u5b58\u5728\uff08\u5185\u5bb9\u53c2\u8003\u6807\u51c6 Nginx \u53cd\u5411\u4ee3\u7406\u914d\u7f6e\uff09\n\n# 3\ufe0f\u3002\u3010\u955c\u50cf\u51c6\u5907\u3011\u786e\u4fdd\u6240\u6709\u8282\u70b9\u80fd\u62c9\u53d6\u6240\u9700\u955c\u50cf\n# - \u65b9\u5f0f A\uff08\u63a8\u8350\uff09\uff1a\u5c06\u955c\u50cf\u63a8\u9001\u5230\u79c1\u6709/\u516c\u5171\u955c\u50cf\u4ed3\u5e93\uff08\u5982 Harbor\u3001Docker Hub\uff09\n# docker tag fastapi-scaff:v1.0.0 your-registry/fastapi-scaff:v1.0.0\n# docker push your-registry/fastapi-scaff:v1.0.0\n# \uff08\u7136\u540e\u5c06 compose \u6587\u4ef6\u4e2d\u7684 image \u6539\u4e3a\u5b8c\u6574\u8def\u5f84\uff09\n# - \u65b9\u5f0f B\uff08\u5355\u8282\u70b9\u6d4b\u8bd5\uff09\uff1a\u5728\u90e8\u7f72\u8282\u70b9\u672c\u5730\u6784\u5efa\u5e76\u52a0\u8f7d\u955c\u50cf\n# \uff08\u4e5f\u53ef\u4f7f\u7528\u811a\u672c build.sh \u7f16\u8bd1\uff09\n# docker build -t fastapi-scaff:v1.0.0 .\n\n# 4\ufe0f\u3002\u3010\u90e8\u7f72\u3011\u4f7f\u7528 docker stack deploy \u542f\u52a8\u670d\u52a1\u6808\n# - \u542f\u52a8 docker-compose.swarm.yaml\n# docker stack deploy -c docker-compose.swarm.yaml fastapi-scaff-prod\n# - \u6808\u540d \"fastapi-scaff-prod\" \u53ef\u81ea\u5b9a\u4e49\uff0c\u670d\u52a1\u540d\u5c06\u53d8\u4e3a fastapi-scaff-prod_backend \u548c fastapi-scaff-prod_nginx\n# - \u786e\u4fdd nginx.conf \u914d\u7f6e\u6b63\u786e\uff1a\n# upstream backend_upstream {\n# server fastapi-scaff-prod_backend:8000;\n# keepalive 32;\n# }\n\n# 5\ufe0f\u3002\u3010\u9a8c\u8bc1\u3011\u68c0\u67e5\u670d\u52a1\u72b6\u6001\n# - \u67e5\u770b\u670d\u52a1\u5217\u8868\uff1a\n# docker service ls\n# - \u67e5\u770b backend \u526f\u672c\u8fd0\u884c\u60c5\u51b5\uff1a\n# docker service ps fastapi-scaff-prod_backend\n# - \u67e5\u770b\u65e5\u5fd7\uff08\u4f8b\u5982\u67e5\u770b\u4e00\u4e2a backend \u5b9e\u4f8b\uff09\uff1a\n# docker service logs fastapi-scaff-prod_backend --follow\n\n# 6\u3002\u3010\u8bbf\u95ee\u3011\u6d4b\u8bd5 API \u662f\u5426\u53ef\u7528\n# - \u5728\u90e8\u7f72\u8282\u70b9\uff08\u6216\u4efb\u610f Swarm \u8282\u70b9\uff0c\u82e5 ports \u4f7f\u7528 ingress \u6a21\u5f0f\uff09\u8bbf\u95ee\uff1a\n# curl http://localhost:8000/api/ping\n# - \u5e94\u8fd4\u56de\u63a5\u53e3\u5b9a\u4e49\u7684\u54cd\u5e94\n\n# 7\ufe0f\u3002\u3010\u66f4\u65b0\u3011\u6eda\u52a8\u5347\u7ea7\u5e94\u7528\uff08\u4f8b\u5982\u5347\u7ea7\u5230 v1.0.1\uff09\n# - \u4fee\u6539 image \u4e3a fastapi-scaff:v1.0.1\n# - \u91cd\u65b0\u6267\u884c\u90e8\u7f72\u547d\u4ee4\uff08Swarm \u81ea\u52a8\u6267\u884c\u6eda\u52a8\u66f4\u65b0\uff09\uff1a\n# docker stack deploy -c docker-compose.swarm.yaml fastapi-scaff-prod\n# - \u89c2\u5bdf\u66f4\u65b0\u8fc7\u7a0b\uff1a\n# watch docker service ps fastapi-scaff-prod_backend\n\n# 8\ufe0f\u3002\u3010\u6e05\u7406\u3011\uff08\u53ef\u9009\uff09\u5220\u9664\u6574\u4e2a\u5e94\u7528\u6808\n# docker stack rm fastapi-scaff-prod\n\n# 9\u3002\u3010\u53c2\u8003\u3011\u5b98\u65b9\u6587\u6863\uff1ahttps://docs.docker.com/manuals/\n\n# 10\u3002\u3010\u6ce8\u610f\u3011\u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\n",
7
+ "docker-compose.yaml": "services:\n\n backend:\n# build:\n# context: .\n# dockerfile: Dockerfile\n image: fastapi-scaff:v1.0.0 # \u5f3a\u70c8\u5efa\u8bae\u4e0d\u8981\u7528 :latest\n restart: unless-stopped\n environment:\n app_env: prod\n volumes:\n - /data/fastapi-scaff/logs:/backend/logs\n ports:\n - \"8000:8000\"\n command: \"uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 3 --log-level info\"\n",
8
+ "Dockerfile": "FROM python:3.12-bullseye\n\nENV PYTHONUNBUFFERED=1 \\\n PYTHONPATH=/backend \\\n TZ=Asia/Shanghai \\\n app_env=prod\n\nRUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone\n\nWORKDIR /backend\n\nCOPY requirements.txt .\nRUN pip install --upgrade pip && \\\n pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/ && \\\n rm requirements.txt\n\nCOPY config ./config\nCOPY app ./app\n\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\", \"--workers\", \"5\", \"--log-level\", \"info\"]\n",
9
+ "Dockerfile-slim": "FROM python:3.12-slim\n\nENV PYTHONUNBUFFERED=1 \\\n PYTHONPATH=/backend \\\n TZ=Asia/Shanghai \\\n app_env=prod\n\nRUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone\n\nWORKDIR /backend\n\n# \u5b89\u88c5\u4f9d\u8d56\u7f16\u8bd1\u6240\u9700\u7684\u57fa\u7840\u5de5\u5177\uff08\u5982\u9700\u8981\u7f16\u8bd1 C \u6269\u5c55\uff09\n# \u5982\u679c requirements.txt \u4e2d\u5168\u662f\u7eaf Python \u5305\uff08\u5982 fastapi, uvicorn\uff09\uff0c\u53ef\u7701\u7565\n# RUN apt-get update && apt-get install -y --no-install-recommends gcc musl-dev && rm -rf /var/lib/apt/lists/*\n\nCOPY requirements.txt .\nRUN pip install --upgrade pip && \\\n pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/ && \\\n rm requirements.txt\n\nCOPY config ./config\nCOPY app ./app\n\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\", \"--workers\", \"5\", \"--log-level\", \"info\"]\n",
3
10
  "LICENSE": "Copyright (c) 2024 axiner\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
4
- "README.md": "# fastapi-scaff\n\n## What is this?\n\n- by: axiner\n- fastapi-scaff\n- This is a fastapi scaff.\n - new project\n - add api\n - about project:\n - auto init project (conf, db, log...)\n - auto register router\n - auto register middleware\n - ...\n - more documents: [\u8bf7\u70b9\u51fb\u94fe\u63a5](https://blog.csdn.net/atpuxiner/article/details/144291336?fromshare=blogdetail&sharetype=blogdetail&sharerId=144291336&sharerefer=PC&sharesource=atpuxiner&sharefrom=from_link)\n\n## Project structure\n\n- ASM: ASM\u6a21\u5f0f\n - A api\n - S service(&schema)\n - M model\n- \u8c03\u7528\u8fc7\u7a0b: main.py(initializer) -> (middleware) - api - service(&schema) - (model)\n- \u7ed3\u6784\u5982\u4e0b: (\u547d\u540d\u7ecf\u8fc7\u591a\u6b21\u4fee\u6539\u6572\u5b9a\uff0c\u7b80\u6d01\u6613\u61c2)\n ```\n \u2514\u2500\u2500 fastapi-scaff\n \u251c\u2500\u2500 app (\u5e94\u7528)\n \u2502 \u251c\u2500\u2500 api \u251c\u2500\u2500 (api)\n \u2502 \u2502 \u2514\u2500\u2500 v1 \u2502 \u2514\u2500\u2500 (v1)\n \u2502 \u251c\u2500\u2500 initializer \u251c\u2500\u2500 (\u521d\u59cb\u5316)\n \u2502 \u2502 \u251c\u2500\u2500 conf \u2502 \u251c\u2500\u2500 (\u914d\u7f6e)\n \u2502 \u2502 \u251c\u2500\u2500 db \u2502 \u251c\u2500\u2500 (\u6570\u636e\u5e93)\n \u2502 \u2502 \u251c\u2500\u2500 log \u2502 \u251c\u2500\u2500 (\u65e5\u5fd7)\n \u2502 \u2502 \u2514\u2500\u2500 ... \u2502 \u2514\u2500\u2500 (...)\n \u2502 \u251c\u2500\u2500 middleware \u251c\u2500\u2500 (\u4e2d\u95f4\u4ef6)\n \u2502 \u251c\u2500\u2500 model \u251c\u2500\u2500 (\u6570\u636e\u6a21\u578b)\n \u2502 \u251c\u2500\u2500 schema \u251c\u2500\u2500 (\u6570\u636e\u7ed3\u6784)\n \u2502 \u251c\u2500\u2500 service \u251c\u2500\u2500 (\u4e1a\u52a1\u903b\u8f91)\n \u2502 \u251c\u2500\u2500 utils \u251c\u2500\u2500 (utils)\n \u2502 \u2514\u2500\u2500 main.py \u2514\u2500\u2500 (main.py)\n \u251c\u2500\u2500 config (\u914d\u7f6e\u76ee\u5f55)\n \u251c\u2500\u2500 deploy (\u90e8\u7f72\u76ee\u5f55)\n \u251c\u2500\u2500 docs (\u6587\u6863\u76ee\u5f55)\n \u251c\u2500\u2500 logs (\u65e5\u5fd7\u76ee\u5f55)\n \u251c\u2500\u2500 tests (\u6d4b\u8bd5\u76ee\u5f55)\n \u251c\u2500\u2500 .gitignore\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 README.md\n \u251c\u2500\u2500 requirements.txt\n \u2514\u2500\u2500 runserver.py\n ```\n\n## Installation\n\nThis package can be installed using pip (Python>=3.11):\n> pip install fastapi-scaff\n\n## Scaff usage\n\n- 1\uff09help document\n - `fastapi-scaff -h`\n- 2\uff09new project\n - `fastapi-scaff new <myproj>`\n- 3\uff09add api\n - `cd to project root dir`\n - `fastapi-scaff add <myapi>`\n\n## Project run\n\n- 1\uff09cd to project root dir\n- 2\uff09modify the configuration, such as for the database\n- 3\uff09`pip install -r requirements.txt`\n- 4\uff09`python runserver.py`\n - more parameters see:\n - about uvicorn: [click here](https://www.uvicorn.org/)\n - about gunicorn: [click here](https://docs.gunicorn.org/en/stable/)\n\n## License\n\nThis project is released under the MIT License (MIT). See [LICENSE](LICENSE)\n",
5
- "requirements.txt": "# -*- coding: utf-8 -*-\n# Python>=3.11\nfastapi==0.116.1\nuvicorn==0.35.0\norjson==3.11.1\ntoollib==1.7.7\npython-dotenv==1.1.1\nPyYAML==6.0.2\nloguru==0.7.3\nSQLAlchemy==2.0.42\naiosqlite==0.21.0\nredis==6.4.0\nPyJWT==2.10.1\nbcrypt==4.3.0\n",
6
- "runserver.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2024/07/29 22:22\n@abstract runserver\uff08\u66f4\u591a\u53c2\u6570\u8bf7\u81ea\u884c\u6307\u5b9a\uff09\n@description\n@history\n\"\"\"\nimport argparse\nimport subprocess\nimport sys\n\nimport uvicorn\n\n\ndef run_by_unicorn(\n host: str,\n port: int,\n workers: int,\n log_level: str,\n is_reload: bool,\n):\n log_config = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"formatters\": {\n \"default\": {\n \"()\": \"uvicorn.logging.DefaultFormatter\",\n \"fmt\": \"%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s\",\n \"use_colors\": None\n },\n \"access\": {\n \"()\": \"uvicorn.logging.AccessFormatter\",\n \"fmt\": \"%(asctime)s %(levelname)s %(client_addr)s - \\\"%(request_line)s\\\" %(status_code)s\"\n }\n },\n \"handlers\": {\n \"default\": {\n \"formatter\": \"default\",\n \"class\": \"logging.StreamHandler\",\n \"stream\": \"ext://sys.stderr\"\n },\n \"access\": {\n \"formatter\": \"access\",\n \"class\": \"logging.StreamHandler\",\n \"stream\": \"ext://sys.stdout\"\n }\n },\n \"loggers\": {\n \"uvicorn\": {\n \"handlers\": [\n \"default\"\n ],\n \"level\": \"INFO\",\n \"propagate\": False\n },\n \"uvicorn.error\": {\n \"level\": \"INFO\"\n },\n \"uvicorn.access\": {\n \"handlers\": [\n \"access\"\n ],\n \"level\": \"INFO\",\n \"propagate\": False\n }\n }\n }\n uvicorn.run(\n app=\"app.main:app\",\n host=host,\n port=port,\n workers=workers,\n log_level=log_level,\n log_config=log_config,\n reload=is_reload,\n )\n\n\ndef run_by_gunicorn(\n host: str,\n port: int,\n workers: int,\n log_level: str,\n is_reload: bool,\n):\n cmd = (\n \"gunicorn app.main:app \"\n \"--worker-class=uvicorn.workers.UvicornWorker \"\n \"--bind={host}:{port} \"\n \"--workers={workers} \"\n \"--log-level={log_level} \"\n \"--access-logfile=- \"\n \"--error-logfile=- \"\n .format(\n host=host,\n port=port,\n workers=workers,\n log_level=log_level,\n )\n )\n if is_reload:\n cmd += f\" --reload\"\n subprocess.run(cmd, shell=True)\n\n\ndef main(\n host: str,\n port: int,\n workers: int,\n log_level: str,\n is_reload: bool,\n is_gunicorn: bool,\n):\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--host\", type=str, metavar=\"\", help=\"host\")\n parser.add_argument(\"--port\", type=int, metavar=\"\", help=\"port\")\n parser.add_argument(\"--workers\", type=int, metavar=\"\", help=\"\u8fdb\u7a0b\u6570\")\n parser.add_argument(\"--log-level\", type=str, metavar=\"\", help=\"\u65e5\u5fd7\u7b49\u7ea7\")\n parser.add_argument(\"--is-reload\", action=\"store_true\", help=\"\u662f\u5426reload\")\n parser.add_argument(\"--is-gunicorn\", action=\"store_true\", help=\"\u662f\u5426gunicorn\")\n args = parser.parse_args()\n kwargs = {\n \"host\": args.host or host,\n \"port\": args.port or port,\n \"workers\": args.workers or workers,\n \"log_level\": args.log_level or log_level,\n \"is_reload\": args.is_reload or is_reload,\n }\n if (args.is_gunicorn or is_gunicorn) and not sys.platform.lower().startswith(\"win\"):\n try:\n import gunicorn # noqa\n except ImportError:\n sys.stderr.write(\"gunicorn\u672a\u627e\u5230\uff0c\u6b63\u5728\u5c1d\u8bd5\u81ea\u52a8\u5b89\u88c5...\\n\")\n try:\n subprocess.run(\n [\"pip\", \"install\", \"gunicorn\"],\n check=True,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE)\n sys.stderr.write(\"gunicorn\u5b89\u88c5\u6210\u529f\\n\")\n except subprocess.CalledProcessError as e:\n sys.stderr.write(f\"gunicorn\u5b89\u88c5\u5931\u8d25: {e.stderr.decode().strip()}\\n\")\n raise\n run_by_gunicorn(**kwargs)\n else:\n run_by_unicorn(**kwargs)\n\n\nif __name__ == '__main__':\n main(\n host=\"0.0.0.0\",\n port=8000,\n workers=3,\n log_level=\"debug\",\n is_reload=False, # \u9002\u7528\u4e8edev\n is_gunicorn=False, # \u4e0d\u652f\u6301win\n )\n",
7
- "app/main.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2024/07/29 22:22\n@abstract main\n@description\n@history\n\"\"\"\nfrom contextlib import asynccontextmanager\nfrom fastapi import FastAPI\nfrom fastapi.responses import ORJSONResponse\n\nfrom app import (\n api,\n middleware,\n)\nfrom app.initializer import g\n\ng.setup()\n# #\nopenapi_url = \"/openapi.json\"\ndocs_url = \"/docs\"\nredoc_url = \"/redoc\"\nif g.config.app_disable_docs is True:\n openapi_url, docs_url, redoc_url = None, None, None\n\n\n@asynccontextmanager\nasync def lifespan(app_: FastAPI):\n g.logger.info(f\"Application using config file '{g.config.yaml_name}'\")\n g.logger.info(f\"Application title '{g.config.app_title}'\")\n g.logger.info(f\"Application version '{g.config.app_version}'\")\n # #\n g.logger.info(\"Application server running\")\n yield\n g.logger.info(\"Application server shutdown\")\n\n\napp = FastAPI(\n title=g.config.app_title,\n summary=g.config.app_summary,\n description=g.config.app_description,\n version=g.config.app_version,\n debug=g.config.app_debug,\n openapi_url=openapi_url,\n docs_url=docs_url,\n redoc_url=redoc_url,\n lifespan=lifespan,\n default_response_class=ORJSONResponse,\n)\n# #\napi.register_routers(app)\nmiddleware.register_middlewares(app)\n",
11
+ "README.md": "# fastapi-scaff\n\n## What is this?\n\n- by: axiner\n- fastapi-scaff\n- This is a fastapi scaff.\n - new project\n - add api\n - about project:\n - auto init project (conf, db, log...)\n - auto register router\n - auto register middleware\n - ...\n - integrated sqlalchemy\n - integrated jwt\\api-key\n - integrated celery\n - integrated docker deployment\n - ...\n - about structure:\n - standard\n - light\n - tiny\n - single\n - more documents: [\u8bf7\u70b9\u51fb\u94fe\u63a5](https://blog.csdn.net/atpuxiner/article/details/144291336?fromshare=blogdetail&sharetype=blogdetail&sharerId=144291336&sharerefer=PC&sharesource=atpuxiner&sharefrom=from_link)\n\n## Project structure\n\n- ASM: ASM mode\n - A api\n - S services(&schemas)\n - M models\n- Calling process: main.py(initializer) -> (middleware) - api - services(&schemas) - (models)\n- Structure: (The naming has been finalized after multiple revisions, making it concise and easy to understand)\n ```\n \u2514\u2500\u2500 fastapi-scaff\n \u251c\u2500\u2500 app (\u5e94\u7528)\n \u2502 \u251c\u2500\u2500 api \u251c\u2500\u2500 (api)\n \u2502 \u2502 \u2514\u2500\u2500 v1 \u2502 \u2514\u2500\u2500 (v1)\n \u2502 \u251c\u2500\u2500 initializer \u251c\u2500\u2500 (\u521d\u59cb\u5316)\n \u2502 \u2502 \u251c\u2500\u2500 conf \u2502 \u251c\u2500\u2500 (\u914d\u7f6e)\n \u2502 \u2502 \u251c\u2500\u2500 db \u2502 \u251c\u2500\u2500 (\u6570\u636e\u5e93)\n \u2502 \u2502 \u251c\u2500\u2500 log \u2502 \u251c\u2500\u2500 (\u65e5\u5fd7)\n \u2502 \u2502 \u2514\u2500\u2500 ... \u2502 \u2514\u2500\u2500 (...)\n \u2502 \u251c\u2500\u2500 middleware \u251c\u2500\u2500 (\u4e2d\u95f4\u4ef6)\n \u2502 \u251c\u2500\u2500 models \u251c\u2500\u2500 (\u6570\u636e\u6a21\u578b)\n \u2502 \u251c\u2500\u2500 schemas \u251c\u2500\u2500 (\u6570\u636e\u7ed3\u6784)\n \u2502 \u251c\u2500\u2500 services \u251c\u2500\u2500 (\u4e1a\u52a1\u903b\u8f91)\n \u2502 \u251c\u2500\u2500 utils \u251c\u2500\u2500 (utils)\n \u2502 \u2514\u2500\u2500 main.py \u2514\u2500\u2500 (main.py)\n \u251c\u2500\u2500 app_celery (\u5e94\u7528-celery)\n \u251c\u2500\u2500 config (\u914d\u7f6e\u76ee\u5f55)\n \u251c\u2500\u2500 docs (\u6587\u6863\u76ee\u5f55)\n \u251c\u2500\u2500 logs (\u65e5\u5fd7\u76ee\u5f55)\n \u251c\u2500\u2500 tests (\u6d4b\u8bd5\u76ee\u5f55)\n \u251c\u2500\u2500 .dockerignore\n \u251c\u2500\u2500 .gitignore\n \u251c\u2500\u2500 .python-version\n \u251c\u2500\u2500 build.sh\n \u251c\u2500\u2500 docker-compose.yaml\n \u251c\u2500\u2500 Dockerfile\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 README.md\n \u251c\u2500\u2500 requirements.txt\n \u2514\u2500\u2500 runserver.py\n ```\n\n- \u3010Other structure\u3011\n - light\uff1aPlease create and view (with `-e light`)\n - tiny\uff1aPlease create and view (with `-e tiny`)\n - single\uff1aPlease create and view (with `-e single`)\n\n## Installation\n\nThis package can be installed using pip (Python>=3.11):\n> pip install fastapi-scaff\n\n## Scaff usage\n\n- 1\uff09help document\n - `fastapi-scaff -h`\n- 2\uff09new project\n - `fastapi-scaff new <myproj>`\n - *light structure*: `fastapi-scaff new <myproj> -e light`\n - *tiny structure*: `fastapi-scaff new <myproj> -e tiny`\n - *single structure*: `fastapi-scaff new <myproj> -e single`\n- 3\uff09add api\n - `cd to project root dir`\n - `fastapi-scaff add <myapi>`\n- 4\uff09integrated celery\n - M1\u3002`new` with `--celery`: `fastapi-scaff new <myproj> --celery`\n - M2\u3002`add` with `--celery`: `fastapi-scaff add <mycelery> --celery`\n\n## Project run\n\n- 1\uff09cd to project root dir\n- 2\uff09modify the configuration, such as for the database\n- 3\uff09`pip install -r requirements.txt`\n- 4\uff09`python runserver.py`\n - more parameters see:\n - about uvicorn: [click here](https://www.uvicorn.org/)\n - about gunicorn: [click here](https://docs.gunicorn.org/en/stable/)\n- 5\uff09docker, please see:\n - project files:\n - build.sh\n - docker-compose.yaml | docker-compose.swarm.yaml[config/nginx.conf]\n - Dockerfile | Dockerfile.slim\n - [more click here](https://docs.docker.com/)\n\n## License\n\nThis project is released under the MIT License (MIT). See [LICENSE](LICENSE)\n",
12
+ "requirements.txt": "# -*- coding: utf-8 -*-\n# Python>=3.11\nfastapi==0.121.0\nuvicorn==0.38.0\norjson==3.11.4\ntoollib==1.9.1\npython-dotenv==1.2.1\nPyYAML==6.0.3\nloguru==0.7.3\nPyJWT==2.10.1\nbcrypt==5.0.0\nSQLAlchemy==2.0.44\naiosqlite==0.21.0\nredis==7.0.1\ncelery==5.5.3\n",
13
+ "runcbeat.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2025/09/20 10:10\n@abstract runcbeat\uff08\u66f4\u591a\u53c2\u6570\u8bf7\u81ea\u884c\u6307\u5b9a\uff09\n@description\n@history\n\"\"\"\nimport argparse\nimport subprocess\n\n\ndef main(\n loglevel: str = \"info\",\n scheduler: str = None,\n pidfile: str = None,\n max_interval: int = 5,\n celery_module: str = \"app_celery\",\n):\n parser = argparse.ArgumentParser(description=\"CeleryBeat\u542f\u52a8\u5668\")\n parser.add_argument(\"-l\", \"--loglevel\", type=str, default=\"info\", metavar=\"\", help=\"\u65e5\u5fd7\u7b49\u7ea7\")\n parser.add_argument(\"-S\", \"--scheduler\", type=str, default=None, metavar=\"\", help=\"\u8c03\u5ea6\u5668\u7c7b\u578b\")\n parser.add_argument(\"--pidfile\", type=str, default=None, metavar=\"\", help=\"pid\u6587\u4ef6\")\n parser.add_argument(\"--max-interval\", type=int, default=5, metavar=\"\", help=\"\u68c0\u6d4b\u4efb\u52a1\u95f4\u9694\")\n parser.add_argument(\"--celery-module\", type=str, default=\"app_celery\", metavar=\"\", help=\"celery\u6a21\u5757\")\n args = parser.parse_args()\n loglevel = args.loglevel or loglevel\n scheduler = args.scheduler or scheduler\n pidfile = args.pidfile or pidfile\n max_interval = args.max_interval or max_interval\n celery_module = args.celery_module or celery_module\n command = [\n \"celery\",\n \"-A\",\n f\"{celery_module}.consumer\",\n \"beat\",\n f\"--loglevel={loglevel}\",\n f\"--max-interval={max_interval}\",\n ]\n if scheduler:\n command.extend([\"--scheduler\", scheduler])\n if pidfile:\n command.extend([\"--pidfile\", pidfile])\n subprocess.run(command, check=True)\n\n\nif __name__ == '__main__':\n main()\n",
14
+ "runcworker.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2025/09/20 10:10\n@abstract runcworker\uff08\u66f4\u591a\u53c2\u6570\u8bf7\u81ea\u884c\u6307\u5b9a\uff09\n@description\n@history\n\"\"\"\nimport argparse\nimport platform\nimport subprocess\nfrom os import cpu_count\n\n\ndef main(\n name: str, # `<celery_module>/consumer/workers`\u4e0b\u7684\u6a21\u5757\u540d\n loglevel: str = \"info\",\n concurrency: int = None,\n pool: str = None,\n celery_module: str = \"app_celery\",\n):\n parser = argparse.ArgumentParser(description=\"CeleryWorker\u542f\u52a8\u5668\")\n parser.add_argument(\"-n\", \"--name\", type=str, metavar=\"\", help=\"\u540d\u79f0\")\n parser.add_argument(\"-l\", \"--loglevel\", type=str, default=\"info\", metavar=\"\", help=\"\u65e5\u5fd7\u7b49\u7ea7\")\n parser.add_argument(\"-c\", \"--concurrency\", type=int, default=None, metavar=\"\", help=\"\u5e76\u53d1\u6570\")\n parser.add_argument(\"-P\", \"--pool\", type=str, default=None, metavar=\"\", help=\"\u5e76\u53d1\u6a21\u578b\")\n parser.add_argument(\"--celery-module\", type=str, default=\"app_celery\", metavar=\"\", help=\"celery\u6a21\u5757\")\n args = parser.parse_args()\n name = args.name or name\n loglevel = args.loglevel or loglevel\n concurrency = args.concurrency or concurrency\n pool = args.pool or pool\n celery_module = args.celery_module or celery_module\n if pool is None:\n if platform.system().lower().startswith(\"win\"):\n pool = 'gevent'\n if not concurrency:\n concurrency = 100\n else:\n pool = 'prefork'\n if not concurrency:\n concurrency = cpu_count()\n command = [\n \"celery\",\n \"-A\",\n f\"{celery_module}.consumer.workers.{name}\",\n \"worker\",\n f\"--loglevel={loglevel}\",\n f\"--concurrency={concurrency}\",\n f\"--pool={pool}\",\n ]\n subprocess.run(\n command,\n check=True,\n )\n\n\nif __name__ == '__main__':\n main(\n name=\"ping\",\n )\n",
15
+ "runserver.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2024/07/29 22:22\n@abstract runserver\uff08\u66f4\u591a\u53c2\u6570\u8bf7\u81ea\u884c\u6307\u5b9a\uff09\n@description\n@history\n\"\"\"\nimport argparse\nimport subprocess\nimport sys\n\nimport uvicorn\n\n\ndef run_by_unicorn(\n host: str,\n port: int,\n workers: int,\n log_level: str,\n is_reload: bool,\n):\n log_config = {\n \"version\": 1,\n \"disable_existing_loggers\": False,\n \"formatters\": {\n \"default\": {\n \"()\": \"uvicorn.logging.DefaultFormatter\",\n \"fmt\": \"%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s\",\n \"use_colors\": None\n },\n \"access\": {\n \"()\": \"uvicorn.logging.AccessFormatter\",\n \"fmt\": \"%(asctime)s %(levelname)s %(client_addr)s - \\\"%(request_line)s\\\" %(status_code)s\"\n }\n },\n \"handlers\": {\n \"default\": {\n \"formatter\": \"default\",\n \"class\": \"logging.StreamHandler\",\n \"stream\": \"ext://sys.stderr\"\n },\n \"access\": {\n \"formatter\": \"access\",\n \"class\": \"logging.StreamHandler\",\n \"stream\": \"ext://sys.stdout\"\n }\n },\n \"loggers\": {\n \"uvicorn\": {\n \"handlers\": [\n \"default\"\n ],\n \"level\": \"INFO\",\n \"propagate\": False\n },\n \"uvicorn.error\": {\n \"level\": \"INFO\"\n },\n \"uvicorn.access\": {\n \"handlers\": [\n \"access\"\n ],\n \"level\": \"INFO\",\n \"propagate\": False\n }\n }\n }\n uvicorn.run(\n app=\"app.main:app\",\n host=host,\n port=port,\n workers=workers,\n log_level=log_level,\n log_config=log_config,\n reload=is_reload,\n )\n\n\ndef run_by_gunicorn(\n host: str,\n port: int,\n workers: int,\n log_level: str,\n is_reload: bool,\n):\n cmd = (\n \"gunicorn app.main:app \"\n \"--worker-class=uvicorn.workers.UvicornWorker \"\n \"--bind={host}:{port} \"\n \"--workers={workers} \"\n \"--log-level={log_level} \"\n \"--access-logfile=- \"\n \"--error-logfile=- \"\n .format(\n host=host,\n port=port,\n workers=workers,\n log_level=log_level,\n )\n )\n if is_reload:\n cmd += f\" --reload\"\n subprocess.run(cmd, shell=True)\n\n\ndef main(\n host: str,\n port: int,\n workers: int,\n log_level: str,\n is_reload: bool,\n is_gunicorn: bool,\n):\n parser = argparse.ArgumentParser(description=\"App\u542f\u52a8\u5668\")\n parser.add_argument(\"--host\", type=str, metavar=\"\", help=\"host\")\n parser.add_argument(\"--port\", type=int, metavar=\"\", help=\"port\")\n parser.add_argument(\"--workers\", type=int, metavar=\"\", help=\"\u8fdb\u7a0b\u6570\")\n parser.add_argument(\"--log-level\", type=str, metavar=\"\", help=\"\u65e5\u5fd7\u7b49\u7ea7\")\n parser.add_argument(\"--is-reload\", action=\"store_true\", help=\"\u662f\u5426reload\")\n parser.add_argument(\"--is-gunicorn\", action=\"store_true\", help=\"\u662f\u5426gunicorn\")\n args = parser.parse_args()\n kwargs = {\n \"host\": args.host or host,\n \"port\": args.port or port,\n \"workers\": args.workers or workers,\n \"log_level\": args.log_level or log_level,\n \"is_reload\": args.is_reload or is_reload,\n }\n if (args.is_gunicorn or is_gunicorn) and not sys.platform.lower().startswith(\"win\"):\n try:\n import gunicorn # noqa\n except ImportError:\n sys.stderr.write(\"gunicorn\u672a\u627e\u5230\uff0c\u6b63\u5728\u5c1d\u8bd5\u81ea\u52a8\u5b89\u88c5...\\n\")\n try:\n subprocess.run(\n [\"pip\", \"install\", \"gunicorn\"],\n check=True,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE)\n sys.stderr.write(\"gunicorn\u5b89\u88c5\u6210\u529f\\n\")\n except subprocess.CalledProcessError as e:\n sys.stderr.write(f\"gunicorn\u5b89\u88c5\u5931\u8d25: {e.stderr.decode().strip()}\\n\")\n raise\n run_by_gunicorn(**kwargs)\n else:\n run_by_unicorn(**kwargs)\n\n\nif __name__ == '__main__':\n main(\n host=\"0.0.0.0\",\n port=8000,\n workers=3,\n log_level=\"debug\",\n is_reload=False, # For development environment\n is_gunicorn=False, # Not supported on Windows\n )\n",
16
+ "app/main.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2024/07/29 22:22\n@abstract main\n@description\n@history\n\"\"\"\nfrom contextlib import asynccontextmanager\nfrom fastapi import FastAPI\nfrom fastapi.responses import ORJSONResponse\nfrom loguru import logger\n\nfrom app import (\n api,\n middleware,\n)\nfrom app.initializer import g\n\ng.setup()\n# #\nopenapi_url = \"/openapi.json\"\ndocs_url = \"/docs\"\nredoc_url = \"/redoc\"\nif g.config.app_disable_docs is True:\n openapi_url, docs_url, redoc_url = None, None, None\n\n\n@asynccontextmanager\nasync def lifespan(xapp: FastAPI):\n logger.info(f\"Application env '{g.config.app_env}'\")\n logger.info(f\"Application yaml '{g.config.app_yaml.name}'\")\n logger.info(f\"Application title '{g.config.app_title}'\")\n logger.info(f\"Application version '{g.config.app_version}'\")\n # #\n logger.info(\"Application server running\")\n yield\n logger.info(\"Application server shutdown\")\n\n\napp = FastAPI(\n title=g.config.app_title,\n summary=g.config.app_summary,\n description=g.config.app_description,\n version=g.config.app_version,\n debug=g.config.app_debug,\n openapi_url=openapi_url,\n docs_url=docs_url,\n redoc_url=redoc_url,\n lifespan=lifespan,\n default_response_class=ORJSONResponse,\n)\n# #\nmiddleware.register_middlewares(app)\napi.register_routers(app)\n",
8
17
  "app/__init__.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2024/07/29 22:22\n@abstract app\n@description\n@history\n\"\"\"\nfrom pathlib import Path\n\nAPP_DIR = Path(__file__).absolute().parent\n",
9
- "app/api/exception.py": "from typing import Any\n\nfrom app.api.status import Status\n\n\nclass CustomException(Exception):\n\n def __init__(\n self,\n msg: str = None,\n code: int = None,\n data: Any = None,\n status: Status = Status.FAILURE,\n ):\n self.msg = msg or status.msg\n self.code = code or status.code\n self.data = data\n self.status = status\n\n def __str__(self) -> str:\n return f\"{self.code} {self.msg}\"\n\n def __repr__(self) -> str:\n return f\"<{self.__class__.__name__}: ({self.code!r}, {self.msg!r})>\"\n",
10
- "app/api/response.py": "import json\nfrom typing import Mapping, get_type_hints, Any\n\nfrom fastapi.encoders import jsonable_encoder\nfrom starlette.background import BackgroundTask\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse, StreamingResponse, ContentStream\nfrom toollib.utils import map_jsontype\n\nfrom app.api.status import Status\n\n\nclass Response:\n\n @staticmethod\n def success(\n data: dict | list | str | None = None,\n msg: str = None,\n code: int = None,\n status: Status = Status.SUCCESS,\n is_encode_data: bool = False,\n request: Request = None,\n status_code: int = 200,\n headers: Mapping[str, str] | None = None,\n media_type: str | None = None,\n background: BackgroundTask | None = None,\n ) -> JSONResponse:\n content = {\n \"msg\": msg or status.msg,\n \"code\": code or status.code,\n \"data\": Response.encode_data(data) if is_encode_data else data,\n }\n if request:\n if request_id := getattr(request.state, 'request_id', None):\n content[\"request_id\"] = request_id\n return JSONResponse(\n content=content,\n status_code=status_code,\n headers=headers,\n media_type=media_type,\n background=background,\n )\n\n @staticmethod\n def failure(\n msg: str = None,\n code: int = None,\n error: str | Exception | None = None,\n data: dict | list | str | None = None,\n status: Status = Status.FAILURE,\n is_encode_data: bool = False,\n request: Request = None,\n status_code: int = 200,\n headers: Mapping[str, str] | None = None,\n media_type: str | None = None,\n background: BackgroundTask | None = None,\n ) -> JSONResponse:\n content = {\n \"msg\": msg or status.msg,\n \"code\": code or status.code,\n \"error\": str(error) if error else None,\n \"data\": Response.encode_data(data) if is_encode_data else data,\n }\n if request:\n if request_id := getattr(request.state, 'request_id', None):\n content[\"request_id\"] = request_id\n return JSONResponse(\n content=content,\n status_code=status_code,\n headers=headers,\n media_type=media_type,\n background=background,\n )\n\n @staticmethod\n def encode_data(data: Any) -> Any:\n if data is None or isinstance(data, (str, int, float, bool)):\n return data\n if isinstance(data, (dict, list)):\n try:\n json.dumps(data)\n return data\n except (TypeError, OverflowError):\n pass\n return jsonable_encoder(data)\n\n @staticmethod\n def stream(\n content: ContentStream,\n status_code: int = 200,\n headers: Mapping[str, str] | None = None,\n media_type: str | None = None,\n background: BackgroundTask | None = None,\n ) -> StreamingResponse:\n return StreamingResponse(\n content=content,\n status_code=status_code,\n headers=headers,\n media_type=media_type,\n background=background,\n )\n\n\ndef response_docs(\n model=None, # \u6a21\u578b(BaseModel): \u81ea\u52a8\u4ece\u6a21\u578b\u4e2d\u89e3\u6790\u5b57\u6bb5\u4e0e\u7c7b\u578b\n data: dict | str = None, # \u6570\u636e(dict/str): \u76f4\u63a5\u7ed9\u5b9a\u5b57\u6bb5\u4e0e\u7c7b\u578b/\u7c7b\u578b\n is_listwrap: bool = False,\n listwrap_key: str = None,\n listwrap_key_extra: dict = None,\n docs_extra: dict = None,\n):\n \"\"\"\u54cd\u5e94\u6587\u6863\"\"\"\n\n def _data_from_model(model_, default: str = \"\u672a\u77e5\") -> dict:\n \"\"\"\u6570\u636e\u6a21\u677f\"\"\"\n data_ = {}\n if hasattr(model_, \"response_fields\"):\n all_fields = set(model_.response_fields())\n else:\n all_fields = set(model_.model_fields.keys())\n type_hints = get_type_hints(model_)\n for field_name in all_fields:\n try:\n t = type_hints.get(field_name)\n t = str(t).replace(\"<class '\", \"\").replace(\"'>\", \"\") if t else default\n except Exception:\n t = default\n data_[field_name] = t\n return data_\n\n final_data = {}\n if model:\n final_data = _data_from_model(model)\n if data:\n if isinstance(data, dict):\n final_data.update(data)\n else:\n final_data = data\n if is_listwrap:\n final_data = [final_data] if not isinstance(final_data, list) else final_data\n if listwrap_key:\n final_data = {listwrap_key: final_data}\n if listwrap_key_extra:\n final_data.update(listwrap_key_extra)\n\n def _format_value(value):\n if isinstance(value, str):\n _value = value.split(\"|\")\n if len(_value) > 1:\n return \" | \".join([map_jsontype(_v.strip(), is_keep_integer=True) for _v in _value])\n return map_jsontype(value, is_keep_integer=True)\n elif isinstance(value, dict):\n return {k: _format_value(v) for k, v in value.items()}\n elif isinstance(value, (list, tuple)):\n return [_format_value(item) for item in value]\n else:\n return str(value)\n\n format_data = _format_value(final_data)\n\n docs = {\n 200: {\n \"description\": \"\u64cd\u4f5c\u6210\u529f\u3010code\u4e3a0 & http\u72b6\u6001\u7801200\u3011\",\n \"content\": {\n \"application/json\": {\n \"example\": {\n \"msg\": \"string\",\n \"code\": \"integer\",\n \"data\": format_data,\n \"request_id\": \"string\",\n }\n }\n }\n },\n 422: {\n \"description\": \"\u64cd\u4f5c\u5931\u8d25\u3010code\u975e0 & http\u72b6\u6001\u7801200\u3011\",\n \"content\": {\n \"application/json\": {\n \"example\": {\n \"msg\": \"string\",\n \"code\": \"integer\",\n \"error\": \"string\",\n \"data\": \"object | array | ...\",\n \"request_id\": \"string\",\n }\n }\n }\n },\n }\n if docs_extra:\n docs.update(docs_extra)\n return docs\n",
11
- "app/api/status.py": "from enum import Enum\n\n\nclass Status(Enum):\n SUCCESS = (0, '\u64cd\u4f5c\u6210\u529f')\n FAILURE = (1, '\u64cd\u4f5c\u5931\u8d25')\n\n PARAMS_ERROR = (400, '\u53c2\u6570\u9519\u8bef')\n UNAUTHORIZED_ERROR = (401, '\u8ba4\u8bc1\u5931\u8d25')\n # \u5efa\u8bae\uff1a\u4e1a\u52a1\u6a21\u5757\u9519\u8bef\u7801\u4ece10000\u5f00\u59cb\n RECORD_NOT_EXIST_ERROR = (10000, '\u8bb0\u5f55\u4e0d\u5b58\u5728')\n RECORD_EXISTS_ERROR = (10001, '\u8bb0\u5f55\u5df2\u5b58\u5728')\n USER_OR_PASSWORD_ERROR = (10002, '\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef')\n\n @property\n def code(self):\n return self.value[0]\n\n @property\n def msg(self):\n return self.value[1]\n\n @classmethod\n def collect_status(cls):\n text = \"\"\n for s in cls:\n text += f\"{s.code} {s.msg}\\n\"\n return text\n",
12
- "app/api/__init__.py": "\"\"\"\napi\n\"\"\"\nimport importlib\nimport sys\nfrom pathlib import Path\n\nfrom fastapi import FastAPI\nfrom loguru import logger\n\nfrom app import APP_DIR\n\n_API_MOD_DIR = APP_DIR.joinpath(\"api\")\n_API_MOD_BASE = \"app.api\"\n\n\ndef register_routers(\n app: FastAPI,\n mod_dir: Path = _API_MOD_DIR,\n mod_base: str = _API_MOD_BASE,\n prefix: str = \"\",\n obj_suffix: str = \"_router\",\n depth: int = 0,\n max_depth: int = 2\n):\n \"\"\"\n \u6ce8\u518c\u8def\u7531\n \u8981\u6c42\uff1a\n \u8def\u7531\u6a21\u5757\uff1a\u975e'__'\u5f00\u5934\u7684\u6a21\u5757\n \u8def\u7531\u5bf9\u8c61\uff1a{\u6a21\u5757\u540d\u79f0}{\u8def\u7531\u5bf9\u8c61\u540e\u7f00}\n :param app: FastAPI\u5e94\u7528\n :param mod_dir: api\u6a21\u5757\u76ee\u5f55\n :param mod_base: api\u6a21\u5757\u57fa\u7840\n :param prefix: url\u524d\u7f00\n :param obj_suffix: \u8def\u7531\u5bf9\u8c61\u540e\u7f00\n :param depth: \u5f53\u524d\u9012\u5f52\u6df1\u5ea6\n :param max_depth: \u6700\u5927\u9012\u5f52\u6df1\u5ea6\n \"\"\"\n if depth > max_depth:\n return\n for item in mod_dir.iterdir():\n if item.name.startswith(\"__\") or item.name == \"__pycache__\":\n continue\n if item.is_dir():\n new_mod_dir = item\n new_mod_base = f\"{mod_base}.{item.name}\"\n new_prefix = prefix\n try:\n mod = importlib.import_module(new_mod_base)\n _prefix = getattr(mod, \"_prefix\", None)\n if _prefix:\n new_prefix = f\"{new_prefix}/{_prefix}\"\n except ImportError:\n logger.error(f\"Register router failed to import module: {new_mod_base}\")\n continue\n register_routers(\n app=app,\n mod_dir=new_mod_dir,\n mod_base=new_mod_base,\n prefix=new_prefix,\n obj_suffix=obj_suffix,\n depth=depth + 1,\n max_depth=max_depth\n )\n elif item.is_file() and item.suffix == \".py\" and depth > 0:\n mod_name = item.stem\n final_mod = f\"{mod_base}.{mod_name}\"\n try:\n mod = importlib.import_module(final_mod)\n if not getattr(mod, \"_active\", True):\n logger.info(f\"Register router skipping inactive module: {final_mod}\")\n sys.modules.pop(final_mod)\n continue\n router_name = f\"{mod_name}{obj_suffix}\"\n if router := getattr(mod, router_name, None):\n tag = getattr(mod, \"_tag\", None)\n if not tag:\n tag = item.parent.stem if depth > 1 else mod_name\n app.include_router(\n router=router,\n prefix=prefix.replace(\"//\", \"/\").rstrip(\"/\"),\n tags=[tag]\n )\n except ImportError:\n logger.error(f\"Register router failed to import module: {final_mod}\")\n continue\n",
13
- "app/api/default/ping.py": "from fastapi import APIRouter\n\nping_router = APIRouter()\n\n\n@ping_router.get(\n path=\"/ping\",\n summary=\"ping\",\n)\ndef ping():\n return \"pong\"\n",
18
+ "app/api/dependencies.py": "from fastapi import Depends, Security\nfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader\nfrom fastapi.security.utils import get_authorization_scheme_param\nfrom pydantic import BaseModel\nfrom starlette.requests import Request\n\nfrom app.api.exceptions import CustomException\nfrom app.api.status import Status\nfrom app.initializer import g\nfrom app.utils.db_async_util import sqlfetch_one\nfrom app.utils.jwt_util import verify_jwt\n\n\n# ======= jwt =======\n\nclass JWTUser(BaseModel):\n # \u4e0e\u5b9e\u9645`user`\u5bf9\u9f50\n id: str = None\n phone: str = None\n name: str = None\n age: int = None\n gender: int = None\n\n @staticmethod\n async def get_user_jwt_key(user_id: str) -> str:\n # \u5efa\u8bae\uff1ajwt_key\u8fdb\u884credis\u7f13\u5b58\n async with g.db_async_session() as session:\n sql = 'SELECT jwt_key FROM `user` WHERE id = :id' # noqa\n if session.bind.dialect.name == \"postgresql\":\n sql = 'SELECT jwt_key FROM \"user\" WHERE id = :id' # noqa\n data = await sqlfetch_one(\n session=session,\n sql=sql,\n params={\"id\": user_id},\n )\n return data.get(\"jwt_key\")\n\n\nclass JWTAuthorizationCredentials(HTTPAuthorizationCredentials):\n jwt_user: JWTUser\n\n\nclass JWTBearer(HTTPBearer):\n\n async def __call__(\n self, request: Request\n ) -> JWTAuthorizationCredentials | None:\n authorization = request.headers.get(\"Authorization\")\n scheme, credentials = get_authorization_scheme_param(authorization)\n if not (authorization and scheme and credentials):\n if self.auto_error:\n raise CustomException(\n msg=\"Not authenticated\",\n status=Status.UNAUTHORIZED_ERROR,\n )\n else:\n return None\n if scheme.lower() != \"bearer\":\n if self.auto_error:\n raise CustomException(\n msg=\"Invalid authentication credentials\",\n status=Status.UNAUTHORIZED_ERROR,\n )\n else:\n return None\n jwt_user = await self.verify_credentials(credentials)\n return JWTAuthorizationCredentials(scheme=scheme, credentials=credentials, jwt_user=jwt_user)\n\n async def verify_credentials(self, credentials: str) -> JWTUser:\n playload = await self._verify_jwt(credentials)\n if playload is None:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR)\n user_jwt_key = await JWTUser.get_user_jwt_key(playload.get(\"id\"))\n if not user_jwt_key:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR)\n await self._verify_jwt(credentials, jwt_key=user_jwt_key)\n return JWTUser(\n id=playload.get(\"id\"),\n phone=playload.get(\"phone\"),\n name=playload.get(\"name\"),\n age=playload.get(\"age\"),\n gender=playload.get(\"gender\"),\n )\n\n @staticmethod\n async def _verify_jwt(token: str, jwt_key: str = None) -> dict:\n try:\n return verify_jwt(token=token, jwt_key=jwt_key)\n except Exception as e:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR, msg=str(e))\n\n\ndef get_current_user(\n credentials: JWTAuthorizationCredentials | None = Depends(JWTBearer(auto_error=True))\n) -> JWTUser:\n if not credentials:\n return JWTUser()\n return credentials.jwt_user\n\n\n# ======= api key =======\n\n_API_KEY_HEADER = APIKeyHeader(name=\"X-API-Key\", auto_error=False)\n\n\nasync def get_current_api_key(api_key: str | None = Security(_API_KEY_HEADER)):\n if not api_key:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR, msg=\"API key is required\")\n if api_key not in g.config.api_keys:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR, msg=\"Invalid API key\")\n return api_key\n",
19
+ "app/api/exceptions.py": "from typing import Any\n\nfrom app.api.status import Status\n\n\nclass CustomException(Exception):\n\n def __init__(\n self,\n msg: str = None,\n code: int = None,\n data: Any = None,\n status: Status = Status.FAILURE,\n ):\n self.msg = msg or status.msg\n self.code = code or status.code\n self.data = data\n self.status = status\n\n def __str__(self) -> str:\n return f\"{self.code} {self.msg}\"\n\n def __repr__(self) -> str:\n return f\"<{self.__class__.__name__}: ({self.code!r}, {self.msg!r})>\"\n",
20
+ "app/api/responses.py": "import json\nfrom typing import Mapping, get_type_hints, Any\n\nfrom fastapi.encoders import jsonable_encoder\nfrom starlette.background import BackgroundTask\nfrom starlette.responses import JSONResponse, StreamingResponse, ContentStream\nfrom toollib.utils import map_jsontype\n\nfrom app.api.status import Status\nfrom app.initializer.context import request_id_var\n\n_EXPOSE_ERROR = True\n\n\nclass Responses:\n\n @staticmethod\n def success(\n data: dict | list | str | None = None,\n msg: str = None,\n code: int = None,\n status: Status = Status.SUCCESS,\n is_encode_data: bool = False,\n status_code: int = 200,\n headers: Mapping[str, str] | None = None,\n media_type: str | None = None,\n background: BackgroundTask | None = None,\n ) -> JSONResponse:\n content = {\n \"msg\": msg or status.msg,\n \"code\": code or status.code,\n \"data\": Responses.encode_data(data) if is_encode_data else data,\n \"request_id\": request_id_var.get(),\n }\n return JSONResponse(\n content=content,\n status_code=status_code,\n headers=headers,\n media_type=media_type,\n background=background,\n )\n\n @staticmethod\n def failure(\n msg: str = None,\n code: int = None,\n error: str | Exception | None = None,\n data: dict | list | str | None = None,\n status: Status = Status.FAILURE,\n is_encode_data: bool = False,\n status_code: int = 200,\n headers: Mapping[str, str] | None = None,\n media_type: str | None = None,\n background: BackgroundTask | None = None,\n ) -> JSONResponse:\n content = {\n \"msg\": msg or status.msg,\n \"code\": code or status.code,\n \"data\": Responses.encode_data(data) if is_encode_data else data,\n \"request_id\": request_id_var.get(),\n }\n if _EXPOSE_ERROR:\n content[\"error\"] = str(error) if error else None\n return JSONResponse(\n content=content,\n status_code=status_code,\n headers=headers,\n media_type=media_type,\n background=background,\n )\n\n @staticmethod\n def encode_data(data: Any) -> Any:\n if data is None or isinstance(data, (str, int, float, bool)):\n return data\n if isinstance(data, (dict, list)):\n try:\n json.dumps(data)\n return data\n except (TypeError, OverflowError):\n pass\n return jsonable_encoder(data)\n\n @staticmethod\n def stream(\n content: ContentStream,\n status_code: int = 200,\n headers: Mapping[str, str] | None = None,\n media_type: str | None = None,\n background: BackgroundTask | None = None,\n ) -> StreamingResponse:\n return StreamingResponse(\n content=content,\n status_code=status_code,\n headers=headers,\n media_type=media_type,\n background=background,\n )\n\n\ndef response_docs(\n model=None, # \u6a21\u578b(BaseModel): \u81ea\u52a8\u4ece\u6a21\u578b\u4e2d\u89e3\u6790\u5b57\u6bb5\u4e0e\u7c7b\u578b\n data: dict | str = None, # \u6570\u636e(dict/str): \u76f4\u63a5\u7ed9\u5b9a\u5b57\u6bb5\u4e0e\u7c7b\u578b/\u7c7b\u578b\n is_listwrap: bool = False,\n listwrap_key: str = None,\n listwrap_key_extra: dict = None,\n docs_extra: dict = None,\n):\n \"\"\"\u54cd\u5e94\u6587\u6863\"\"\"\n\n def _data_from_model(model_, default: str = \"\u672a\u77e5\") -> dict:\n \"\"\"\u6570\u636e\u6a21\u677f\"\"\"\n data_ = {}\n if hasattr(model_, \"response_fields\"):\n all_fields = set(model_.response_fields())\n else:\n all_fields = set(model_.model_fields.keys())\n type_hints = get_type_hints(model_)\n for field_name in all_fields:\n try:\n t = type_hints.get(field_name)\n t = str(t).replace(\"<class '\", \"\").replace(\"'>\", \"\") if t else default\n except Exception:\n t = default\n data_[field_name] = t\n return data_\n\n final_data = {}\n if model:\n final_data = _data_from_model(model)\n if data:\n if isinstance(data, dict):\n final_data.update(data)\n else:\n final_data = data\n if is_listwrap:\n final_data = [final_data] if not isinstance(final_data, list) else final_data\n if listwrap_key:\n final_data = {listwrap_key: final_data}\n if listwrap_key_extra:\n final_data.update(listwrap_key_extra)\n\n def _format_value(value):\n if isinstance(value, str):\n _value = value.split(\"|\")\n if len(_value) > 1:\n return \" | \".join([map_jsontype(_v.strip(), is_keep_integer=True) for _v in _value])\n return map_jsontype(value, is_keep_integer=True)\n elif isinstance(value, dict):\n return {k: _format_value(v) for k, v in value.items()}\n elif isinstance(value, (list, tuple)):\n return [_format_value(item) for item in value]\n else:\n return str(value)\n\n format_data = _format_value(final_data)\n\n docs = {\n 200: {\n \"description\": \"\u64cd\u4f5c\u6210\u529f\u3010code\u4e3a0 & http\u72b6\u6001\u7801200\u3011\",\n \"content\": {\n \"application/json\": {\n \"example\": {\n \"msg\": \"string\",\n \"code\": \"integer\",\n \"data\": format_data or \"object | array | ...\",\n \"request_id\": \"string\",\n }\n }\n }\n },\n 422: {\n \"description\": \"\u64cd\u4f5c\u5931\u8d25\u3010code\u975e0 & http\u72b6\u6001\u7801200\u3011\",\n \"content\": {\n \"application/json\": {\n \"example\": {\n \"msg\": \"string\",\n \"code\": \"integer\",\n \"error\": \"string\",\n \"data\": \"object | array | ...\",\n \"request_id\": \"string\",\n }\n }\n }\n },\n }\n if docs_extra:\n docs.update(docs_extra)\n return docs\n",
21
+ "app/api/status.py": "from enum import Enum\n\n\nclass Status(Enum):\n SUCCESS = (0, '\u64cd\u4f5c\u6210\u529f')\n FAILURE = (1, '\u64cd\u4f5c\u5931\u8d25')\n\n PARAMS_ERROR = (400, '\u53c2\u6570\u9519\u8bef')\n UNAUTHORIZED_ERROR = (401, '\u8ba4\u8bc1\u5931\u8d25')\n INTERNAL_SERVER_ERROR = (500, '\u5185\u90e8\u670d\u52a1\u5668\u9519\u8bef')\n # \u5efa\u8bae\uff1a\u4e1a\u52a1\u6a21\u5757\u9519\u8bef\u7801\u4ece10000\u5f00\u59cb\n RECORD_NOT_EXIST_ERROR = (10000, '\u8bb0\u5f55\u4e0d\u5b58\u5728')\n RECORD_EXISTS_ERROR = (10001, '\u8bb0\u5f55\u5df2\u5b58\u5728')\n USER_OR_PASSWORD_ERROR = (10002, '\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef')\n\n @property\n def code(self):\n return self.value[0]\n\n @property\n def msg(self):\n return self.value[1]\n\n @classmethod\n def collect_status(cls):\n text = \"\"\n for s in cls:\n text += f\"{s.code} {s.msg}\\n\"\n return text\n\n\nif __name__ == '__main__':\n print(Status.collect_status())\n",
22
+ "app/api/__init__.py": "\"\"\"\napi\n\"\"\"\nimport importlib\nimport sys\nfrom pathlib import Path\n\nfrom fastapi import FastAPI, APIRouter\nfrom loguru import logger\n\nfrom app import APP_DIR\n\n_API_MOD_DIR = APP_DIR.joinpath(\"api\")\n_API_MOD_BASE = \"app.api\"\n\n\ndef register_routers(\n app: FastAPI,\n mod_dir: Path = _API_MOD_DIR,\n mod_base: str = _API_MOD_BASE,\n name: str = \"router\",\n prefix: str = \"\",\n depth: int = 0,\n min_depth: int = 1,\n max_depth: int = 2,\n):\n \"\"\"\n \u6ce8\u518c\u8def\u7531\n \u8981\u6c42\uff1a\n \u8def\u7531\u6a21\u5757\uff1a\u975e'__'\u5f00\u5934\u7684\u6a21\u5757\n \u8def\u7531\u540d\u79f0\uff1a{name}\n :param app: FastAPI\u5e94\u7528\n :param mod_dir: api\u6a21\u5757\u76ee\u5f55\n :param mod_base: api\u6a21\u5757\u57fa\u7840\n :param name: \u8def\u7531\u540d\u79f0\n :param prefix: url\u524d\u7f00\n :param depth: \u5f53\u524d\u9012\u5f52\u6df1\u5ea6\n :param min_depth: \u6700\u5c0f\u9012\u5f52\u6df1\u5ea6\n :param max_depth: \u6700\u5927\u9012\u5f52\u6df1\u5ea6\n \"\"\"\n if depth > max_depth:\n return\n for item in mod_dir.iterdir():\n if item.name.startswith(\"__\"):\n continue\n if item.is_dir():\n new_mod_dir = item\n new_mod_base = f\"{mod_base}.{item.name}\"\n new_prefix = prefix\n try:\n mod = importlib.import_module(new_mod_base)\n _prefix = getattr(mod, \"_prefix\", None)\n if _prefix:\n new_prefix = f\"{new_prefix}/{_prefix}\"\n except ImportError:\n logger.error(f\"Register router failed to import module: {new_mod_base}\")\n continue\n register_routers(\n app=app,\n mod_dir=new_mod_dir,\n mod_base=new_mod_base,\n prefix=new_prefix,\n name=name,\n depth=depth + 1,\n max_depth=max_depth\n )\n elif item.is_file() and item.suffix == \".py\" and depth >= min_depth:\n mod_name = item.stem\n final_mod = f\"{mod_base}.{mod_name}\"\n try:\n mod = importlib.import_module(final_mod)\n if not getattr(mod, \"_active\", True):\n logger.info(f\"Register router skipping inactive module: {final_mod}\")\n sys.modules.pop(final_mod)\n continue\n if router := getattr(mod, name, None):\n if isinstance(router, APIRouter):\n tag = getattr(mod, \"_tag\", None)\n if not tag:\n tag = item.parent.stem if depth > 1 else mod_name\n app.include_router(\n router=router,\n prefix=prefix.replace(\"//\", \"/\").rstrip(\"/\"),\n tags=[tag]\n )\n except ImportError as e:\n logger.error(f\"Register router failed to import module: {final_mod} ({e})\")\n continue\n",
23
+ "app/api/default/aping.py": "from fastapi import APIRouter\n\nfrom app_celery.producer import publisher\n\nrouter = APIRouter()\n\n\n@router.get(\n path=\"/aping\",\n summary=\"aping\",\n)\nasync def ping():\n task_id = publisher.publish(\"ping\")\n return f\"pong > {task_id}\"\n",
24
+ "app/api/default/ping.py": "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n\n@router.get(\n path=\"/ping\",\n summary=\"ping\",\n)\nasync def ping():\n return \"pong\"\n",
14
25
  "app/api/default/__init__.py": "\"\"\"\napi-default\n\"\"\"\n\n_prefix = \"/api\"\n",
15
- "app/api/v1/user.py": "import traceback\n\nfrom fastapi import APIRouter, Depends\nfrom starlette.requests import Request\n\nfrom app.api.response import Response, response_docs\nfrom app.api.status import Status\nfrom app.service.user import (\n UserDetailSvc,\n UserListSvc,\n UserCreateSvc,\n UserUpdateSvc,\n UserDeleteSvc,\n UserLoginSvc,\n UserTokenSvc,\n)\nfrom app.initializer import g\nfrom app.middleware.auth import JWTUser, get_current_user\n\nuser_router = APIRouter()\n_active = True # \u6fc0\u6d3b\u72b6\u6001\uff08\u9ed8\u8ba4\u6fc0\u6d3b\uff09\n_tag = \"user\" # \u6807\u7b7e\uff08\u9ed8\u8ba4\u6a21\u5757\u540d\u6216\u5b50\u76ee\u5f55\u540d\uff09\n\n\n# \u6ce8\u610f\uff1a`user`\u4ec5\u4e3a\u6a21\u5757\u793a\u4f8b\uff0c\u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\n# \u6ce8\u610f\uff1a`user`\u4ec5\u4e3a\u6a21\u5757\u793a\u4f8b\uff0c\u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\n# \u6ce8\u610f\uff1a`user`\u4ec5\u4e3a\u6a21\u5757\u793a\u4f8b\uff0c\u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\n\n\n@user_router.get(\n path=\"/user/{user_id}\",\n summary=\"userDetail\",\n responses=response_docs(\n model=UserDetailSvc,\n ),\n)\nasync def detail(\n request: Request,\n user_id: str,\n current_user: JWTUser = Depends(get_current_user), # \u8ba4\u8bc1\n):\n try:\n user_svc = UserDetailSvc(id=user_id)\n data = await user_svc.detail()\n if not data:\n return Response.failure(status=Status.RECORD_NOT_EXIST_ERROR)\n except Exception as e:\n g.logger.error(traceback.format_exc())\n return Response.failure(msg=\"userDetail\u5931\u8d25\", error=e, request=request)\n return Response.success(data=data, request=request)\n\n\n@user_router.get(\n path=\"/user\",\n summary=\"userList\",\n responses=response_docs(\n model=UserListSvc,\n is_listwrap=True,\n listwrap_key=\"items\",\n listwrap_key_extra={\n \"total\": \"int\",\n },\n ),\n)\nasync def lst(\n request: Request,\n page: int = 1,\n size: int = 10,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n user_svc = UserListSvc(page=page, size=size)\n data, total = await user_svc.lst()\n except Exception as e:\n g.logger.error(traceback.format_exc())\n return Response.failure(msg=\"userList\u5931\u8d25\", error=e, request=request)\n return Response.success(data={\"items\": data, \"total\": total}, request=request)\n\n\n@user_router.post(\n path=\"/user\",\n summary=\"userCreate\",\n responses=response_docs(data={\n \"id\": \"str\",\n }),\n)\nasync def create(\n request: Request,\n user_svc: UserCreateSvc,\n):\n try:\n user_id = await user_svc.create()\n if not user_id:\n return Response.failure(status=Status.RECORD_EXISTS_ERROR, request=request)\n except Exception as e:\n g.logger.error(traceback.format_exc())\n return Response.failure(msg=\"userCreate\u5931\u8d25\", error=e, request=request)\n return Response.success(data={\"id\": user_id}, request=request)\n\n\n@user_router.put(\n path=\"/user/{user_id}\",\n summary=\"userUpdate\",\n responses=response_docs(data={\n \"id\": \"str\",\n }),\n)\nasync def update(\n request: Request,\n user_id: str,\n user_svc: UserUpdateSvc,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n updated_ids = await user_svc.update(user_id)\n if not updated_ids:\n return Response.failure(status=Status.RECORD_NOT_EXIST_ERROR, request=request)\n except Exception as e:\n g.logger.error(traceback.format_exc())\n return Response.failure(msg=\"userUpdate\u5931\u8d25\", error=e, request=request)\n return Response.success(data={\"id\": user_id}, request=request)\n\n\n@user_router.delete(\n path=\"/user/{user_id}\",\n summary=\"userDelete\",\n responses=response_docs(data={\n \"id\": \"str\",\n }),\n)\nasync def delete(\n request: Request,\n user_id: str,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n user_svc = UserDeleteSvc()\n deleted_ids = await user_svc.delete(user_id)\n if not deleted_ids:\n return Response.failure(status=Status.RECORD_NOT_EXIST_ERROR, request=request)\n except Exception as e:\n g.logger.error(traceback.format_exc())\n return Response.failure(msg=\"userDelete\u5931\u8d25\", error=e, request=request)\n return Response.success(data={\"id\": user_id}, request=request)\n\n\n@user_router.post(\n path=\"/user/login\",\n summary=\"userLogin\",\n responses=response_docs(data={\n \"token\": \"str\",\n }),\n)\nasync def login(\n request: Request,\n user_svc: UserLoginSvc,\n):\n try:\n data = await user_svc.login()\n if not data:\n return Response.failure(status=Status.USER_OR_PASSWORD_ERROR, request=request)\n except Exception as e:\n g.logger.error(traceback.format_exc())\n return Response.failure(msg=\"userLogin\u5931\u8d25\", error=e, request=request)\n return Response.success(data={\"token\": data}, request=request)\n\n\n@user_router.post(\n path=\"/user/token\",\n summary=\"userToken\",\n responses=response_docs(data={\n \"token\": \"str\",\n }),\n)\nasync def token(\n request: Request,\n user_svc: UserTokenSvc,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n data = await user_svc.token()\n if not data:\n return Response.failure(status=Status.RECORD_NOT_EXIST_ERROR, request=request)\n except Exception as e:\n g.logger.error(traceback.format_exc())\n return Response.failure(msg=\"userToken\u5931\u8d25\", error=e, request=request)\n return Response.success(data={\"token\": data}, request=request)\n",
26
+ "app/api/v1/user.py": "from fastapi import APIRouter, Depends\nfrom loguru import logger\n\nfrom app.api.dependencies import JWTUser, get_current_user\nfrom app.api.responses import Responses, response_docs\nfrom app.api.status import Status\nfrom app.services.user import (\n UserDetailSvc,\n UserListSvc,\n UserCreateSvc,\n UserUpdateSvc,\n UserDeleteSvc,\n UserLoginSvc,\n UserTokenSvc,\n)\n\nrouter = APIRouter()\n_active = True # \u6fc0\u6d3b\u72b6\u6001\uff08\u9ed8\u8ba4\u6fc0\u6d3b\uff09\n_tag = \"user\" # \u6807\u7b7e\uff08\u9ed8\u8ba4\u6a21\u5757\u540d\uff09\n\n\n# \u6ce8\u610f\uff1a`user`\u4ec5\u4e3a\u6a21\u5757\u793a\u4f8b\uff0c\u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\n# \u6ce8\u610f\uff1a`user`\u4ec5\u4e3a\u6a21\u5757\u793a\u4f8b\uff0c\u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\n# \u6ce8\u610f\uff1a`user`\u4ec5\u4e3a\u6a21\u5757\u793a\u4f8b\uff0c\u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\n\n\n@router.get(\n path=\"/user/{user_id}\",\n summary=\"userDetail\",\n responses=response_docs(\n model=UserDetailSvc,\n ),\n)\nasync def detail(\n user_id: str,\n current_user: JWTUser = Depends(get_current_user), # \u8ba4\u8bc1\n):\n try:\n user_svc = UserDetailSvc(id=user_id)\n data = await user_svc.detail()\n if not data:\n return Responses.failure(status=Status.RECORD_NOT_EXIST_ERROR)\n except Exception as e:\n msg = \"userDetail\u64cd\u4f5c\u5f02\u5e38\"\n logger.exception(msg)\n return Responses.failure(msg=msg, error=e)\n return Responses.success(data=data)\n\n\n@router.get(\n path=\"/user\",\n summary=\"userList\",\n responses=response_docs(\n model=UserListSvc,\n is_listwrap=True,\n listwrap_key=\"items\",\n listwrap_key_extra={\n \"total\": \"int\",\n },\n ),\n)\nasync def lst(\n page: int = 1,\n size: int = 10,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n user_svc = UserListSvc(page=page, size=size)\n data, total = await user_svc.lst()\n except Exception as e:\n msg = \"userList\u64cd\u4f5c\u5f02\u5e38\"\n logger.exception(msg)\n return Responses.failure(msg=msg, error=e)\n return Responses.success(data={\"items\": data, \"total\": total})\n\n\n@router.post(\n path=\"/user\",\n summary=\"userCreate\",\n responses=response_docs(data={\n \"id\": \"str\",\n }),\n)\nasync def create(\n user_svc: UserCreateSvc,\n):\n try:\n user_id = await user_svc.create()\n if not user_id:\n return Responses.failure(status=Status.RECORD_EXISTS_ERROR)\n except Exception as e:\n msg = \"userCreate\u64cd\u4f5c\u5f02\u5e38\"\n logger.exception(msg)\n return Responses.failure(msg=msg, error=e)\n return Responses.success(data={\"id\": user_id})\n\n\n@router.put(\n path=\"/user/{user_id}\",\n summary=\"userUpdate\",\n responses=response_docs(data={\n \"id\": \"str\",\n }),\n)\nasync def update(\n user_id: str,\n user_svc: UserUpdateSvc,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n updated_ids = await user_svc.update(user_id)\n if not updated_ids:\n return Responses.failure(status=Status.RECORD_NOT_EXIST_ERROR)\n except Exception as e:\n msg = \"userUpdate\u64cd\u4f5c\u5f02\u5e38\"\n logger.exception(msg)\n return Responses.failure(msg=msg, error=e)\n return Responses.success(data={\"id\": user_id})\n\n\n@router.delete(\n path=\"/user/{user_id}\",\n summary=\"userDelete\",\n responses=response_docs(data={\n \"id\": \"str\",\n }),\n)\nasync def delete(\n user_id: str,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n user_svc = UserDeleteSvc()\n deleted_ids = await user_svc.delete(user_id)\n if not deleted_ids:\n return Responses.failure(status=Status.RECORD_NOT_EXIST_ERROR)\n except Exception as e:\n msg = \"userDelete\u64cd\u4f5c\u5f02\u5e38\"\n logger.exception(msg)\n return Responses.failure(msg=msg, error=e)\n return Responses.success(data={\"id\": user_id})\n\n\n@router.post(\n path=\"/user/login\",\n summary=\"userLogin\",\n responses=response_docs(data={\n \"token\": \"str\",\n }),\n)\nasync def login(\n user_svc: UserLoginSvc,\n):\n try:\n data = await user_svc.login()\n if not data:\n return Responses.failure(status=Status.USER_OR_PASSWORD_ERROR)\n except Exception as e:\n msg = \"userLogin\u64cd\u4f5c\u5f02\u5e38\"\n logger.exception(msg)\n return Responses.failure(msg=msg, error=e)\n return Responses.success(data={\"token\": data})\n\n\n@router.post(\n path=\"/user/token\",\n summary=\"userToken\",\n responses=response_docs(data={\n \"token\": \"str\",\n }),\n)\nasync def token(\n user_svc: UserTokenSvc,\n current_user: JWTUser = Depends(get_current_user),\n):\n try:\n data = await user_svc.token()\n if not data:\n return Responses.failure(status=Status.RECORD_NOT_EXIST_ERROR)\n except Exception as e:\n msg = \"userToken\u64cd\u4f5c\u5f02\u5e38\"\n logger.exception(msg)\n return Responses.failure(msg=msg, error=e)\n return Responses.success(data={\"token\": data})\n",
16
27
  "app/api/v1/__init__.py": "\"\"\"\napi-v1\n\"\"\"\n\n_prefix = \"/api/v1\"\n",
17
- "app/aplugin/__init__.py": "\"\"\"\n\u63d2\u4ef6\n\"\"\"\n",
18
- "app/atask/__init__.py": "\"\"\"\n\u5f02\u6b65\u4efb\u52a1\n\"\"\"\n",
19
- "app/initializer/context.py": "from contextvars import ContextVar\n\nrequest_id_ctx_var: ContextVar[str] = ContextVar(\"request_id\", default=\"N/A\")\n",
20
- "app/initializer/_conf.py": "import os\nfrom pathlib import Path\n\nimport yaml\nfrom dotenv import load_dotenv\nfrom toollib.utils import get_cls_attrs, parse_variable\n\nfrom app import APP_DIR\n\n_CONFIG_DIR = APP_DIR.parent.joinpath(\"config\")\n\nload_dotenv(dotenv_path=os.environ.setdefault(\n key=\"env_path\",\n value=str(_CONFIG_DIR.joinpath(\".env\")))\n)\n# #\napp_yaml = Path(\n os.environ.get(\"app_yaml\") or\n _CONFIG_DIR.joinpath(f\"app_{os.environ.setdefault(key='app_env', value='dev')}.yaml\")\n)\nif not app_yaml.is_file():\n raise RuntimeError(f\"\u914d\u7f6e\u6587\u4ef6\u4e0d\u5b58\u5728\uff1a{app_yaml}\")\n\n\nclass EnvConfig:\n \"\"\"env\u914d\u7f6e\"\"\"\n snow_datacenter_id: int = None\n\n def setattr_from_env(self):\n cls_attrs = get_cls_attrs(EnvConfig)\n for k, item in cls_attrs.items():\n v_type, v = item\n if callable(v_type):\n v = parse_variable(k=k, v_type=v_type, v_from=os.environ, default=v)\n setattr(self, k, v)\n\n\nclass Config(EnvConfig):\n \"\"\"\u914d\u7f6e\"\"\"\n _yaml_conf: dict = None\n yaml_name: str = app_yaml.name\n #\n app_title: str = \"xApp\"\n app_summary: str = \"xxApp\"\n app_description: str = \"xxxApp\"\n app_version: str = \"1.0.0\"\n app_debug: bool = True\n app_log_dir: str = \"./logs\"\n app_disable_docs: bool = True\n app_allow_origins: list = [\"*\"]\n # #\n redis_host: str = None\n redis_port: int = None\n redis_db: int = None\n redis_password: str = None\n redis_max_connections: int = None\n db_url: str = None\n db_async_url: str = None\n\n def setup(self):\n self.setattr_from_env()\n self.setattr_from_yaml()\n return self\n\n def setattr_from_yaml(self):\n cls_attrs = get_cls_attrs(Config)\n for k, item in cls_attrs.items():\n v_type, v = item\n if callable(v_type):\n v = parse_variable(k=k, v_type=v_type, v_from=self.load_yaml(), default=v)\n setattr(self, k, v)\n\n def load_yaml(self, reload: bool = False) -> dict:\n if self._yaml_conf and not reload:\n return self._yaml_conf\n with open(app_yaml, mode=\"r\", encoding=\"utf-8\") as file:\n self._yaml_conf = yaml.load(file, Loader=yaml.FullLoader)\n return self._yaml_conf\n\n\ndef init_config() -> Config:\n return Config().setup()\n",
21
- "app/initializer/_db.py": "import asyncio\nimport importlib\n\nfrom sqlalchemy import create_engine, exc\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncSession\nfrom sqlalchemy.orm import sessionmaker, scoped_session\n\nfrom app import APP_DIR\n\n_DSCHEMA_MOD_DIR = APP_DIR.joinpath(\"model\")\n_DSCHEMA_MOD_BASE = \"app.model\"\n_TABLES_CREATED = False\n\n\ndef init_db_session(\n db_url: str,\n db_echo: bool,\n db_pool_size: int = 10,\n db_max_overflow: int = 5,\n db_pool_recycle: int = 3600,\n is_create_tables: bool = True,\n) -> scoped_session:\n db_echo = db_echo or False\n kwargs = {\n \"pool_size\": db_pool_size,\n \"max_overflow\": db_max_overflow,\n \"pool_recycle\": db_pool_recycle,\n }\n if db_url.startswith(\"sqlite\"):\n kwargs = {}\n engine = create_engine(\n url=db_url,\n echo=db_echo,\n echo_pool=db_echo,\n **kwargs,\n )\n db_session = sessionmaker(engine, expire_on_commit=False)\n\n def create_tables():\n from app.model import DeclBase\n _import_tables()\n try:\n DeclBase.metadata.create_all(engine)\n except (\n exc.OperationalError,\n exc.IntegrityError,\n exc.ProgrammingError,\n ) as e:\n if \"already exists\" not in str(e):\n raise\n\n global _TABLES_CREATED\n if is_create_tables and not _TABLES_CREATED:\n create_tables()\n _TABLES_CREATED = True\n\n return scoped_session(db_session)\n\n\ndef init_db_async_session(\n db_url: str,\n db_echo: bool,\n db_pool_size: int = 10,\n db_max_overflow: int = 5,\n db_pool_recycle: int = 3600,\n is_create_tables: bool = True,\n) -> sessionmaker:\n db_echo = db_echo or False\n kwargs = {\n \"pool_size\": db_pool_size,\n \"max_overflow\": db_max_overflow,\n \"pool_recycle\": db_pool_recycle,\n }\n if db_url.startswith(\"sqlite\"):\n kwargs = {}\n async_engine = create_async_engine(\n url=db_url,\n echo=db_echo,\n echo_pool=db_echo,\n **kwargs,\n )\n db_async_session = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) # noqa\n\n async def create_tables():\n from app.model import DeclBase\n _import_tables()\n async with async_engine.begin() as conn:\n try:\n await conn.run_sync(DeclBase.metadata.create_all)\n except (\n exc.OperationalError,\n exc.IntegrityError,\n exc.ProgrammingError,\n ) as e:\n if \"already exists\" not in str(e):\n raise\n\n global _TABLES_CREATED\n if is_create_tables and not _TABLES_CREATED:\n try:\n loop = asyncio.get_running_loop()\n except RuntimeError:\n loop = asyncio.new_event_loop()\n asyncio.set_event_loop(loop)\n task = loop.create_task(create_tables())\n task.add_done_callback(lambda t: t.result() if not t.cancelled() else None)\n if not loop.is_running():\n loop.run_until_complete(task)\n _TABLES_CREATED = True\n return db_async_session\n\n\ndef _import_tables():\n \"\"\"\u5bfc\u5165\u8868\"\"\"\n for f in _DSCHEMA_MOD_DIR.glob(\"*.py\"):\n if not f.name.startswith(\"__\"):\n _ = importlib.import_module(f\"{_DSCHEMA_MOD_BASE}.{f.stem}\")\n",
22
- "app/initializer/_log.py": "import os\nimport sys\nfrom pathlib import Path\n\nfrom loguru import logger\nfrom loguru._logger import Logger # noqa\n\nfrom app.initializer.context import request_id_ctx_var\n\n_LOG_CONSOLE_FORMAT = \"{time:YYYY-MM-DD HH:mm:ss.SSS} {level} {extra[request_id]} {file}:{line} {message}\"\n_LOG_FILE_FORMAT = \"{time:YYYY-MM-DD HH:mm:ss.SSS} {level} {extra[request_id]} {file}:{line} {message}\"\n_LOG_FILE_PREFIX = \"app\"\n_LOG_ROTATION = \"100 MB\"\n_LOG_RETENTION = \"15 days\"\n_LOG_COMPRESSION = None\n_LOG_ENQUEUE = True\n_LOG_BACKTRACE = False\n_LOG_DIAGNOSE = False\n_LOG_CATCH = False\n_LOG_PID = False\n\n\ndef init_logger(\n debug: bool,\n log_dir: str = None,\n) -> Logger:\n logger.remove(None)\n _lever = \"DEBUG\" if debug else \"INFO\"\n\n def _filter(record: dict) -> bool:\n record[\"extra\"][\"request_id\"] = request_id_ctx_var.get()\n return True\n\n logger.add(\n sys.stdout,\n format=_LOG_CONSOLE_FORMAT,\n level=_lever,\n enqueue=_LOG_ENQUEUE,\n backtrace=_LOG_BACKTRACE,\n diagnose=_LOG_DIAGNOSE,\n catch=_LOG_CATCH,\n filter=_filter,\n )\n if log_dir:\n _log_dir = Path(log_dir)\n _log_access_file = _log_dir.joinpath(f\"{_LOG_FILE_PREFIX}-access.log\")\n _log_error_file = _log_dir.joinpath(f\"{_LOG_FILE_PREFIX}-error.log\")\n if _LOG_PID:\n _log_access_file = str(_log_access_file).replace(\".log\", f\".{os.getpid()}.log\")\n _log_error_file = str(_log_error_file).replace(\".log\", f\".{os.getpid()}.log\")\n logger.add(\n _log_access_file,\n encoding=\"utf-8\",\n format=_LOG_FILE_FORMAT,\n level=_lever,\n rotation=_LOG_ROTATION,\n retention=_LOG_RETENTION,\n compression=_LOG_COMPRESSION,\n enqueue=_LOG_ENQUEUE,\n backtrace=_LOG_BACKTRACE,\n diagnose=_LOG_DIAGNOSE,\n catch=_LOG_CATCH,\n )\n logger.add(\n _log_error_file,\n encoding=\"utf-8\",\n format=_LOG_FILE_FORMAT,\n level=\"ERROR\",\n rotation=_LOG_ROTATION,\n retention=_LOG_RETENTION,\n compression=_LOG_COMPRESSION,\n enqueue=_LOG_ENQUEUE,\n backtrace=_LOG_BACKTRACE,\n diagnose=_LOG_DIAGNOSE,\n catch=_LOG_CATCH,\n )\n return logger\n",
23
- "app/initializer/_redis.py": "from toollib.rediser import RedisCli\n\n\ndef init_redis_cli(\n host: str,\n port: int,\n db: int,\n password: str = None,\n max_connections: int = None,\n **kwargs,\n) -> RedisCli:\n if not host:\n return RedisCli()\n return RedisCli(\n host=host,\n port=port,\n db=db,\n password=password,\n max_connections=max_connections,\n **kwargs,\n )\n",
24
- "app/initializer/_snow.py": "import os\n\nfrom loguru import logger\nfrom toollib.guid import SnowFlake\nfrom toollib.rediser import RedisCli\nfrom toollib.utils import localip\n\n_CACHE_KEY_SNOW_WORKER_ID_INCR = \"config:snow_worker_id_incr\"\n_CACHE_KEY_SNOW_DATACENTER_ID_INCR = \"config:snow_datacenter_id_incr\"\n_CACHE_EXPIRE_SNOW = 120\n\n\ndef init_snow_cli(\n redis_cli: RedisCli,\n datacenter_id: int = None,\n to_str: bool = True,\n) -> SnowFlake: # \u5efa\u8bae\uff1a\u91c7\u7528\u670d\u52a1\u7684\u65b9\u5f0f\u8c03\u7528api\u83b7\u53d6\n if datacenter_id is None:\n datacenter_id = _snow_incr(redis_cli, _CACHE_KEY_SNOW_DATACENTER_ID_INCR, _CACHE_EXPIRE_SNOW)\n if datacenter_id is None:\n local_ip = localip()\n if local_ip:\n ip_parts = list(map(int, local_ip.split('.')))\n ip_int = (ip_parts[0] << 24) + (ip_parts[1] << 16) + (ip_parts[2] << 8) + ip_parts[3]\n datacenter_id = ip_int % 32\n worker_id = _snow_incr(redis_cli, _CACHE_KEY_SNOW_WORKER_ID_INCR, _CACHE_EXPIRE_SNOW)\n if worker_id is None:\n worker_id = os.getpid() % 32\n return SnowFlake(worker_id=worker_id, datacenter_id=datacenter_id, to_str=to_str)\n\n\ndef _snow_incr(redis_cli, cache_key: str, cache_expire: int):\n incr = None\n try:\n with redis_cli.connection() as r:\n resp = r.ping()\n if resp:\n lua_script = \"\"\"\n if redis.call('exists', KEYS[1]) == 1 then\n redis.call('expire', KEYS[1], ARGV[1])\n return redis.call('incr', KEYS[1])\n else\n redis.call('set', KEYS[1], 0)\n redis.call('expire', KEYS[1], ARGV[1])\n return 0\n end\n \"\"\"\n incr = r.eval(lua_script, 1, cache_key, cache_expire)\n except Exception as e:\n logger.warning(f\"snow\u521d\u59cb\u5316id\u5c06\u91c7\u7528\u672c\u5730\u65b9\u5f0f\uff0c\u7531\u4e8e\uff08{e}\uff09\")\n return incr\n",
25
- "app/initializer/__init__.py": "\"\"\"\n\u521d\u59cb\u5316\n\"\"\"\nfrom loguru._logger import Logger # noqa\nfrom sqlalchemy.orm import sessionmaker, scoped_session\nfrom toollib.guid import SnowFlake\nfrom toollib.rediser import RedisCli\nfrom toollib.utils import Singleton\n\nfrom app.initializer._conf import init_config\nfrom app.initializer._db import init_db_session, init_db_async_session\nfrom app.initializer._log import init_logger\nfrom app.initializer._redis import init_redis_cli\nfrom app.initializer._snow import init_snow_cli\n\n\nclass G(metaclass=Singleton):\n \"\"\"\n \u5168\u5c40\u53d8\u91cf\n \"\"\"\n config = None\n logger: Logger = None\n redis_cli: RedisCli = None\n snow_cli: SnowFlake = None\n db_session: scoped_session = None\n db_async_session: sessionmaker = None\n\n def __getattribute__(self, name):\n try:\n value = super().__getattribute__(name)\n except AttributeError:\n value = None\n if value is None:\n getter_name = f\"_get_{name}\"\n getter_method = getattr(self.__class__, getter_name, None)\n if callable(getter_method):\n value = getter_method()\n setattr(self, name, value)\n return value\n\n @classmethod\n def _get_config(cls):\n if not cls.config:\n cls.config = init_config()\n return cls.config\n\n @classmethod\n def _get_logger(cls):\n if not cls.logger:\n cls.logger = init_logger(\n debug=cls.config.app_debug,\n log_dir=cls.config.app_log_dir,\n )\n return cls.logger\n\n @classmethod\n def _get_redis_cli(cls):\n if not cls.redis_cli:\n cls.redis_cli = init_redis_cli(\n host=cls.config.redis_host,\n port=cls.config.redis_port,\n db=cls.config.redis_db,\n password=cls.config.redis_password,\n max_connections=cls.config.redis_max_connections,\n )\n return cls.redis_cli\n\n @classmethod\n def _get_snow_cli(cls):\n if not cls.snow_cli:\n cls.snow_cli = init_snow_cli(\n redis_cli=cls.redis_cli,\n datacenter_id=cls.config.snow_datacenter_id,\n )\n return cls.snow_cli\n\n @classmethod\n def _get_db_session(cls):\n if not cls.db_session:\n cls.db_session = init_db_session(\n db_url=cls.config.db_url,\n db_echo=cls.config.debug,\n )\n return cls.db_session\n\n @classmethod\n def _get_db_async_session(cls):\n if not cls.db_async_session:\n cls.db_async_session = init_db_async_session(\n db_url=cls.config.db_async_url,\n db_echo=cls.config.app_debug,\n )\n return cls.db_async_session\n\n @classmethod\n def setup(cls):\n \"\"\"\n \u521d\u59cb\u5316\n \"\"\"\n cls._get_config()\n cls._get_logger()\n cls._get_redis_cli()\n cls._get_snow_cli()\n # cls._get_db_session()\n cls._get_db_async_session()\n\n\ng = G()\n",
26
- "app/middleware/auth.py": "from fastapi import Depends\nfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentials\nfrom typing import Optional\n\nfrom fastapi.security.utils import get_authorization_scheme_param\nfrom pydantic import BaseModel\nfrom starlette.requests import Request\n\nfrom app.api.exception import CustomException\nfrom app.api.status import Status\nfrom app.initializer import g\nfrom app.model.user import User\nfrom app.utils import db_async\nfrom app.utils.auth import verify_jwt\n\n\nclass JWTUser(BaseModel):\n # \u5b57\u6bb5\u4e0eUser\u5bf9\u9f50\n id: str = None\n phone: str = None\n name: str = None\n age: int = None\n gender: int = None\n\n\nclass JWTAuthorizationCredentials(HTTPAuthorizationCredentials):\n user: JWTUser\n\n\nclass JWTBearer(HTTPBearer):\n\n async def __call__(\n self, request: Request\n ) -> Optional[JWTAuthorizationCredentials]:\n authorization = request.headers.get(\"Authorization\")\n scheme, credentials = get_authorization_scheme_param(authorization)\n if not (authorization and scheme and credentials):\n if self.auto_error:\n raise CustomException(\n msg=\"Not authenticated\",\n status=Status.UNAUTHORIZED_ERROR,\n )\n else:\n return None\n if scheme.lower() != \"bearer\":\n if self.auto_error:\n raise CustomException(\n msg=\"Invalid authentication credentials\",\n status=Status.UNAUTHORIZED_ERROR,\n )\n else:\n return None\n user = await self.verify_credentials(credentials)\n return JWTAuthorizationCredentials(scheme=scheme, credentials=credentials, user=user)\n\n async def verify_credentials(self, credentials: str) -> JWTUser:\n playload = await self._verify_jwt(credentials)\n if playload is None:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR)\n # \u5efa\u8bae\uff1ajwt_key\u8fdb\u884credis\u7f13\u5b58\n async with g.db_async_session() as session:\n data = await db_async.query_one(\n session=session,\n model=User,\n fields=[\"jwt_key\"],\n filter_by={\"id\": playload.get(\"id\")}\n )\n if not data:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR)\n # <<< \u5efa\u8bae\n await self._verify_jwt(credentials, jwt_key=data.get(\"jwt_key\"))\n return JWTUser(\n id=playload.get(\"id\"),\n phone=playload.get(\"phone\"),\n name=playload.get(\"name\"),\n age=playload.get(\"age\"),\n gender=playload.get(\"gender\"),\n )\n\n @staticmethod\n async def _verify_jwt(token: str, jwt_key: str = None) -> dict:\n try:\n return verify_jwt(token=token, jwt_key=jwt_key)\n except Exception as e:\n raise CustomException(status=Status.UNAUTHORIZED_ERROR, msg=str(e))\n\n\ndef get_current_user(\n credentials: Optional[JWTAuthorizationCredentials] = Depends(JWTBearer(auto_error=True))\n) -> JWTUser:\n if not credentials:\n return JWTUser()\n return credentials.user\n",
27
- "app/middleware/cors.py": "from fastapi.middleware.cors import CORSMiddleware\n\nfrom app.initializer import g\n\n\nclass Cors:\n middleware_class = CORSMiddleware\n allow_origins = g.config.app_allow_origins\n allow_credentials = True\n allow_methods = [\"*\"]\n allow_headers = [\"*\"]\n",
28
- "app/middleware/exception.py": "import traceback\n\nfrom fastapi.exceptions import RequestValidationError\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse\n\nfrom app.api.exception import CustomException\nfrom app.api.response import Response\nfrom app.api.status import Status\nfrom app.initializer import g\n\n\nclass ExceptionHandler:\n\n @staticmethod\n async def custom_exception_handler(\n request: Request,\n exc: CustomException,\n is_traceback: bool = False,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {exc.code} {exc.msg}'\n if is_traceback:\n lmsg = traceback.format_exc()\n g.logger.error(lmsg)\n return Response.failure(\n msg=exc.msg,\n code=exc.code,\n data=exc.data,\n request=request,\n )\n\n @staticmethod\n async def http_exception_handler(\n request: Request,\n exc: HTTPException,\n is_traceback: bool = False,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {exc.status_code} {exc.detail}'\n if is_traceback:\n lmsg = traceback.format_exc()\n g.logger.error(lmsg)\n return Response.failure(\n msg=exc.detail,\n code=exc.status_code,\n request=request,\n )\n\n @staticmethod\n async def validation_exception_handler(\n request: Request,\n exc: RequestValidationError,\n is_display_all: bool = False,\n is_traceback: bool = False,\n ) -> JSONResponse:\n if is_display_all:\n msg = \", \".join([f\"'{item['loc'][1] if len(item['loc']) > 1 else item['loc'][0]}' {item['msg'].lower()}\" for item in exc.errors()]) # noqa: E501\n else:\n _first_error = exc.errors()[0]\n msg = f\"'{_first_error['loc'][1] if len(_first_error['loc']) > 1 else _first_error['loc'][0]}' {_first_error['msg'].lower()}\" # noqa: E501\n lmsg = f'- \"{request.method} {request.url.path}\" {Status.PARAMS_ERROR.code} {msg}'\n if is_traceback:\n lmsg = traceback.format_exc()\n g.logger.error(lmsg)\n return Response.failure(\n msg=msg,\n status=Status.PARAMS_ERROR,\n request=request,\n )\n",
29
- "app/middleware/headers.py": "import uuid\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.requests import Request\n\nfrom app.initializer.context import request_id_ctx_var\n\n\nclass HeadersMiddleware(BaseHTTPMiddleware):\n \"\"\"\u5934\u5904\u7406\u4e2d\u95f4\u4ef6\"\"\"\n _HEADERS = {\n # \u53ef\u6dfb\u52a0\u76f8\u5173\u5934\n }\n\n async def dispatch(self, request: Request, call_next):\n request_id = self._get_or_create_request_id(request)\n request.state.request_id = request_id\n ctx_token = request_id_ctx_var.set(request_id)\n try:\n response = await call_next(request)\n response.headers[\"X-Request-ID\"] = request_id\n for key, value in self._HEADERS.items():\n if key not in response.headers:\n response.headers[key] = value\n return response\n finally:\n request_id_ctx_var.reset(ctx_token)\n\n @staticmethod\n def _get_or_create_request_id(request: Request) -> str:\n request_id = request.headers.get(\"X-Request-ID\")\n if not request_id:\n request_id = f\"req-{uuid.uuid4()}\"\n return request_id\n",
30
- "app/middleware/__init__.py": "\"\"\"\n\u4e2d\u95f4\u4ef6\n\"\"\"\nfrom fastapi import FastAPI\nfrom fastapi.exceptions import RequestValidationError\nfrom starlette.exceptions import HTTPException\n\nfrom app.api.exception import CustomException\nfrom app.middleware.cors import Cors\nfrom app.middleware.exception import ExceptionHandler\nfrom app.middleware.headers import HeadersMiddleware\n\n\ndef register_middlewares(app: FastAPI):\n \"\"\"\u6ce8\u518c\u4e2d\u95f4\u4ef6\"\"\"\n app.add_middleware(HeadersMiddleware) # type: ignore\n app.add_middleware(\n middleware_class=Cors.middleware_class,\n allow_origins=Cors.allow_origins,\n allow_credentials=Cors.allow_credentials,\n allow_methods=Cors.allow_methods,\n allow_headers=Cors.allow_headers,\n )\n app.add_exception_handler(CustomException, ExceptionHandler.custom_exception_handler) # type: ignore\n app.add_exception_handler(HTTPException, ExceptionHandler.http_exception_handler) # type: ignore\n app.add_exception_handler(RequestValidationError, ExceptionHandler.validation_exception_handler) # type: ignore\n",
31
- "app/model/user.py": "from sqlalchemy import Column, BigInteger, Integer, String\nfrom toollib.utils import now2timestamp\n\nfrom app.model import DeclBase\nfrom app.initializer import g\n\n\nclass User(DeclBase):\n __tablename__ = \"user\"\n\n id = Column(String(20), primary_key=True, default=g.snow_cli.gen_uid, comment=\"\u4e3b\u952e\")\n phone = Column(String(15), unique=True, index=True, nullable=False, comment=\"\u624b\u673a\u53f7\")\n password = Column(String(128), nullable=True, comment=\"\u5bc6\u7801\")\n jwt_key = Column(String(128), nullable=True, comment=\"jwtKey\")\n name = Column(String(50), nullable=True, comment=\"\u540d\u79f0\")\n age = Column(Integer, nullable=True, comment=\"\u5e74\u9f84\")\n gender = Column(Integer, nullable=True, comment=\"\u6027\u522b\")\n created_at = Column(BigInteger, default=now2timestamp, comment=\"\u521b\u5efa\u65f6\u95f4\")\n updated_at = Column(BigInteger, default=now2timestamp, onupdate=now2timestamp, comment=\"\u66f4\u65b0\u65f6\u95f4\")\n",
32
- "app/model/__init__.py": "\"\"\"\n\u6570\u636e\u6a21\u578b\n\"\"\"\nfrom sqlalchemy.orm import declarative_base\n\nDeclBase = declarative_base()\n",
33
- "app/schema/user.py": "import re\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field, field_validator\n\nfrom app.schema import filter_fields\n\n\nclass UserDetail(BaseModel):\n id: str = Field(...)\n # #\n phone: str = None\n name: str = None\n age: int = None\n gender: int = None\n created_at: int = None\n updated_at: int = None\n\n @classmethod\n def response_fields(cls):\n return filter_fields(\n cls,\n exclude=[]\n )\n\n\nclass UserList(BaseModel):\n page: int = Field(1, ge=1)\n size: int = Field(10, ge=1)\n # #\n id: str = None\n phone: str = None\n name: str = None\n age: int = None\n gender: int = None\n created_at: int = None\n updated_at: int = None\n\n @classmethod\n def response_fields(cls):\n return filter_fields(\n cls,\n exclude=[\n \"page\",\n \"size\",\n ]\n )\n\n\nclass UserCreate(BaseModel):\n phone: str = Field(..., pattern=r\"^1[3-9]\\d{9}$\")\n password: str = Field(...)\n name: str | None = Field(None)\n age: int | None = Field(None, ge=0, le=200)\n gender: Literal[1, 2] | None = Field(None)\n\n @field_validator(\"password\")\n def validate_password(cls, v):\n if not re.match(r\"^(?=.*[A-Za-z])(?=.*\\d)\\S{6,20}$\", v):\n raise ValueError(\"\u5bc6\u7801\u5fc5\u987b\u5305\u542b\u81f3\u5c11\u4e00\u4e2a\u5b57\u6bcd\u548c\u4e00\u4e2a\u6570\u5b57\uff0c\u957f\u5ea6\u4e3a6-20\u4f4d\u7684\u975e\u7a7a\u767d\u5b57\u7b26\u7ec4\u5408\")\n return v\n\n @field_validator(\"name\")\n def validate_name(cls, v, info):\n if not v and (phone := info.data.get(\"phone\")):\n return f\"\u7528\u6237{phone[-4:]}\"\n if v and not re.match(r\"^[\\u4e00-\\u9fffA-Za-z0-9_\\-.]{1,50}$\", v):\n raise ValueError(\"\u540d\u79f0\u4ec5\u96501-50\u4f4d\u7684\u4e2d\u6587\u3001\u82f1\u6587\u3001\u6570\u5b57\u3001_-.\u7ec4\u5408\")\n return v\n\n\nclass UserUpdate(BaseModel):\n name: str | None = Field(None)\n age: int | None = Field(None, ge=0, le=200)\n gender: Literal[1, 2] | None = Field(None)\n\n @field_validator(\"name\")\n def validate_name(cls, v):\n if v and not re.match(r\"^[\\u4e00-\\u9fffA-Za-z0-9_\\-.]{1,50}$\", v):\n raise ValueError(\"\u540d\u79f0\u4ec5\u96501-50\u4f4d\u7684\u4e2d\u6587\u3001\u82f1\u6587\u3001\u6570\u5b57\u3001_-.\u7ec4\u5408\")\n return v\n\n\nclass UserDelete(BaseModel):\n pass\n\n\nclass UserLogin(BaseModel):\n phone: str = Field(...)\n password: str = Field(...)\n\n\nclass UserToken(BaseModel):\n id: str = Field(...)\n exp_minutes: int = Field(24 * 60 * 30, ge=1)\n",
34
- "app/schema/__init__.py": "\"\"\"\n\u6570\u636e\u7ed3\u6784\n\"\"\"\n\n\ndef filter_fields(\n model,\n exclude: list = None,\n):\n if exclude:\n return list(set(model.model_fields.keys()) - set(exclude))\n return list(model.model_fields.keys())\n",
35
- "app/service/user.py": "from app.model.user import User\nfrom app.schema.user import (\n UserDetail,\n UserList,\n UserCreate,\n UserUpdate,\n UserDelete,\n UserLogin,\n UserToken,\n)\nfrom app.initializer import g\nfrom app.utils import auth, db_async\n\n\nclass UserDetailSvc(UserDetail):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserDetail\"\n }\n }\n\n async def detail(self):\n async with g.db_async_session() as session:\n data = await db_async.query_one(\n session=session,\n model=User,\n fields=self.response_fields(),\n filter_by={\"id\": self.id},\n )\n return data\n\n\nclass UserListSvc(UserList):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserList\"\n }\n }\n\n async def lst(self):\n async with g.db_async_session() as session:\n data = await db_async.query_all(\n session=session,\n model=User,\n fields=self.response_fields(),\n page=self.page,\n size=self.size,\n )\n total = await db_async.query_total(session, User)\n return data, total\n\n\nclass UserCreateSvc(UserCreate):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserCreate\"\n }\n }\n\n async def create(self):\n async with g.db_async_session() as session:\n return await db_async.create(\n session=session,\n model=User,\n data={\n \"name\": self.name,\n \"phone\": self.phone,\n \"age\": self.age,\n \"gender\": self.gender,\n \"password\": auth.hash_password(self.password),\n \"jwt_key\": auth.gen_jwt_key(),\n },\n filter_by={\"phone\": self.phone},\n )\n\n\nclass UserUpdateSvc(UserUpdate):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserUpdate\"\n }\n }\n\n async def update(self, user_id: str):\n async with g.db_async_session() as session:\n return await db_async.update(\n session=session,\n model=User,\n data=self.model_dump(),\n filter_by={\"id\": user_id},\n )\n\n\nclass UserDeleteSvc(UserDelete):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserDelete\"\n }\n }\n\n @staticmethod\n async def delete(user_id: str):\n async with g.db_async_session() as session:\n return await db_async.delete(\n session=session,\n model=User,\n filter_by={\"id\": user_id},\n )\n\n\nclass UserLoginSvc(UserLogin):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserLogin\"\n }\n }\n\n async def login(self):\n async with g.db_async_session() as session:\n data = await db_async.query_one(\n session=session,\n model=User,\n filter_by={\"phone\": self.phone},\n )\n if not data or not auth.verify_password(self.password, data.get(\"password\")):\n return None\n new_jwt_key = auth.gen_jwt_key()\n token = auth.gen_jwt(\n payload={\n \"id\": data.get(\"id\"),\n \"phone\": data.get(\"phone\"),\n \"name\": data.get(\"name\"),\n \"age\": data.get(\"age\"),\n \"gender\": data.get(\"gender\"),\n },\n jwt_key=new_jwt_key,\n exp_minutes=24 * 60 * 30,\n )\n # \u66f4\u65b0jwt_key\n await db_async.update(\n session=session,\n model=User,\n data={\"jwt_key\": new_jwt_key},\n filter_by={\"phone\": self.phone},\n )\n return token\n\n\nclass UserTokenSvc(UserToken):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserToken\"\n }\n }\n\n async def token(self):\n async with g.db_async_session() as session:\n data = await db_async.query_one(\n session=session,\n model=User,\n filter_by={\"id\": self.id},\n )\n if not data:\n return None\n new_jwt_key = auth.gen_jwt_key()\n token = auth.gen_jwt(\n payload={\n \"id\": data.get(\"id\"),\n \"phone\": data.get(\"phone\"),\n \"name\": data.get(\"name\"),\n \"age\": data.get(\"age\"),\n \"gender\": data.get(\"gender\"),\n },\n jwt_key=new_jwt_key,\n exp_minutes=self.exp_minutes,\n )\n # \u66f4\u65b0jwt_key\n await db_async.update(\n session=session,\n model=User,\n data={\"jwt_key\": new_jwt_key},\n filter_by={\"id\": self.id},\n )\n return token\n",
36
- "app/service/__init__.py": "\"\"\"\n\u4e1a\u52a1\u903b\u8f91\n\"\"\"\n",
37
- "app/utils/auth.py": "import secrets\nfrom datetime import datetime, timedelta\n\nimport bcrypt\nimport jwt\n\n_ALGORITHM = \"HS256\"\n\n\ndef gen_jwt(payload: dict, jwt_key: str, exp_minutes: int = 24 * 60 * 30):\n payload.update({\"exp\": datetime.utcnow() + timedelta(minutes=exp_minutes)})\n encoded_jwt = jwt.encode(payload=payload, key=jwt_key, algorithm=_ALGORITHM)\n return encoded_jwt\n\n\ndef verify_jwt(token: str, jwt_key: str = None) -> dict:\n if not jwt_key:\n return jwt.decode(jwt=token, options={\"verify_signature\": False})\n return jwt.decode(jwt=token, key=jwt_key, algorithms=[_ALGORITHM])\n\n\ndef gen_jwt_key():\n return secrets.token_hex(16)\n\n\ndef hash_password(password: str) -> str:\n salt = bcrypt.gensalt()\n hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt)\n return hashed_password.decode('utf-8')\n\n\ndef verify_password(password: str, hashed_password: str) -> bool:\n return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))\n",
38
- "app/utils/db_async.py": "from sqlalchemy import (\n select,\n func,\n update as update_,\n delete as delete_,\n)\n\n\ndef format_all(\n rows,\n fields: list[str],\n) -> list[dict]:\n if not rows:\n return list()\n return [dict(zip(fields, row)) for row in rows]\n\n\ndef format_one(\n row,\n fields: list[str],\n) -> dict:\n if not row:\n return dict()\n return dict(zip(fields, row))\n\n\ndef model_dict(\n model,\n fields: list[str] = None,\n) -> dict:\n if not model:\n return dict()\n if not fields:\n fields = [field.name for field in model.__table__.columns]\n return {field: getattr(model, field) for field in fields}\n\n\nasync def query_one(\n session,\n model,\n fields: list[str] = None,\n filter_by: dict = None,\n) -> dict:\n if not fields:\n fields = [field.name for field in model.__table__.columns]\n query = select(*[getattr(model, field) for field in fields if hasattr(model, field)]).select_from(model)\n if filter_by:\n query = query.filter_by(**filter_by)\n result = await session.execute(query)\n return format_one(result.fetchone(), fields)\n\n\nasync def query_all(\n session,\n model,\n fields: list[str] = None,\n filter_by: dict = None,\n page: int = None,\n size: int = None,\n) -> list[dict]:\n if not fields:\n fields = [field.name for field in model.__table__.columns]\n query = select(*[getattr(model, field) for field in fields if hasattr(model, field)]).select_from(model)\n if filter_by:\n query = query.filter_by(**filter_by)\n if page and size:\n query = query.offset((page - 1) * size).limit(size)\n result = await session.execute(query)\n return format_all(result.fetchall(), fields)\n\n\nasync def query_total(\n session,\n model,\n filter_by: dict = None,\n) -> int:\n query = select(func.count()).select_from(model)\n if filter_by:\n query = query.filter_by(**filter_by)\n result = await session.execute(query)\n return result.scalar()\n\n\nasync def create(\n session,\n model,\n data: dict,\n filter_by: dict = None,\n) -> int:\n try:\n if filter_by:\n result = await query_one(session, model, filter_by=filter_by)\n if result:\n return 0\n stmt = model(**data)\n session.add(stmt)\n await session.commit()\n except Exception:\n await session.rollback()\n raise\n return stmt.id\n\n\nasync def update(\n session,\n model,\n data: dict,\n filter_by: dict | None,\n is_exclude_none: bool = True,\n) -> list:\n try:\n if is_exclude_none:\n data = {k: v for k, v in data.items() if v is not None}\n stmt = update_(model).values(**data)\n if filter_by:\n stmt = stmt.filter_by(**filter_by)\n if session.bind.dialect.name == \"postgresql\":\n stmt = stmt.returning(model.id)\n result = await session.execute(stmt)\n updated_ids = [row[0] for row in result]\n else:\n query_stmt = select(model.id).filter_by(**filter_by)\n result = await session.execute(query_stmt)\n updated_ids = result.scalars().all()\n if updated_ids:\n await session.execute(stmt)\n await session.commit()\n except Exception:\n await session.rollback()\n raise\n return updated_ids\n\n\nasync def delete(\n session,\n model,\n filter_by: dict | None,\n) -> list:\n try:\n stmt = delete_(model)\n if filter_by:\n stmt = stmt.filter_by(**filter_by)\n if session.bind.dialect.name == \"postgresql\":\n stmt = stmt.returning(model.id)\n result = await session.execute(stmt)\n deleted_ids = [row[0] for row in result]\n else:\n query_stmt = select(model.id).filter_by(**filter_by)\n result = await session.execute(query_stmt)\n deleted_ids = result.scalars().all()\n if deleted_ids:\n await session.execute(stmt)\n await session.commit()\n except Exception:\n await session.rollback()\n raise\n return deleted_ids\n",
28
+ "app/initializer/context.py": "from contextvars import ContextVar\n\nrequest_id_var: ContextVar[str] = ContextVar(\"request_id\", default=\"N/A\")\n",
29
+ "app/initializer/_conf.py": "import os\nfrom pathlib import Path\n\nimport yaml\nfrom dotenv import load_dotenv\nfrom toollib.utils import get_cls_attrs, parse_variable\n\nfrom app import APP_DIR\n\n_CONFIG_DIR = APP_DIR.parent.joinpath(\"config\")\n\nload_dotenv(dotenv_path=os.environ.setdefault(\n key=\"env_path\",\n value=str(_CONFIG_DIR.joinpath(\".env\")))\n)\n# #\napp_yaml = Path(\n os.environ.get(\"app_yaml\") or\n _CONFIG_DIR.joinpath(f\"app_{os.environ.setdefault(key='app_env', value='dev')}.yaml\")\n)\nif not app_yaml.is_file():\n raise RuntimeError(f\"\u914d\u7f6e\u6587\u4ef6\u4e0d\u5b58\u5728\uff1a{app_yaml}\")\n\n\nclass Config:\n \"\"\"\u914d\u7f6e\"\"\"\n _yaml_conf: dict = None\n app_dir: Path = APP_DIR\n # #\n app_env: str = \"dev\"\n app_yaml: Path = app_yaml\n api_keys: list = []\n snow_datacenter_id: int = None\n # #\n app_title: str = \"xApp\"\n app_summary: str = \"xxApp\"\n app_description: str = \"xxxApp\"\n app_version: str = \"1.0.0\"\n app_debug: bool = True\n app_log_serialize: bool = False\n app_log_basedir: str = \"./logs\"\n app_disable_docs: bool = False\n app_allow_credentials: bool = True\n app_allow_origins: list = [\"*\"]\n app_allow_methods: list = [\"*\"]\n app_allow_headers: list = [\"*\"]\n # #\n db_url: str = None\n db_async_url: str = None\n redis_host: str = None\n redis_port: int = None\n redis_db: int = None\n redis_password: str = None\n redis_max_connections: int = None\n\n def setup(self):\n for k, item in get_cls_attrs(Config).items():\n v_type, v = item\n if callable(v_type):\n if k in os.environ: # \u4f18\u5148\u73af\u5883\u53d8\u91cf\n v = parse_variable(k=k, v_type=v_type, v_from=os.environ, default=v)\n else:\n v = parse_variable(k=k, v_type=v_type, v_from=self.load_yaml(), default=v)\n setattr(self, k, v)\n return self\n\n def load_yaml(self, reload: bool = False) -> dict:\n if self._yaml_conf and not reload:\n return self._yaml_conf\n with open(app_yaml, mode=\"r\", encoding=\"utf-8\") as file:\n self._yaml_conf = yaml.load(file, Loader=yaml.FullLoader)\n return self._yaml_conf\n\n\ndef init_config() -> Config:\n return Config().setup()\n",
30
+ "app/initializer/_db.py": "import asyncio\nimport importlib\nimport re\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncSession\nfrom sqlalchemy.orm import sessionmaker, scoped_session\nfrom sqlalchemy.orm.decl_api import DeclarativeAttributeIntercept\n\nfrom app import APP_DIR\n\n_MODELS_MOD_DIR = APP_DIR.joinpath(\"models\")\n_MODELS_MOD_BASE = \"app.models\"\n_DECL_BASE_NAME = \"DeclBase\"\n_TABLES_CREATED = False\n\n\ndef init_db_session(\n db_url: str,\n db_echo: bool,\n db_pool_size: int = 10,\n db_max_overflow: int = 5,\n db_pool_recycle: int = 3600,\n is_create_tables: bool = False,\n) -> scoped_session:\n db_echo = db_echo or False\n kwargs = {\n \"pool_size\": db_pool_size,\n \"max_overflow\": db_max_overflow,\n \"pool_recycle\": db_pool_recycle,\n }\n if db_url.startswith(\"sqlite\"):\n kwargs = {}\n engine = create_engine(\n url=db_url,\n echo=db_echo,\n echo_pool=db_echo,\n **kwargs,\n )\n db_session = sessionmaker(engine, expire_on_commit=False)\n\n def create_tables():\n decl_base = _import_tables()\n if decl_base:\n try:\n decl_base.metadata.create_all(engine) # noqa\n except Exception as e:\n if \"already exists\" not in str(e):\n raise\n\n global _TABLES_CREATED\n if is_create_tables and not _TABLES_CREATED:\n create_tables()\n _TABLES_CREATED = True\n\n return scoped_session(db_session)\n\n\ndef init_db_async_session(\n db_url: str,\n db_echo: bool,\n db_pool_size: int = 10,\n db_max_overflow: int = 5,\n db_pool_recycle: int = 3600,\n is_create_tables: bool = False,\n) -> sessionmaker:\n db_echo = db_echo or False\n kwargs = {\n \"pool_size\": db_pool_size,\n \"max_overflow\": db_max_overflow,\n \"pool_recycle\": db_pool_recycle,\n }\n if db_url.startswith(\"sqlite\"):\n kwargs = {}\n async_engine = create_async_engine(\n url=db_url,\n echo=db_echo,\n echo_pool=db_echo,\n **kwargs,\n )\n db_async_session = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) # noqa\n\n async def create_tables():\n decl_base = _import_tables()\n if decl_base:\n async with async_engine.begin() as conn:\n try:\n await conn.run_sync(decl_base.metadata.create_all) # noqa\n except Exception as e:\n if \"already exists\" not in str(e):\n raise\n\n global _TABLES_CREATED\n if is_create_tables and not _TABLES_CREATED:\n try:\n loop = asyncio.get_running_loop()\n except RuntimeError:\n loop = asyncio.new_event_loop()\n asyncio.set_event_loop(loop)\n task = loop.create_task(create_tables())\n task.add_done_callback(lambda t: t.result() if not t.cancelled() else None)\n if not loop.is_running():\n loop.run_until_complete(task)\n _TABLES_CREATED = True\n return db_async_session\n\n\ndef _import_tables() -> DeclarativeAttributeIntercept | None:\n decl_base = getattr(importlib.import_module(_MODELS_MOD_BASE), _DECL_BASE_NAME, None)\n if isinstance(decl_base, DeclarativeAttributeIntercept):\n pat = re.compile(rf\"^\\s*class\\s+[A-Za-z_]\\w*\\s*\\(\\s*{_DECL_BASE_NAME}\\s*\\)\\s*:\", re.MULTILINE)\n for f in _MODELS_MOD_DIR.rglob(\"*.py\"):\n if f.name.startswith(\"__\"):\n continue\n if pat.search(f.read_text(\"utf-8\")):\n rel = f.relative_to(_MODELS_MOD_DIR).with_suffix(\"\")\n _ = importlib.import_module(f\"{_MODELS_MOD_BASE}.{'.'.join(rel.parts)}\")\n return decl_base\n",
31
+ "app/initializer/_log.py": "import os\n\nfrom loguru._logger import Logger # noqa\nfrom toollib import logu\n\nfrom app.initializer.context import request_id_var\n\n\ndef init_logger(\n level: str,\n serialize: bool = False,\n basedir: str = None,\n) -> Logger:\n enable_console, enable_file = True, True\n if os.getenv(\"app_env\") == \"prod\":\n enable_console, enable_file = False, True # \u6309\u9700\u8c03\u6574\n _logger = logu.init_logger(\n level=level,\n request_id_var=request_id_var,\n serialize=serialize,\n enable_console=enable_console,\n enable_file=enable_file,\n basedir=basedir,\n )\n # _logger.add \u53ef\u6dfb\u52a0\u5176\u4ed6 handler\n return _logger\n",
32
+ "app/initializer/_redis.py": "from toollib.rediser import RedisClient\n\n\ndef init_redis_client(\n host: str,\n port: int,\n db: int,\n password: str = None,\n max_connections: int = None,\n **kwargs,\n) -> RedisClient:\n if not host:\n return RedisClient()\n return RedisClient(\n host=host,\n port=port,\n db=db,\n password=password,\n max_connections=max_connections,\n **kwargs,\n )\n",
33
+ "app/initializer/_snow.py": "import os\n\nfrom loguru import logger\nfrom toollib.guid import SnowFlake\nfrom toollib.rediser import RedisClient\nfrom toollib.utils import localip\n\n_CACHE_KEY_SNOW_WORKER_ID_INCR = \"config:snow_worker_id_incr\"\n_CACHE_KEY_SNOW_DATACENTER_ID_INCR = \"config:snow_datacenter_id_incr\"\n_CACHE_EXPIRE_SNOW = 120\n\n\ndef init_snow_client(\n redis_client: RedisClient,\n datacenter_id: int = None,\n to_str: bool = True,\n) -> SnowFlake: # \u5efa\u8bae\uff1a\u91c7\u7528\u670d\u52a1\u7684\u65b9\u5f0f\u8c03\u7528api\u83b7\u53d6\n if datacenter_id is None:\n datacenter_id = _snow_incr(redis_client, _CACHE_KEY_SNOW_DATACENTER_ID_INCR, _CACHE_EXPIRE_SNOW)\n if datacenter_id is None:\n local_ip = localip()\n if local_ip:\n ip_parts = list(map(int, local_ip.split('.')))\n ip_int = (ip_parts[0] << 24) + (ip_parts[1] << 16) + (ip_parts[2] << 8) + ip_parts[3]\n datacenter_id = ip_int % 32\n worker_id = _snow_incr(redis_client, _CACHE_KEY_SNOW_WORKER_ID_INCR, _CACHE_EXPIRE_SNOW)\n if worker_id is None:\n worker_id = os.getpid() % 32\n return SnowFlake(worker_id=worker_id, datacenter_id=datacenter_id, to_str=to_str)\n\n\ndef _snow_incr(redis_client, cache_key: str, cache_expire: int):\n incr = None\n try:\n with redis_client.connection() as r:\n resp = r.ping()\n if resp:\n lua_script = \"\"\"\n if redis.call('exists', KEYS[1]) == 1 then\n redis.call('expire', KEYS[1], ARGV[1])\n return redis.call('incr', KEYS[1])\n else\n redis.call('set', KEYS[1], 0)\n redis.call('expire', KEYS[1], ARGV[1])\n return 0\n end\n \"\"\"\n incr = r.eval(lua_script, 1, cache_key, cache_expire)\n except Exception as e:\n logger.warning(f\"snow\u521d\u59cb\u5316id\u5c06\u91c7\u7528\u672c\u5730\u65b9\u5f0f\uff0c\u7531\u4e8e\uff08{e}\uff09\")\n return incr\n",
34
+ "app/initializer/__init__.py": "\"\"\"\n\u521d\u59cb\u5316\n\"\"\"\nimport threading\nfrom functools import cached_property\n\nfrom loguru import logger\nfrom loguru._logger import Logger # noqa\nfrom sqlalchemy.orm import sessionmaker, scoped_session\nfrom toollib.guid import SnowFlake\nfrom toollib.rediser import RedisClient\nfrom toollib.utils import Singleton\n\nfrom app.initializer._conf import Config, init_config\nfrom app.initializer._db import init_db_session, init_db_async_session\nfrom app.initializer._log import init_logger\nfrom app.initializer._redis import init_redis_client\nfrom app.initializer._snow import init_snow_client\n\n\nclass G(metaclass=Singleton):\n \"\"\"\n \u5168\u5c40\u53d8\u91cf\n \"\"\"\n _initialized = False\n _init_lock = threading.Lock()\n _init_properties = [\n 'config',\n 'logger',\n 'redis_client',\n 'snow_client',\n # 'db_session',\n 'db_async_session',\n ]\n\n def __init__(self):\n self._initialized = False\n\n @cached_property\n def config(self) -> Config:\n return init_config()\n\n @cached_property\n def logger(self) -> Logger:\n return init_logger(\n level=\"DEBUG\" if self.config.app_debug else \"INFO\",\n serialize=self.config.app_log_serialize,\n basedir=self.config.app_log_basedir,\n )\n\n @cached_property\n def redis_client(self) -> RedisClient:\n return init_redis_client(\n host=self.config.redis_host,\n port=self.config.redis_port,\n db=self.config.redis_db,\n password=self.config.redis_password,\n max_connections=self.config.redis_max_connections,\n )\n\n @cached_property\n def snow_client(self) -> SnowFlake:\n return init_snow_client(\n redis_client=self.redis_client,\n datacenter_id=self.config.snow_datacenter_id,\n )\n\n @cached_property\n def db_session(self) -> scoped_session:\n return init_db_session(\n db_url=self.config.db_url,\n db_echo=self.config.app_debug,\n is_create_tables=True,\n )\n\n @cached_property\n def db_async_session(self) -> sessionmaker:\n return init_db_async_session(\n db_url=self.config.db_async_url,\n db_echo=self.config.app_debug,\n is_create_tables=True,\n )\n\n def setup(self):\n with self._init_lock:\n if not self._initialized:\n for prop_name in self._init_properties:\n if hasattr(self, prop_name):\n getattr(self, prop_name)\n else:\n logger.warning(f\"{prop_name} not found\")\n self._initialized = True\n\n\ng = G()\n",
35
+ "app/middleware/cors.py": "from starlette.middleware.cors import CORSMiddleware\n\nfrom app.initializer import g\n\n\nclass CorsMiddleware(CORSMiddleware):\n def __init__(self, app, **kwargs):\n super().__init__(\n app,\n allow_credentials=g.config.app_allow_credentials,\n allow_origins=g.config.app_allow_origins,\n allow_methods=g.config.app_allow_methods,\n allow_headers=g.config.app_allow_headers,\n **kwargs\n )\n",
36
+ "app/middleware/exceptions.py": "from fastapi.exceptions import RequestValidationError\nfrom loguru import logger\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse\n\nfrom app.api.exceptions import CustomException\nfrom app.api.responses import Responses\nfrom app.api.status import Status\n\n\nclass ExceptionsHandler:\n\n @staticmethod\n async def custom_exception_handler(\n request: Request,\n exc: CustomException,\n is_traceback: bool = True,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {exc.code} {exc.msg}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n msg=exc.msg,\n code=exc.code,\n error=exc,\n data=exc.data,\n )\n\n @staticmethod\n async def request_validation_handler(\n request: Request,\n exc: RequestValidationError,\n display_all: bool = False,\n is_traceback: bool = True,\n ) -> JSONResponse:\n if display_all:\n msg = \" & \".join([\n f\"{error['loc'][-1]} ({error['type']}) {error['msg'].replace('Value error, ', '').lower()}\"\n for error in exc.errors()\n ])\n else:\n error = exc.errors()[0]\n msg = f\"{error['loc'][-1]} ({error['type']}) {error['msg'].replace('Value error, ', '').lower()}\"\n lmsg = f'- \"{request.method} {request.url.path}\" {Status.PARAMS_ERROR.code} {msg}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n msg=msg,\n error=exc,\n status=Status.PARAMS_ERROR,\n )\n\n @staticmethod\n async def http_exception_handler(\n request: Request,\n exc: HTTPException,\n is_traceback: bool = True,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {exc.status_code} {exc.detail}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n msg=exc.detail,\n code=exc.status_code,\n error=exc,\n )\n",
37
+ "app/middleware/http.py": "import uuid\n\nfrom loguru import logger\nfrom starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint\nfrom starlette.requests import Request\nfrom starlette.responses import Response, JSONResponse\n\nfrom app.api.responses import Responses\nfrom app.api.status import Status\nfrom app.initializer.context import request_id_var\n\n\nclass HttpMiddleware(BaseHTTPMiddleware):\n _HEADERS = {\n # \u53ef\u6dfb\u52a0\u76f8\u5173\u5934\n }\n\n async def dispatch(\n self, request: Request,\n call_next: RequestResponseEndpoint,\n ) -> Response:\n request_id = self._get_or_create_request_id(request)\n request.state.request_id = request_id\n token = request_id_var.set(request_id)\n try:\n response = await call_next(request)\n response.headers[\"X-Request-ID\"] = request_id\n for key, value in self._HEADERS.items():\n if key not in response.headers:\n response.headers[key] = value\n return response\n except Exception as exc:\n return await self.handle_exception(request, exc)\n finally:\n request_id_var.reset(token)\n\n @staticmethod\n def _get_or_create_request_id(request: Request, prefix: str = \"req-\") -> str:\n request_id = request.headers.get(\"X-Request-ID\")\n if not request_id:\n request_id = f\"{prefix}{uuid.uuid4().hex}\"\n return request_id\n\n @staticmethod\n async def handle_exception(\n request: Request,\n exc: Exception,\n is_traceback: bool = True,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {Status.INTERNAL_SERVER_ERROR.code} {type(exc).__name__}: {exc}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n error=exc,\n status=Status.INTERNAL_SERVER_ERROR,\n )\n",
38
+ "app/middleware/__init__.py": "\"\"\"\n\u4e2d\u95f4\u4ef6\n\"\"\"\nfrom fastapi import FastAPI\nfrom fastapi.exceptions import RequestValidationError\nfrom starlette.exceptions import HTTPException\n\nfrom app.api.exceptions import CustomException\nfrom app.middleware.cors import CorsMiddleware\nfrom app.middleware.exceptions import ExceptionsHandler\nfrom app.middleware.http import HttpMiddleware\n\n\ndef register_middlewares(app: FastAPI):\n \"\"\"\u6ce8\u518c\u4e2d\u95f4\u4ef6 & \u9519\u8bef\u5904\u7406\"\"\"\n app.add_middleware(CorsMiddleware) # type: ignore\n app.add_middleware(HttpMiddleware) # type: ignore\n # #\n app.add_exception_handler(CustomException, ExceptionsHandler.custom_exception_handler) # type: ignore\n app.add_exception_handler(RequestValidationError, ExceptionsHandler.request_validation_handler) # type: ignore\n app.add_exception_handler(HTTPException, ExceptionsHandler.http_exception_handler) # type: ignore\n",
39
+ "app/models/user.py": "from sqlalchemy import Column, BigInteger, Integer, String\nfrom toollib.utils import now2timestamp\n\nfrom app.initializer import g\nfrom app.models import DeclBase\n\n\nclass User(DeclBase):\n __tablename__ = \"user\"\n\n id = Column(String(20), primary_key=True, default=g.snow_client.gen_uid, comment=\"\u4e3b\u952e\")\n phone = Column(String(15), unique=True, index=True, nullable=False, comment=\"\u624b\u673a\u53f7\")\n password = Column(String(128), nullable=True, comment=\"\u5bc6\u7801\")\n jwt_key = Column(String(128), nullable=True, comment=\"jwtKey\")\n name = Column(String(50), nullable=True, comment=\"\u540d\u79f0\")\n age = Column(Integer, nullable=True, comment=\"\u5e74\u9f84\")\n gender = Column(Integer, nullable=True, comment=\"\u6027\u522b\")\n created_at = Column(BigInteger, default=now2timestamp, comment=\"\u521b\u5efa\u65f6\u95f4\")\n updated_at = Column(BigInteger, default=now2timestamp, onupdate=now2timestamp, comment=\"\u66f4\u65b0\u65f6\u95f4\")\n",
40
+ "app/models/__init__.py": "\"\"\"\n\u6570\u636e\u6a21\u578b\n\"\"\"\nfrom sqlalchemy.orm import DeclarativeBase\n\n\nclass DeclBase(DeclarativeBase):\n pass\n",
41
+ "app/schemas/user.py": "import re\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field, field_validator\n\nfrom app.schemas import filter_fields\n\n\nclass UserDetail(BaseModel):\n id: str = Field(...)\n # #\n phone: str = None\n name: str = None\n age: int = None\n gender: int = None\n created_at: int = None\n updated_at: int = None\n\n @classmethod\n def response_fields(cls):\n return filter_fields(\n cls,\n exclude=[]\n )\n\n\nclass UserList(BaseModel):\n page: int = Field(1, ge=1)\n size: int = Field(10, ge=1)\n # #\n id: str = None\n phone: str = None\n name: str = None\n age: int = None\n gender: int = None\n created_at: int = None\n updated_at: int = None\n\n @classmethod\n def response_fields(cls):\n return filter_fields(\n cls,\n exclude=[\n \"page\",\n \"size\",\n ]\n )\n\n\nclass UserCreate(BaseModel):\n phone: str = Field(..., pattern=r\"^1[3-9]\\d{9}$\")\n password: str = Field(...)\n name: str | None = Field(None)\n age: int | None = Field(None, ge=0, le=200)\n gender: Literal[1, 2] | None = Field(None)\n\n @field_validator(\"password\")\n def validate_password(cls, v):\n if not re.match(r\"^(?=.*[A-Za-z])(?=.*\\d)\\S{6,20}$\", v):\n raise ValueError(\"\u5bc6\u7801\u5fc5\u987b\u5305\u542b\u81f3\u5c11\u4e00\u4e2a\u5b57\u6bcd\u548c\u4e00\u4e2a\u6570\u5b57\uff0c\u957f\u5ea6\u4e3a6-20\u4f4d\u7684\u975e\u7a7a\u767d\u5b57\u7b26\u7ec4\u5408\")\n return v\n\n @field_validator(\"name\")\n def validate_name(cls, v, info):\n if not v and (phone := info.data.get(\"phone\")):\n return f\"\u7528\u6237{phone[-4:]}\"\n if v and not re.match(r\"^[\\u4e00-\\u9fffA-Za-z0-9_\\-.]{1,50}$\", v):\n raise ValueError(\"\u540d\u79f0\u4ec5\u96501-50\u4f4d\u7684\u4e2d\u6587\u3001\u82f1\u6587\u3001\u6570\u5b57\u3001_-.\u7ec4\u5408\")\n return v\n\n\nclass UserUpdate(BaseModel):\n name: str | None = Field(None)\n age: int | None = Field(None, ge=0, le=200)\n gender: Literal[1, 2] | None = Field(None)\n\n @field_validator(\"name\")\n def validate_name(cls, v):\n if v and not re.match(r\"^[\\u4e00-\\u9fffA-Za-z0-9_\\-.]{1,50}$\", v):\n raise ValueError(\"\u540d\u79f0\u4ec5\u96501-50\u4f4d\u7684\u4e2d\u6587\u3001\u82f1\u6587\u3001\u6570\u5b57\u3001_-.\u7ec4\u5408\")\n return v\n\n\nclass UserDelete(BaseModel):\n pass\n\n\nclass UserLogin(BaseModel):\n phone: str = Field(...)\n password: str = Field(...)\n\n\nclass UserToken(BaseModel):\n id: str = Field(...)\n exp_minutes: int = Field(24 * 60 * 30, ge=1)\n",
42
+ "app/schemas/__init__.py": "\"\"\"\n\u6570\u636e\u7ed3\u6784\n\"\"\"\n\n\ndef filter_fields(\n model,\n exclude: list = None,\n):\n if exclude:\n return list(set(model.model_fields.keys()) - set(exclude))\n return list(model.model_fields.keys())\n",
43
+ "app/services/user.py": "from app.initializer import g\nfrom app.models.user import User\nfrom app.schemas.user import (\n UserDetail,\n UserList,\n UserCreate,\n UserUpdate,\n UserDelete,\n UserLogin,\n UserToken,\n)\nfrom app.utils import jwt_util, db_async_util\n\n\nclass UserDetailSvc(UserDetail):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserDetail\"\n }\n }\n\n async def detail(self):\n async with g.db_async_session() as session:\n data = await db_async_util.fetch_one(\n session=session,\n model=User,\n fields=self.response_fields(),\n filter_by={\"id\": self.id},\n )\n return data\n\n\nclass UserListSvc(UserList):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserList\"\n }\n }\n\n async def lst(self):\n async with g.db_async_session() as session:\n data = await db_async_util.fetch_all(\n session=session,\n model=User,\n fields=self.response_fields(),\n page=self.page,\n size=self.size,\n )\n total = await db_async_util.fetch_total(session, User)\n return data, total\n\n\nclass UserCreateSvc(UserCreate):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserCreate\"\n }\n }\n\n async def create(self):\n async with g.db_async_session() as session:\n return await db_async_util.create(\n session=session,\n model=User,\n data={\n \"name\": self.name,\n \"phone\": self.phone,\n \"age\": self.age,\n \"gender\": self.gender,\n \"password\": jwt_util.hash_password(self.password),\n \"jwt_key\": jwt_util.gen_jwt_key(),\n },\n filter_by={\"phone\": self.phone},\n )\n\n\nclass UserUpdateSvc(UserUpdate):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserUpdate\"\n }\n }\n\n async def update(self, user_id: str):\n async with g.db_async_session() as session:\n return await db_async_util.update(\n session=session,\n model=User,\n data=self.model_dump(),\n filter_by={\"id\": user_id},\n )\n\n\nclass UserDeleteSvc(UserDelete):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserDelete\"\n }\n }\n\n @staticmethod\n async def delete(user_id: str):\n async with g.db_async_session() as session:\n return await db_async_util.delete(\n session=session,\n model=User,\n filter_by={\"id\": user_id},\n )\n\n\nclass UserLoginSvc(UserLogin):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserLogin\"\n }\n }\n\n async def login(self):\n async with g.db_async_session() as session:\n data = await db_async_util.fetch_one(\n session=session,\n model=User,\n filter_by={\"phone\": self.phone},\n )\n if not data or not jwt_util.verify_password(self.password, data.get(\"password\")):\n return None\n new_jwt_key = jwt_util.gen_jwt_key()\n token = jwt_util.gen_jwt(\n payload={\n \"id\": data.get(\"id\"),\n \"phone\": data.get(\"phone\"),\n \"name\": data.get(\"name\"),\n \"age\": data.get(\"age\"),\n \"gender\": data.get(\"gender\"),\n },\n jwt_key=new_jwt_key,\n exp_minutes=24 * 60 * 30,\n )\n # \u66f4\u65b0jwt_key\n await db_async_util.update(\n session=session,\n model=User,\n data={\"jwt_key\": new_jwt_key},\n filter_by={\"phone\": self.phone},\n )\n return token\n\n\nclass UserTokenSvc(UserToken):\n model_config = {\n \"json_schema_extra\": {\n \"title\": \"UserToken\"\n }\n }\n\n async def token(self):\n async with g.db_async_session() as session:\n data = await db_async_util.fetch_one(\n session=session,\n model=User,\n filter_by={\"id\": self.id},\n )\n if not data:\n return None\n new_jwt_key = jwt_util.gen_jwt_key()\n token = jwt_util.gen_jwt(\n payload={\n \"id\": data.get(\"id\"),\n \"phone\": data.get(\"phone\"),\n \"name\": data.get(\"name\"),\n \"age\": data.get(\"age\"),\n \"gender\": data.get(\"gender\"),\n },\n jwt_key=new_jwt_key,\n exp_minutes=self.exp_minutes,\n )\n # \u66f4\u65b0jwt_key\n await db_async_util.update(\n session=session,\n model=User,\n data={\"jwt_key\": new_jwt_key},\n filter_by={\"id\": self.id},\n )\n return token\n",
44
+ "app/services/__init__.py": "\"\"\"\n\u4e1a\u52a1\u903b\u8f91\n\"\"\"\n",
45
+ "app/utils/api_key_util.py": "import secrets\n\n_API_KEY_LENGTH = 45\n\n\ndef gen_api_key(prefix: str = \"\", length: int = _API_KEY_LENGTH) -> str:\n api_key = secrets.token_urlsafe(length)[:length]\n if prefix:\n return f\"{prefix}_{api_key}\"\n return api_key\n\n\nif __name__ == '__main__':\n print(gen_api_key())\n",
46
+ "app/utils/db_async_util.py": "from sqlalchemy import (\n select,\n func,\n inspect,\n text,\n update as update_,\n delete as delete_,\n)\n\n\ndef format_all(\n rows,\n fields: list[str],\n) -> list[dict]:\n if not rows:\n return list()\n return [dict(zip(fields, row)) for row in rows]\n\n\ndef format_one(\n row,\n fields: list[str],\n) -> dict:\n if not row:\n return {}\n return dict(zip(fields, row))\n\n\ndef model_dict(\n model,\n fields: list[str] = None,\n) -> dict:\n if not model:\n return {}\n if not fields:\n fields = [field.name for field in model.__table__.columns]\n return {field: getattr(model, field) for field in fields}\n\n\nasync def fetch_one(\n session,\n model,\n fields: list[str] = None,\n filter_by: dict = None,\n) -> dict:\n if not fields:\n fields = [field.name for field in model.__table__.columns]\n query = select(*[getattr(model, field) for field in fields if hasattr(model, field)]).select_from(model)\n if filter_by:\n query = query.filter_by(**filter_by)\n result = await session.execute(query)\n return format_one(result.fetchone(), fields)\n\n\nasync def fetch_all(\n session,\n model,\n fields: list[str] = None,\n filter_by: dict = None,\n page: int = None,\n size: int = None,\n) -> list[dict]:\n if not fields:\n fields = [field.name for field in model.__table__.columns]\n query = select(*[getattr(model, field) for field in fields if hasattr(model, field)]).select_from(model)\n if filter_by:\n query = query.filter_by(**filter_by)\n if page and size:\n query = query.offset((page - 1) * size).limit(size)\n result = await session.execute(query)\n return format_all(result.fetchall(), fields)\n\n\nasync def fetch_total(\n session,\n model,\n filter_by: dict = None,\n column: str = None,\n) -> int:\n if column is None:\n primary_key = inspect(model).primary_key\n if primary_key:\n column = primary_key[0]\n else:\n column = 1\n query = select(func.count(column)).select_from(model)\n if filter_by:\n query = query.filter_by(**filter_by)\n result = await session.execute(query)\n return result.scalar()\n\n\nasync def create(\n session,\n model,\n data: dict,\n filter_by: dict = None,\n) -> int:\n try:\n if filter_by:\n result = await fetch_one(session, model, filter_by=filter_by)\n if result:\n return 0\n stmt = model(**data)\n session.add(stmt)\n await session.commit()\n except Exception:\n await session.rollback()\n raise\n return stmt.id\n\n\nasync def update(\n session,\n model,\n data: dict,\n filter_by: dict | None,\n is_exclude_none: bool = True,\n) -> list:\n try:\n if is_exclude_none:\n data = {k: v for k, v in data.items() if v is not None}\n stmt = update_(model).values(**data)\n if filter_by:\n stmt = stmt.filter_by(**filter_by)\n if session.bind.dialect.name == \"postgresql\":\n stmt = stmt.returning(model.id)\n result = await session.execute(stmt)\n updated_ids = [row[0] for row in result]\n else:\n query_stmt = select(model.id).filter_by(**filter_by)\n result = await session.execute(query_stmt)\n updated_ids = result.scalars().all()\n if updated_ids:\n await session.execute(stmt)\n await session.commit()\n except Exception:\n await session.rollback()\n raise\n return updated_ids\n\n\nasync def delete(\n session,\n model,\n filter_by: dict | None,\n) -> list:\n try:\n stmt = delete_(model)\n if filter_by:\n stmt = stmt.filter_by(**filter_by)\n if session.bind.dialect.name == \"postgresql\":\n stmt = stmt.returning(model.id)\n result = await session.execute(stmt)\n deleted_ids = [row[0] for row in result]\n else:\n query_stmt = select(model.id).filter_by(**filter_by)\n result = await session.execute(query_stmt)\n deleted_ids = result.scalars().all()\n if deleted_ids:\n await session.execute(stmt)\n await session.commit()\n except Exception:\n await session.rollback()\n raise\n return deleted_ids\n\n\nasync def sqlfetch_one(\n session,\n sql: str,\n params: dict = None,\n) -> dict:\n result = await session.execute(text(sql), params)\n row = result.fetchone()\n if row is None:\n return {}\n return row._asdict() # noqa\n\n\nasync def sqlfetch_all(\n session,\n sql: str,\n params: dict = None,\n) -> list[dict]:\n result = await session.execute(text(sql), params)\n rows = result.fetchall()\n return [row._asdict() for row in rows] # noqa\n",
47
+ "app/utils/jwt_util.py": "import secrets\nfrom datetime import datetime, timedelta\n\nimport bcrypt\nimport jwt\n\n_JWT_ALGORITHM = \"HS256\"\n\n\ndef gen_jwt(payload: dict, jwt_key: str, exp_minutes: int = 24 * 60 * 30, algorithm: str = _JWT_ALGORITHM):\n payload.update({\"exp\": datetime.utcnow() + timedelta(minutes=exp_minutes)})\n encoded_jwt = jwt.encode(payload=payload, key=jwt_key, algorithm=algorithm)\n return encoded_jwt\n\n\ndef verify_jwt(token: str, jwt_key: str = None, algorithms: tuple = (_JWT_ALGORITHM,)) -> dict:\n if not jwt_key:\n return jwt.decode(jwt=token, options={\"verify_signature\": False})\n return jwt.decode(jwt=token, key=jwt_key, algorithms=algorithms)\n\n\ndef gen_jwt_key():\n return secrets.token_hex(16)\n\n\ndef hash_password(password: str) -> str:\n salt = bcrypt.gensalt()\n hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt)\n return hashed_password.decode('utf-8')\n\n\ndef verify_password(password: str, hashed_password: str) -> bool:\n return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))\n",
39
48
  "app/utils/__init__.py": "\"\"\"\nutils\n\"\"\"\n",
40
- "config/.env": "# -----\u5747\u53ef\u76f4\u63a5\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf-----\n# -----\u5747\u53ef\u76f4\u63a5\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf-----\n# -----\u5747\u53ef\u76f4\u63a5\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf-----\n# \u5e94\u7528\u73af\u5883\uff08\u5b9a\u4f4dyaml\u914d\u7f6e\uff09\napp_env=dev\n# \u5e94\u7528\u914d\u7f6e\uff08\u6307\u5b9ayaml\u914d\u7f6e\uff0c\u4f18\u4e8e`app_env`\u5b9a\u4f4d\uff09\napp_yaml=\n# \u96ea\u82b1\u7b97\u6cd5\u6570\u636e\u4e2d\u5fc3id\uff08\u53d6\u503c\uff1a0-31\uff0c\u5728\u5206\u5e03\u5f0f\u90e8\u7f72\u65f6\u9700\u786e\u4fdd\u6bcf\u4e2a\u8282\u70b9\u7684\u53d6\u503c\u4e0d\u540c\uff09\nsnow_datacenter_id=0",
41
- "config/app_dev.yaml": "# \u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\napp_title: xApp-dev\napp_summary: xxApp-dev\napp_description: xxxApp-dev\napp_version: 1.0.0\napp_debug: true\napp_log_dir: ./logs\napp_disable_docs: false\napp_allow_origins:\n - '*'\n# #\nredis_host:\nredis_port:\nredis_db:\nredis_password:\nredis_max_connections:\ndb_url: sqlite:///app_dev.sqlite\ndb_async_url: sqlite+aiosqlite:///app_dev.sqlite\n",
42
- "config/app_prod.yaml": "# \u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\napp_title: xApp-prod\napp_summary: xxApp-prod\napp_description: xxxApp-prod\napp_version: 1.0.0\napp_debug: false\napp_log_dir: ./logs\napp_disable_docs: true\napp_allow_origins:\n - '*'\n# #\nredis_host:\nredis_port:\nredis_db:\nredis_password:\nredis_max_connections:\ndb_url: sqlite:///app_prod.sqlite\ndb_async_url: sqlite+aiosqlite:///app_prod.sqlite\n",
43
- "config/app_test.yaml": "# \u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\napp_title: xApp-test\napp_summary: xxApp-test\napp_description: xxxApp-test\napp_version: 1.0.0\napp_debug: true\napp_log_dir: ./logs\napp_disable_docs: false\napp_allow_origins:\n - '*'\n# #\nredis_host:\nredis_port:\nredis_db:\nredis_password:\nredis_max_connections:\ndb_url: sqlite:///app_test.sqlite\ndb_async_url: sqlite+aiosqlite:///app_test.sqlite\n",
44
- "deploy/.gitkeep": "",
49
+ "app_celery/conf.py": "import os\nfrom pathlib import Path\n\nimport yaml\nfrom dotenv import load_dotenv\nfrom toollib.utils import get_cls_attrs, parse_variable\n\n_APP_DIR = Path(__file__).absolute().parent\n_CONFIG_DIR = _APP_DIR.parent.joinpath(\"config\")\n\nload_dotenv(dotenv_path=os.environ.setdefault(\n key=\"env_path\",\n value=str(_CONFIG_DIR.joinpath(\".env\")))\n)\n# #\napp_yaml = Path(\n os.environ.get(\"app_yaml\") or\n _CONFIG_DIR.joinpath(f\"app_{os.environ.setdefault(key='app_env', value='dev')}.yaml\")\n)\nif not app_yaml.is_file():\n raise RuntimeError(f\"\u914d\u7f6e\u6587\u4ef6\u4e0d\u5b58\u5728\uff1a{app_yaml}\")\n\n\nclass Config:\n \"\"\"\u914d\u7f6e\"\"\"\n _yaml_conf: dict = None\n app_dir: Path = _APP_DIR\n # #\n app_env: str = \"dev\"\n app_yaml: Path = app_yaml\n # #\n celery_broker_url: str\n celery_backend_url: str\n celery_timezone: str = \"Asia/Shanghai\"\n celery_enable_utc: bool = True\n celery_task_serializer: str = \"json\"\n celery_result_serializer: str = \"json\"\n celery_accept_content: list = [\"json\"]\n celery_task_ignore_result: bool = False\n celery_result_expire: int = 86400\n celery_task_track_started: bool = True\n celery_worker_concurrency: int = 8\n celery_worker_prefetch_multiplier: int = 2\n celery_worker_max_tasks_per_child: int = 100\n celery_broker_connection_retry_on_startup: bool = True\n celery_task_reject_on_worker_lost: bool = True\n\n def setup(self):\n for k, item in get_cls_attrs(Config).items():\n v_type, v = item\n if callable(v_type):\n if k in os.environ: # \u4f18\u5148\u73af\u5883\u53d8\u91cf\n v = parse_variable(k=k, v_type=v_type, v_from=os.environ, default=v)\n else:\n v = parse_variable(k=k, v_type=v_type, v_from=self.load_yaml(), default=v)\n setattr(self, k, v)\n return self\n\n def load_yaml(self, reload: bool = False) -> dict:\n if self._yaml_conf and not reload:\n return self._yaml_conf\n with open(app_yaml, mode=\"r\", encoding=\"utf-8\") as file:\n self._yaml_conf = yaml.load(file, Loader=yaml.FullLoader)\n return self._yaml_conf\n\n\nconfig = Config().setup()\n",
50
+ "app_celery/README.md": "# app-celery\n\n## \u7b80\u4ecb\n\n### producer\uff1a\u751f\u4ea7\u8005\uff08\u53d1\u5e03\u4efb\u52a1\uff09\n\n- register\uff1a\u6ce8\u518c\u4e2d\u5fc3\n - \u5c06`consumer`\u7684`tasks`\u6ce8\u518c\u5230`producer`\u7684`register`\u4e2d\n- publisher\uff1a\u53d1\u5e03\u8005\n - \u9879\u76ee\u4e2d\u901a\u8fc7`publisher.publish`\u6765\u53d1\u5e03\u4efb\u52a1\n\n### consumer\uff1a\u6d88\u8d39\u8005\uff08\u6267\u884c\u4efb\u52a1\uff09\n\n- tasks: \u4efb\u52a1\n - \u5b9a\u65f6\u4efb\u52a1\uff08beat_xxx\uff09\n - 1\u3002\u521b\u5efa\u5b9a\u65f6\u4efb\u52a1\n - 2\u3002\u53d1\u5e03\u5b9a\u65f6\u4efb\u52a1\uff08\u901a\u8fc7celery\u5185\u90e8\u7684`beat`\u8c03\u7528\uff09\n - \u8fdb\u5165`app_celery`\u7236\u7ea7\u76ee\u5f55\uff0c\u5373\u5de5\u4f5c\u76ee\u5f55\n - \u542f\u52a8\u547d\u4ee4\uff1a\uff08\u66f4\u591a\u53c2\u6570\u8bf7\u81ea\u884c\u6307\u5b9a\uff09\n - \u65b9\u5f0f1\u3002\u76f4\u63a5\u6267\u884c\u811a\u672c: `python runcbeat.py --celery-module=app_celery`\n - \u65b9\u5f0f2\u3002\u4f7f\u7528\u547d\u4ee4\u884c\uff1a`celery -A app_celery.consumer beat --loglevel=info --max-interval=5`\n - 3\u3002\u542f\u52a8\u6d88\u8d39\u8005worker\n - \u5f02\u6b65\u4efb\u52a1\uff08xxx)\n - 1\u3002\u521b\u5efa\u5f02\u6b65\u4efb\u52a1\uff0c\u5e76\u6ce8\u518c\u5230`producer`\u7684`register`\uff0c\u6839\u636e\u6ce8\u518c\u7684\u89c4\u5219\u8fdb\u884c`\u4efb\u52a1\u8c03\u7528`\u548c`worker\u542f\u52a8`\n - 2\u3002\u53d1\u5e03\u5f02\u6b65\u4efb\u52a1\uff08\u901a\u8fc7\u751f\u4ea7\u8005\u7684`publisher.publish`\u8c03\u7528\uff09\n - 3\u3002\u542f\u52a8\u6d88\u8d39\u8005worker\n- workers: \u5de5\u4f5c\u8005\n - 1\u3002\u521b\u5efaworker\u670d\u52a1\uff0c\u5b9a\u4e49\u961f\u5217\u7b49\u5c5e\u6027\uff08\u4e3a\u65b9\u4fbf\u6269\u5c55\u5efa\u8bae\u4e00\u7c7b\u4efb\u52a1\u4e00\u4e2a\u670d\u52a1\uff09\n - 2\u3002\u542f\u52a8worker\u670d\u52a1\uff1a\n - 1\u3002\u8fdb\u5165`app_celery`\u7236\u7ea7\u76ee\u5f55\uff0c\u5373\u5de5\u4f5c\u76ee\u5f55\n - 2\u3002\u542f\u52a8\u547d\u4ee4\uff1a\uff08\u66f4\u591a\u53c2\u6570\u8bf7\u81ea\u884c\u6307\u5b9a\uff09\n - \u65b9\u5f0f1\u3002\u76f4\u63a5\u6267\u884c\u811a\u672c: `python runcworker.py -n ping --celery-module=app_celery`\n - \u65b9\u5f0f2\u3002\u4f7f\u7528\u547d\u4ee4\u884c\uff1a`celery -A app_celery.consumer.workers.ping worker --loglevel=info --concurrency=5`\n- yaml\u914d\u7f6e\n\n```yaml\ncelery_broker_url: redis://:<password>@<host>:<port>/<db>\ncelery_backend_url: redis://:<password>@<host>:<port>/<db>\ncelery_timezone: Asia/Shanghai\ncelery_enable_utc: true\ncelery_task_serializer: json\ncelery_result_serializer: json\ncelery_accept_content: [ json ]\ncelery_task_ignore_result: false\ncelery_result_expire: 86400\ncelery_task_track_started: true\ncelery_worker_concurrency: 8\ncelery_worker_prefetch_multiplier: 2\ncelery_worker_max_tasks_per_child: 100\ncelery_broker_connection_retry_on_startup: true\ncelery_task_reject_on_worker_lost: true\n```\n\n- \u6d88\u8d39\u7aef\u4f9d\u8d56\n\n```text\ncelery\nredis\n```\n\n### \u6ce8\u610f\uff1a\n\n- \u6700\u597d\u4e0e`app`\u89e3\u8026\uff0c\u5373\uff1a\n - \u53ea`app`\u5355\u5411\u8c03\u7528`app_celery`\n - \u4f46`app_celery`\u4e0d\u8c03\u7528`app`\n",
51
+ "app_celery/requirements.txt": "# -*- coding: utf-8 -*-\n# Python>=3.11\ncelery==5.5.3\nredis==7.0.1\ngevent==25.9.1\ntoollib==1.9.1\npython-dotenv==1.2.1\nPyYAML==6.0.3\npydantic==2.12.4\n",
52
+ "app_celery/__init__.py": "\"\"\"\n@author axiner\n@version v0.0.1\n@created 2025/09/20 10:10\n@abstract app-celery\n@description\n@history\n\"\"\"\nfrom celery import Celery\n\nfrom app_celery.conf import config\n\n\ndef make_celery(include: list = None, configs: dict = None):\n app = Celery(\n main=\"app_celery\",\n broker=config.celery_broker_url,\n backend=config.celery_backend_url,\n include=include,\n )\n app.conf.update(\n timezone=config.celery_timezone,\n enable_utc=config.celery_enable_utc,\n task_serializer=config.celery_task_serializer,\n result_serializer=config.celery_result_serializer,\n accept_content=config.celery_accept_content,\n celery_task_ignore_result=config.celery_task_ignore_result,\n celery_result_expire=config.celery_result_expire,\n celery_task_track_started=config.celery_task_track_started,\n worker_concurrency=config.celery_worker_concurrency,\n worker_prefetch_multiplier=config.celery_worker_prefetch_multiplier,\n worker_max_tasks_per_child=config.celery_worker_max_tasks_per_child,\n broker_connection_retry_on_startup=config.celery_broker_connection_retry_on_startup,\n task_reject_on_worker_lost=config.celery_task_reject_on_worker_lost,\n )\n if configs:\n app.conf.update(configs)\n return app\n",
53
+ "app_celery/consumer/__init__.py": "\"\"\"\n\u6d88\u8d39\u8005\n\"\"\"\nimport re\nfrom pathlib import Path\n\nfrom app_celery import make_celery\n\n\ndef autodiscover_task_modules(\n task_name: str = \"tasks\",\n task_module: str = \"app_celery.consumer.tasks\",\n) -> list:\n \"\"\"\n \u81ea\u52a8\u53d1\u73b0\u4efb\u52a1\u6a21\u5757\n - \u53ef\u5728\u6a21\u5757\u4e2d\u52a0\u5165`_active = False`\u6765\u53d6\u6d88\u6fc0\u6d3b\n \"\"\"\n task_modules = []\n active_pat = re.compile(r\"^_active\\s*=\\s*False\\s*(?:#.*)?$\", re.MULTILINE)\n for p in Path(__file__).parent.joinpath(task_name).rglob(\"*.py\"):\n if p.stem == \"__init__\":\n continue\n if active_pat.search(p.read_text(encoding=\"utf-8\")):\n continue\n task_modules.append(f\"{task_module}.{p.stem}\")\n return task_modules\n\n\ncelery_app = make_celery(\n include=autodiscover_task_modules()\n)\n",
54
+ "app_celery/consumer/tasks/beat_ping.py": "import logging\n\nfrom celery.schedules import crontab\n\nfrom app_celery.consumer import celery_app\n\nlogger = logging.getLogger(__name__)\n\ncelery_app.conf.beat_schedule.setdefault(\n 'beat_ping', {\n 'task': 'app_celery.consumer.tasks.beat_ping.ping',\n 'schedule': crontab(minute='*/2'), # \u6bcfx\u5206\u949f\u6267\u884c\u4e00\u6b21\n 'options': {'queue': 'beat_ping'}\n }\n)\n\n\n@celery_app.task(\n bind=True,\n autoretry_for=(Exception,),\n max_retries=3,\n retry_backoff=True,\n retry_backoff_max=300,\n retry_jitter=True,\n time_limit=360,\n soft_time_limit=300,\n acks_late=True,\n)\ndef ping(self, text: str = \"\u8fd9\u662f\u4e00\u4e2a\u5b9a\u65f6\u4efb\u52a1\u6d4b\u8bd5\"):\n logger.info(f\"pong: {text}\")\n",
55
+ "app_celery/consumer/tasks/ping.py": "import logging\n\nfrom app_celery.consumer import celery_app\n\nlogger = logging.getLogger(__name__)\n\n\n@celery_app.task(\n bind=True,\n autoretry_for=(Exception,),\n max_retries=3,\n retry_backoff=True,\n retry_backoff_max=300,\n retry_jitter=True,\n time_limit=360,\n soft_time_limit=300,\n acks_late=True,\n)\ndef ping(self, text: str = \"\u8fd9\u662f\u4e00\u4e2a\u5f02\u6b65\u4efb\u52a1\u6d4b\u8bd5\"):\n logger.info(f\"pong: {text}\")\n",
56
+ "app_celery/consumer/tasks/__init__.py": "\"\"\"\n\u4efb\u52a1\uff08\u5b9a\u65f6&\u5f02\u6b65\uff09\n\"\"\"\n",
57
+ "app_celery/consumer/workers/beat_ping.py": "from app_celery.consumer import celery_app\n\ncelery_app.conf.update(\n task_queues={\n \"beat_ping\": {\n \"exchange_type\": \"direct\",\n \"exchange\": \"beat_ping\",\n \"routing_key\": \"beat_ping\",\n },\n },\n task_routes={\n \"app_celery.consumer.tasks.beat_ping.ping\": {\"queue\": \"beat_ping\"},\n }\n)\n",
58
+ "app_celery/consumer/workers/ping.py": "from app_celery.consumer import celery_app\n\ncelery_app.conf.update(\n task_queues={\n \"ping\": {\n \"exchange_type\": \"direct\",\n \"exchange\": \"ping\",\n \"routing_key\": \"ping\",\n },\n },\n task_routes={\n \"app_celery.consumer.tasks.ping.ping\": {\"queue\": \"ping\"},\n }\n)\n",
59
+ "app_celery/consumer/workers/__init__.py": "\"\"\"\n\u5de5\u4f5c\u8005\n\"\"\"",
60
+ "app_celery/producer/publisher.py": "import logging\n\nfrom app_celery.producer import celery_app\nfrom app_celery.producer.registry import AllTasks\n\nlogger = logging.getLogger(__name__)\n\n\ndef publish(\n task_label: str,\n task_args: tuple = None,\n task_kwargs: dict = None,\n task_id: str = None,\n **task_options,\n) -> str:\n \"\"\"\u53d1\u5e03\u4efb\u52a1\"\"\"\n if task_label not in AllTasks:\n raise ValueError(f\"UNKNOWN TASK: {task_label}\")\n task_params = AllTasks[task_label]\n task_options_merged = task_params.options or {}\n task_options_merged.update(task_options)\n result = celery_app.send_task(\n name=task_params.name,\n args=task_args,\n kwargs=task_kwargs,\n task_id=task_id,\n queue=task_params.queue, # enforced queue consistency\n **task_options_merged,\n )\n logger.info(f\"PUBLISH TASK: {task_params.name} | ID={result.id} | QUEUE={task_params.queue}\")\n return result.id\n",
61
+ "app_celery/producer/registry.py": "from pydantic import BaseModel\n\n\nclass TaskParams(BaseModel):\n name: str\n queue: str\n options: dict = {}\n\n\nAllTasks: dict[str, TaskParams] = { # label: TaskParams\n \"ping\": TaskParams(\n name=\"app_celery.consumer.tasks.ping.ping\",\n queue=\"ping\"\n ),\n}\n",
62
+ "app_celery/producer/tests.py": "import unittest\n\nfrom app_celery.producer.publisher import publish\n\n\nclass TestPublisher(unittest.TestCase):\n\n def test_publish_ping(self):\n publish(\"ping\")\n",
63
+ "app_celery/producer/__init__.py": "\"\"\"\n\u751f\u4ea7\u8005\n\"\"\"\nfrom app_celery import make_celery\n\ncelery_app = make_celery()\n",
64
+ "config/.env": "# ------- \u5747\u53ef\u76f4\u63a5\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf -------\n# ------- \u5747\u53ef\u76f4\u63a5\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf -------\n# ------- \u5747\u53ef\u76f4\u63a5\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf -------\n# \u5e94\u7528\u73af\u5883\uff08\u5b9a\u4f4dyaml\u914d\u7f6e\uff09\napp_env=dev\n# \u5e94\u7528\u914d\u7f6e\uff08\u6307\u5b9ayaml\u914d\u7f6e\uff0c\u4f18\u4e8e`app_env`\u5b9a\u4f4d\uff09\napp_yaml=\n# ------- Config -------\napi_keys=\n# \u96ea\u82b1\u7b97\u6cd5\u6570\u636e\u4e2d\u5fc3id\uff08\u53d6\u503c\uff1a0-31\uff0c\u5728\u5206\u5e03\u5f0f\u90e8\u7f72\u65f6\u9700\u786e\u4fdd\u6bcf\u4e2a\u8282\u70b9\u7684\u53d6\u503c\u4e0d\u540c\uff09\nsnow_datacenter_id=0\n",
65
+ "config/app_dev.yaml": "# \u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\napp_title: xApp-dev\napp_summary: xxApp-dev\napp_description: xxxApp-dev\napp_version: 1.0.0\napp_debug: true\napp_log_serialize: false\napp_log_basedir: ./logs\napp_disable_docs: false\napp_allow_credentials: true\napp_allow_origins:\n - '*'\napp_allow_methods:\n - '*'\napp_allow_headers:\n - '*'\n# #\ndb_url: sqlite:///app_dev.sqlite\ndb_async_url: sqlite+aiosqlite:///app_dev.sqlite\nredis_host:\nredis_port:\nredis_db:\nredis_password:\nredis_max_connections:\n# #\ncelery_broker_url: redis://:<password>@<host>:<port>/<db>\ncelery_backend_url: redis://:<password>@<host>:<port>/<db>\ncelery_timezone: Asia/Shanghai\ncelery_enable_utc: true\ncelery_task_serializer: json\ncelery_result_serializer: json\ncelery_accept_content: [ json ]\ncelery_task_ignore_result: false\ncelery_result_expire: 86400\ncelery_task_track_started: true\ncelery_worker_concurrency: 8\ncelery_worker_prefetch_multiplier: 2\ncelery_worker_max_tasks_per_child: 100\ncelery_broker_connection_retry_on_startup: true\ncelery_task_reject_on_worker_lost: true\n",
66
+ "config/app_prod.yaml": "# \u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\napp_title: xApp-prod\napp_summary: xxApp-prod\napp_description: xxxApp-prod\napp_version: 1.0.0\napp_debug: false\napp_log_serialize: false\napp_log_basedir: ./logs\napp_disable_docs: true\napp_allow_credentials: true\napp_allow_origins:\n - '*'\napp_allow_methods:\n - '*'\napp_allow_headers:\n - '*'\n# #\ndb_url: sqlite:///app_prod.sqlite\ndb_async_url: sqlite+aiosqlite:///app_prod.sqlite\nredis_host:\nredis_port:\nredis_db:\nredis_password:\nredis_max_connections:\n# #\ncelery_broker_url: redis://:<password>@<host>:<port>/<db>\ncelery_backend_url: redis://:<password>@<host>:<port>/<db>\ncelery_timezone: Asia/Shanghai\ncelery_enable_utc: true\ncelery_task_serializer: json\ncelery_result_serializer: json\ncelery_accept_content: [ json ]\ncelery_task_ignore_result: false\ncelery_result_expire: 86400\ncelery_task_track_started: true\ncelery_worker_concurrency: 8\ncelery_worker_prefetch_multiplier: 2\ncelery_worker_max_tasks_per_child: 100\ncelery_broker_connection_retry_on_startup: true\ncelery_task_reject_on_worker_lost: true\n",
67
+ "config/app_test.yaml": "# \u8bf7\u6839\u636e\u81ea\u8eab\u9700\u6c42\u4fee\u6539\napp_title: xApp-test\napp_summary: xxApp-test\napp_description: xxxApp-test\napp_version: 1.0.0\napp_debug: true\napp_log_serialize: false\napp_log_basedir: ./logs\napp_disable_docs: false\napp_allow_credentials: true\napp_allow_origins:\n - '*'\napp_allow_methods:\n - '*'\napp_allow_headers:\n - '*'\n# #\ndb_url: sqlite:///app_test.sqlite\ndb_async_url: sqlite+aiosqlite:///app_test.sqlite\nredis_host:\nredis_port:\nredis_db:\nredis_password:\nredis_max_connections:\n# #\ncelery_broker_url: redis://:<password>@<host>:<port>/<db>\ncelery_backend_url: redis://:<password>@<host>:<port>/<db>\ncelery_timezone: Asia/Shanghai\ncelery_enable_utc: true\ncelery_task_serializer: json\ncelery_result_serializer: json\ncelery_accept_content: [ json ]\ncelery_task_ignore_result: false\ncelery_result_expire: 86400\ncelery_task_track_started: true\ncelery_worker_concurrency: 8\ncelery_worker_prefetch_multiplier: 2\ncelery_worker_max_tasks_per_child: 100\ncelery_broker_connection_retry_on_startup: true\ncelery_task_reject_on_worker_lost: true\n",
68
+ "config/nginx.conf": "upstream backend_upstream {\n server backend:8000;\n keepalive 32;\n}\n\nserver {\n listen 80;\n\n client_max_body_size 20M;\n keepalive_timeout 30s;\n keepalive_requests 1000;\n\n # \u5b89\u5168\u5934\n add_header X-Content-Type-Options \"nosniff\" always;\n add_header X-Frame-Options \"DENY\" always;\n add_header X-XSS-Protection \"1; mode=block\" always;\n add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always;\n\n location /api/ {\n proxy_pass http://backend_upstream; # \u6ce8\u610f\uff1a\u7ed3\u5c3e\u65e0 /\uff0clocation \u6709 /\n\n proxy_http_version 1.1;\n proxy_set_header Connection \"\";\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n\n proxy_connect_timeout 5s;\n proxy_send_timeout 60s;\n proxy_read_timeout 60s;\n\n proxy_buffering on;\n proxy_buffers 8 16k;\n proxy_buffer_size 32k;\n proxy_busy_buffers_size 64k;\n\n # gzip \u538b\u7f29\n gzip on;\n gzip_vary on;\n gzip_min_length 1024;\n gzip_types\n application/json\n application/javascript\n text/css\n text/plain;\n }\n\n # \u62d2\u7edd\u5176\u4ed6\u8def\u5f84\n location / {\n return 404;\n }\n\n # \u53ef\u9009\uff1a\u5065\u5eb7\u68c0\u67e5\u7aef\u70b9\uff08\u4f9b\u5916\u90e8 LB \u4f7f\u7528\uff09\n location /healthz {\n access_log off;\n return 200 \"OK\\n\";\n }\n}\n",
45
69
  "docs/.gitkeep": "",
46
70
  "logs/.gitkeep": "",
47
- "tests/__init__.py": "\"\"\"\n\u6d4b\u8bd5\n\"\"\"\n"
71
+ "tests/__init__.py": "\"\"\"\n\u6d4b\u8bd5\n\"\"\"\n",
72
+ "app/tiny_initializer.py": "\"\"\"\n\u521d\u59cb\u5316\n\"\"\"\nimport os\nimport threading\nfrom contextvars import ContextVar\nfrom functools import cached_property\nfrom pathlib import Path\n\nimport yaml\nfrom dotenv import load_dotenv\nfrom loguru import logger\nfrom loguru._logger import Logger # noqa\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncSession\nfrom sqlalchemy.orm import scoped_session, sessionmaker\nfrom toollib import logu\nfrom toollib.utils import Singleton, get_cls_attrs, parse_variable\n\nfrom app import APP_DIR\n\n__all__ = [\n \"g\",\n \"request_id_var\",\n]\n\n_CONFIG_DIR = APP_DIR.parent.joinpath(\"config\")\n\nload_dotenv(dotenv_path=os.environ.setdefault(\n key=\"env_path\",\n value=str(_CONFIG_DIR.joinpath(\".env\")))\n)\n# #\napp_yaml = Path(\n os.environ.get(\"app_yaml\") or\n _CONFIG_DIR.joinpath(f\"app_{os.environ.setdefault(key='app_env', value='dev')}.yaml\")\n)\nif not app_yaml.is_file():\n raise RuntimeError(f\"\u914d\u7f6e\u6587\u4ef6\u4e0d\u5b58\u5728\uff1a{app_yaml}\")\n\n\nclass Config:\n \"\"\"\u914d\u7f6e\"\"\"\n _yaml_conf: dict = None\n app_dir: Path = APP_DIR\n # #\n app_env: str = \"dev\"\n app_yaml: Path = app_yaml\n api_keys: list = []\n # #\n app_title: str = \"xApp\"\n app_summary: str = \"xxApp\"\n app_description: str = \"xxxApp\"\n app_version: str = \"1.0.0\"\n app_debug: bool = True\n app_log_serialize: bool = False\n app_log_basedir: str = \"./logs\"\n app_disable_docs: bool = False\n app_allow_credentials: bool = True\n app_allow_origins: list = [\"*\"]\n app_allow_methods: list = [\"*\"]\n app_allow_headers: list = [\"*\"]\n # #\n db_url: str = None\n db_async_url: str = None\n\n def setup(self):\n for k, item in get_cls_attrs(Config).items():\n v_type, v = item\n if callable(v_type):\n if k in os.environ: # \u4f18\u5148\u73af\u5883\u53d8\u91cf\n v = parse_variable(k=k, v_type=v_type, v_from=os.environ, default=v)\n else:\n v = parse_variable(k=k, v_type=v_type, v_from=self.load_yaml(), default=v)\n setattr(self, k, v)\n return self\n\n def load_yaml(self, reload: bool = False) -> dict:\n if self._yaml_conf and not reload:\n return self._yaml_conf\n with open(app_yaml, mode=\"r\", encoding=\"utf-8\") as file:\n self._yaml_conf = yaml.load(file, Loader=yaml.FullLoader)\n return self._yaml_conf\n\n\ndef init_logger(\n level: str,\n serialize: bool = False,\n basedir: str = None,\n) -> Logger:\n enable_console, enable_file = True, True\n if os.getenv(\"app_env\") == \"prod\":\n enable_console, enable_file = False, True # \u6309\u9700\u8c03\u6574\n _logger = logu.init_logger(\n level=level,\n request_id_var=request_id_var,\n serialize=serialize,\n enable_console=enable_console,\n enable_file=enable_file,\n basedir=basedir,\n )\n # _logger.add \u53ef\u6dfb\u52a0\u5176\u4ed6 handler\n return _logger\n\n\ndef init_db_session(\n db_url: str,\n db_echo: bool,\n db_pool_size: int = 10,\n db_max_overflow: int = 5,\n db_pool_recycle: int = 3600,\n) -> scoped_session:\n db_echo = db_echo or False\n kwargs = {\n \"pool_size\": db_pool_size,\n \"max_overflow\": db_max_overflow,\n \"pool_recycle\": db_pool_recycle,\n }\n if db_url.startswith(\"sqlite\"):\n kwargs = {}\n engine = create_engine(\n url=db_url,\n echo=db_echo,\n echo_pool=db_echo,\n **kwargs,\n )\n db_session = sessionmaker(engine, expire_on_commit=False)\n return scoped_session(db_session)\n\n\ndef init_db_async_session(\n db_url: str,\n db_echo: bool,\n db_pool_size: int = 10,\n db_max_overflow: int = 5,\n db_pool_recycle: int = 3600,\n) -> sessionmaker:\n db_echo = db_echo or False\n kwargs = {\n \"pool_size\": db_pool_size,\n \"max_overflow\": db_max_overflow,\n \"pool_recycle\": db_pool_recycle,\n }\n if db_url.startswith(\"sqlite\"):\n kwargs = {}\n async_engine = create_async_engine(\n url=db_url,\n echo=db_echo,\n echo_pool=db_echo,\n **kwargs,\n )\n db_async_session = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) # noqa\n return db_async_session\n\n\nclass G(metaclass=Singleton):\n \"\"\"\n \u5168\u5c40\u53d8\u91cf\n \"\"\"\n _initialized = False\n _init_lock = threading.Lock()\n _init_properties = [\n 'config',\n 'logger',\n # 'db_session',\n 'db_async_session',\n ]\n\n def __init__(self):\n self._initialized = False\n\n @cached_property\n def config(self) -> Config:\n return Config().setup()\n\n @cached_property\n def logger(self) -> Logger:\n return init_logger(\n level=\"DEBUG\" if self.config.app_debug else \"INFO\",\n serialize=self.config.app_log_serialize,\n basedir=self.config.app_log_basedir,\n )\n\n @cached_property\n def db_session(self) -> scoped_session:\n return init_db_session(\n db_url=self.config.db_url,\n db_echo=self.config.app_debug,\n )\n\n @cached_property\n def db_async_session(self) -> sessionmaker:\n return init_db_async_session(\n db_url=self.config.db_async_url,\n db_echo=self.config.app_debug,\n )\n\n def setup(self):\n with self._init_lock:\n if not self._initialized:\n for prop_name in self._init_properties:\n if hasattr(self, prop_name):\n getattr(self, prop_name)\n else:\n logger.warning(f\"{prop_name} not found\")\n self._initialized = True\n\n\ng = G()\nrequest_id_var: ContextVar[str] = ContextVar(\"request_id\", default=\"N/A\")\n",
73
+ "app/tiny_middleware.py": "\"\"\"\n\u4e2d\u95f4\u4ef6\n\"\"\"\nimport uuid\n\nfrom fastapi import FastAPI\nfrom fastapi.exceptions import RequestValidationError\nfrom loguru import logger\nfrom starlette.exceptions import HTTPException\nfrom starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint\nfrom starlette.middleware.cors import CORSMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse, Response\n\nfrom app.api.exceptions import CustomException\nfrom app.api.responses import Responses\nfrom app.api.status import Status\nfrom app.initializer import g, request_id_var\n\n__all__ = [\n \"register_middlewares\",\n]\n\n\ndef register_middlewares(app: FastAPI):\n \"\"\"\u6ce8\u518c\u4e2d\u95f4\u4ef6\"\"\"\n app.add_middleware(CorsMiddleware) # type: ignore\n app.add_middleware(HttpMiddleware) # type: ignore\n # #\n app.add_exception_handler(CustomException, ExceptionsHandler.custom_exception_handler) # type: ignore\n app.add_exception_handler(RequestValidationError, ExceptionsHandler.request_validation_handler) # type: ignore\n app.add_exception_handler(HTTPException, ExceptionsHandler.http_exception_handler) # type: ignore\n\n\nclass CorsMiddleware(CORSMiddleware):\n def __init__(self, app, **kwargs):\n super().__init__(\n app,\n allow_credentials=g.config.app_allow_credentials,\n allow_origins=g.config.app_allow_origins,\n allow_methods=g.config.app_allow_methods,\n allow_headers=g.config.app_allow_headers,\n **kwargs\n )\n\n\nclass HttpMiddleware(BaseHTTPMiddleware):\n _HEADERS = {\n # \u53ef\u6dfb\u52a0\u76f8\u5173\u5934\n }\n\n async def dispatch(\n self, request: Request,\n call_next: RequestResponseEndpoint,\n ) -> Response:\n request_id = self._get_or_create_request_id(request)\n request.state.request_id = request_id\n token = request_id_var.set(request_id)\n try:\n response = await call_next(request)\n response.headers[\"X-Request-ID\"] = request_id\n for key, value in self._HEADERS.items():\n if key not in response.headers:\n response.headers[key] = value\n return response\n except Exception as exc:\n return await self.handle_exception(request, exc)\n finally:\n request_id_var.reset(token)\n\n @staticmethod\n def _get_or_create_request_id(request: Request, prefix: str = \"req-\") -> str:\n request_id = request.headers.get(\"X-Request-ID\")\n if not request_id:\n request_id = f\"{prefix}{uuid.uuid4().hex}\"\n return request_id\n\n @staticmethod\n async def handle_exception(\n request: Request,\n exc: Exception,\n is_traceback: bool = True,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {Status.INTERNAL_SERVER_ERROR.code} {type(exc).__name__}: {exc}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n error=exc,\n status=Status.INTERNAL_SERVER_ERROR,\n )\n\n\nclass ExceptionsHandler:\n\n @staticmethod\n async def custom_exception_handler(\n request: Request,\n exc: CustomException,\n is_traceback: bool = True,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {exc.code} {exc.msg}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n msg=exc.msg,\n code=exc.code,\n error=exc,\n data=exc.data,\n )\n\n @staticmethod\n async def request_validation_handler(\n request: Request,\n exc: RequestValidationError,\n display_all: bool = False,\n is_traceback: bool = True,\n ) -> JSONResponse:\n if display_all:\n msg = \" & \".join([\n f\"{error['loc'][-1]} ({error['type']}) {error['msg'].replace('Value error, ', '').lower()}\"\n for error in exc.errors()\n ])\n else:\n error = exc.errors()[0]\n msg = f\"{error['loc'][-1]} ({error['type']}) {error['msg'].replace('Value error, ', '').lower()}\"\n lmsg = f'- \"{request.method} {request.url.path}\" {Status.PARAMS_ERROR.code} {msg}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n msg=msg,\n error=exc,\n status=Status.PARAMS_ERROR,\n )\n\n @staticmethod\n async def http_exception_handler(\n request: Request,\n exc: HTTPException,\n is_traceback: bool = True,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {exc.status_code} {exc.detail}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n return Responses.failure(\n msg=exc.detail,\n code=exc.status_code,\n error=exc,\n )\n",
74
+ "app/single_api.py": "from fastapi import APIRouter, Security\nfrom fastapi.security import APIKeyHeader\nfrom starlette.exceptions import HTTPException\nfrom starlette.status import HTTP_401_UNAUTHORIZED\n\nfrom app.core import config\n\nrouter = APIRouter()\n\n_API_KEY_HEADER = APIKeyHeader(name=\"X-API-Key\", auto_error=False)\n\n\nasync def get_current_api_key(api_key: str | None = Security(_API_KEY_HEADER)):\n if not api_key:\n raise HTTPException(\n status_code=HTTP_401_UNAUTHORIZED,\n detail=\"API key is required\"\n )\n if api_key not in config.api_keys:\n raise HTTPException(\n status_code=HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\"\n )\n return api_key\n\n\n@router.get(\n path=\"/api/ping\",\n summary=\"ping\",\n)\nasync def ping():\n return \"pong\"\n",
75
+ "app/single_core.py": "import os\nfrom contextvars import ContextVar\nfrom pathlib import Path\n\nimport yaml\nfrom dotenv import load_dotenv\nfrom toollib.utils import Singleton, get_cls_attrs, parse_variable\n\n_APP_DIR = Path(__file__).absolute().parent\n_CONFIG_DIR = _APP_DIR.parent.joinpath(\"config\")\n\nload_dotenv(dotenv_path=os.environ.setdefault(\n key=\"env_path\",\n value=str(_CONFIG_DIR.joinpath(\".env\")))\n)\n# #\napp_yaml = Path(\n os.environ.get(\"app_yaml\") or\n _CONFIG_DIR.joinpath(f\"app_{os.environ.setdefault(key='app_env', value='dev')}.yaml\")\n)\nif not app_yaml.is_file():\n raise RuntimeError(f\"\u914d\u7f6e\u6587\u4ef6\u4e0d\u5b58\u5728\uff1a{app_yaml}\")\n\n\nclass Config(metaclass=Singleton):\n \"\"\"\u914d\u7f6e\"\"\"\n _yaml_conf: dict = None\n app_dir: Path = _APP_DIR\n # #\n app_env: str = \"dev\"\n app_yaml: Path = app_yaml\n api_keys: list = []\n # #\n app_title: str = \"xApp\"\n app_summary: str = \"xxApp\"\n app_description: str = \"xxxApp\"\n app_version: str = \"1.0.0\"\n app_debug: bool = True\n app_log_serialize: bool = False\n app_log_basedir: str = \"./logs\"\n app_disable_docs: bool = False\n app_allow_credentials: bool = True\n app_allow_origins: list = [\"*\"]\n app_allow_methods: list = [\"*\"]\n app_allow_headers: list = [\"*\"]\n\n def setup(self):\n for k, item in get_cls_attrs(Config).items():\n v_type, v = item\n if callable(v_type):\n if k in os.environ: # \u4f18\u5148\u73af\u5883\u53d8\u91cf\n v = parse_variable(k=k, v_type=v_type, v_from=os.environ, default=v)\n else:\n v = parse_variable(k=k, v_type=v_type, v_from=self.load_yaml(), default=v)\n setattr(self, k, v)\n return self\n\n def load_yaml(self, reload: bool = False) -> dict:\n if self._yaml_conf and not reload:\n return self._yaml_conf\n with open(app_yaml, mode=\"r\", encoding=\"utf-8\") as file:\n self._yaml_conf = yaml.load(file, Loader=yaml.FullLoader)\n return self._yaml_conf\n\n\nconfig = Config()\nrequest_id_var: ContextVar[str] = ContextVar(\"request_id\", default=\"N/A\")\n",
76
+ "app/single_main.py": "import uuid\nfrom contextlib import asynccontextmanager\n\nfrom fastapi import FastAPI\nfrom fastapi.exceptions import RequestValidationError\nfrom fastapi.responses import ORJSONResponse\nfrom starlette.exceptions import HTTPException\nfrom starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint\nfrom starlette.middleware.cors import CORSMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response, JSONResponse\nfrom toollib.logu import init_logger\n\nfrom app.api import router\nfrom app.core import config, request_id_var\n\n_EXPOSE_ERROR = True\n\nconfig.setup()\nenable_console, enable_file = True, True\nif config.app_env == \"prod\":\n enable_console, enable_file = False, True # \u6309\u9700\u8c03\u6574\nlogger = init_logger(\n __name__,\n level=\"DEBUG\" if config.app_debug else \"INFO\",\n request_id_var=request_id_var,\n serialize=config.app_log_serialize,\n enable_console=enable_console,\n enable_file=enable_file,\n basedir=config.app_log_basedir,\n)\n# logger.add \u53ef\u6dfb\u52a0\u5176\u4ed6 handler\n# #\nopenapi_url = \"/openapi.json\"\ndocs_url = \"/docs\"\nredoc_url = \"/redoc\"\nif config.app_disable_docs is True:\n openapi_url, docs_url, redoc_url = None, None, None\n\n\n@asynccontextmanager\nasync def lifespan(xapp: FastAPI):\n logger.info(f\"Application env '{config.app_env}'\")\n logger.info(f\"Application yaml '{config.app_yaml.name}'\")\n logger.info(f\"Application title '{config.app_title}'\")\n logger.info(f\"Application version '{config.app_version}'\")\n # #\n logger.info(\"Application server running\")\n yield\n logger.info(\"Application server shutdown\")\n\n\nclass CorsMiddleware(CORSMiddleware):\n def __init__(self, xapp, **kwargs):\n super().__init__(\n xapp,\n allow_credentials=config.app_allow_credentials,\n allow_origins=config.app_allow_origins,\n allow_methods=config.app_allow_methods,\n allow_headers=config.app_allow_headers,\n **kwargs\n )\n\n\nclass HttpMiddleware(BaseHTTPMiddleware):\n _HEADERS = {\n # \u53ef\u6dfb\u52a0\u76f8\u5173\u5934\n }\n\n async def dispatch(\n self, request: Request,\n call_next: RequestResponseEndpoint,\n ) -> Response:\n request_id = self._get_or_create_request_id(request)\n request.state.request_id = request_id\n token = request_id_var.set(request_id)\n try:\n response = await call_next(request)\n response.headers[\"X-Request-ID\"] = request_id\n for key, value in self._HEADERS.items():\n if key not in response.headers:\n response.headers[key] = value\n return response\n except Exception as exc:\n return await self.handle_exception(request, exc)\n finally:\n request_id_var.reset(token)\n\n @staticmethod\n def _get_or_create_request_id(request: Request, prefix: str = \"req-\") -> str:\n request_id = request.headers.get(\"X-Request-ID\")\n if not request_id:\n request_id = f\"{prefix}{uuid.uuid4().hex}\"\n return request_id\n\n @staticmethod\n async def handle_exception(\n request: Request,\n exc: Exception,\n is_traceback: bool = True,\n ) -> JSONResponse:\n msg = \"\u5185\u90e8\u670d\u52a1\u5668\u9519\u8bef\"\n code = 500\n lmsg = f'- \"{request.method} {request.url.path}\" {code} {type(exc).__name__}: {exc}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n content = {\n \"msg\": msg,\n \"code\": code,\n \"request_id\": request.state.request_id,\n }\n if _EXPOSE_ERROR:\n content[\"error\"] = str(exc)\n return JSONResponse(\n content=content,\n )\n\n\nclass ExceptionsHandler:\n\n @staticmethod\n async def request_validation_handler(\n request: Request,\n exc: RequestValidationError,\n display_all: bool = False,\n is_traceback: bool = True,\n ) -> JSONResponse:\n if display_all:\n msg = \" & \".join([\n f\"{error['loc'][-1]} ({error['type']}) {error['msg'].replace('Value error, ', '').lower()}\"\n for error in exc.errors()\n ])\n else:\n error = exc.errors()[0]\n msg = f\"{error['loc'][-1]} ({error['type']}) {error['msg'].replace('Value error, ', '').lower()}\"\n code = 400\n lmsg = f'- \"{request.method} {request.url.path}\" {code} {msg}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n content = {\n \"msg\": msg,\n \"code\": code,\n \"request_id\": request.state.request_id,\n }\n if _EXPOSE_ERROR:\n content[\"error\"] = str(exc)\n return JSONResponse(\n content=content,\n )\n\n @staticmethod\n async def http_exception_handler(\n request: Request,\n exc: HTTPException,\n is_traceback: bool = True,\n ) -> JSONResponse:\n lmsg = f'- \"{request.method} {request.url.path}\" {exc.status_code} {exc.detail}'\n if is_traceback:\n logger.exception(lmsg)\n else:\n logger.error(lmsg)\n content = {\n \"msg\": exc.detail,\n \"code\": exc.status_code,\n \"request_id\": request.state.request_id,\n }\n if _EXPOSE_ERROR:\n content[\"error\"] = str(exc)\n return JSONResponse(\n content=content,\n )\n\n\napp = FastAPI(\n title=config.app_title,\n summary=config.app_summary,\n description=config.app_description,\n version=config.app_version,\n debug=config.app_debug,\n openapi_url=openapi_url,\n docs_url=docs_url,\n redoc_url=redoc_url,\n lifespan=lifespan,\n default_response_class=ORJSONResponse,\n)\n# #\napp.add_middleware(CorsMiddleware) # type: ignore\napp.add_middleware(HttpMiddleware) # type: ignore\napp.add_exception_handler(RequestValidationError, ExceptionsHandler.request_validation_handler) # type: ignore\napp.add_exception_handler(HTTPException, ExceptionsHandler.http_exception_handler) # type: ignore\napp.include_router(router)\n",
77
+ "app/single___init__.py": "\"\"\"\n@author axiner\n@version v1.0.0\n@created 2024/07/29 22:22\n@abstract app\n@description\n@history\n\"\"\"\n"
48
78
  }