├── .flake8 ├── .github └── workflows │ └── pytest.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── compose ├── app │ └── Dockerfile └── postgres │ └── docker_postgres_init.sql ├── docker-compose.yml ├── fixture.py ├── mypy.ini ├── package.json ├── pnpm-lock.yaml ├── poetry.lock ├── pyproject.toml ├── settings.env.docker ├── settings.env.docker.test ├── tailwind.config.js ├── tests ├── __init__.py ├── conftest.py ├── e2e │ ├── __init__.py │ ├── admin │ │ ├── __init__.py │ │ └── test_channel.py │ ├── health │ │ ├── __init__.py │ │ └── test_health.py │ └── user │ │ └── __init__.py ├── functional │ ├── __init__.py │ ├── test_product.py │ └── test_staff.py ├── test_app.py └── unit │ ├── __init__.py │ └── test_auth.py └── tifa ├── __init__.py ├── api.py ├── app.py ├── apps ├── __init__.py ├── admin │ └── __init__.py ├── deps.py ├── health │ └── __init__.py ├── home │ ├── __init__.py │ └── router.py └── user │ ├── __init__.py │ └── router.py ├── asgi └── __init__.py ├── auth.py ├── cli ├── __init__.py ├── base.py ├── web.py └── worker.py ├── consts.py ├── contrib ├── __init__.py ├── fastapi_plus.py └── redis.py ├── db.py ├── exceptions.py ├── globals.py ├── models ├── __init__.py ├── system.py └── user.py ├── scripts └── __init__.py ├── settings.py ├── static ├── css │ ├── input.css │ └── main.css └── index.js ├── templates ├── clicked.html ├── index.html └── modal.html ├── utils ├── __init__.py ├── pkg.py └── shell.py └── worker.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | exclude = 4 | .git, 5 | .eggs 6 | __pycache__, 7 | docs/source/conf.py, 8 | old, 9 | build, 10 | venv, 11 | migrations, 12 | ignore=E131,W503,E203 13 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | push: 6 | branches: 7 | - master 8 | - ci/* 9 | 10 | jobs: 11 | pytest: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Just up the stack 16 | run: docker-compose up -d postgres redis 17 | - name: Run Pytest 18 | run: make test 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | .idea 104 | config.yaml 105 | .volume 106 | elasticsearch-analysis-ik 107 | .pytest_cache 108 | node_modules/ 109 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: https://github.com/ambv/black 4 | rev: 19.10b0 5 | hooks: 6 | - id: black 7 | language_version: python3.8 8 | 9 | 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v1.2.3 12 | hooks: 13 | - id: flake8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 twocucao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | .DEFAULT_GOAL := help 3 | 4 | define PRINT_HELP_PYSCRIPT 5 | import re, sys 6 | 7 | for line in sys.stdin: 8 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 9 | if match: 10 | target, help = match.groups() 11 | print("%-30s %s" % (target, help)) 12 | endef 13 | export PRINT_HELP_PYSCRIPT 14 | 15 | help: 16 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 17 | 18 | flake8: ## lint 19 | poetry run flake8 tifa 20 | 21 | mypy: ## mypy 22 | poetry run mypy tifa 23 | 24 | publish: ## publish package to pypi 25 | poetry publish --build 26 | 27 | test: ## test 28 | docker-compose run --rm tifa-toolbox-test bash -c "python -m pytest tests" 29 | 30 | test.verbose: ## test.verbose 31 | docker-compose run --rm tifa-toolbox-test bash -c "python -m pytest tests -v --pdb --pdbcls=IPython.terminal.debugger:Pdb" 32 | 33 | format: ## publish package to pypi 34 | poetry run ruff format . 35 | 36 | shell_plus: 37 | docker-compose run --rm tifa-toolbox bash -c "tifa-cli shell_plus" 38 | 39 | db.init: 40 | docker-compose run --rm tifa-toolbox bash -c "tifa-cli db init" 41 | 42 | db.makemigrations: 43 | docker-compose run --rm tifa-toolbox bash -c "tifa-cli makemigrations" 44 | 45 | db.migrate: 46 | docker-compose run --rm tifa-toolbox bash -c "tifa-cli migrate" 47 | 48 | docker-build: ## build and compose up 49 | docker-compose build && docker-compose up 50 | 51 | docker-build-no-cache: ## build --no-cache 52 | docker-compose build --no-cache && docker-compose up 53 | 54 | start: ## runserver 55 | docker-compose run --rm --service-ports tifa-web 56 | 57 | beat: ## beat 58 | docker-compose up tifa-beat 59 | 60 | worker: ## worker 61 | docker-compose up tifa-worker 62 | 63 | monitor: ## flower 64 | docker-compose up tifa-monitor 65 | 66 | watch-css: ## flower 67 | npx tailwindcss -i ./tifa/static/css/input.css -o ./tifa/static/css/main.css --watch 68 | 69 | # docker images 70 | 71 | build-tifa: ## > tifa 72 | docker build -t 'tifa:local' -f 'compose/app/Dockerfile' . 73 | 74 | build-tifa-no-cache: ## > tifa 75 | docker build -t 'tifa:local' -f 'compose/app/Dockerfile' --no-cache . 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tifa 2 | 3 | Yet another **opinionated** fastapi-start-kit with best practice 4 | 5 | ![tifa](https://user-images.githubusercontent.com/5625783/118087406-19244200-b3f8-11eb-839d-f8faf3044f2d.gif) 6 | 7 | for my goddess -- Tifa 8 | 9 | ## Feature 10 | 11 | 1. Async! Async! Async! 12 | - async web framework by fastapi 13 | - async orm tortoise orm 14 | - htmx && tailwind && alpinejs 15 | 2. Much Less History Burden 16 | - newer sdk 17 | - newer python (3.11+) 18 | - newer docker compose way for developing experience 19 | 3. Best Practice 20 | - try automate every boring stuff with makefile/docker 21 | - embeded ipython repl for fast debugging and code prototype 22 | - type hint 23 | - build with poetry 24 | 25 | ## Quick Setup 26 | 27 | ```bash 28 | poetry install 29 | # build local docker image 30 | make build-tifa 31 | # make start 32 | make debug 33 | ``` 34 | 35 | ## Credits 36 | 37 | 0. saleor 38 | 1. https://github.com/ryanwang520/create-flask-skeleton 39 | 2. https://github.com/tiangolo/full-stack-fastapi-postgresql 40 | 41 | -------------------------------------------------------------------------------- /compose/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.8-bullseye 2 | ENV TZ=Asia/Shanghai 3 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | RUN apt-get update &&\ 6 | apt-get upgrade -y &&\ 7 | apt-get install -y \ 8 | liblzma-dev \ 9 | build-essential \ 10 | cmake \ 11 | curl \ 12 | freetds-bin \ 13 | gcc \ 14 | git \ 15 | krb5-user \ 16 | ldap-utils \ 17 | libbz2-dev \ 18 | libffi-dev \ 19 | libncurses5-dev \ 20 | libncursesw5-dev \ 21 | libreadline-dev \ 22 | libsasl2-2 \ 23 | libsasl2-modules \ 24 | libsqlite3-dev \ 25 | libssl-dev \ 26 | libssl1.1 \ 27 | llvm \ 28 | locales \ 29 | lsb-release \ 30 | sasl2-bin \ 31 | sqlite3 \ 32 | unixodbc \ 33 | vim \ 34 | wget \ 35 | xz-utils \ 36 | zlib1g-dev \ 37 | tk-dev 38 | 39 | ENV PYPI=https://mirrors.cloud.tencent.com/pypi/simple 40 | ENV PIP_DEFAULT_TIMEOUT=1000 41 | RUN pip install -U pip -i $PYPI 42 | RUN pip install -U poetry -i $PYPI 43 | ENV POETRY_VIRTUALENVS_CREATE=false 44 | WORKDIR /opt/tifa 45 | COPY . . 46 | RUN poetry install 47 | CMD ["start"] 48 | -------------------------------------------------------------------------------- /compose/postgres/docker_postgres_init.sql: -------------------------------------------------------------------------------- 1 | CREATE USER tifa WITH PASSWORD 'tifa&123' CREATEDB; 2 | CREATE DATABASE tifa 3 | WITH 4 | OWNER = tifa 5 | ENCODING = 'UTF8' 6 | LC_COLLATE = 'en_US.utf8' 7 | LC_CTYPE = 'en_US.utf8' 8 | TABLESPACE = pg_default 9 | CONNECTION LIMIT = -1; 10 | 11 | CREATE DATABASE tifa_test 12 | WITH 13 | OWNER = tifa 14 | ENCODING = 'UTF8' 15 | LC_COLLATE = 'en_US.utf8' 16 | LC_CTYPE = 'en_US.utf8' 17 | TABLESPACE = pg_default 18 | CONNECTION LIMIT = -1; 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | x-tifa-common: 4 | &tifa-common 5 | image: tifa:local 6 | volumes: 7 | - .:/opt/tifa 8 | environment: 9 | &tifa-common-env 10 | SECRET_KEY: "ZF8dr8z@L*asd3oR?R/pe1}|l12enem(ppFV=U=10'} 17 | dev: true 18 | 19 | /@isaacs/cliui@8.0.2: 20 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 21 | engines: {node: '>=12'} 22 | dependencies: 23 | string-width: 5.1.2 24 | string-width-cjs: /string-width@4.2.3 25 | strip-ansi: 7.1.0 26 | strip-ansi-cjs: /strip-ansi@6.0.1 27 | wrap-ansi: 8.1.0 28 | wrap-ansi-cjs: /wrap-ansi@7.0.0 29 | dev: true 30 | 31 | /@jridgewell/gen-mapping@0.3.5: 32 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 33 | engines: {node: '>=6.0.0'} 34 | dependencies: 35 | '@jridgewell/set-array': 1.2.1 36 | '@jridgewell/sourcemap-codec': 1.4.15 37 | '@jridgewell/trace-mapping': 0.3.25 38 | dev: true 39 | 40 | /@jridgewell/resolve-uri@3.1.2: 41 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 42 | engines: {node: '>=6.0.0'} 43 | dev: true 44 | 45 | /@jridgewell/set-array@1.2.1: 46 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 47 | engines: {node: '>=6.0.0'} 48 | dev: true 49 | 50 | /@jridgewell/sourcemap-codec@1.4.15: 51 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 52 | dev: true 53 | 54 | /@jridgewell/trace-mapping@0.3.25: 55 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 56 | dependencies: 57 | '@jridgewell/resolve-uri': 3.1.2 58 | '@jridgewell/sourcemap-codec': 1.4.15 59 | dev: true 60 | 61 | /@nodelib/fs.scandir@2.1.5: 62 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 63 | engines: {node: '>= 8'} 64 | dependencies: 65 | '@nodelib/fs.stat': 2.0.5 66 | run-parallel: 1.2.0 67 | dev: true 68 | 69 | /@nodelib/fs.stat@2.0.5: 70 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 71 | engines: {node: '>= 8'} 72 | dev: true 73 | 74 | /@nodelib/fs.walk@1.2.8: 75 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 76 | engines: {node: '>= 8'} 77 | dependencies: 78 | '@nodelib/fs.scandir': 2.1.5 79 | fastq: 1.17.1 80 | dev: true 81 | 82 | /@pkgjs/parseargs@0.11.0: 83 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 84 | engines: {node: '>=14'} 85 | requiresBuild: true 86 | dev: true 87 | optional: true 88 | 89 | /ansi-regex@5.0.1: 90 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 91 | engines: {node: '>=8'} 92 | dev: true 93 | 94 | /ansi-regex@6.0.1: 95 | resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 96 | engines: {node: '>=12'} 97 | dev: true 98 | 99 | /ansi-styles@4.3.0: 100 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 101 | engines: {node: '>=8'} 102 | dependencies: 103 | color-convert: 2.0.1 104 | dev: true 105 | 106 | /ansi-styles@6.2.1: 107 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 108 | engines: {node: '>=12'} 109 | dev: true 110 | 111 | /any-promise@1.3.0: 112 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 113 | dev: true 114 | 115 | /anymatch@3.1.3: 116 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 117 | engines: {node: '>= 8'} 118 | dependencies: 119 | normalize-path: 3.0.0 120 | picomatch: 2.3.1 121 | dev: true 122 | 123 | /arg@5.0.2: 124 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 125 | dev: true 126 | 127 | /balanced-match@1.0.2: 128 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 129 | dev: true 130 | 131 | /binary-extensions@2.3.0: 132 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 133 | engines: {node: '>=8'} 134 | dev: true 135 | 136 | /brace-expansion@2.0.1: 137 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 138 | dependencies: 139 | balanced-match: 1.0.2 140 | dev: true 141 | 142 | /braces@3.0.2: 143 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 144 | engines: {node: '>=8'} 145 | dependencies: 146 | fill-range: 7.0.1 147 | dev: true 148 | 149 | /camelcase-css@2.0.1: 150 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 151 | engines: {node: '>= 6'} 152 | dev: true 153 | 154 | /chokidar@3.6.0: 155 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 156 | engines: {node: '>= 8.10.0'} 157 | dependencies: 158 | anymatch: 3.1.3 159 | braces: 3.0.2 160 | glob-parent: 5.1.2 161 | is-binary-path: 2.1.0 162 | is-glob: 4.0.3 163 | normalize-path: 3.0.0 164 | readdirp: 3.6.0 165 | optionalDependencies: 166 | fsevents: 2.3.3 167 | dev: true 168 | 169 | /color-convert@2.0.1: 170 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 171 | engines: {node: '>=7.0.0'} 172 | dependencies: 173 | color-name: 1.1.4 174 | dev: true 175 | 176 | /color-name@1.1.4: 177 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 178 | dev: true 179 | 180 | /commander@4.1.1: 181 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 182 | engines: {node: '>= 6'} 183 | dev: true 184 | 185 | /cross-spawn@7.0.3: 186 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 187 | engines: {node: '>= 8'} 188 | dependencies: 189 | path-key: 3.1.1 190 | shebang-command: 2.0.0 191 | which: 2.0.2 192 | dev: true 193 | 194 | /cssesc@3.0.0: 195 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 196 | engines: {node: '>=4'} 197 | hasBin: true 198 | dev: true 199 | 200 | /didyoumean@1.2.2: 201 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 202 | dev: true 203 | 204 | /dlv@1.1.3: 205 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 206 | dev: true 207 | 208 | /eastasianwidth@0.2.0: 209 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 210 | dev: true 211 | 212 | /emoji-regex@8.0.0: 213 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 214 | dev: true 215 | 216 | /emoji-regex@9.2.2: 217 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 218 | dev: true 219 | 220 | /fast-glob@3.3.2: 221 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 222 | engines: {node: '>=8.6.0'} 223 | dependencies: 224 | '@nodelib/fs.stat': 2.0.5 225 | '@nodelib/fs.walk': 1.2.8 226 | glob-parent: 5.1.2 227 | merge2: 1.4.1 228 | micromatch: 4.0.5 229 | dev: true 230 | 231 | /fastq@1.17.1: 232 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 233 | dependencies: 234 | reusify: 1.0.4 235 | dev: true 236 | 237 | /fill-range@7.0.1: 238 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 239 | engines: {node: '>=8'} 240 | dependencies: 241 | to-regex-range: 5.0.1 242 | dev: true 243 | 244 | /foreground-child@3.1.1: 245 | resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} 246 | engines: {node: '>=14'} 247 | dependencies: 248 | cross-spawn: 7.0.3 249 | signal-exit: 4.1.0 250 | dev: true 251 | 252 | /fsevents@2.3.3: 253 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 254 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 255 | os: [darwin] 256 | requiresBuild: true 257 | dev: true 258 | optional: true 259 | 260 | /function-bind@1.1.2: 261 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 262 | dev: true 263 | 264 | /glob-parent@5.1.2: 265 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 266 | engines: {node: '>= 6'} 267 | dependencies: 268 | is-glob: 4.0.3 269 | dev: true 270 | 271 | /glob-parent@6.0.2: 272 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 273 | engines: {node: '>=10.13.0'} 274 | dependencies: 275 | is-glob: 4.0.3 276 | dev: true 277 | 278 | /glob@10.3.10: 279 | resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} 280 | engines: {node: '>=16 || 14 >=14.17'} 281 | hasBin: true 282 | dependencies: 283 | foreground-child: 3.1.1 284 | jackspeak: 2.3.6 285 | minimatch: 9.0.3 286 | minipass: 7.0.4 287 | path-scurry: 1.10.1 288 | dev: true 289 | 290 | /hasown@2.0.2: 291 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 292 | engines: {node: '>= 0.4'} 293 | dependencies: 294 | function-bind: 1.1.2 295 | dev: true 296 | 297 | /is-binary-path@2.1.0: 298 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 299 | engines: {node: '>=8'} 300 | dependencies: 301 | binary-extensions: 2.3.0 302 | dev: true 303 | 304 | /is-core-module@2.13.1: 305 | resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} 306 | dependencies: 307 | hasown: 2.0.2 308 | dev: true 309 | 310 | /is-extglob@2.1.1: 311 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 312 | engines: {node: '>=0.10.0'} 313 | dev: true 314 | 315 | /is-fullwidth-code-point@3.0.0: 316 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 317 | engines: {node: '>=8'} 318 | dev: true 319 | 320 | /is-glob@4.0.3: 321 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 322 | engines: {node: '>=0.10.0'} 323 | dependencies: 324 | is-extglob: 2.1.1 325 | dev: true 326 | 327 | /is-number@7.0.0: 328 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 329 | engines: {node: '>=0.12.0'} 330 | dev: true 331 | 332 | /isexe@2.0.0: 333 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 334 | dev: true 335 | 336 | /jackspeak@2.3.6: 337 | resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} 338 | engines: {node: '>=14'} 339 | dependencies: 340 | '@isaacs/cliui': 8.0.2 341 | optionalDependencies: 342 | '@pkgjs/parseargs': 0.11.0 343 | dev: true 344 | 345 | /jiti@1.21.0: 346 | resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} 347 | hasBin: true 348 | dev: true 349 | 350 | /lilconfig@2.1.0: 351 | resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 352 | engines: {node: '>=10'} 353 | dev: true 354 | 355 | /lilconfig@3.1.1: 356 | resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} 357 | engines: {node: '>=14'} 358 | dev: true 359 | 360 | /lines-and-columns@1.2.4: 361 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 362 | dev: true 363 | 364 | /lru-cache@10.2.0: 365 | resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} 366 | engines: {node: 14 || >=16.14} 367 | dev: true 368 | 369 | /merge2@1.4.1: 370 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 371 | engines: {node: '>= 8'} 372 | dev: true 373 | 374 | /micromatch@4.0.5: 375 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 376 | engines: {node: '>=8.6'} 377 | dependencies: 378 | braces: 3.0.2 379 | picomatch: 2.3.1 380 | dev: true 381 | 382 | /minimatch@9.0.3: 383 | resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} 384 | engines: {node: '>=16 || 14 >=14.17'} 385 | dependencies: 386 | brace-expansion: 2.0.1 387 | dev: true 388 | 389 | /minipass@7.0.4: 390 | resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} 391 | engines: {node: '>=16 || 14 >=14.17'} 392 | dev: true 393 | 394 | /mz@2.7.0: 395 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 396 | dependencies: 397 | any-promise: 1.3.0 398 | object-assign: 4.1.1 399 | thenify-all: 1.6.0 400 | dev: true 401 | 402 | /nanoid@3.3.7: 403 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 404 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 405 | hasBin: true 406 | dev: true 407 | 408 | /normalize-path@3.0.0: 409 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 410 | engines: {node: '>=0.10.0'} 411 | dev: true 412 | 413 | /object-assign@4.1.1: 414 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 415 | engines: {node: '>=0.10.0'} 416 | dev: true 417 | 418 | /object-hash@3.0.0: 419 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 420 | engines: {node: '>= 6'} 421 | dev: true 422 | 423 | /path-key@3.1.1: 424 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 425 | engines: {node: '>=8'} 426 | dev: true 427 | 428 | /path-parse@1.0.7: 429 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 430 | dev: true 431 | 432 | /path-scurry@1.10.1: 433 | resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} 434 | engines: {node: '>=16 || 14 >=14.17'} 435 | dependencies: 436 | lru-cache: 10.2.0 437 | minipass: 7.0.4 438 | dev: true 439 | 440 | /picocolors@1.0.0: 441 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 442 | dev: true 443 | 444 | /picomatch@2.3.1: 445 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 446 | engines: {node: '>=8.6'} 447 | dev: true 448 | 449 | /pify@2.3.0: 450 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 451 | engines: {node: '>=0.10.0'} 452 | dev: true 453 | 454 | /pirates@4.0.6: 455 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 456 | engines: {node: '>= 6'} 457 | dev: true 458 | 459 | /postcss-import@15.1.0(postcss@8.4.38): 460 | resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 461 | engines: {node: '>=14.0.0'} 462 | peerDependencies: 463 | postcss: ^8.0.0 464 | dependencies: 465 | postcss: 8.4.38 466 | postcss-value-parser: 4.2.0 467 | read-cache: 1.0.0 468 | resolve: 1.22.8 469 | dev: true 470 | 471 | /postcss-js@4.0.1(postcss@8.4.38): 472 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 473 | engines: {node: ^12 || ^14 || >= 16} 474 | peerDependencies: 475 | postcss: ^8.4.21 476 | dependencies: 477 | camelcase-css: 2.0.1 478 | postcss: 8.4.38 479 | dev: true 480 | 481 | /postcss-load-config@4.0.2(postcss@8.4.38): 482 | resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 483 | engines: {node: '>= 14'} 484 | peerDependencies: 485 | postcss: '>=8.0.9' 486 | ts-node: '>=9.0.0' 487 | peerDependenciesMeta: 488 | postcss: 489 | optional: true 490 | ts-node: 491 | optional: true 492 | dependencies: 493 | lilconfig: 3.1.1 494 | postcss: 8.4.38 495 | yaml: 2.4.1 496 | dev: true 497 | 498 | /postcss-nested@6.0.1(postcss@8.4.38): 499 | resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} 500 | engines: {node: '>=12.0'} 501 | peerDependencies: 502 | postcss: ^8.2.14 503 | dependencies: 504 | postcss: 8.4.38 505 | postcss-selector-parser: 6.0.16 506 | dev: true 507 | 508 | /postcss-selector-parser@6.0.16: 509 | resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} 510 | engines: {node: '>=4'} 511 | dependencies: 512 | cssesc: 3.0.0 513 | util-deprecate: 1.0.2 514 | dev: true 515 | 516 | /postcss-value-parser@4.2.0: 517 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 518 | dev: true 519 | 520 | /postcss@8.4.38: 521 | resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} 522 | engines: {node: ^10 || ^12 || >=14} 523 | dependencies: 524 | nanoid: 3.3.7 525 | picocolors: 1.0.0 526 | source-map-js: 1.2.0 527 | dev: true 528 | 529 | /queue-microtask@1.2.3: 530 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 531 | dev: true 532 | 533 | /read-cache@1.0.0: 534 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 535 | dependencies: 536 | pify: 2.3.0 537 | dev: true 538 | 539 | /readdirp@3.6.0: 540 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 541 | engines: {node: '>=8.10.0'} 542 | dependencies: 543 | picomatch: 2.3.1 544 | dev: true 545 | 546 | /resolve@1.22.8: 547 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 548 | hasBin: true 549 | dependencies: 550 | is-core-module: 2.13.1 551 | path-parse: 1.0.7 552 | supports-preserve-symlinks-flag: 1.0.0 553 | dev: true 554 | 555 | /reusify@1.0.4: 556 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 557 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 558 | dev: true 559 | 560 | /run-parallel@1.2.0: 561 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 562 | dependencies: 563 | queue-microtask: 1.2.3 564 | dev: true 565 | 566 | /shebang-command@2.0.0: 567 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 568 | engines: {node: '>=8'} 569 | dependencies: 570 | shebang-regex: 3.0.0 571 | dev: true 572 | 573 | /shebang-regex@3.0.0: 574 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 575 | engines: {node: '>=8'} 576 | dev: true 577 | 578 | /signal-exit@4.1.0: 579 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 580 | engines: {node: '>=14'} 581 | dev: true 582 | 583 | /source-map-js@1.2.0: 584 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 585 | engines: {node: '>=0.10.0'} 586 | dev: true 587 | 588 | /string-width@4.2.3: 589 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 590 | engines: {node: '>=8'} 591 | dependencies: 592 | emoji-regex: 8.0.0 593 | is-fullwidth-code-point: 3.0.0 594 | strip-ansi: 6.0.1 595 | dev: true 596 | 597 | /string-width@5.1.2: 598 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 599 | engines: {node: '>=12'} 600 | dependencies: 601 | eastasianwidth: 0.2.0 602 | emoji-regex: 9.2.2 603 | strip-ansi: 7.1.0 604 | dev: true 605 | 606 | /strip-ansi@6.0.1: 607 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 608 | engines: {node: '>=8'} 609 | dependencies: 610 | ansi-regex: 5.0.1 611 | dev: true 612 | 613 | /strip-ansi@7.1.0: 614 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 615 | engines: {node: '>=12'} 616 | dependencies: 617 | ansi-regex: 6.0.1 618 | dev: true 619 | 620 | /sucrase@3.35.0: 621 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 622 | engines: {node: '>=16 || 14 >=14.17'} 623 | hasBin: true 624 | dependencies: 625 | '@jridgewell/gen-mapping': 0.3.5 626 | commander: 4.1.1 627 | glob: 10.3.10 628 | lines-and-columns: 1.2.4 629 | mz: 2.7.0 630 | pirates: 4.0.6 631 | ts-interface-checker: 0.1.13 632 | dev: true 633 | 634 | /supports-preserve-symlinks-flag@1.0.0: 635 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 636 | engines: {node: '>= 0.4'} 637 | dev: true 638 | 639 | /tailwindcss@3.4.1: 640 | resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} 641 | engines: {node: '>=14.0.0'} 642 | hasBin: true 643 | dependencies: 644 | '@alloc/quick-lru': 5.2.0 645 | arg: 5.0.2 646 | chokidar: 3.6.0 647 | didyoumean: 1.2.2 648 | dlv: 1.1.3 649 | fast-glob: 3.3.2 650 | glob-parent: 6.0.2 651 | is-glob: 4.0.3 652 | jiti: 1.21.0 653 | lilconfig: 2.1.0 654 | micromatch: 4.0.5 655 | normalize-path: 3.0.0 656 | object-hash: 3.0.0 657 | picocolors: 1.0.0 658 | postcss: 8.4.38 659 | postcss-import: 15.1.0(postcss@8.4.38) 660 | postcss-js: 4.0.1(postcss@8.4.38) 661 | postcss-load-config: 4.0.2(postcss@8.4.38) 662 | postcss-nested: 6.0.1(postcss@8.4.38) 663 | postcss-selector-parser: 6.0.16 664 | resolve: 1.22.8 665 | sucrase: 3.35.0 666 | transitivePeerDependencies: 667 | - ts-node 668 | dev: true 669 | 670 | /thenify-all@1.6.0: 671 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 672 | engines: {node: '>=0.8'} 673 | dependencies: 674 | thenify: 3.3.1 675 | dev: true 676 | 677 | /thenify@3.3.1: 678 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 679 | dependencies: 680 | any-promise: 1.3.0 681 | dev: true 682 | 683 | /to-regex-range@5.0.1: 684 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 685 | engines: {node: '>=8.0'} 686 | dependencies: 687 | is-number: 7.0.0 688 | dev: true 689 | 690 | /ts-interface-checker@0.1.13: 691 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 692 | dev: true 693 | 694 | /util-deprecate@1.0.2: 695 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 696 | dev: true 697 | 698 | /which@2.0.2: 699 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 700 | engines: {node: '>= 8'} 701 | hasBin: true 702 | dependencies: 703 | isexe: 2.0.0 704 | dev: true 705 | 706 | /wrap-ansi@7.0.0: 707 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 708 | engines: {node: '>=10'} 709 | dependencies: 710 | ansi-styles: 4.3.0 711 | string-width: 4.2.3 712 | strip-ansi: 6.0.1 713 | dev: true 714 | 715 | /wrap-ansi@8.1.0: 716 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 717 | engines: {node: '>=12'} 718 | dependencies: 719 | ansi-styles: 6.2.1 720 | string-width: 5.1.2 721 | strip-ansi: 7.1.0 722 | dev: true 723 | 724 | /yaml@2.4.1: 725 | resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} 726 | engines: {node: '>= 14'} 727 | hasBin: true 728 | dev: true 729 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tifa" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["twocucao "] 6 | include = ["tifa/templates/", "tifa/static/"] 7 | 8 | [[tool.poetry.source]] 9 | name = "tencent" 10 | url = 'https://mirrors.cloud.tencent.com/pypi/simple' 11 | priority = 'primary' 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.11" 15 | aiobotocore = "^2.12.1" 16 | aiofiles = "^23.2.1" 17 | aiohttp = "^3.9.3" 18 | aiomysql = "^0.2.0" 19 | aioredis = "^2.0.1" 20 | alembic = "^1.13.1" 21 | asyncer = "^0.0.5" 22 | attrs = "^23.2.0" 23 | celery = "^5.3.6" 24 | devtools = "^0.12.2" 25 | fabric = "^3.2.2" 26 | fastapi = "^0.110.0" 27 | greenlet = "^3.0.3" 28 | gunicorn = "^21.2.0" 29 | httpx = "^0.27.0" 30 | invoke = "^2.2.0" 31 | ipython = "^8.22.2" 32 | jinja2 = "^3.1.3" 33 | loguru = "^0.7.2" 34 | markdown = "^3.5.2" 35 | orjson = "^3.9.15" 36 | pandas = "^2.2.1" 37 | passlib = "^1.7.4" 38 | pillow = "^10.2.0" 39 | pydantic = "^2.6.4" 40 | pydantic-core = "^2.16.3" 41 | pydantic-settings = "^2.2.1" 42 | pymysql = "^1.1.0" 43 | python-dotenv = "^1.0.1" 44 | python-jose = "^3.3.0" 45 | qrcode = "^7.4.2" 46 | raven = "^6.10.0" 47 | redis = { extras = ["hiredis"], version = "^5.0.3" } 48 | requests = "^2.31.0" 49 | rich = "^13.7.1" 50 | tenacity = "^8.2.3" 51 | typer = "^0.9.0" 52 | uvicorn = {extras = ["standard"], version = "^0.29.0"} 53 | xlrd = "^2.0.1" 54 | xlsxwriter = "^3.2.0" 55 | xlwt = "^1.3.0" 56 | arq = "^0.25.0" 57 | aerich = "^0.7.2" 58 | tortoise-orm = { extras = ["asyncmy"], version = "^0.20.0" } 59 | ruff = "^0.3.4" 60 | 61 | [tool.poetry.dev-dependencies] 62 | coverage = "*" 63 | mypy = "*" 64 | pytest = "*" 65 | pytest-cov = "*" 66 | pre-commit = "*" 67 | pytest-asyncio = "^0.15.1" 68 | 69 | [tool.poetry.group.dev.dependencies] 70 | ruff = "^0.3.3" 71 | 72 | [tool.black] 73 | exclude = ''' 74 | /( 75 | \.git 76 | | \.hg 77 | | \.mypy_cache 78 | | \.tox 79 | | \.venv 80 | | _build 81 | | buck-out 82 | | build 83 | | dist 84 | )/ 85 | ''' 86 | [tool.poetry.scripts] 87 | fastcli = 'tifa.cli:cli' 88 | tifa-cli = 'tifa.cli:cli' 89 | 90 | [build-system] 91 | requires = ["poetry-core>=1.0.0"] 92 | build-backend = "poetry.core.masonry.api" 93 | -------------------------------------------------------------------------------- /settings.env.docker: -------------------------------------------------------------------------------- 1 | ENV=LOCAL 2 | DATABASE_URL=postgresql://tifa:tifa%26123@postgres:5432/tifa 3 | CELERY_BROKER_URL=redis://redis:6379/0 4 | USE_SENTRY=False 5 | SECRET_KEY=django-insecure-6w3v4+m(-rmmw*!o=%7a-jfdkm#q7dyfjzm^lk4(2^k8mz!%!o 6 | -------------------------------------------------------------------------------- /settings.env.docker.test: -------------------------------------------------------------------------------- 1 | ENV=TEST 2 | DATABASE_URL=postgresql://tifa:tifa%26123@postgres:5432/test_tifa 3 | CELERY_BROKER_URL=redis://redis:6379/0 4 | USE_SENTRY=False 5 | SECRET_KEY=django-insecure-6w3v4+m(-rmmw*!o=%7a-jfdkm#q7dyfjzm^lk4(2^k8mz!%!o 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./tifa/**/*.{html,js}" 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } 11 | 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | 4 | import pytest 5 | from requests import Response 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | from starlette.testclient import TestClient 8 | 9 | from tifa.app import create_app 10 | from tifa.auth import gen_jwt 11 | from tifa.db.adal import AsyncDal 12 | from tifa.globals import db 13 | from tifa.models.attr import Attribute, AttributeValue 14 | from tifa.models.product import ProductType 15 | from tifa.models.product_attr import AttributeProduct 16 | from tifa.models.system import Staff 17 | from tifa.models.user import User 18 | 19 | 20 | class ApiClient(TestClient): 21 | def __init__(self, app, prefix="", user=None, *args, **kwargs): 22 | self.user = user 23 | self.prefix = prefix 24 | super().__init__(app, *args, **kwargs) 25 | 26 | def get(self, url: str, **kwargs) -> Response: 27 | return super().get(url, **kwargs) 28 | 29 | def op(self, url: str, json=None, **kwargs) -> Response: 30 | res = super().post(url, None, json, **kwargs).json() 31 | if "item" in res: 32 | return res["item"] 33 | return res 34 | 35 | 36 | app = create_app() 37 | 38 | 39 | @pytest.fixture(scope="session") 40 | def event_loop(): 41 | loop = asyncio.get_event_loop_policy().new_event_loop() 42 | yield loop 43 | loop.close() 44 | 45 | 46 | @pytest.fixture(scope="session", autouse=True) 47 | def setup_db(): 48 | with db.engine.begin() as conn: 49 | db.drop_all(conn) 50 | db.create_all(conn) 51 | yield 52 | conn.close() 53 | 54 | 55 | @pytest.fixture(scope="session") 56 | async def session(setup_db) -> AsyncSession: 57 | async with db.AsyncSession() as session: 58 | yield session 59 | 60 | 61 | @pytest.fixture(scope="session") 62 | async def staff(session: AsyncSession): 63 | adal = AsyncDal(session) 64 | ins = adal.add( 65 | Staff, 66 | name="admin", 67 | ) 68 | await adal.commit() 69 | return ins 70 | 71 | 72 | @pytest.fixture(scope="session") 73 | async def user(session: AsyncSession): 74 | adal = AsyncDal(session) 75 | ins = adal.add( 76 | User, 77 | email="alphago@gmail.com", 78 | is_active=True, 79 | password="plain_password", 80 | last_login_at=datetime.datetime.now(), 81 | first_name="alpha", 82 | last_name="go", 83 | ) 84 | await adal.commit() 85 | return ins 86 | 87 | 88 | @pytest.fixture(scope="session") 89 | def staff_client(staff: Staff): 90 | client = ApiClient(app, staff) 91 | token = gen_jwt('{"admin":1}', 60 * 24) 92 | client.headers.update({"Authorization": f"Bearer {token}"}) 93 | return client 94 | 95 | 96 | @pytest.fixture(scope="session") 97 | def user_client(user: User): 98 | return ApiClient(app, user) 99 | 100 | 101 | @pytest.fixture(scope="session") 102 | def health_client(): 103 | return ApiClient(app) 104 | 105 | 106 | @pytest.fixture(scope="session") 107 | async def color_attribute(session: AsyncSession): 108 | adal = AsyncDal(session) 109 | attribute = adal.add( 110 | Attribute, 111 | slug="color", 112 | name="Color", 113 | type=Attribute.Type.PRODUCT, 114 | input_type=Attribute.InputType.DROPDOWN, 115 | filterable_in_storefront=True, 116 | filterable_in_dashboard=True, 117 | available_in_grid=True, 118 | ) 119 | adal.add(AttributeValue, attribute=attribute, name="Red", slug="red", value="red") 120 | adal.add( 121 | AttributeValue, attribute=attribute, name="Blue", slug="blue", value="blue" 122 | ) 123 | await adal.commit() 124 | return attribute 125 | 126 | 127 | @pytest.fixture(scope="session") 128 | async def size_attribute(session: AsyncSession): 129 | adal = AsyncDal(session) 130 | attribute = adal.add( 131 | Attribute, 132 | slug="size", 133 | name="Size", 134 | type=Attribute.Type.PRODUCT, 135 | input_type=Attribute.InputType.DROPDOWN, 136 | filterable_in_storefront=True, 137 | filterable_in_dashboard=True, 138 | available_in_grid=True, 139 | ) 140 | adal.add(AttributeValue, attribute=attribute, name="3XL", slug="3xl", value="3xl") 141 | adal.add(AttributeValue, attribute=attribute, name="2XL", slug="2xl", value="2xl") 142 | adal.add(AttributeValue, attribute=attribute, name="XL", slug="xl", value="xl") 143 | adal.add(AttributeValue, attribute=attribute, name="L", slug="l", value="l") 144 | await adal.commit() 145 | return attribute 146 | 147 | 148 | @pytest.fixture(scope="session") 149 | async def date_attribute(session: AsyncSession): 150 | adal = AsyncDal(session) 151 | attribute = adal.add( 152 | Attribute, 153 | slug="release-date", 154 | name="Release date", 155 | type=Attribute.Type.PRODUCT, 156 | input_type=Attribute.InputType.DATE, 157 | filterable_in_storefront=True, 158 | filterable_in_dashboard=True, 159 | available_in_grid=True, 160 | ) 161 | for value in [ 162 | datetime.datetime(2020, 10, 5), 163 | datetime.datetime(2020, 11, 5), 164 | ]: 165 | adal.add( 166 | AttributeValue, 167 | attribute=attribute, 168 | name=f"{attribute.name}: {value.date()}", 169 | slug=f"{value.date()}_{attribute.id}", 170 | value=f"{value.date()}", 171 | ) 172 | await adal.commit() 173 | 174 | return attribute 175 | 176 | 177 | @pytest.fixture(scope="session") 178 | async def date_time_attribute(session: AsyncSession): 179 | adal = AsyncDal(session) 180 | attribute = adal.add( 181 | Attribute, 182 | slug="release-date-time", 183 | name="Release date time", 184 | type=Attribute.Type.PRODUCT, 185 | input_type=Attribute.InputType.DATE_TIME, 186 | filterable_in_storefront=True, 187 | filterable_in_dashboard=True, 188 | available_in_grid=True, 189 | ) 190 | 191 | for value in [ 192 | datetime.datetime(2020, 10, 5), 193 | datetime.datetime(2020, 11, 5), 194 | ]: 195 | adal.add( 196 | AttributeValue, 197 | attribute=attribute, 198 | name=f"{attribute.name}: {value.date()}", 199 | slug=f"{value.date()}_{attribute.id}", 200 | value=f"{value.date()}", 201 | ) 202 | await adal.commit() 203 | 204 | return attribute 205 | 206 | 207 | @pytest.fixture(scope="session") 208 | async def product_type(session: AsyncSession, color_attribute, size_attribute): 209 | adal = AsyncDal(session) 210 | product_type = adal.add( 211 | ProductType, 212 | name="Default Type", 213 | slug="default-type", 214 | has_variants=True, 215 | is_shipping_required=True, 216 | weight=0.1, 217 | is_digital=True, 218 | ) 219 | adal.add( 220 | AttributeProduct, 221 | attribute=color_attribute, 222 | product_type=product_type, 223 | ) 224 | adal.add( 225 | AttributeProduct, 226 | attribute=size_attribute, 227 | product_type=product_type, 228 | ) 229 | await adal.commit() 230 | return product_type 231 | -------------------------------------------------------------------------------- /tests/e2e/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/e2e/__init__.py -------------------------------------------------------------------------------- /tests/e2e/admin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/e2e/admin/__init__.py -------------------------------------------------------------------------------- /tests/e2e/admin/test_channel.py: -------------------------------------------------------------------------------- 1 | def test_channel_curd(staff_client): 2 | channel = staff_client.op( 3 | "/admin/channel/create", 4 | json={ 5 | "name": "test channel", 6 | "isActive": True, 7 | "slug": "test_channel_slug", 8 | "currencyCode": "usa", 9 | }, 10 | ) 11 | assert channel["id"] 12 | assert channel["isActive"] 13 | 14 | channel = staff_client.op( 15 | "/admin/channel/update", 16 | json={ 17 | "id": channel["id"], 18 | "name": "test channel", 19 | "isActive": False, 20 | "slug": "test_channel_slug", 21 | "currencyCode": "usa", 22 | }, 23 | ) 24 | assert not channel["isActive"] 25 | -------------------------------------------------------------------------------- /tests/e2e/health/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/e2e/health/__init__.py -------------------------------------------------------------------------------- /tests/e2e/health/test_health.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize( 5 | "path,expected_status,expected_response", 6 | [ 7 | ("/health/test_1_plus_1/2", 200, {"result": True}), 8 | ("/health/test/1_plus_1_2", 200, {"result": True}), 9 | ("/health/test/1_plus/_1_2", 404, None), 10 | ], 11 | ) 12 | def test_get_path(health_client, path, expected_status, expected_response): 13 | resp = health_client.get(path) 14 | assert resp.status_code == expected_status 15 | if resp.status_code == 200: 16 | assert resp.json() == expected_response 17 | -------------------------------------------------------------------------------- /tests/e2e/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/e2e/user/__init__.py -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/functional/__init__.py -------------------------------------------------------------------------------- /tests/functional/test_product.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from tifa.db.adal import AsyncDal 5 | from tifa.models.product import ProductType 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_filtering_by_attribute( 10 | session: AsyncSession, 11 | product_type, 12 | # color_attribute, 13 | # size_attribute, 14 | # # category, 15 | # date_attribute, 16 | # date_time_attribute, 17 | # # boolean_attribute, 18 | ): 19 | adal = AsyncDal(session) 20 | assert len(await adal.all(ProductType)) == 1 21 | ... 22 | 23 | 24 | # product_type_a = ProductType.objects.create( 25 | # name="New class", slug="new-class1", has_variants=True 26 | # ) 27 | # product_type_a.product_attributes.add(color_attribute) 28 | # product_type_b = ProductType.objects.create( 29 | # name="New class", slug="new-class2", has_variants=True 30 | # ) 31 | # product_type_b.variant_attributes.add(color_attribute) 32 | # product_a = Product.objects.create( 33 | # name="Test product a", 34 | # slug="test-product-a", 35 | # product_type=product_type_a, 36 | # category=category, 37 | # ) 38 | # variant_a = ProductVariant.objects.create(product=product_a, sku="1234") 39 | # # ProductVariantChannelListing.objects.create( 40 | # # variant=variant_a, 41 | # # channel=channel_USD, 42 | # # cost_price_amount=Decimal(1), 43 | # # price_amount=Decimal(10), 44 | # # currency=channel_USD.currency_code, 45 | # # ) 46 | # # product_b = Product.objects.create( 47 | # # name="Test product b", 48 | # # slug="test-product-b", 49 | # # product_type=product_type_b, 50 | # # category=category, 51 | # # ) 52 | # # variant_b = ProductVariant.objects.create(product=product_b, sku="12345") 53 | -------------------------------------------------------------------------------- /tests/functional/test_staff.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from tifa.db.adal import AsyncDal 5 | from tifa.globals import db 6 | from tifa.db.dal import Dal 7 | from tifa.models.system import Staff 8 | from tifa.models.user import User 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_staff_count(session: AsyncSession, staff): 13 | adal = AsyncDal(session) 14 | assert len(await adal.all(Staff)) == 1 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_user_count(session: AsyncSession, user): 19 | adal = AsyncDal(session) 20 | assert len(await adal.all(User)) == 1 21 | -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/test_app.py -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_auth.py: -------------------------------------------------------------------------------- 1 | from tifa.auth import decode_jwt, gen_jwt 2 | 3 | 4 | def test_jwt(): 5 | token = gen_jwt("staff:1", 30) 6 | payload = decode_jwt(token) 7 | assert payload["sub"] == "staff:1" 8 | -------------------------------------------------------------------------------- /tifa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tifa/__init__.py -------------------------------------------------------------------------------- /tifa/api.py: -------------------------------------------------------------------------------- 1 | from fastapi.responses import ORJSONResponse 2 | 3 | 4 | class ApiResult: 5 | def __init__(self, value, status_code=200, next_page=None): 6 | self.value = value 7 | self.status_code = status_code 8 | self.nex_page = next_page 9 | 10 | def to_response(self): 11 | return ORJSONResponse( 12 | self.value, 13 | status_code=self.status_code, 14 | ) 15 | -------------------------------------------------------------------------------- /tifa/app.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pathlib import Path 3 | 4 | from fastapi import FastAPI, Request 5 | from starlette.middleware.base import BaseHTTPMiddleware 6 | from starlette.staticfiles import StaticFiles 7 | 8 | from tifa.settings import settings 9 | from tifa.utils.pkg import import_submodules 10 | 11 | 12 | def setup_routers(app: FastAPI): 13 | from tifa.apps import user, health, admin, home 14 | 15 | app.mount("/health", health.bp) 16 | app.mount("/admin", admin.bp) 17 | app.mount("/user", user.bp) 18 | app.mount("/", home.bp) 19 | 20 | 21 | def setup_cli(app: FastAPI): 22 | pass 23 | 24 | 25 | def setup_logging(app: FastAPI): 26 | pass 27 | 28 | 29 | def setup_middleware(app: FastAPI): 30 | async def add_process_time_header(request: Request, call_next): 31 | start_time = time.time() 32 | response = await call_next(request) 33 | process_time = time.time() - start_time 34 | response.headers["X-Process-Time"] = str(process_time) 35 | return response 36 | 37 | app.add_middleware(BaseHTTPMiddleware, dispatch=add_process_time_header) 38 | 39 | 40 | def setup_static_files(app: FastAPI): 41 | print(settings.STATIC_DIR, settings.STATIC_PATH) 42 | static_files_app = StaticFiles(directory=settings.STATIC_DIR) 43 | app.mount(path=settings.STATIC_PATH, app=static_files_app, name="static") 44 | 45 | 46 | def setup_db_models(app): 47 | import_submodules("tifa.models") 48 | 49 | 50 | def setup_sentry(app): 51 | import sentry_sdk 52 | 53 | sentry_sdk.init( 54 | "sentry_sdk", 55 | ) 56 | 57 | 58 | def create_app(): 59 | app = FastAPI( 60 | debug=settings.DEBUG, 61 | title=settings.TITLE, 62 | description=settings.DESCRIPTION, 63 | ) 64 | setup_db_models(app) 65 | setup_static_files(app) 66 | setup_routers(app) 67 | setup_middleware(app) 68 | setup_logging(app) 69 | if settings.SENTRY_DSN: 70 | setup_sentry(app) 71 | 72 | return app 73 | 74 | 75 | current_app = create_app() 76 | -------------------------------------------------------------------------------- /tifa/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tifa/apps/__init__.py -------------------------------------------------------------------------------- /tifa/apps/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi.responses import ORJSONResponse 2 | from starlette.exceptions import HTTPException 3 | 4 | from tifa.contrib.fastapi_plus import create_bp 5 | 6 | bp = create_bp() 7 | 8 | 9 | @bp.exception_handler(HTTPException) 10 | async def http_exception_handler(request, exc): 11 | return ORJSONResponse( 12 | { 13 | "detail": exc.detail, 14 | }, 15 | status_code=exc.status_code, 16 | ) 17 | 18 | 19 | @bp.get("/test_sentry") 20 | def test_sentry(): 21 | 1 / 0 22 | 23 | 24 | @bp.get("/test_1_plus_1/{result}") 25 | def test_1_plus_1(result: int): 26 | return {"result": result == 2} 27 | 28 | 29 | @bp.get("/test/1_plus_1_{result}") 30 | def test_1_plus_1_v2(result: int): 31 | return {"result": result == 2} 32 | -------------------------------------------------------------------------------- /tifa/apps/deps.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tifa/apps/deps.py -------------------------------------------------------------------------------- /tifa/apps/health/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from tifa.contrib.fastapi_plus import create_bp 4 | from fastapi.responses import ORJSONResponse 5 | from starlette.exceptions import HTTPException 6 | 7 | bp = create_bp() 8 | 9 | 10 | @bp.exception_handler(HTTPException) 11 | async def http_exception_handler(request, exc): 12 | return ORJSONResponse( 13 | { 14 | "detail": exc.detail, 15 | }, 16 | status_code=exc.status_code, 17 | ) 18 | 19 | 20 | @bp.get("/test_sentry") 21 | def test_sentry(): 22 | 1 / 0 23 | 24 | 25 | @bp.get("/test_1_plus_1/{result}") 26 | def test_1_plus_1(result: int): 27 | return {"result": result == 2} 28 | 29 | 30 | @bp.get("/test/1_plus_1_{result}") 31 | def test_1_plus_1_v2(result: int): 32 | return {"result": result == 2} 33 | -------------------------------------------------------------------------------- /tifa/apps/home/__init__.py: -------------------------------------------------------------------------------- 1 | from .router import bp 2 | -------------------------------------------------------------------------------- /tifa/apps/home/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, Request 2 | from tifa.contrib.fastapi_plus import create_bp 3 | from fastapi.templating import Jinja2Templates 4 | 5 | from tifa.settings import settings 6 | 7 | bp = create_bp() 8 | 9 | templates = Jinja2Templates(directory=settings.TEMPLATE_PATH) 10 | 11 | 12 | @bp.get("/") 13 | async def home(request: Request): 14 | return templates.TemplateResponse( 15 | request=request, name="index.html", context={"id": 1} 16 | ) 17 | 18 | 19 | @bp.post("/clicked") 20 | async def home(request: Request): 21 | return templates.TemplateResponse( 22 | request=request, name="clicked.html", context={"id": 1} 23 | ) 24 | 25 | 26 | @bp.get("/modal") 27 | async def get_modal(request: Request): 28 | return templates.TemplateResponse( 29 | request=request, name="modal.html", context={"id": 1} 30 | ) 31 | -------------------------------------------------------------------------------- /tifa/apps/user/__init__.py: -------------------------------------------------------------------------------- 1 | from .router import bp 2 | -------------------------------------------------------------------------------- /tifa/apps/user/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | from starlette.requests import Request 3 | 4 | from tifa.auth import decode_jwt 5 | from tifa.contrib.fastapi_plus import create_bp 6 | from tifa.exceptions import NotFound, NotAuthorized 7 | 8 | ALLOW_LIST = ("/user/login",) 9 | 10 | 11 | async def before_request( 12 | request: Request, 13 | ): 14 | if request.url.path not in ALLOW_LIST: 15 | try: 16 | authorization = request.headers.get("Authorization", None) 17 | if not authorization: 18 | raise NotAuthorized("Authorization Headers Required") 19 | token = authorization.split(" ")[1] 20 | content = decode_jwt(token) 21 | # staff = await adal.get_or_404(User, json.loads(content["sub"])["user"]) 22 | # g.staff = staff 23 | except NotAuthorized as e: 24 | raise e 25 | except NotFound as e: 26 | raise NotAuthorized("no such staff") 27 | except Exception: 28 | # TODO: more specific error 29 | raise NotAuthorized("token not correct") 30 | 31 | 32 | bp = create_bp( 33 | [ 34 | Depends(before_request), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /tifa/asgi/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from tifa.app import current_app 4 | 5 | app = FastAPI() 6 | 7 | application = current_app 8 | -------------------------------------------------------------------------------- /tifa/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Union, Any 3 | 4 | from jose import jwt 5 | 6 | from tifa.exceptions import ApiException 7 | from tifa.settings import settings 8 | from passlib.context import CryptContext 9 | 10 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 11 | 12 | ALGORITHM = "HS256" 13 | 14 | 15 | def decode_jwt(token): 16 | try: 17 | payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) 18 | return payload 19 | except jwt.JWTError: 20 | raise ApiException( 21 | message="Could not validate credentials", 22 | status_code=403, 23 | ) 24 | 25 | 26 | def gen_jwt(subject: Union[str, Any], minutes: int) -> str: 27 | expire = datetime.datetime.now() + datetime.timedelta(minutes=minutes) 28 | to_encode = {"exp": expire, "sub": str(subject)} 29 | encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) 30 | return encoded_jwt 31 | 32 | 33 | def verify_password(plain_password: str, hashed_password: str) -> bool: 34 | return pwd_context.verify(plain_password, hashed_password) 35 | 36 | 37 | def get_password_hash(password: str) -> str: 38 | return pwd_context.hash(password) 39 | -------------------------------------------------------------------------------- /tifa/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | banner = """ 5 | _______ _ __ 6 | |__ __| (_) / _| 7 | | | _ | |_ __ _ 8 | | | | | | _| / _` | 9 | | | | | | | | (_| | 10 | |_| |_| |_| \__,_| 11 | 12 | An opinionated fastapi starter-kit 13 | by @twocucao 14 | """ 15 | 16 | 17 | def cli(): 18 | print("tifafasda") 19 | ... 20 | 21 | 22 | if __name__ == "__main__": 23 | cli() 24 | -------------------------------------------------------------------------------- /tifa/cli/base.py: -------------------------------------------------------------------------------- 1 | import typer 2 | 3 | cli = typer.Typer() 4 | -------------------------------------------------------------------------------- /tifa/cli/web.py: -------------------------------------------------------------------------------- 1 | import typer 2 | import uvicorn 3 | 4 | group_web = typer.Typer() 5 | 6 | 7 | @group_web.command("start") 8 | def start_tifa(): 9 | uvicorn.run( 10 | "tifa.app:create_app", 11 | factory=True, 12 | reload=True, 13 | host="0.0.0.0", 14 | port=8000, 15 | log_level="info", 16 | ) 17 | 18 | 19 | @group_web.command("whiteboard") 20 | def start_whiteboard(): 21 | uvicorn.run( 22 | "tifa.app:create_app", 23 | factory=True, 24 | reload=True, 25 | host="0.0.0.0", 26 | port=8001, 27 | log_level="info", 28 | ) 29 | -------------------------------------------------------------------------------- /tifa/cli/worker.py: -------------------------------------------------------------------------------- 1 | import typer 2 | 3 | group_worker = typer.Typer() 4 | 5 | 6 | @group_worker.command("start") 7 | def start_worker(): 8 | """ 9 | 依据具体业务情况定制多个 worker 10 | """ 11 | ... 12 | 13 | 14 | @group_worker.command("beat") 15 | def start_scheduler(): 16 | """ 17 | 一般全局一个 scheduler 18 | """ 19 | ... 20 | 21 | 22 | @group_worker.command("monitor") 23 | def start_monitor(): 24 | """ 25 | TODO: 监控 26 | """ 27 | ... 28 | 29 | 30 | @group_worker.command("pg_to_es") 31 | def pg_to_es(): ... 32 | 33 | 34 | @group_worker.command("test") 35 | def test_worker(): ... 36 | -------------------------------------------------------------------------------- /tifa/consts.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class LovingCharacterNameEnum(str, Enum): 5 | tifa = "tifa" 6 | cloud = "cloud" 7 | -------------------------------------------------------------------------------- /tifa/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tifa/contrib/__init__.py -------------------------------------------------------------------------------- /tifa/contrib/fastapi_plus.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException, Request 2 | from pydantic import ValidationError, BaseModel, ConfigDict 3 | 4 | from tifa.exceptions import ApiException, UnicornException 5 | 6 | 7 | class ApiModel(BaseModel): 8 | model_config = ConfigDict(coerce_numbers_to_str=True) 9 | 10 | 11 | class FastAPIPlus(FastAPI): 12 | ... 13 | 14 | 15 | def setup_error_handlers(app: FastAPI): 16 | @app.exception_handler(UnicornException) 17 | def unicorn_exception_handler(request: Request, exc: UnicornException): 18 | raise ApiException(exc.name, status_code=500) 19 | 20 | @app.exception_handler(ValidationError) 21 | async def validation_handler(request: Request, exc: ValidationError): 22 | raise ApiException( 23 | "validate error", 24 | status_code=500, 25 | errors=exc.errors(), 26 | ) 27 | 28 | @app.exception_handler(HTTPException) 29 | async def http_exception_handler(request: Request, exc: HTTPException): 30 | raise ApiException(exc.detail, exc.status_code) 31 | 32 | @app.exception_handler(Exception) 33 | def handle_exc(request: Request, exc): 34 | raise exc 35 | 36 | app.add_exception_handler(ApiException, lambda request, err: err.to_result()) 37 | 38 | 39 | def create_bp(dependencies: list = None) -> FastAPIPlus: 40 | if not dependencies: 41 | dependencies = [] 42 | app = FastAPIPlus(dependencies=dependencies) 43 | setup_error_handlers(app) 44 | return app 45 | -------------------------------------------------------------------------------- /tifa/contrib/redis.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import aioredis 4 | 5 | from tifa.settings import settings 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class MyRedis: 11 | def __init__(self, pool): 12 | self.pool = pool 13 | 14 | @classmethod 15 | async def create(cls): 16 | _pool = await aioredis.Redis(host=settings.REDIS_CACHE_URI) 17 | return cls(pool=_pool) 18 | 19 | 20 | redis = MyRedis.create() 21 | -------------------------------------------------------------------------------- /tifa/db.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as t 4 | 5 | from loguru import logger 6 | from pydantic import BaseModel 7 | from tortoise import fields, Tortoise, connections 8 | from tortoise.expressions import Q 9 | from tortoise.models import Model as TTModel 10 | 11 | from tifa.settings import tortoise_config 12 | 13 | 14 | class Model(TTModel): 15 | class Meta: 16 | abstract = True 17 | 18 | id = fields.BigIntField(pk=True) 19 | 20 | @classmethod 21 | def find_first(cls, *args: Q, **kwargs: t.Any) -> t.Self: 22 | return cls.filter(*args, **kwargs).first() 23 | 24 | def __str__(self): 25 | return f"{self.__class__.__name__}-#{self.id}" 26 | 27 | 28 | def setup_db_models(app): 29 | logger.info("setup db models") 30 | 31 | @app.on_event("startup") 32 | async def init_orm() -> None: 33 | await db_hook_on_startup() 34 | 35 | @app.on_event("shutdown") 36 | async def close_orm() -> None: # pylint: disable=W0612 37 | await db_hook_on_shutdown() 38 | 39 | 40 | async def db_hook_on_startup(): 41 | await Tortoise.init(config=tortoise_config) 42 | logger.info("Tortoise-ORM started") 43 | 44 | 45 | async def db_hook_on_shutdown(): 46 | await connections.close_all() 47 | logger.info("Tortoise-ORM shutdown") 48 | -------------------------------------------------------------------------------- /tifa/exceptions.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from typing import Optional 3 | 4 | from fastapi.responses import ORJSONResponse 5 | from starlette.requests import Request 6 | 7 | 8 | class HttpCodeEnum(enum.Enum): 9 | OK = 200 10 | BAD_REQUEST = 400 11 | UNAUTHORIZED = 401 12 | FORBIDDEN = 403 13 | NOT_FOUND = 404 14 | SERVER_ERROR = 500 15 | 16 | 17 | class BizCodeEnum(enum.Enum): 18 | # 业务状态码 19 | OK = "200" 20 | FAIL = "500" 21 | NOT_EXISTS = "404" 22 | 23 | 24 | error_message = { 25 | HttpCodeEnum.BAD_REQUEST.name: "错误请求", 26 | HttpCodeEnum.UNAUTHORIZED.name: "未授权", 27 | HttpCodeEnum.FORBIDDEN.name: "权限不足", 28 | HttpCodeEnum.NOT_FOUND.name: "未找到资源", 29 | HttpCodeEnum.SERVER_ERROR.name: "服务器异常", 30 | BizCodeEnum.FAIL: "未知错误", 31 | } 32 | 33 | 34 | class ApiException(Exception): 35 | status_code: int = 400 36 | code: Optional[int] 37 | message: Optional[str] = None 38 | 39 | def __init__(self, message, biz_code=None, status_code=400, errors=None): 40 | self.status_code = status_code or self.status_code 41 | self.code = self.status_code or self.code 42 | self.message = message or self.message 43 | self.errors = errors or [] 44 | 45 | def to_result(self): 46 | rv = {"message": self.message} 47 | if self.code: 48 | rv["code"] = self.code 49 | if self.errors: 50 | rv["errors"] = self.errors 51 | return ORJSONResponse(rv, status_code=self.status_code) 52 | 53 | 54 | class NotAuthorized(ApiException): 55 | status_code = HttpCodeEnum.UNAUTHORIZED.value 56 | 57 | 58 | class NotFound(ApiException): 59 | status_code = HttpCodeEnum.NOT_FOUND.value 60 | detail = error_message[HttpCodeEnum.NOT_FOUND.name] 61 | 62 | 63 | class InvalidToken(ApiException): 64 | status_code = HttpCodeEnum.UNAUTHORIZED.value 65 | 66 | 67 | class AuthExpired(ApiException): 68 | status_code = HttpCodeEnum.UNAUTHORIZED.value 69 | 70 | 71 | class UnicornException(Exception): 72 | def __init__(self, name: str): 73 | self.name = name 74 | 75 | 76 | -------------------------------------------------------------------------------- /tifa/globals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from tifa.settings import settings 4 | 5 | # pytest no need to check 6 | if not settings.ENV == "TEST": 7 | use_console_exporter = True 8 | -------------------------------------------------------------------------------- /tifa/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import * # noqa 2 | from .system import * # noqa 3 | -------------------------------------------------------------------------------- /tifa/models/system.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | 3 | from tifa.db import Model 4 | 5 | 6 | class SysUser(Model): 7 | class Meta: 8 | table = "sys_user" 9 | table_description = "系统管理员表" 10 | 11 | def __str__(self): 12 | return self.username 13 | 14 | id = fields.BigIntField(pk=True) 15 | username = fields.CharField(max_length=20, default='', description='用户名') 16 | password_hash = fields.CharField(max_length=300, default='', description='密码') 17 | avatar = fields.CharField(max_length=300, default='', description='头像') 18 | created_at = fields.DatetimeField(auto_now_add=True) 19 | updated_at = fields.DatetimeField(auto_now=True) 20 | -------------------------------------------------------------------------------- /tifa/models/user.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from tortoise import fields 4 | 5 | from tifa.db import Model 6 | 7 | 8 | class User(Model): 9 | class Meta: 10 | table = "user" 11 | 12 | id = fields.BigIntField(pk=True) 13 | username = fields.CharField(max_length=50, index=True) 14 | password_hash = fields.CharField(max_length=200) 15 | created_at = fields.DatetimeField(auto_now_add=True) 16 | updated_at = fields.DatetimeField(auto_now=True) 17 | -------------------------------------------------------------------------------- /tifa/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tifa/scripts/__init__.py -------------------------------------------------------------------------------- /tifa/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import warnings 4 | from typing import Optional 5 | 6 | from pydantic_settings import BaseSettings 7 | 8 | ROOT = pathlib.Path(__file__).parent.absolute() 9 | 10 | APP_SETTINGS = os.environ.get("APP_SETTINGS") 11 | if not APP_SETTINGS: 12 | warnings.warn("!!!未指定APP_SETTINGS!!!, 极有可能运行错误") 13 | 14 | 15 | class GlobalSetting(BaseSettings): 16 | TITLE: str = "Tifa" 17 | DESCRIPTION: str = "Yet another opinionated fastapi-start-kit with best practice" 18 | 19 | TEMPLATE_PATH: str = f"{ROOT}/templates" 20 | 21 | STATIC_PATH: str = "/static" 22 | STATIC_DIR: str = f"{ROOT}/static" 23 | 24 | # SENTRY_DSN: Optional[str] 25 | 26 | DEBUG: bool = False 27 | ENV: str = "LOCAL" 28 | SECRET_KEY: str = "change me" 29 | SENTRY_DSN: Optional[str] = "" 30 | 31 | ASYNC_DATABASE_URL: str = "mysql://root:@mysql:3306/tifa?charset=utf8mb4" 32 | REDIS_CACHE_URI: str = "redis://localhost:6379" 33 | REDIS_CELERY_URL: str = "redis://redis:6379/6" 34 | 35 | 36 | settings = GlobalSetting(_env_file=APP_SETTINGS) 37 | 38 | tortoise_config = { 39 | "connections": {"default": settings.ASYNC_DATABASE_URL}, 40 | "apps": { 41 | "models": { 42 | "models": ["tifa.models", "aerich.models"], 43 | "default_connection": "default", 44 | }, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /tifa/static/css/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | #modal { 6 | /* Underlay covers entire screen. */ 7 | position: fixed; 8 | top: 0px; 9 | bottom: 0px; 10 | left: 0px; 11 | right: 0px; 12 | background-color: rgba(0, 0, 0, 0.5); 13 | z-index: 1000; 14 | 15 | /* Flexbox centers the .modal-content vertically and horizontally */ 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | 20 | /* Animate when opening */ 21 | animation-name: fadeIn; 22 | animation-duration: 150ms; 23 | animation-timing-function: ease; 24 | } 25 | 26 | #modal > .modal-underlay { 27 | /* underlay takes up the entire viewport. This is only 28 | required if you want to click to dismiss the popup */ 29 | position: absolute; 30 | z-index: -1; 31 | top: 0px; 32 | bottom: 0px; 33 | left: 0px; 34 | right: 0px; 35 | } 36 | 37 | #modal > .modal-content { 38 | /* Position visible dialog near the top of the window */ 39 | margin-top: 10vh; 40 | 41 | /* Sizing for visible dialog */ 42 | width: 80%; 43 | max-width: 600px; 44 | 45 | /* Display properties for visible dialog*/ 46 | border: solid 1px #999; 47 | border-radius: 8px; 48 | box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.3); 49 | background-color: white; 50 | padding: 20px; 51 | 52 | /* Animate when opening */ 53 | animation-name: zoomIn; 54 | animation-duration: 150ms; 55 | animation-timing-function: ease; 56 | } 57 | 58 | #modal.closing { 59 | /* Animate when closing */ 60 | animation-name: fadeOut; 61 | animation-duration: 150ms; 62 | animation-timing-function: ease; 63 | } 64 | 65 | #modal.closing > .modal-content { 66 | /* Animate when closing */ 67 | animation-name: zoomOut; 68 | animation-duration: 150ms; 69 | animation-timing-function: ease; 70 | } 71 | 72 | @keyframes fadeIn { 73 | 0% { 74 | opacity: 0; 75 | } 76 | 100% { 77 | opacity: 1; 78 | } 79 | } 80 | 81 | @keyframes fadeOut { 82 | 0% { 83 | opacity: 1; 84 | } 85 | 100% { 86 | opacity: 0; 87 | } 88 | } 89 | 90 | @keyframes zoomIn { 91 | 0% { 92 | transform: scale(0.9); 93 | } 94 | 100% { 95 | transform: scale(1); 96 | } 97 | } 98 | 99 | @keyframes zoomOut { 100 | 0% { 101 | transform: scale(1); 102 | } 103 | 100% { 104 | transform: scale(0.9); 105 | } 106 | } -------------------------------------------------------------------------------- /tifa/static/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | 7. Disable tap highlights on iOS 36 | */ 37 | 38 | html, 39 | :host { 40 | line-height: 1.5; 41 | /* 1 */ 42 | -webkit-text-size-adjust: 100%; 43 | /* 2 */ 44 | -moz-tab-size: 4; 45 | /* 3 */ 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | /* 3 */ 49 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 50 | /* 4 */ 51 | font-feature-settings: normal; 52 | /* 5 */ 53 | font-variation-settings: normal; 54 | /* 6 */ 55 | -webkit-tap-highlight-color: transparent; 56 | /* 7 */ 57 | } 58 | 59 | /* 60 | 1. Remove the margin in all browsers. 61 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 62 | */ 63 | 64 | body { 65 | margin: 0; 66 | /* 1 */ 67 | line-height: inherit; 68 | /* 2 */ 69 | } 70 | 71 | /* 72 | 1. Add the correct height in Firefox. 73 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 74 | 3. Ensure horizontal rules are visible by default. 75 | */ 76 | 77 | hr { 78 | height: 0; 79 | /* 1 */ 80 | color: inherit; 81 | /* 2 */ 82 | border-top-width: 1px; 83 | /* 3 */ 84 | } 85 | 86 | /* 87 | Add the correct text decoration in Chrome, Edge, and Safari. 88 | */ 89 | 90 | abbr:where([title]) { 91 | -webkit-text-decoration: underline dotted; 92 | text-decoration: underline dotted; 93 | } 94 | 95 | /* 96 | Remove the default font size and weight for headings. 97 | */ 98 | 99 | h1, 100 | h2, 101 | h3, 102 | h4, 103 | h5, 104 | h6 { 105 | font-size: inherit; 106 | font-weight: inherit; 107 | } 108 | 109 | /* 110 | Reset links to optimize for opt-in styling instead of opt-out. 111 | */ 112 | 113 | a { 114 | color: inherit; 115 | text-decoration: inherit; 116 | } 117 | 118 | /* 119 | Add the correct font weight in Edge and Safari. 120 | */ 121 | 122 | b, 123 | strong { 124 | font-weight: bolder; 125 | } 126 | 127 | /* 128 | 1. Use the user's configured `mono` font-family by default. 129 | 2. Use the user's configured `mono` font-feature-settings by default. 130 | 3. Use the user's configured `mono` font-variation-settings by default. 131 | 4. Correct the odd `em` font sizing in all browsers. 132 | */ 133 | 134 | code, 135 | kbd, 136 | samp, 137 | pre { 138 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 139 | /* 1 */ 140 | font-feature-settings: normal; 141 | /* 2 */ 142 | font-variation-settings: normal; 143 | /* 3 */ 144 | font-size: 1em; 145 | /* 4 */ 146 | } 147 | 148 | /* 149 | Add the correct font size in all browsers. 150 | */ 151 | 152 | small { 153 | font-size: 80%; 154 | } 155 | 156 | /* 157 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 158 | */ 159 | 160 | sub, 161 | sup { 162 | font-size: 75%; 163 | line-height: 0; 164 | position: relative; 165 | vertical-align: baseline; 166 | } 167 | 168 | sub { 169 | bottom: -0.25em; 170 | } 171 | 172 | sup { 173 | top: -0.5em; 174 | } 175 | 176 | /* 177 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 178 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 179 | 3. Remove gaps between table borders by default. 180 | */ 181 | 182 | table { 183 | text-indent: 0; 184 | /* 1 */ 185 | border-color: inherit; 186 | /* 2 */ 187 | border-collapse: collapse; 188 | /* 3 */ 189 | } 190 | 191 | /* 192 | 1. Change the font styles in all browsers. 193 | 2. Remove the margin in Firefox and Safari. 194 | 3. Remove default padding in all browsers. 195 | */ 196 | 197 | button, 198 | input, 199 | optgroup, 200 | select, 201 | textarea { 202 | font-family: inherit; 203 | /* 1 */ 204 | font-feature-settings: inherit; 205 | /* 1 */ 206 | font-variation-settings: inherit; 207 | /* 1 */ 208 | font-size: 100%; 209 | /* 1 */ 210 | font-weight: inherit; 211 | /* 1 */ 212 | line-height: inherit; 213 | /* 1 */ 214 | color: inherit; 215 | /* 1 */ 216 | margin: 0; 217 | /* 2 */ 218 | padding: 0; 219 | /* 3 */ 220 | } 221 | 222 | /* 223 | Remove the inheritance of text transform in Edge and Firefox. 224 | */ 225 | 226 | button, 227 | select { 228 | text-transform: none; 229 | } 230 | 231 | /* 232 | 1. Correct the inability to style clickable types in iOS and Safari. 233 | 2. Remove default button styles. 234 | */ 235 | 236 | button, 237 | [type='button'], 238 | [type='reset'], 239 | [type='submit'] { 240 | -webkit-appearance: button; 241 | /* 1 */ 242 | background-color: transparent; 243 | /* 2 */ 244 | background-image: none; 245 | /* 2 */ 246 | } 247 | 248 | /* 249 | Use the modern Firefox focus style for all focusable elements. 250 | */ 251 | 252 | :-moz-focusring { 253 | outline: auto; 254 | } 255 | 256 | /* 257 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 258 | */ 259 | 260 | :-moz-ui-invalid { 261 | box-shadow: none; 262 | } 263 | 264 | /* 265 | Add the correct vertical alignment in Chrome and Firefox. 266 | */ 267 | 268 | progress { 269 | vertical-align: baseline; 270 | } 271 | 272 | /* 273 | Correct the cursor style of increment and decrement buttons in Safari. 274 | */ 275 | 276 | ::-webkit-inner-spin-button, 277 | ::-webkit-outer-spin-button { 278 | height: auto; 279 | } 280 | 281 | /* 282 | 1. Correct the odd appearance in Chrome and Safari. 283 | 2. Correct the outline style in Safari. 284 | */ 285 | 286 | [type='search'] { 287 | -webkit-appearance: textfield; 288 | /* 1 */ 289 | outline-offset: -2px; 290 | /* 2 */ 291 | } 292 | 293 | /* 294 | Remove the inner padding in Chrome and Safari on macOS. 295 | */ 296 | 297 | ::-webkit-search-decoration { 298 | -webkit-appearance: none; 299 | } 300 | 301 | /* 302 | 1. Correct the inability to style clickable types in iOS and Safari. 303 | 2. Change font properties to `inherit` in Safari. 304 | */ 305 | 306 | ::-webkit-file-upload-button { 307 | -webkit-appearance: button; 308 | /* 1 */ 309 | font: inherit; 310 | /* 2 */ 311 | } 312 | 313 | /* 314 | Add the correct display in Chrome and Safari. 315 | */ 316 | 317 | summary { 318 | display: list-item; 319 | } 320 | 321 | /* 322 | Removes the default spacing and border for appropriate elements. 323 | */ 324 | 325 | blockquote, 326 | dl, 327 | dd, 328 | h1, 329 | h2, 330 | h3, 331 | h4, 332 | h5, 333 | h6, 334 | hr, 335 | figure, 336 | p, 337 | pre { 338 | margin: 0; 339 | } 340 | 341 | fieldset { 342 | margin: 0; 343 | padding: 0; 344 | } 345 | 346 | legend { 347 | padding: 0; 348 | } 349 | 350 | ol, 351 | ul, 352 | menu { 353 | list-style: none; 354 | margin: 0; 355 | padding: 0; 356 | } 357 | 358 | /* 359 | Reset default styling for dialogs. 360 | */ 361 | 362 | dialog { 363 | padding: 0; 364 | } 365 | 366 | /* 367 | Prevent resizing textareas horizontally by default. 368 | */ 369 | 370 | textarea { 371 | resize: vertical; 372 | } 373 | 374 | /* 375 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 376 | 2. Set the default placeholder color to the user's configured gray 400 color. 377 | */ 378 | 379 | input::-moz-placeholder, textarea::-moz-placeholder { 380 | opacity: 1; 381 | /* 1 */ 382 | color: #9ca3af; 383 | /* 2 */ 384 | } 385 | 386 | input::placeholder, 387 | textarea::placeholder { 388 | opacity: 1; 389 | /* 1 */ 390 | color: #9ca3af; 391 | /* 2 */ 392 | } 393 | 394 | /* 395 | Set the default cursor for buttons. 396 | */ 397 | 398 | button, 399 | [role="button"] { 400 | cursor: pointer; 401 | } 402 | 403 | /* 404 | Make sure disabled buttons don't get the pointer cursor. 405 | */ 406 | 407 | :disabled { 408 | cursor: default; 409 | } 410 | 411 | /* 412 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 413 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 414 | This can trigger a poorly considered lint error in some tools but is included by design. 415 | */ 416 | 417 | img, 418 | svg, 419 | video, 420 | canvas, 421 | audio, 422 | iframe, 423 | embed, 424 | object { 425 | display: block; 426 | /* 1 */ 427 | vertical-align: middle; 428 | /* 2 */ 429 | } 430 | 431 | /* 432 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 433 | */ 434 | 435 | img, 436 | video { 437 | max-width: 100%; 438 | height: auto; 439 | } 440 | 441 | /* Make elements with the HTML hidden attribute stay hidden by default */ 442 | 443 | [hidden] { 444 | display: none; 445 | } 446 | 447 | *, ::before, ::after { 448 | --tw-border-spacing-x: 0; 449 | --tw-border-spacing-y: 0; 450 | --tw-translate-x: 0; 451 | --tw-translate-y: 0; 452 | --tw-rotate: 0; 453 | --tw-skew-x: 0; 454 | --tw-skew-y: 0; 455 | --tw-scale-x: 1; 456 | --tw-scale-y: 1; 457 | --tw-pan-x: ; 458 | --tw-pan-y: ; 459 | --tw-pinch-zoom: ; 460 | --tw-scroll-snap-strictness: proximity; 461 | --tw-gradient-from-position: ; 462 | --tw-gradient-via-position: ; 463 | --tw-gradient-to-position: ; 464 | --tw-ordinal: ; 465 | --tw-slashed-zero: ; 466 | --tw-numeric-figure: ; 467 | --tw-numeric-spacing: ; 468 | --tw-numeric-fraction: ; 469 | --tw-ring-inset: ; 470 | --tw-ring-offset-width: 0px; 471 | --tw-ring-offset-color: #fff; 472 | --tw-ring-color: rgb(59 130 246 / 0.5); 473 | --tw-ring-offset-shadow: 0 0 #0000; 474 | --tw-ring-shadow: 0 0 #0000; 475 | --tw-shadow: 0 0 #0000; 476 | --tw-shadow-colored: 0 0 #0000; 477 | --tw-blur: ; 478 | --tw-brightness: ; 479 | --tw-contrast: ; 480 | --tw-grayscale: ; 481 | --tw-hue-rotate: ; 482 | --tw-invert: ; 483 | --tw-saturate: ; 484 | --tw-sepia: ; 485 | --tw-drop-shadow: ; 486 | --tw-backdrop-blur: ; 487 | --tw-backdrop-brightness: ; 488 | --tw-backdrop-contrast: ; 489 | --tw-backdrop-grayscale: ; 490 | --tw-backdrop-hue-rotate: ; 491 | --tw-backdrop-invert: ; 492 | --tw-backdrop-opacity: ; 493 | --tw-backdrop-saturate: ; 494 | --tw-backdrop-sepia: ; 495 | } 496 | 497 | ::backdrop { 498 | --tw-border-spacing-x: 0; 499 | --tw-border-spacing-y: 0; 500 | --tw-translate-x: 0; 501 | --tw-translate-y: 0; 502 | --tw-rotate: 0; 503 | --tw-skew-x: 0; 504 | --tw-skew-y: 0; 505 | --tw-scale-x: 1; 506 | --tw-scale-y: 1; 507 | --tw-pan-x: ; 508 | --tw-pan-y: ; 509 | --tw-pinch-zoom: ; 510 | --tw-scroll-snap-strictness: proximity; 511 | --tw-gradient-from-position: ; 512 | --tw-gradient-via-position: ; 513 | --tw-gradient-to-position: ; 514 | --tw-ordinal: ; 515 | --tw-slashed-zero: ; 516 | --tw-numeric-figure: ; 517 | --tw-numeric-spacing: ; 518 | --tw-numeric-fraction: ; 519 | --tw-ring-inset: ; 520 | --tw-ring-offset-width: 0px; 521 | --tw-ring-offset-color: #fff; 522 | --tw-ring-color: rgb(59 130 246 / 0.5); 523 | --tw-ring-offset-shadow: 0 0 #0000; 524 | --tw-ring-shadow: 0 0 #0000; 525 | --tw-shadow: 0 0 #0000; 526 | --tw-shadow-colored: 0 0 #0000; 527 | --tw-blur: ; 528 | --tw-brightness: ; 529 | --tw-contrast: ; 530 | --tw-grayscale: ; 531 | --tw-hue-rotate: ; 532 | --tw-invert: ; 533 | --tw-saturate: ; 534 | --tw-sepia: ; 535 | --tw-drop-shadow: ; 536 | --tw-backdrop-blur: ; 537 | --tw-backdrop-brightness: ; 538 | --tw-backdrop-contrast: ; 539 | --tw-backdrop-grayscale: ; 540 | --tw-backdrop-hue-rotate: ; 541 | --tw-backdrop-invert: ; 542 | --tw-backdrop-opacity: ; 543 | --tw-backdrop-saturate: ; 544 | --tw-backdrop-sepia: ; 545 | } 546 | 547 | .mb-2 { 548 | margin-bottom: 0.5rem; 549 | } 550 | 551 | .ml-2 { 552 | margin-left: 0.5rem; 553 | } 554 | 555 | .mr-2 { 556 | margin-right: 0.5rem; 557 | } 558 | 559 | .mt-12 { 560 | margin-top: 3rem; 561 | } 562 | 563 | .mt-4 { 564 | margin-top: 1rem; 565 | } 566 | 567 | .mt-8 { 568 | margin-top: 2rem; 569 | } 570 | 571 | .flex { 572 | display: flex; 573 | } 574 | 575 | .inline-flex { 576 | display: inline-flex; 577 | } 578 | 579 | .grid { 580 | display: grid; 581 | } 582 | 583 | .h-10 { 584 | height: 2.5rem; 585 | } 586 | 587 | .h-4 { 588 | height: 1rem; 589 | } 590 | 591 | .h-6 { 592 | height: 1.5rem; 593 | } 594 | 595 | .h-screen { 596 | height: 100vh; 597 | } 598 | 599 | .min-h-screen { 600 | min-height: 100vh; 601 | } 602 | 603 | .w-1\/2 { 604 | width: 50%; 605 | } 606 | 607 | .w-4 { 608 | width: 1rem; 609 | } 610 | 611 | .w-6 { 612 | width: 1.5rem; 613 | } 614 | 615 | .w-\[300px\] { 616 | width: 300px; 617 | } 618 | 619 | .w-\[750px\] { 620 | width: 750px; 621 | } 622 | 623 | .w-fit { 624 | width: -moz-fit-content; 625 | width: fit-content; 626 | } 627 | 628 | .w-full { 629 | width: 100%; 630 | } 631 | 632 | .flex-1 { 633 | flex: 1 1 0%; 634 | } 635 | 636 | .shrink-0 { 637 | flex-shrink: 0; 638 | } 639 | 640 | .grid-cols-3 { 641 | grid-template-columns: repeat(3, minmax(0, 1fr)); 642 | } 643 | 644 | .items-center { 645 | align-items: center; 646 | } 647 | 648 | .justify-center { 649 | justify-content: center; 650 | } 651 | 652 | .justify-between { 653 | justify-content: space-between; 654 | } 655 | 656 | .gap-4 { 657 | gap: 1rem; 658 | } 659 | 660 | .space-x-4 > :not([hidden]) ~ :not([hidden]) { 661 | --tw-space-x-reverse: 0; 662 | margin-right: calc(1rem * var(--tw-space-x-reverse)); 663 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); 664 | } 665 | 666 | .space-y-1 > :not([hidden]) ~ :not([hidden]) { 667 | --tw-space-y-reverse: 0; 668 | margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); 669 | margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); 670 | } 671 | 672 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 673 | --tw-space-y-reverse: 0; 674 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 675 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 676 | } 677 | 678 | .whitespace-nowrap { 679 | white-space: nowrap; 680 | } 681 | 682 | .rounded-full { 683 | border-radius: 9999px; 684 | } 685 | 686 | .rounded-lg { 687 | border-radius: 0.5rem; 688 | } 689 | 690 | .rounded-md { 691 | border-radius: 0.375rem; 692 | } 693 | 694 | .rounded-sm { 695 | border-radius: 0.125rem; 696 | } 697 | 698 | .rounded-t-lg { 699 | border-top-left-radius: 0.5rem; 700 | border-top-right-radius: 0.5rem; 701 | } 702 | 703 | .border { 704 | border-width: 1px; 705 | } 706 | 707 | .border-b { 708 | border-bottom-width: 1px; 709 | } 710 | 711 | .border-b-2 { 712 | border-bottom-width: 2px; 713 | } 714 | 715 | .border-red-500 { 716 | --tw-border-opacity: 1; 717 | border-color: rgb(239 68 68 / var(--tw-border-opacity)); 718 | } 719 | 720 | .border-transparent { 721 | border-color: transparent; 722 | } 723 | 724 | .bg-gray-100 { 725 | --tw-bg-opacity: 1; 726 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); 727 | } 728 | 729 | .bg-red-100 { 730 | --tw-bg-opacity: 1; 731 | background-color: rgb(254 226 226 / var(--tw-bg-opacity)); 732 | } 733 | 734 | .bg-red-500 { 735 | --tw-bg-opacity: 1; 736 | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); 737 | } 738 | 739 | .bg-white { 740 | --tw-bg-opacity: 1; 741 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 742 | } 743 | 744 | .bg-gradient-to-b { 745 | background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); 746 | } 747 | 748 | .bg-gradient-to-br { 749 | background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); 750 | } 751 | 752 | .from-\[\#ffe0e9\] { 753 | --tw-gradient-from: #ffe0e9 var(--tw-gradient-from-position); 754 | --tw-gradient-to: rgb(255 224 233 / 0) var(--tw-gradient-to-position); 755 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 756 | } 757 | 758 | .from-pink-200 { 759 | --tw-gradient-from: #fbcfe8 var(--tw-gradient-from-position); 760 | --tw-gradient-to: rgb(251 207 232 / 0) var(--tw-gradient-to-position); 761 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 762 | } 763 | 764 | .to-\[\#ffcad4\] { 765 | --tw-gradient-to: #ffcad4 var(--tw-gradient-to-position); 766 | } 767 | 768 | .to-pink-100 { 769 | --tw-gradient-to: #fce7f3 var(--tw-gradient-to-position); 770 | } 771 | 772 | .p-2 { 773 | padding: 0.5rem; 774 | } 775 | 776 | .p-6 { 777 | padding: 1.5rem; 778 | } 779 | 780 | .px-2 { 781 | padding-left: 0.5rem; 782 | padding-right: 0.5rem; 783 | } 784 | 785 | .px-2\.5 { 786 | padding-left: 0.625rem; 787 | padding-right: 0.625rem; 788 | } 789 | 790 | .px-3 { 791 | padding-left: 0.75rem; 792 | padding-right: 0.75rem; 793 | } 794 | 795 | .px-4 { 796 | padding-left: 1rem; 797 | padding-right: 1rem; 798 | } 799 | 800 | .px-8 { 801 | padding-left: 2rem; 802 | padding-right: 2rem; 803 | } 804 | 805 | .py-0 { 806 | padding-top: 0px; 807 | padding-bottom: 0px; 808 | } 809 | 810 | .py-0\.5 { 811 | padding-top: 0.125rem; 812 | padding-bottom: 0.125rem; 813 | } 814 | 815 | .py-12 { 816 | padding-top: 3rem; 817 | padding-bottom: 3rem; 818 | } 819 | 820 | .py-2 { 821 | padding-top: 0.5rem; 822 | padding-bottom: 0.5rem; 823 | } 824 | 825 | .py-3 { 826 | padding-top: 0.75rem; 827 | padding-bottom: 0.75rem; 828 | } 829 | 830 | .pb-1 { 831 | padding-bottom: 0.25rem; 832 | } 833 | 834 | .text-center { 835 | text-align: center; 836 | } 837 | 838 | .text-3xl { 839 | font-size: 1.875rem; 840 | line-height: 2.25rem; 841 | } 842 | 843 | .text-4xl { 844 | font-size: 2.25rem; 845 | line-height: 2.5rem; 846 | } 847 | 848 | .text-lg { 849 | font-size: 1.125rem; 850 | line-height: 1.75rem; 851 | } 852 | 853 | .text-sm { 854 | font-size: 0.875rem; 855 | line-height: 1.25rem; 856 | } 857 | 858 | .text-xl { 859 | font-size: 1.25rem; 860 | line-height: 1.75rem; 861 | } 862 | 863 | .text-xs { 864 | font-size: 0.75rem; 865 | line-height: 1rem; 866 | } 867 | 868 | .font-bold { 869 | font-weight: 700; 870 | } 871 | 872 | .font-medium { 873 | font-weight: 500; 874 | } 875 | 876 | .font-semibold { 877 | font-weight: 600; 878 | } 879 | 880 | .text-gray-400 { 881 | --tw-text-opacity: 1; 882 | color: rgb(156 163 175 / var(--tw-text-opacity)); 883 | } 884 | 885 | .text-gray-500 { 886 | --tw-text-opacity: 1; 887 | color: rgb(107 114 128 / var(--tw-text-opacity)); 888 | } 889 | 890 | .text-gray-600 { 891 | --tw-text-opacity: 1; 892 | color: rgb(75 85 99 / var(--tw-text-opacity)); 893 | } 894 | 895 | .text-red-500 { 896 | --tw-text-opacity: 1; 897 | color: rgb(239 68 68 / var(--tw-text-opacity)); 898 | } 899 | 900 | .text-white { 901 | --tw-text-opacity: 1; 902 | color: rgb(255 255 255 / var(--tw-text-opacity)); 903 | } 904 | 905 | .shadow-sm { 906 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 907 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); 908 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 909 | } 910 | 911 | .shadow-xl { 912 | --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 913 | --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); 914 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 915 | } 916 | 917 | .transition-colors { 918 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; 919 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 920 | transition-duration: 150ms; 921 | } 922 | 923 | #modal { 924 | /* Underlay covers entire screen. */ 925 | position: fixed; 926 | top: 0px; 927 | bottom: 0px; 928 | left: 0px; 929 | right: 0px; 930 | background-color: rgba(0, 0, 0, 0.5); 931 | z-index: 1000; 932 | /* Flexbox centers the .modal-content vertically and horizontally */ 933 | display: flex; 934 | flex-direction: column; 935 | align-items: center; 936 | /* Animate when opening */ 937 | animation-name: fadeIn; 938 | animation-duration: 150ms; 939 | animation-timing-function: ease; 940 | } 941 | 942 | #modal > .modal-underlay { 943 | /* underlay takes up the entire viewport. This is only 944 | required if you want to click to dismiss the popup */ 945 | position: absolute; 946 | z-index: -1; 947 | top: 0px; 948 | bottom: 0px; 949 | left: 0px; 950 | right: 0px; 951 | } 952 | 953 | #modal > .modal-content { 954 | /* Position visible dialog near the top of the window */ 955 | margin-top: 10vh; 956 | /* Sizing for visible dialog */ 957 | width: 80%; 958 | max-width: 600px; 959 | /* Display properties for visible dialog*/ 960 | border: solid 1px #999; 961 | border-radius: 8px; 962 | box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.3); 963 | background-color: white; 964 | padding: 20px; 965 | /* Animate when opening */ 966 | animation-name: zoomIn; 967 | animation-duration: 150ms; 968 | animation-timing-function: ease; 969 | } 970 | 971 | #modal.closing { 972 | /* Animate when closing */ 973 | animation-name: fadeOut; 974 | animation-duration: 150ms; 975 | animation-timing-function: ease; 976 | } 977 | 978 | #modal.closing > .modal-content { 979 | /* Animate when closing */ 980 | animation-name: zoomOut; 981 | animation-duration: 150ms; 982 | animation-timing-function: ease; 983 | } 984 | 985 | @keyframes fadeIn { 986 | 0% { 987 | opacity: 0; 988 | } 989 | 990 | 100% { 991 | opacity: 1; 992 | } 993 | } 994 | 995 | @keyframes fadeOut { 996 | 0% { 997 | opacity: 1; 998 | } 999 | 1000 | 100% { 1001 | opacity: 0; 1002 | } 1003 | } 1004 | 1005 | @keyframes zoomIn { 1006 | 0% { 1007 | transform: scale(0.9); 1008 | } 1009 | 1010 | 100% { 1011 | transform: scale(1); 1012 | } 1013 | } 1014 | 1015 | @keyframes zoomOut { 1016 | 0% { 1017 | transform: scale(1); 1018 | } 1019 | 1020 | 100% { 1021 | transform: scale(0.9); 1022 | } 1023 | } 1024 | 1025 | .file\:border-0::file-selector-button { 1026 | border-width: 0px; 1027 | } 1028 | 1029 | .file\:bg-transparent::file-selector-button { 1030 | background-color: transparent; 1031 | } 1032 | 1033 | .file\:text-sm::file-selector-button { 1034 | font-size: 0.875rem; 1035 | line-height: 1.25rem; 1036 | } 1037 | 1038 | .file\:font-medium::file-selector-button { 1039 | font-weight: 500; 1040 | } 1041 | 1042 | .hover\:text-gray-600:hover { 1043 | --tw-text-opacity: 1; 1044 | color: rgb(75 85 99 / var(--tw-text-opacity)); 1045 | } 1046 | 1047 | .hover\:text-gray-700:hover { 1048 | --tw-text-opacity: 1; 1049 | color: rgb(55 65 81 / var(--tw-text-opacity)); 1050 | } 1051 | 1052 | .focus\:outline-none:focus { 1053 | outline: 2px solid transparent; 1054 | outline-offset: 2px; 1055 | } 1056 | 1057 | .focus\:ring-2:focus { 1058 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1059 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1060 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1061 | } 1062 | 1063 | .focus\:ring-offset-2:focus { 1064 | --tw-ring-offset-width: 2px; 1065 | } 1066 | 1067 | .focus-visible\:outline-none:focus-visible { 1068 | outline: 2px solid transparent; 1069 | outline-offset: 2px; 1070 | } 1071 | 1072 | .focus-visible\:ring-2:focus-visible { 1073 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1074 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1075 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1076 | } 1077 | 1078 | .focus-visible\:ring-offset-2:focus-visible { 1079 | --tw-ring-offset-width: 2px; 1080 | } 1081 | 1082 | .disabled\:pointer-events-none:disabled { 1083 | pointer-events: none; 1084 | } 1085 | 1086 | .disabled\:cursor-not-allowed:disabled { 1087 | cursor: not-allowed; 1088 | } 1089 | 1090 | .disabled\:opacity-50:disabled { 1091 | opacity: 0.5; 1092 | } -------------------------------------------------------------------------------- /tifa/static/index.js: -------------------------------------------------------------------------------- 1 | const a = 1; -------------------------------------------------------------------------------- /tifa/templates/clicked.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

登录

5 | 23 |
24 |
25 |
26 |

xxxxxxxxxxxx

27 |
    28 |
  • √ xxxxxxxx
  • 29 |
  • √ xxxxxxxx
  • 30 |
  • √ xxxxxxxx
  • 31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 | +86 41 | 46 |
47 |
48 | 53 | 56 |
57 | 60 |
61 | 73 | 74 | 忘记密码 75 | 76 |
77 |
78 |
79 |
80 |
81 |
82 | 83 | -------------------------------------------------------------------------------- /tifa/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | Document 11 | 12 | 13 |
14 |
15 | 27 | 28 | 29 | 30 | 44 |
45 |
46 |

????????????????

47 |

????????????????

48 |

???????????????? | ???????????????? | ????????????????

49 | 52 |
53 |
54 |
55 |
56 | Model 1 64 |
65 | feat01 66 |
67 | 生态圈 68 |
69 |
70 |
71 |
72 |
73 |
74 | Model 2 82 |
83 | feat01 84 |
85 | 生态圈 86 |
87 |
88 |
89 |
90 |
91 |
92 | Model 3 100 |
101 | feat01 102 |
103 | 生态圈 104 |
105 |
106 |
107 |
108 |
109 |
110 | 113 | 114 | 115 | 116 | 117 | {##} 127 | 128 | 129 | -------------------------------------------------------------------------------- /tifa/templates/modal.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tifa/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/tifa/8696ef487a89c07006f1973fe980d7674708331d/tifa/utils/__init__.py -------------------------------------------------------------------------------- /tifa/utils/pkg.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import pkgutil 3 | 4 | 5 | def import_submodules(package, recursive=True): 6 | if isinstance(package, str): 7 | package = importlib.import_module(package) 8 | results = {} 9 | for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): 10 | full_name = package.__name__ + "." + name 11 | results[full_name] = importlib.import_module(full_name) 12 | if recursive and is_pkg: 13 | results.update(import_submodules(full_name)) 14 | return results 15 | 16 | 17 | def register_fastapi_models(): 18 | pass 19 | -------------------------------------------------------------------------------- /tifa/utils/shell.py: -------------------------------------------------------------------------------- 1 | SHELL_PLUS_FASTAPI_IMPORTS = [ 2 | "from fastapi import cache", 3 | ] 4 | -------------------------------------------------------------------------------- /tifa/worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asgiref.sync import async_to_sync 4 | from raven import Client 5 | 6 | from tifa.globals import celery 7 | from tifa.settings import settings 8 | 9 | client_sentry = Client(settings.SENTRY_DSN) 10 | 11 | 12 | @celery.task(name="test_celery") 13 | def test_celery(word: str) -> str: 14 | f = f"test_celery {word}" 15 | print(f) 16 | return f 17 | 18 | 19 | def task_cpu_bound(): 20 | return "good result" 21 | 22 | 23 | @celery.task(name="test_celery_asyncio_cpu_bound") 24 | def test_celery_asyncio_cpu_bound(): 25 | # assume cpu bound 26 | return task_cpu_bound() 27 | 28 | 29 | async def task_io_bound(): 30 | await asyncio.sleep(1) 31 | return "good result" 32 | 33 | 34 | @celery.task(name="test_celery_asyncio_io_bound") 35 | def test_celery_asyncio_io_bound(): 36 | async_to_sync(task_io_bound)() 37 | --------------------------------------------------------------------------------