diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1d17dae --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.venv diff --git a/.gitignore b/.gitignore index 80d6cbe..10e812a 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,5 @@ ENV/ config.yaml .volume elasticsearch-analysis-ik -.pytest_cache \ No newline at end of file +.pytest_cache +.venv diff --git a/Makefile b/Makefile index cde9858..83dd075 100644 --- a/Makefile +++ b/Makefile @@ -67,22 +67,8 @@ monitor: ## flower # docker images build-tifa: ## > tifa - docker build -t 'twocucao/tifa:latest' -f 'compose/app/Dockerfile' . + docker build -t 'tifa:local' -f 'compose/app/Dockerfile' . build-tifa-no-cache: ## > tifa - docker build -t 'twocucao/tifa:latest' -f 'compose/app/Dockerfile' --no-cache . - -build-elasticsearch: ## > elasticsearch - docker build -t 'elasticsearch:local' -f 'compose/elasticsearch/Dockerfile' . - -build-elasticsearch-no-cache: ## > elasticsearch - docker build -t 'elasticsearch:local' -f 'compose/elasticsearch/Dockerfile' . --no-cache - -publish-tifa-image: ## > build and publish tifa image - echo ${DOCKER_PASS} | docker login -u twocucao --password-stdin - docker pull twocucao/tifa:latest || true - docker build -t 'tifa:latest' -f 'compose/app/Dockerfile' . --cache-from=twocucao/tifa:latest - docker tag 'tifa:latest' twocucao/tifa:latest - docker push twocucao/tifa:latest || true - + docker build -t 'tifa:local' -f 'compose/app/Dockerfile' --no-cache . diff --git a/compose/app/Dockerfile b/compose/app/Dockerfile index e332712..259ae71 100644 --- a/compose/app/Dockerfile +++ b/compose/app/Dockerfile @@ -1,35 +1,47 @@ -FROM python:3.10.9-buster +FROM python:3.11.8-bullseye ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update &&\ apt-get upgrade -y &&\ apt-get install -y \ - vim \ - git \ - gcc \ + liblzma-dev \ build-essential \ - libffi-dev \ + cmake \ + curl \ freetds-bin \ + gcc \ + git \ krb5-user \ ldap-utils \ - libffi6 \ + libbz2-dev \ + libffi-dev \ + libncurses5-dev \ + libncursesw5-dev \ + libreadline-dev \ libsasl2-2 \ libsasl2-modules \ + libsqlite3-dev \ + libssl-dev \ libssl1.1 \ + llvm \ locales \ lsb-release \ sasl2-bin \ sqlite3 \ - unixodbc - + unixodbc \ + vim \ + wget \ + xz-utils \ + zlib1g-dev \ + tk-dev +ENV RYE_NO_AUTO_INSTALL=1 +RUN curl -sSf https://rye-up.com/get | RYE_INSTALL_OPTION="--yes" bash +RUN /root/.rye/shims/rye pin 3.11.8 ENV PYPI=https://mirrors.cloud.tencent.com/pypi/simple -ENV PIP_DEFAULT_TIMEOUT=1000 RUN pip install -U pip -i $PYPI -RUN pip install -U poetry -i $PYPI -ENV POETRY_VIRTUALENVS_CREATE=false WORKDIR /opt/tifa COPY . . -RUN poetry install +RUN --mount=type=cache,target=/root/.cache /root/.rye/shims/rye sync CMD ["start"] diff --git a/docker-compose.yml b/docker-compose.yml index 8e6b4a6..f01964c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.7" x-tifa-common: &tifa-common - image: twocucao/tifa:latest + image: tifa:local volumes: - .:/opt/tifa environment: diff --git a/pyproject.toml b/pyproject.toml index 3168c8e..ea30ed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,89 +1,63 @@ -[tool.poetry] +[project] name = "tifa" version = "0.1.0" -description = "" -authors = ["twocucao "] -include = ["tifa/templates/", "tifa/static/"] +description = "Add your description here" +authors = [ + { name = "twocucao", email = "twocucao@gmail.com" } +] +dependencies = [ + "fastapi>=0.110.0", + "ipython>=8.22.2", + "pandas>=2.2.1", + "aiofiles>=23.2.1", + "tenacity>=8.2.3", + "gunicorn>=21.2.0", + "celery>=5.3.6", + "aiohttp>=3.9.3", + "sqlalchemy[asyncio]>=2.0.28", + "loguru>=0.7.2", + "aiobotocore>=2.12.1", + "aiomysql>=0.2.0", + "aioredis>=2.0.1", + "greenlet>=3.0.3", + "jinja2>=3.1.3", + "orjson>=3.9.15", + "passlib>=1.7.4", + "pillow>=10.2.0", + "pydantic>=2.6.4", + "pydantic-core>=2.16.3", + "pydantic-settings>=2.2.1", + "alembic>=1.13.1", + "asyncer>=0.0.5", + "python-dotenv>=1.0.1", + "python-jose>=3.3.0", + "requests>=2.31.0", + "rich>=13.7.1", + "typer>=0.9.0", + "uvicorn>=0.28.0", +] +readme = "README.md" +requires-python = ">= 3.11" -[[tool.poetry.source]] -name = "tencent" -url = 'https://mirrors.cloud.tencent.com/pypi/simple' -priority = 'default' +[project.scripts] +fastcli = 'tifa.cli.cli:app' -[tool.poetry.dependencies] -python = "^3.10" -ipython = "*" -markdown = "*" -xlsxwriter = "*" -xlwt = "*" -fastapi = "^0.70.1" -uvicorn = "*" -typer = "*" -python-jose = {extras = ["cryptography"], version = "^3.1.0"} -passlib = {extras = ["bcrypt"], version = "^1.7.2"} -aiofiles = "*" -orjson = "*" -python-dotenv = "^0.12.0" -asyncpg = "*" -aioredis = "*" -aiokafka = "*" -prometheus_client = "*" -devtools = "*" -python-socketio = {extras = ["asyncio_client"], version = "^5.2.1"} -jinja2 = "*" -requests = "^2.25.1" -aiohttp = "^3.7.4" -celery = "^5.3.1" -fastapi-utils = "^0.2.1" -gunicorn = "*" -pandas = "*" -pillow = "*" -psycopg2-binary = "^2.9.6" -pypinyin = "^0.42.0" -qrcode = "^7.1" -raven = "^6.10.0" -redis = "^3.5.3" -tenacity = "^7.0.0" -xlrd = "^2.0.1" -opentelemetry-api = "^1.4.0" -opentelemetry-exporter-jaeger = "^1.4.0" -opentelemetry-sdk = "^1.4.0" -opentelemetry-instrumentation-fastapi = "^0.23b2" -opentelemetry-instrumentation-aiohttp-client = "^0.23b2" -elasticsearch = "^8.9.0" -django = "^4.2.3" -dj-database-url = "^2.0.0" +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry.dev-dependencies] -black = "*" -autoflake = "*" -coverage = "*" -"flake8" = "*" -mypy = "*" -pytest = "*" -pytest-cov = "*" -pre-commit = "*" -pytest-asyncio = "^0.15.1" +[tool.rye] +managed = true +dev-dependencies = [ + "ruff>=0.3.2", + "pytest>=8.1.1", + "mypy>=1.9.0", + "pytest-asyncio>=0.23.5.post1", +] -[tool.black] -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist -)/ -''' -[tool.poetry.scripts] -fastcli = 'tifa.cli:cli' -tifa-cli = 'tifa.cli:cli' -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.hatch.metadata] +allow-direct-references = true +[tool.hatch.build.targets.wheel] +packages = ["src/tifa"] diff --git a/pyproject.toml.bak b/pyproject.toml.bak new file mode 100644 index 0000000..66f9c86 --- /dev/null +++ b/pyproject.toml.bak @@ -0,0 +1,92 @@ +project = { dependencies = [ + "fastapi>=0.110.0", +] , requires-python = ">= 3.11" } +[tool.poetry] +name = "tifa" +version = "0.1.0" +description = "" +authors = ["twocucao "] +include = ["tifa/templates/", "tifa/static/"] + +[[tool.poetry.source]] +name = "tencent" +url = 'https://mirrors.cloud.tencent.com/pypi/simple' +priority = 'default' + +[tool.poetry.dependencies] +python = "^3.10" +ipython = "*" +markdown = "*" +xlsxwriter = "*" +xlwt = "*" +fastapi = "^0.70.1" +uvicorn = "*" +typer = "*" +python-jose = {extras = ["cryptography"], version = "^3.1.0"} +passlib = {extras = ["bcrypt"], version = "^1.7.2"} +aiofiles = "*" +orjson = "*" +python-dotenv = "^0.12.0" +asyncpg = "*" +aioredis = "*" +aiokafka = "*" +prometheus_client = "*" +devtools = "*" +python-socketio = {extras = ["asyncio_client"], version = "^5.2.1"} +jinja2 = "*" +requests = "^2.25.1" +aiohttp = "^3.7.4" +celery = "^5.3.1" +fastapi-utils = "^0.2.1" +gunicorn = "*" +pandas = "*" +pillow = "*" +psycopg2-binary = "^2.9.6" +pypinyin = "^0.42.0" +qrcode = "^7.1" +raven = "^6.10.0" +redis = "^3.5.3" +tenacity = "^7.0.0" +xlrd = "^2.0.1" +opentelemetry-api = "^1.4.0" +opentelemetry-exporter-jaeger = "^1.4.0" +opentelemetry-sdk = "^1.4.0" +opentelemetry-instrumentation-fastapi = "^0.23b2" +opentelemetry-instrumentation-aiohttp-client = "^0.23b2" +elasticsearch = "^8.9.0" +django = "^4.2.3" +dj-database-url = "^2.0.0" + +[tool.poetry.dev-dependencies] +black = "*" +autoflake = "*" +coverage = "*" +"flake8" = "*" +mypy = "*" +pytest = "*" +pytest-cov = "*" +pre-commit = "*" +pytest-asyncio = "^0.15.1" + +[tool.black] +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' +[tool.poetry.scripts] +fastcli = 'tifa.cli:cli' +tifa-cli = 'tifa.cli:cli' + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..67bed4c --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,230 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false + +-e file:. +aiobotocore==2.12.1 + # via tifa +aiofiles==23.2.1 + # via tifa +aiohttp==3.9.3 + # via aiobotocore + # via tifa +aioitertools==0.11.0 + # via aiobotocore +aiomysql==0.2.0 + # via tifa +aioredis==2.0.1 + # via tifa +aiosignal==1.3.1 + # via aiohttp +alembic==1.13.1 + # via tifa +amqp==5.2.0 + # via kombu +annotated-types==0.6.0 + # via pydantic +anyio==4.3.0 + # via asyncer + # via starlette +asttokens==2.4.1 + # via stack-data +async-timeout==4.0.3 + # via aioredis +asyncer==0.0.5 + # via tifa +attrs==23.2.0 + # via aiohttp +billiard==4.2.0 + # via celery +botocore==1.34.51 + # via aiobotocore +celery==5.3.6 + # via tifa +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via celery + # via click-didyoumean + # via click-plugins + # via click-repl + # via typer + # via uvicorn +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.3.0 + # via celery +decorator==5.1.1 + # via ipython +ecdsa==0.18.0 + # via python-jose +executing==2.0.1 + # via stack-data +fastapi==0.110.0 + # via tifa +frozenlist==1.4.1 + # via aiohttp + # via aiosignal +greenlet==3.0.3 + # via sqlalchemy + # via tifa +gunicorn==21.2.0 + # via tifa +h11==0.14.0 + # via uvicorn +idna==3.6 + # via anyio + # via requests + # via yarl +iniconfig==2.0.0 + # via pytest +ipython==8.22.2 + # via tifa +jedi==0.19.1 + # via ipython +jinja2==3.1.3 + # via tifa +jmespath==1.0.1 + # via botocore +kombu==5.3.5 + # via celery +loguru==0.7.2 + # via tifa +mako==1.3.2 + # via alembic +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 + # via mako +matplotlib-inline==0.1.6 + # via ipython +mdurl==0.1.2 + # via markdown-it-py +multidict==6.0.5 + # via aiohttp + # via yarl +mypy==1.9.0 +mypy-extensions==1.0.0 + # via mypy +numpy==1.26.4 + # via pandas +orjson==3.9.15 + # via tifa +packaging==24.0 + # via gunicorn + # via pytest +pandas==2.2.1 + # via tifa +parso==0.8.3 + # via jedi +passlib==1.7.4 + # via tifa +pexpect==4.9.0 + # via ipython +pillow==10.2.0 + # via tifa +pluggy==1.4.0 + # via pytest +prompt-toolkit==3.0.43 + # via click-repl + # via ipython +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.2 + # via stack-data +pyasn1==0.5.1 + # via python-jose + # via rsa +pydantic==2.6.4 + # via fastapi + # via pydantic-settings + # via tifa +pydantic-core==2.16.3 + # via pydantic + # via tifa +pydantic-settings==2.2.1 + # via tifa +pygments==2.17.2 + # via ipython + # via rich +pymysql==1.1.0 + # via aiomysql +pytest==8.1.1 + # via pytest-asyncio +pytest-asyncio==0.23.5.post1 +python-dateutil==2.9.0.post0 + # via botocore + # via celery + # via pandas +python-dotenv==1.0.1 + # via pydantic-settings + # via tifa +python-jose==3.3.0 + # via tifa +pytz==2024.1 + # via pandas +requests==2.31.0 + # via tifa +rich==13.7.1 + # via tifa +rsa==4.9 + # via python-jose +ruff==0.3.2 +six==1.16.0 + # via asttokens + # via ecdsa + # via python-dateutil +sniffio==1.3.1 + # via anyio +sqlalchemy==2.0.28 + # via alembic + # via sqlalchemy + # via tifa +stack-data==0.6.3 + # via ipython +starlette==0.36.3 + # via fastapi +tenacity==8.2.3 + # via tifa +traitlets==5.14.2 + # via ipython + # via matplotlib-inline +typer==0.9.0 + # via tifa +typing-extensions==4.10.0 + # via aioredis + # via alembic + # via fastapi + # via mypy + # via pydantic + # via pydantic-core + # via sqlalchemy + # via typer +tzdata==2024.1 + # via celery + # via pandas +urllib3==2.0.7 + # via botocore + # via requests +uvicorn==0.28.0 + # via tifa +vine==5.1.0 + # via amqp + # via celery + # via kombu +wcwidth==0.2.13 + # via prompt-toolkit +wrapt==1.16.0 + # via aiobotocore +yarl==1.9.4 + # via aiohttp diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..0167ce2 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,217 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false + +-e file:. +aiobotocore==2.12.1 + # via tifa +aiofiles==23.2.1 + # via tifa +aiohttp==3.9.3 + # via aiobotocore + # via tifa +aioitertools==0.11.0 + # via aiobotocore +aiomysql==0.2.0 + # via tifa +aioredis==2.0.1 + # via tifa +aiosignal==1.3.1 + # via aiohttp +alembic==1.13.1 + # via tifa +amqp==5.2.0 + # via kombu +annotated-types==0.6.0 + # via pydantic +anyio==4.3.0 + # via asyncer + # via starlette +asttokens==2.4.1 + # via stack-data +async-timeout==4.0.3 + # via aioredis +asyncer==0.0.5 + # via tifa +attrs==23.2.0 + # via aiohttp +billiard==4.2.0 + # via celery +botocore==1.34.51 + # via aiobotocore +celery==5.3.6 + # via tifa +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via celery + # via click-didyoumean + # via click-plugins + # via click-repl + # via typer + # via uvicorn +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.3.0 + # via celery +decorator==5.1.1 + # via ipython +ecdsa==0.18.0 + # via python-jose +executing==2.0.1 + # via stack-data +fastapi==0.110.0 + # via tifa +frozenlist==1.4.1 + # via aiohttp + # via aiosignal +greenlet==3.0.3 + # via sqlalchemy + # via tifa +gunicorn==21.2.0 + # via tifa +h11==0.14.0 + # via uvicorn +idna==3.6 + # via anyio + # via requests + # via yarl +ipython==8.22.2 + # via tifa +jedi==0.19.1 + # via ipython +jinja2==3.1.3 + # via tifa +jmespath==1.0.1 + # via botocore +kombu==5.3.5 + # via celery +loguru==0.7.2 + # via tifa +mako==1.3.2 + # via alembic +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 + # via mako +matplotlib-inline==0.1.6 + # via ipython +mdurl==0.1.2 + # via markdown-it-py +multidict==6.0.5 + # via aiohttp + # via yarl +numpy==1.26.4 + # via pandas +orjson==3.9.15 + # via tifa +packaging==24.0 + # via gunicorn +pandas==2.2.1 + # via tifa +parso==0.8.3 + # via jedi +passlib==1.7.4 + # via tifa +pexpect==4.9.0 + # via ipython +pillow==10.2.0 + # via tifa +prompt-toolkit==3.0.43 + # via click-repl + # via ipython +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.2 + # via stack-data +pyasn1==0.5.1 + # via python-jose + # via rsa +pydantic==2.6.4 + # via fastapi + # via pydantic-settings + # via tifa +pydantic-core==2.16.3 + # via pydantic + # via tifa +pydantic-settings==2.2.1 + # via tifa +pygments==2.17.2 + # via ipython + # via rich +pymysql==1.1.0 + # via aiomysql +python-dateutil==2.9.0.post0 + # via botocore + # via celery + # via pandas +python-dotenv==1.0.1 + # via pydantic-settings + # via tifa +python-jose==3.3.0 + # via tifa +pytz==2024.1 + # via pandas +requests==2.31.0 + # via tifa +rich==13.7.1 + # via tifa +rsa==4.9 + # via python-jose +six==1.16.0 + # via asttokens + # via ecdsa + # via python-dateutil +sniffio==1.3.1 + # via anyio +sqlalchemy==2.0.28 + # via alembic + # via sqlalchemy + # via tifa +stack-data==0.6.3 + # via ipython +starlette==0.36.3 + # via fastapi +tenacity==8.2.3 + # via tifa +traitlets==5.14.2 + # via ipython + # via matplotlib-inline +typer==0.9.0 + # via tifa +typing-extensions==4.10.0 + # via aioredis + # via alembic + # via fastapi + # via pydantic + # via pydantic-core + # via sqlalchemy + # via typer +tzdata==2024.1 + # via celery + # via pandas +urllib3==2.0.7 + # via botocore + # via requests +uvicorn==0.28.0 + # via tifa +vine==5.1.0 + # via amqp + # via celery + # via kombu +wcwidth==0.2.13 + # via prompt-toolkit +wrapt==1.16.0 + # via aiobotocore +yarl==1.9.4 + # via aiohttp diff --git a/tifa/__init__.py b/src/tifa/__init__.py similarity index 100% rename from tifa/__init__.py rename to src/tifa/__init__.py diff --git a/tifa/api.py b/src/tifa/api.py similarity index 96% rename from tifa/api.py rename to src/tifa/api.py index e06a130..22e970e 100644 --- a/tifa/api.py +++ b/src/tifa/api.py @@ -1,5 +1,3 @@ -import json - from fastapi.responses import ORJSONResponse diff --git a/tifa/app.py b/src/tifa/app.py similarity index 87% rename from tifa/app.py rename to src/tifa/app.py index 45ee89c..af31bba 100644 --- a/tifa/app.py +++ b/src/tifa/app.py @@ -5,18 +5,15 @@ from starlette.middleware.base import BaseHTTPMiddleware from starlette.staticfiles import StaticFiles -from tifa.contrib.globals import GlobalsMiddleware from tifa.settings import settings from tifa.utils.pkg import import_submodules def setup_routers(app: FastAPI): - from tifa.apps import user, health, whiteboard, admin + from tifa.apps import user, health app.mount("/health", health.bp) - # app.mount("/admin", admin.bp) app.mount("/user", user.bp) - app.mount("/whiteboard", whiteboard.bp) app.mount("/metrics", make_asgi_app()) @@ -62,8 +59,6 @@ def create_app(): title=settings.TITLE, description=settings.DESCRIPTION, ) - # thread local just flask like g - app.add_middleware(GlobalsMiddleware) # 注册 db models setup_db_models(app) # 初始化路由 diff --git a/tifa/apps/__init__.py b/src/tifa/apps/__init__.py similarity index 100% rename from tifa/apps/__init__.py rename to src/tifa/apps/__init__.py diff --git a/tifa/apps/admin/__init__.py b/src/tifa/apps/admin/__init__.py similarity index 100% rename from tifa/apps/admin/__init__.py rename to src/tifa/apps/admin/__init__.py diff --git a/tifa/apps/deps.py b/src/tifa/apps/deps.py similarity index 100% rename from tifa/apps/deps.py rename to src/tifa/apps/deps.py diff --git a/tifa/apps/health/__init__.py b/src/tifa/apps/health/__init__.py similarity index 100% rename from tifa/apps/health/__init__.py rename to src/tifa/apps/health/__init__.py diff --git a/tifa/apps/user/__init__.py b/src/tifa/apps/user/__init__.py similarity index 100% rename from tifa/apps/user/__init__.py rename to src/tifa/apps/user/__init__.py diff --git a/tifa/apps/user/router.py b/src/tifa/apps/user/router.py similarity index 98% rename from tifa/apps/user/router.py rename to src/tifa/apps/user/router.py index 4d06b2b..b61c131 100644 --- a/tifa/apps/user/router.py +++ b/src/tifa/apps/user/router.py @@ -1,5 +1,3 @@ -import json - from fastapi import Depends from starlette.requests import Request diff --git a/tifa/asgi/__init__.py b/src/tifa/asgi/__init__.py similarity index 100% rename from tifa/asgi/__init__.py rename to src/tifa/asgi/__init__.py diff --git a/tifa/asgi/health_check.py b/src/tifa/asgi/health_check.py similarity index 100% rename from tifa/asgi/health_check.py rename to src/tifa/asgi/health_check.py diff --git a/tifa/asgi/urls.py b/src/tifa/asgi/urls.py similarity index 100% rename from tifa/asgi/urls.py rename to src/tifa/asgi/urls.py diff --git a/tifa/asgi/worker.py b/src/tifa/asgi/worker.py similarity index 100% rename from tifa/asgi/worker.py rename to src/tifa/asgi/worker.py diff --git a/tifa/auth.py b/src/tifa/auth.py similarity index 94% rename from tifa/auth.py rename to src/tifa/auth.py index 17db0fc..11205a2 100644 --- a/tifa/auth.py +++ b/src/tifa/auth.py @@ -18,8 +18,8 @@ def decode_jwt(token): return payload except jwt.JWTError: raise ApiException( + "Could not validate credentials", status_code=403, - message="Could not validate credentials", ) diff --git a/tifa/contrib/__init__.py b/src/tifa/cli/__init__.py similarity index 100% rename from tifa/contrib/__init__.py rename to src/tifa/cli/__init__.py diff --git a/tifa/cli/base.py b/src/tifa/cli/base.py similarity index 100% rename from tifa/cli/base.py rename to src/tifa/cli/base.py diff --git a/src/tifa/cli/cli.py b/src/tifa/cli/cli.py new file mode 100644 index 0000000..bafa846 --- /dev/null +++ b/src/tifa/cli/cli.py @@ -0,0 +1,64 @@ +import importlib + +import typer +from loguru import logger + +banner = """ + _______ _ __ + |__ __| (_) / _| + | | _ | |_ __ _ + | | | | | _| / _` | + | | | | | | | (_| | + |_| |_| |_| \__,_| + + An opinionated fastapi starter-kit + by @hylarucoder +""" + +app = typer.Typer() + + +@app.command("shell_plus") +def shell_plus(): + from tifa.db import session_factory + from tifa.utils.pkg import scan_models + from labelsmith.settings import settings # noqa + from IPython import embed + from traitlets.config import get_config + from tifa.db import async_session_factory + import cProfile + import pdb + + logger.info("shell plus setup complete") + + main = importlib.import_module("__main__") + ctx = main.__dict__ + async_session = async_session_factory() + + # async def cleanup(): + # await async_session.close() + # session.close() + + session = session_factory() + ctx.update( + { + "async_session": async_session, + "session": session, + "pdb": pdb, + "cProfile": cProfile, + } + ) + + models = scan_models() + ctx.update(models) + c = get_config() + embed(user_ns=ctx, banner2=banner, config=c, using="asyncio") + + +@app.command() +def hello(name: str): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/tifa/cli/web.py b/src/tifa/cli/web.py similarity index 100% rename from tifa/cli/web.py rename to src/tifa/cli/web.py diff --git a/tifa/cli/worker.py b/src/tifa/cli/worker.py similarity index 89% rename from tifa/cli/worker.py rename to src/tifa/cli/worker.py index b335073..a81786b 100644 --- a/tifa/cli/worker.py +++ b/src/tifa/cli/worker.py @@ -28,10 +28,8 @@ def start_monitor(): @group_worker.command("pg_to_es") -def pg_to_es(): - ... +def pg_to_es(): ... @group_worker.command("test") -def test_worker(): - ... +def test_worker(): ... diff --git a/tifa/consts.py b/src/tifa/consts.py similarity index 100% rename from tifa/consts.py rename to src/tifa/consts.py diff --git a/tifa/management/__init__.py b/src/tifa/contrib/__init__.py similarity index 100% rename from tifa/management/__init__.py rename to src/tifa/contrib/__init__.py diff --git a/src/tifa/contrib/fastapi_plus.py b/src/tifa/contrib/fastapi_plus.py new file mode 100644 index 0000000..3eff401 --- /dev/null +++ b/src/tifa/contrib/fastapi_plus.py @@ -0,0 +1,48 @@ +from fastapi import FastAPI, HTTPException +from pydantic import ValidationError, BaseModel +from starlette.requests import Request +from starlette.responses import JSONResponse + +from tifa.exceptions import ApiException, UnicornException, unicorn_exception_handler + + +class ApiModel(BaseModel): + ... + + +class FastAPIPlus(FastAPI): + ... + + +def setup_error_handlers(app: FastAPI): + app.add_exception_handler(UnicornException, unicorn_exception_handler) + app.add_exception_handler(ApiException, lambda request, err: err.to_result()) + + @app.exception_handler(ValidationError) + async def validation_handler(request: Request, exc: ValidationError): + return JSONResponse( + status_code=400, + content={ + "message": "参数错误", + "errors": exc.errors(), + }, + ) + + @app.exception_handler(HTTPException) + async def http_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=exc.status_code, + content={"message": exc.detail}, + ) + + @app.exception_handler(Exception) + def handle_exc(request: Request, exc): + raise exc + + +def create_bp(dependencies: list = None) -> FastAPIPlus: + if not dependencies: + dependencies = [] + app = FastAPIPlus(dependencies=dependencies) + setup_error_handlers(app) + return app diff --git a/tifa/contrib/kafka.py b/src/tifa/contrib/kafka.py similarity index 100% rename from tifa/contrib/kafka.py rename to src/tifa/contrib/kafka.py diff --git a/tifa/contrib/redis.py b/src/tifa/contrib/redis.py similarity index 100% rename from tifa/contrib/redis.py rename to src/tifa/contrib/redis.py diff --git a/src/tifa/db.py b/src/tifa/db.py new file mode 100644 index 0000000..b21e273 --- /dev/null +++ b/src/tifa/db.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import typing as t + +import sqlalchemy as sa +from sqlalchemy import ScalarResult, create_engine +from sqlalchemy.ext.asyncio import ( + create_async_engine, + async_sessionmaker, + AsyncSession, +) +from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker +from typing_extensions import Self + +from tifa.exceptions import NotFound +from tifa.settings import settings + + +class Model(DeclarativeBase): + @classmethod + def find_by_id(cls, session: Session, id: str) -> Self | None: + ins = session.get(cls, id) + return ins + + @classmethod + def create(cls, session: Session, **kwargs) -> Self: + ins = cls(**kwargs) + session.add(ins) + session.commit() + return ins + + @classmethod + async def async_find_by_id( + cls, session: AsyncSession, id: str | int + ) -> Self | None: + return await session.get(cls, id) + + @classmethod + async def async_bulk_find_by_ids( + cls, session: AsyncSession, ids: list[str | int] + ) -> ScalarResult[Self]: + stmt = sa.select(cls).filter(cls.id.in_(ids)) + items = (await session.execute(stmt)).scalars() + return items + + @classmethod + async def async_find(cls, session: AsyncSession, stmt: sa.Select) -> list[Self]: + return list((await session.execute(sa.select(cls).filter(stmt))).scalars()) + + @classmethod + async def async_find_first(cls, session: AsyncSession, stmt: sa.Select) -> Self: + return (await session.execute(sa.select(cls).filter(stmt))).scalar_one_or_none() + + @classmethod + async def async_create(cls, session: AsyncSession, **kwargs) -> Self: + ins = cls(**kwargs) + session.add(ins) + await session.commit() + return ins + + @classmethod + async def async_bulk_create( + cls, session: AsyncSession, items: list[dict] + ) -> list[Self]: + ins_list = [] + for item in items: + ins = cls(**item) + ins_list.append(ins) + session.add_all(ins_list) + await session.commit() + return ins_list + + @classmethod + async def async_find_first_or_404( + cls, session: AsyncSession, stmt: sa.Select + ) -> Self: + ins = await cls.async_find_first(session, stmt) + if not ins: + raise NotFound("Not Found") + return ins + + +async def get_async_session() -> t.AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + try: + yield session + # await session.commit() + except sa.exc.SQLAlchemyError as error: + await session.rollback() + raise + + +engine = create_engine( + settings.DATABASE_URL, + echo=False, +) + +session_factory = sessionmaker(autocommit=False, expire_on_commit=False, bind=engine) + +async_engine = create_async_engine(settings.ASYNC_DATABASE_URL, **{}) +async_session_factory = async_sessionmaker( + autocommit=False, expire_on_commit=False, bind=async_engine +) diff --git a/tifa/exceptions.py b/src/tifa/exceptions.py similarity index 100% rename from tifa/exceptions.py rename to src/tifa/exceptions.py diff --git a/tifa/globals.py b/src/tifa/globals.py similarity index 100% rename from tifa/globals.py rename to src/tifa/globals.py diff --git a/src/tifa/models/__init__.py b/src/tifa/models/__init__.py new file mode 100644 index 0000000..2467f42 --- /dev/null +++ b/src/tifa/models/__init__.py @@ -0,0 +1 @@ +from .project import * # noqa diff --git a/src/tifa/models/user.py b/src/tifa/models/user.py new file mode 100644 index 0000000..c17f1c0 --- /dev/null +++ b/src/tifa/models/user.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import datetime + +import sqlalchemy as sa +from sqlalchemy.orm import Mapped, mapped_column + +from tifa.db import Model + + +class User(Model): + __tablename__ = "user" + + id: Mapped[int] = mapped_column(sa.BigInteger, primary_key=True) + username: Mapped[str] = mapped_column(sa.String(100), index=True) + created_at: Mapped[datetime.datetime] = mapped_column( + sa.DateTime, default=datetime.datetime.now + ) + updated_at: Mapped[datetime.datetime] = mapped_column( + sa.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now + ) diff --git a/src/tifa/settings.py b/src/tifa/settings.py new file mode 100644 index 0000000..3508ac8 --- /dev/null +++ b/src/tifa/settings.py @@ -0,0 +1,37 @@ +import os +import pathlib +import warnings +from typing import Optional + +from pydantic_settings import BaseSettings + +ROOT = pathlib.Path(__file__).parent.absolute() + +APP_SETTINGS = os.environ.get("APP_SETTINGS") +if not APP_SETTINGS: + warnings.warn("!!!未指定APP_SETTINGS!!!, 极有可能运行错误") + + +class GlobalSetting(BaseSettings): + TITLE: str = "Tifa" + DESCRIPTION: str = "Yet another opinionated fastapi-start-kit with best practice" + + TEMPLATE_PATH: str = f"{ROOT}/templates" + + STATIC_PATH: str = "/static" + STATIC_DIR: str = f"{ROOT}/static" + + SENTRY_DSN: Optional[str] + + DEBUG: bool = False + ENV: str = "LOCAL" + SECRET_KEY: str = "change me" + + DATABASE_URL: str = "postgresql://tifa:tifa%26123@postgres:5432/tifa" + ASYNC_DATABASE_URL: str = "postgresql://tifa:tifa%26123@postgres:5432/tifa" + REDIS_CACHE_URI: str = "redis://localhost:6379" + REDIS_CELERY_URL: str = "redis://redis:6379/6" + WHITEBOARD_URI: str = "redis://redis:6379/1" + + +settings = GlobalSetting(_env_file=APP_SETTINGS) diff --git a/tifa/static/index.js b/src/tifa/static/index.js similarity index 100% rename from tifa/static/index.js rename to src/tifa/static/index.js diff --git a/tifa/templates/index.html b/src/tifa/templates/index.html similarity index 100% rename from tifa/templates/index.html rename to src/tifa/templates/index.html diff --git a/tifa/management/commands/__init__.py b/src/tifa/utils/__init__.py similarity index 100% rename from tifa/management/commands/__init__.py rename to src/tifa/utils/__init__.py diff --git a/src/tifa/utils/pkg.py b/src/tifa/utils/pkg.py new file mode 100644 index 0000000..38eb61e --- /dev/null +++ b/src/tifa/utils/pkg.py @@ -0,0 +1,52 @@ +import importlib +import inspect +import itertools +import pkgutil + + +def import_submodules(package, recursive=True): + if isinstance(package, str): + package = importlib.import_module(package) + results = {} + for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): + full_name = package.__name__ + "." + name + results[full_name] = importlib.import_module(full_name) + if recursive and is_pkg: + results.update(import_submodules(full_name)) + return results + + +def register_fastapi_models(): + pass + + +def scan_models(): + from tifa.db import Model + + models = {} + for exc_class in itertools.chain( + class_scanner("labelsmith.models", lambda exc: issubclass(exc, (Model))) + ): + models[exc_class.__name__] = exc_class + return models + + +def class_scanner(pkg, filter_=lambda _: True): + """ + :param pkg: a module instance or name of the module + :param filter_: a function to filter matching classes + :return: all filtered class instances + """ + + def scan_pkg_file(pkg): + return {cls[1] for cls in inspect.getmembers(pkg, inspect.isclass) if filter_(cls[1])} + + if isinstance(pkg, str): + pkg = importlib.import_module(pkg) + if not hasattr(pkg, "__path__"): + return scan_pkg_file(pkg) + classes = scan_pkg_file(pkg) + for _, modname, _ in pkgutil.iter_modules(pkg.__path__): + module = importlib.import_module("." + modname, pkg.__name__) + classes |= class_scanner(module, filter_) + return classes diff --git a/tifa/utils/shell.py b/src/tifa/utils/shell.py similarity index 100% rename from tifa/utils/shell.py rename to src/tifa/utils/shell.py diff --git a/tifa/worker.py b/src/tifa/worker.py similarity index 74% rename from tifa/worker.py rename to src/tifa/worker.py index 6a78392..d15ad82 100644 --- a/tifa/worker.py +++ b/src/tifa/worker.py @@ -1,12 +1,6 @@ import asyncio -from asgiref.sync import async_to_sync -from raven import Client - from tifa.globals import celery -from tifa.settings import settings - -client_sentry = Client(settings.SENTRY_DSN) @celery.task(name="test_celery") @@ -33,4 +27,5 @@ async def task_io_bound(): @celery.task(name="test_celery_asyncio_io_bound") def test_celery_asyncio_io_bound(): - async_to_sync(task_io_bound)() + ... + # async_to_sync(task_io_bound)() diff --git a/tests/conftest.py b/tests/conftest.py index baaa36e..f0b9e19 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,7 +36,7 @@ def op(self, url: str, json=None, **kwargs) -> Response: app = create_app() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def event_loop(): loop = asyncio.get_event_loop_policy().new_event_loop() yield loop @@ -88,10 +88,8 @@ async def user(session: AsyncSession): @pytest.fixture(scope="session") def staff_client(staff: Staff): client = ApiClient(app, staff) - token = gen_jwt("{\"admin\":1}", 60 * 24) - client.headers.update({ - "Authorization": f"Bearer {token}" - }) + token = gen_jwt('{"admin":1}', 60 * 24) + client.headers.update({"Authorization": f"Bearer {token}"}) return client @@ -119,7 +117,9 @@ async def color_attribute(session: AsyncSession): available_in_grid=True, ) adal.add(AttributeValue, attribute=attribute, name="Red", slug="red", value="red") - adal.add(AttributeValue, attribute=attribute, name="Blue", slug="blue", value="blue") + adal.add( + AttributeValue, attribute=attribute, name="Blue", slug="blue", value="blue" + ) await adal.commit() return attribute diff --git a/tests/e2e/admin/test_channel.py b/tests/e2e/admin/test_channel.py index e4906a3..1561fd7 100644 --- a/tests/e2e/admin/test_channel.py +++ b/tests/e2e/admin/test_channel.py @@ -6,7 +6,7 @@ def test_channel_curd(staff_client): "isActive": True, "slug": "test_channel_slug", "currencyCode": "usa", - } + }, ) assert channel["id"] assert channel["isActive"] @@ -19,6 +19,6 @@ def test_channel_curd(staff_client): "isActive": False, "slug": "test_channel_slug", "currencyCode": "usa", - } + }, ) assert not channel["isActive"] diff --git a/tests/functional/test_product.py b/tests/functional/test_product.py index 5d63dbc..7daa718 100644 --- a/tests/functional/test_product.py +++ b/tests/functional/test_product.py @@ -7,18 +7,20 @@ @pytest.mark.asyncio async def test_filtering_by_attribute( - session: AsyncSession, - product_type, - # color_attribute, - # size_attribute, - # # category, - # date_attribute, - # date_time_attribute, - # # boolean_attribute, + session: AsyncSession, + product_type, + # color_attribute, + # size_attribute, + # # category, + # date_attribute, + # date_time_attribute, + # # boolean_attribute, ): adal = AsyncDal(session) assert len(await adal.all(ProductType)) == 1 ... + + # product_type_a = ProductType.objects.create( # name="New class", slug="new-class1", has_variants=True # ) diff --git a/tifa/apps/whiteboard/__init__.py b/tifa/apps/whiteboard/__init__.py deleted file mode 100644 index 1b2ed26..0000000 --- a/tifa/apps/whiteboard/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -import socketio -from fastapi import Request -from socketio import AsyncServer, AsyncRedisManager -from starlette.responses import HTMLResponse -from starlette.templating import Jinja2Templates - -from tifa.contrib.fastapi_plus import create_bp -from tifa.settings import settings - -sio = AsyncServer( - client_manager=AsyncRedisManager(settings.WHITEBOARD_URI), - async_mode="asgi", - cors_allowed_origins=["*"], -) - -bp = create_bp() - -bp.mount( - "/whiteboard", - app=socketio.ASGIApp(socketio_server=sio, socketio_path="/socket.io"), # type: ignore -) - - -@sio.on("connect") -async def on_connect(sid, environ, auth): - ... - - -@sio.on("drawing") -async def on_drawing(sid, data): - await sio.emit("drawing", data, broadcast=True) - - -templates = Jinja2Templates(directory=settings.TEMPLATE_PATH) - - -@bp.get("/", response_class=HTMLResponse) -async def index(request: Request): - return templates.TemplateResponse( - "/whiteboard/index.html", {"request": request, "id": 1} - ) diff --git a/tifa/cli/__init__.py b/tifa/cli/__init__.py deleted file mode 100644 index 7dfa789..0000000 --- a/tifa/cli/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -import sys - -banner = """ - _______ _ __ - |__ __| (_) / _| - | | _ | |_ __ _ - | | | | | _| / _` | - | | | | | | | (_| | - |_| |_| |_| \__,_| - - An opinionated fastapi starter-kit - by @twocucao -""" - - -def cli(): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tifa.settings") - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - cli() diff --git a/tifa/contrib/fastapi_plus.py b/tifa/contrib/fastapi_plus.py deleted file mode 100644 index ff3f23c..0000000 --- a/tifa/contrib/fastapi_plus.py +++ /dev/null @@ -1,128 +0,0 @@ -from typing import Optional, List - -from fastapi import FastAPI, HTTPException -from fastapi_utils.api_model import APIModel -from pydantic import create_model, ValidationError -from starlette.requests import Request -from starlette.responses import JSONResponse - -from tifa.exceptions import ApiException, UnicornException, unicorn_exception_handler - - -def snake_convert(s): - if not isinstance(s, str): - return s - components = s.split("_") - return "".join(x.title() for x in components) - - -def path_to_cls_name(path): - return snake_convert(path[1:].replace("/", "_")) - - -class FastAPIPlus(FastAPI): - def item(self, path, out, summary: str = "Item", tags: Optional[list[str]] = None): - if "-Item" not in summary: - summary = summary + "-Item" - cls_name = "Item" + path_to_cls_name(path) - item_schema = create_model( # type: ignore - cls_name, - item=(out, ...), - ) - return self.get(path, response_model=item_schema, tags=tags, summary=summary) - - def list(self, path, out, summary: str = "List", tags: Optional[list[str]] = None): - if "-List" not in summary: - summary = summary + "-List" - cls_name = "List" + path_to_cls_name(path) - list_schema = create_model( # type: ignore - cls_name, - items=(list[out], ...), # type: ignore - ) - return self.get(path, response_model=list_schema, tags=tags, summary=summary) - - def page(self, path, out, summary: str = "Page", tags: Optional[List[str]] = None): - if "-Page" not in summary: - summary = summary + "-Page" - cls_name = "Page" + path_to_cls_name(path) - page_schema = create_model( # type: ignore - cls_name, - items=(list[out], ...), # type: ignore - page=(Optional[int], ...), - per_page=(Optional[int], ...), - total=(Optional[int], ...), - __base__=APIModel, - ) - return self.get(path, response_model=page_schema, tags=tags, summary=summary) - - def op( - self, path: str, out=None, summary: str = "操作", tags: Optional[List[str]] = None - ): - summary = suffix_summary(path, summary) - cls_name = "Item" + path_to_cls_name(path) - item_schema = create_model( # type: ignore - cls_name, - item=(out, ...), # type: ignore - ) - return self.post(path, response_model=item_schema, tags=tags, summary=summary) - - -def suffix_summary(path, summary): - kv = { - "/create": "-Create", - "/bulk_create": "-BulkCreate", - "/update": "-Update", - "/refresh": "-Refresh", - "/verify": "-Verify", - "/reorder": "-Reorder", - "/delete": "-Delete", - "/bulk_delete": "-BulkDelete", - "/publish": "-Publish", - "/bulk_publish": "-BulkPublish", - "/translate": "-Translate", - "/activate": "-Activate", - "/deactivate": "-Deactivate", - } - for k, v in kv.items(): - if path.endswith(k): - if v not in summary: - summary = summary + v - return summary - - -def setup_error_handlers(app: FastAPI): - app.add_exception_handler(UnicornException, unicorn_exception_handler) - - async def validation_handler(request: Request, exc: ValidationError): - return JSONResponse( - status_code=400, - content={ - "message": "参数错误", - "errors": exc.errors(), - }, - ) - - app.add_exception_handler(ValidationError, validation_handler) - - app.add_exception_handler(ApiException, lambda request, err: err.to_result()) - - async def http_exception_handler(request: Request, exc: HTTPException): - return JSONResponse( - status_code=exc.status_code, - content={"message": exc.detail}, - ) - - app.add_exception_handler(HTTPException, http_exception_handler) - - def handle_exc(request: Request, exc): - raise exc - - app.add_exception_handler(Exception, handle_exc) - - -def create_bp(dependencies: list = None) -> FastAPIPlus: - if not dependencies: - dependencies = [] - app = FastAPIPlus(dependencies=dependencies) - setup_error_handlers(app) - return app diff --git a/tifa/contrib/globals.py b/tifa/contrib/globals.py deleted file mode 100644 index 114cce2..0000000 --- a/tifa/contrib/globals.py +++ /dev/null @@ -1,47 +0,0 @@ -# thanks to https://gist.github.com/ddanier/ead419826ac6c3d75c96f9d89bea9bd0 -from contextvars import ContextVar -from typing import Any - -from starlette.types import ASGIApp, Scope, Receive, Send - - -class Globals: - __slots__ = ("_vars",) - - _vars: dict[str, ContextVar] - - def __init__(self) -> None: - object.__setattr__(self, "_vars", {}) - - def reset(self) -> None: - for _name, var in self._vars.items(): - var.set(None) - - def _ensure_var(self, item: str) -> None: - if item not in self._vars: - self._vars[item] = ContextVar(f"globals:{item}") - self._vars[item].set(None) - - def __getattr__(self, item: str) -> Any: - self._ensure_var(item) - try: - return self._vars[item].get() - except LookupError: - self._vars[item].set(None) - return None - - def __setattr__(self, item: str, value: Any) -> None: - self._ensure_var(item) - self._vars[item].set(value) - - -class GlobalsMiddleware: - def __init__(self, app: ASGIApp) -> None: - self.app = app - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - glb.reset() - await self.app(scope, receive, send) - - -glb = Globals() diff --git a/tifa/management/commands/shell_plus.py b/tifa/management/commands/shell_plus.py deleted file mode 100644 index bc9b8d9..0000000 --- a/tifa/management/commands/shell_plus.py +++ /dev/null @@ -1,28 +0,0 @@ -import importlib -import logging - -from django.core.management.base import BaseCommand - -logger = logging.getLogger(__name__) - - -class Command(BaseCommand): - help = "shell plus" - - def handle(self, *args, **options): - from IPython import embed - import cProfile - import pdb - import django.apps - - models = {model.__name__: model for model in django.apps.apps.get_models()} - main = importlib.import_module("__main__") - ctx = main.__dict__ - ctx.update( - { - **models, - "ipdb": pdb, - "cProfile": cProfile, - } - ) - embed(user_ns=ctx, banner2="", colors="neutral", using='asyncio') diff --git a/tifa/migrations/0001_initial.py b/tifa/migrations/0001_initial.py deleted file mode 100644 index ce12c27..0000000 --- a/tifa/migrations/0001_initial.py +++ /dev/null @@ -1,119 +0,0 @@ -# Generated by Django 4.2.3 on 2023-08-02 13:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="SdImage", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("sort_num", models.IntegerField(db_index=True, default=999)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "sd_image", - }, - ), - migrations.CreateModel( - name="SdModelCheckpoint", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("name", models.CharField(max_length=100)), - ("slug", models.CharField(max_length=100, unique=True)), - ("sort_num", models.IntegerField(db_index=True, default=999)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "sd_model_checkpoint", - }, - ), - migrations.CreateModel( - name="SdModelLora", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("name", models.CharField(max_length=100)), - ("slug", models.CharField(max_length=100, unique=True)), - ("sort_num", models.IntegerField(db_index=True, default=999)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "sd_model_lora", - }, - ), - migrations.CreateModel( - name="SdModelVae", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("name", models.CharField(max_length=100)), - ("slug", models.CharField(max_length=100, unique=True)), - ("sort_num", models.IntegerField(db_index=True, default=999)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "sd_model_vae", - }, - ), - migrations.CreateModel( - name="SdPrompt", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("checkpoint", models.CharField(max_length=100)), - ("prompt", models.TextField()), - ("n_prompt", models.TextField()), - ("sampler", models.CharField(max_length=50)), - ("cfg_scale", models.IntegerField(default=7)), - ("steps", models.IntegerField(default=30)), - ("seed", models.BigIntegerField(default=-1)), - ("sort_num", models.IntegerField(db_index=True, default=999)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "sd_prompt", - }, - ), - migrations.CreateModel( - name="SdPromptTag", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("name", models.CharField(max_length=100)), - ("name_zh", models.CharField(max_length=100)), - ("slug", models.CharField(max_length=100, unique=True)), - ("sort_num", models.IntegerField(db_index=True, default=999)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "sd_prompt_tag", - }, - ), - migrations.CreateModel( - name="SdSpellTemplate", - fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("checkpoint", models.CharField(max_length=100)), - ("prompt", models.TextField()), - ("n_prompt", models.TextField()), - ("sampler", models.CharField(max_length=50)), - ("cfg_scale", models.IntegerField(default=7)), - ("steps", models.IntegerField(default=30)), - ("seed", models.BigIntegerField(default=-1)), - ("sort_num", models.IntegerField(db_index=True, default=999)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "sd_spell_template", - }, - ), - ] diff --git a/tifa/migrations/0002_sdprompt_slug_sdspelltemplate_slug.py b/tifa/migrations/0002_sdprompt_slug_sdspelltemplate_slug.py deleted file mode 100644 index fea1f4d..0000000 --- a/tifa/migrations/0002_sdprompt_slug_sdspelltemplate_slug.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.3 on 2023-08-02 13:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("tifa", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="sdprompt", - name="slug", - field=models.CharField(default="", max_length=100), - preserve_default=False, - ), - migrations.AddField( - model_name="sdspelltemplate", - name="slug", - field=models.CharField(default="", max_length=100), - preserve_default=False, - ), - ] diff --git a/tifa/migrations/0003_rename_slug_sdprompt_template_slug.py b/tifa/migrations/0003_rename_slug_sdprompt_template_slug.py deleted file mode 100644 index 140a2f1..0000000 --- a/tifa/migrations/0003_rename_slug_sdprompt_template_slug.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.3 on 2023-08-02 13:18 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("tifa", "0002_sdprompt_slug_sdspelltemplate_slug"), - ] - - operations = [ - migrations.RenameField( - model_name="sdprompt", - old_name="slug", - new_name="template_slug", - ), - ] diff --git a/tifa/migrations/__init__.py b/tifa/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tifa/models/__init__.py b/tifa/models/__init__.py deleted file mode 100644 index e11e2f5..0000000 --- a/tifa/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .sd import * # noqa diff --git a/tifa/models/base.py b/tifa/models/base.py deleted file mode 100644 index 37700aa..0000000 --- a/tifa/models/base.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -from typing import Any, Optional - -from django.core.exceptions import ValidationError -from django.db import models -from django.db.models import F, QuerySet -from django.http import Http404 -from django.shortcuts import get_object_or_404 as _get_object_or_404 -from typing_extensions import Self - - -def get_object_or_404(queryset, *filter_args, **filter_kwargs) -> Model: - try: - return _get_object_or_404(queryset, *filter_args, **filter_kwargs) # type: ignore - except (TypeError, ValueError, ValidationError): - raise Http404() - - -class BaseManager(models.Manager): - ... - - -class Model(models.Model): - id: Optional[int] | models.AutoField | models.BigAutoField - - class Meta: - abstract = True - - objects = BaseManager() - - @classmethod - def filter(cls, *args, **kwargs) -> QuerySet[Model]: - return cls.objects.filter(*args, **kwargs) - - @classmethod - def all(cls) -> QuerySet[Self] | list[Self]: - return cls.objects.all() - - - - @classmethod - def get(cls, pk) -> Self: - return cls.objects.get(pk=pk) - - @classmethod - def create(cls, **kwargs) -> Self: - return cls.objects.create(**kwargs) - - @classmethod - def get_or_404(cls, **kwargs) -> Self: - return get_object_or_404(cls.objects.filter(**kwargs)) - - @classmethod - def find_or_404(cls, **kwargs) -> Self: - return get_object_or_404(cls.objects.filter(**kwargs)) - - @classmethod - def find_first(cls, order_by: Optional[list[str]] = None, **kwargs) -> Self | None: - if not order_by: - order_by = ["-id"] - return cls.objects.filter(**kwargs).order_by(*order_by).first() - - @classmethod - def one_or_404(cls, **kwargs) -> Self: - return get_object_or_404(cls.objects.filter(**kwargs)) - - @classmethod - def first_or_404(cls, **kwargs) -> Self: - return get_object_or_404(cls.objects.filter(**kwargs)) - - def update_from_dict(self, obj: dict[str, Any], *fields: str): - if not fields: - for k, v in obj.items(): - setattr(self, k, v) - else: - for field in fields: - setattr(self, field, obj[field]) - - def partial_from_dict(self, obj: dict[str, Any], *fields: str): - if not fields: - for k, v in obj.items(): - setattr(self, k, v) - else: - for field in fields: - setattr(self, field, obj[field]) - - def incr(self, field, value=1): - return self.__class__.objects.filter(id=self.id).update(**{field: F(field) + value}) - - def decr(self, field, value=1): - return self.__class__.objects.filter(id=self.id).update(**{field: F(field) - value}) diff --git a/tifa/models/sd.py b/tifa/models/sd.py deleted file mode 100644 index 1c76f97..0000000 --- a/tifa/models/sd.py +++ /dev/null @@ -1,110 +0,0 @@ -from django.db import models - -from tifa.models.base import Model - - -class SdModelCheckpoint(Model): - class Meta: - db_table = "sd_model_checkpoint" - - id = models.BigAutoField(primary_key=True) - name = models.CharField(max_length=100) - slug = models.CharField(max_length=100, unique=True) - sort_num = models.IntegerField(db_index=True, default=999) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - -class SdModelVae(Model): - class Meta: - db_table = "sd_model_vae" - - id = models.BigAutoField(primary_key=True) - name = models.CharField(max_length=100) - slug = models.CharField(max_length=100, unique=True) - sort_num = models.IntegerField(db_index=True, default=999) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - -class SdModelLora(Model): - class Meta: - db_table = "sd_model_lora" - - id = models.BigAutoField(primary_key=True) - name = models.CharField(max_length=100) - slug = models.CharField(max_length=100, unique=True) - sort_num = models.IntegerField(db_index=True, default=999) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - -class SdPromptTag(Model): - class Meta: - db_table = "sd_prompt_tag" - - id = models.BigAutoField(primary_key=True) - name = models.CharField(max_length=100) - name_zh = models.CharField(max_length=100) - slug = models.CharField(max_length=100, unique=True) - sort_num = models.IntegerField(db_index=True, default=999) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"<#{self.id}> - {self.name}|{self.name_zh}" - - -class SdSpellTemplate(Model): - class Meta: - db_table = "sd_spell_template" - - id = models.BigAutoField(primary_key=True) - slug = models.CharField(max_length=100) - checkpoint = models.CharField(max_length=100) - prompt = models.TextField() - n_prompt = models.TextField() - sampler = models.CharField(max_length=50) - cfg_scale = models.IntegerField(default=7) - steps = models.IntegerField(default=30) - seed = models.BigIntegerField(default=-1) - sort_num = models.IntegerField(db_index=True, default=999) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"<#{self.id}> - {self.prompt}" - - -class SdPrompt(Model): - class Meta: - db_table = "sd_prompt" - - id = models.BigAutoField(primary_key=True) - checkpoint = models.CharField(max_length=100) - template_slug = models.CharField(max_length=100) - prompt = models.TextField() - n_prompt = models.TextField() - sampler = models.CharField(max_length=50) - cfg_scale = models.IntegerField(default=7) - steps = models.IntegerField(default=30) - seed = models.BigIntegerField(default=-1) - sort_num = models.IntegerField(db_index=True, default=999) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"<#{self.id}> - {self.name}" - - -class SdImage(Model): - class Meta: - db_table = "sd_image" - - id = models.BigAutoField(primary_key=True) - sort_num = models.IntegerField(db_index=True, default=999) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"<#{self.id}> - {self.sort_num}" diff --git a/tifa/scripts/__init__.py b/tifa/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tifa/settings.py b/tifa/settings.py deleted file mode 100644 index 001dc51..0000000 --- a/tifa/settings.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import pathlib -from pathlib import Path -from typing import Optional - -import dj_database_url -import dotenv -import warnings - -from pydantic import BaseSettings - -ROOT = pathlib.Path(__file__).parent.absolute() - -APP_SETTINGS = os.environ.get("APP_SETTINGS") -if not APP_SETTINGS: - warnings.warn("!!!未指定APP_SETTINGS!!!, 极有可能运行错误") - - -class GlobalSetting(BaseSettings): - TITLE: str = "Tifa" - DESCRIPTION: str = "Yet another opinionated fastapi-start-kit with best practice" - - TEMPLATE_PATH: str = f"{ROOT}/templates" - - STATIC_PATH: str = "/static" - STATIC_DIR: str = f"{ROOT}/static" - - SENTRY_DSN: Optional[str] - - DEBUG: bool = False - ENV: str = "LOCAL" - SECRET_KEY: str = "change me" - - DATABASE_URL: str = "postgresql://tifa:tifa%26123@postgres:5432/tifa" - REDIS_CACHE_URI: str = "redis://localhost:6379" - REDIS_CELERY_URL: str = "redis://redis:6379/6" - WHITEBOARD_URI: str = "redis://redis:6379/1" - - class Config: - env_file = APP_SETTINGS - - -settings = GlobalSetting() - -""" -django settings, only orm and cache -""" - - -SECRET_KEY = settings.SECRET_KEY -DEBUG = False -INSTALLED_APPS = [ - "tifa", - "django.contrib.postgres" -] - -MIDDLEWARE = [] -ROOT_URLCONF = "tifa.asgi.urls" - -TEMPLATES = [] - -WSGI_APPLICATION = "tifa.wsgi.application" -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -DATABASES = { - "default": dj_database_url.config( - default=settings.DATABASE_URL, - conn_max_age=600, - ), -} - -DATABASES["default"]["OPTIONS"] = {} - -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.redis.RedisCache", - "LOCATION": settings.REDIS_CACHE_URI - } -} - -TIME_ZONE = "UTC" -USE_I18N = True -USE_TZ = True -LOGGING = {} -CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379") -# CELERY_RESULT_BACKEND = "django-db" -CELERY_RESULT_SERIALIZER = "json" -CELERY_TASK_TRACK_STARTED = True -CELERYD_HIJACK_ROOT_LOGGER = False diff --git a/tifa/static/whiteboard/main.js b/tifa/static/whiteboard/main.js deleted file mode 100644 index 0780c17..0000000 --- a/tifa/static/whiteboard/main.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -(function () { - - var socket = io("ws://localhost:8000", { - path: '/whiteboard/whiteboard/socket.io' - }); - var canvas = document.getElementsByClassName('whiteboard')[0]; - var colors = document.getElementsByClassName('color'); - var context = canvas.getContext('2d'); - - var current = { - color: 'black' - }; - var drawing = false; - - canvas.addEventListener('mousedown', onMouseDown, false); - canvas.addEventListener('mouseup', onMouseUp, false); - canvas.addEventListener('mouseout', onMouseUp, false); - canvas.addEventListener('mousemove', throttle(onMouseMove, 10), false); - - //Touch support for mobile devices - canvas.addEventListener('touchstart', onMouseDown, false); - canvas.addEventListener('touchend', onMouseUp, false); - canvas.addEventListener('touchcancel', onMouseUp, false); - canvas.addEventListener('touchmove', throttle(onMouseMove, 10), false); - - for (var i = 0; i < colors.length; i++) { - colors[i].addEventListener('click', onColorUpdate, false); - } - - socket.on('drawing', onDrawingEvent); - - window.addEventListener('resize', onResize, false); - onResize(); - - - function drawLine(x0, y0, x1, y1, color, emit) { - context.beginPath(); - context.moveTo(x0, y0); - context.lineTo(x1, y1); - context.strokeStyle = color; - context.lineWidth = 2; - context.stroke(); - context.closePath(); - - if (!emit) { - return; - } - var w = canvas.width; - var h = canvas.height; - - socket.emit('drawing', { - x0: x0 / w, - y0: y0 / h, - x1: x1 / w, - y1: y1 / h, - color: color - }); - } - - function onMouseDown(e) { - drawing = true; - current.x = e.clientX || e.touches[0].clientX; - current.y = e.clientY || e.touches[0].clientY; - } - - function onMouseUp(e) { - if (!drawing) { - return; - } - drawing = false; - drawLine(current.x, current.y, e.clientX || e.touches[0].clientX, e.clientY || e.touches[0].clientY, current.color, true); - } - - function onMouseMove(e) { - if (!drawing) { - return; - } - drawLine(current.x, current.y, e.clientX || e.touches[0].clientX, e.clientY || e.touches[0].clientY, current.color, true); - current.x = e.clientX || e.touches[0].clientX; - current.y = e.clientY || e.touches[0].clientY; - } - - function onColorUpdate(e) { - current.color = e.target.className.split(' ')[1]; - } - - // limit the number of events per second - function throttle(callback, delay) { - var previousCall = new Date().getTime(); - return function () { - var time = new Date().getTime(); - - if ((time - previousCall) >= delay) { - previousCall = time; - callback.apply(null, arguments); - } - }; - } - - function onDrawingEvent(data) { - var w = canvas.width; - var h = canvas.height; - drawLine(data.x0 * w, data.y0 * h, data.x1 * w, data.y1 * h, data.color); - } - - // make the canvas fill its parent - function onResize() { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - } - -})(); \ No newline at end of file diff --git a/tifa/static/whiteboard/style.css b/tifa/static/whiteboard/style.css deleted file mode 100644 index 760c6e4..0000000 --- a/tifa/static/whiteboard/style.css +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Fix user-agent - */ - -* { - box-sizing: border-box; -} - -html, body { - height: 100%; - margin: 0; - padding: 0; -} - -/** - * Canvas - */ - -.whiteboard { - height: 100%; - width: 100%; - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; -} - -.colors { - position: fixed; -} - -.color { - display: inline-block; - height: 48px; - width: 48px; -} - -.color.black { - background-color: black; -} - -.color.red { - background-color: red; -} - -.color.green { - background-color: green; -} - -.color.blue { - background-color: blue; -} - -.color.yellow { - background-color: yellow; -} \ No newline at end of file diff --git a/tifa/templates/whiteboard/index.html b/tifa/templates/whiteboard/index.html deleted file mode 100644 index 09aa0e5..0000000 --- a/tifa/templates/whiteboard/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - Socket.IO whiteboard - - - - - - -
-
-
-
-
-
-
- - - - - \ No newline at end of file diff --git a/tifa/utils/__init__.py b/tifa/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tifa/utils/pkg.py b/tifa/utils/pkg.py deleted file mode 100644 index 238add5..0000000 --- a/tifa/utils/pkg.py +++ /dev/null @@ -1,18 +0,0 @@ -import importlib -import pkgutil - - -def import_submodules(package, recursive=True): - if isinstance(package, str): - package = importlib.import_module(package) - results = {} - for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): - full_name = package.__name__ + "." + name - results[full_name] = importlib.import_module(full_name) - if recursive and is_pkg: - results.update(import_submodules(full_name)) - return results - - -def register_fastapi_models(): - pass