diff --git a/.gitignore b/.gitignore index b522636..b09fc48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # Python __pycache__ -# Skaffold -tags.json +# Pants workspace files +/.pants.d/ +/dist/ +/.pids +/.pants.workdir.file_lock* diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..b2f1df9 --- /dev/null +++ b/BUILD @@ -0,0 +1 @@ +python_requirements() diff --git a/Makefile b/Makefile index 842919e..8073bf8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ SHELL=bash - MICROSERVICES = users @@ -8,32 +7,6 @@ help: ## Show this help @egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' -define lint - isort ${1}/app --check && \ - flake8 ${1}/app ${1}/tests && \ - black ${1}/app ${1}tests --check -endef - - -.PHONY: lint -lint: ## Linter code - @echo "🚨 Linting code..." - @for i in $(MICROSERVICES); do $(call lint,$$i); done - - -define format - isort ${1}/app && \ - autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place ${1}/app ${1}/tests --exclude=__init__.py && \ - black ${1}/app ${1}tests -endef - - -.PHONY: format -format: ## Format code - @echo "🎨 Formatting code..." - @for i in $(MICROSERVICES); do $(call format,$$i); done - - .PHONY: tests tests: ## Run tests @echo "🍜 Running tests..." diff --git a/README.md b/README.md index 0def301..00669d7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The full stack of this project is composed by: * [ARQ](https://arq-docs.helpmanual.io/) - Job queues and RPC in python with asyncio and redis. * [PostgreSQL](https://www.postgresql.org/) - The World's Most Advanced Open Source Relational Database * [Redis](https://redis.io/) - An open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker. +* [Minikube](https://minikube.sigs.k8s.io/) - A Kubernetes cluster manager for local development. * [Tilt](https://tilt.dev/) - A multi-service dev environment for teams on Kubernetes. ## Installation diff --git a/Tiltfile b/Tiltfile index 00b7be7..30645e6 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,7 +1,7 @@ -services = ['users', 'users-worker', 'redis', 'postgres', 'ingress'] +services = ['users', 'worker', 'redis', 'postgres', 'ingress'] yaml_files = ["k8s/%s.yaml" % service for service in services] k8s_yaml(yaml_files) -docker_build('users', 'users', dockerfile='users/docker/backend.dockerfile') -docker_build('users-worker', 'users', dockerfile='users/docker/worker.dockerfile') +docker_build('kludex/users', '.', dockerfile='services/users/Dockerfile') +docker_build('kludex/worker', '.', dockerfile='services/worker/Dockerfile') k8s_resource(workload="users-deployment", port_forwards="8000:80") diff --git a/k8s/users.yaml b/k8s/users.yaml index 8bf8525..d65cd6e 100644 --- a/k8s/users.yaml +++ b/k8s/users.yaml @@ -61,7 +61,7 @@ spec: - name: POSTGRES_HOST value: "postgres-service" - name: perform-migrations - image: users + image: kludex/users command: ["alembic", "upgrade", "head"] env: - name: POSTGRES_HOST @@ -76,7 +76,7 @@ spec: name: postgres-configuration containers: - name: users - image: users + image: kludex/users ports: - containerPort: 80 livenessProbe: diff --git a/k8s/users-worker.yaml b/k8s/worker.yaml similarity index 70% rename from k8s/users-worker.yaml rename to k8s/worker.yaml index 4e07b05..55698fe 100644 --- a/k8s/users-worker.yaml +++ b/k8s/worker.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: users-worker-service + name: worker-service labels: - app: users-worker + app: worker spec: type: NodePort ports: @@ -15,22 +15,22 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - name: users-worker-deployment + name: worker-deployment labels: - app: users-worker + app: worker spec: replicas: 1 selector: matchLabels: - app: users-worker + app: worker template: metadata: labels: - app: users-worker + app: worker spec: containers: - - name: users-worker - image: users-worker + - name: worker + image: kludex/worker ports: - containerPort: 8000 env: diff --git a/pants b/pants new file mode 100755 index 0000000..2e9a10c --- /dev/null +++ b/pants @@ -0,0 +1,352 @@ +#!/usr/bin/env bash +# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +# =============================== NOTE =============================== +# This ./pants bootstrap script comes from the pantsbuild/setup +# project. It is intended to be checked into your code repository so +# that other developers have the same setup. +# +# Learn more here: https://www.pantsbuild.org/docs/installation +# ==================================================================== + +set -eou pipefail + +# NOTE: To use an unreleased version of Pants from the pantsbuild/pants master branch, +# locate the master branch SHA, set PANTS_SHA= in the environment, and run this script as usual. +# +# E.g., PANTS_SHA=725fdaf504237190f6787dda3d72c39010a4c574 ./pants --version + +PYTHON_BIN_NAME="${PYTHON:-unspecified}" + +# Set this to specify a non-standard location for this script to read the Pants version from. +# NB: This will *not* cause Pants itself to use this location as a config file. +# You can use PANTS_CONFIG_FILES or --pants-config-files to do so. +PANTS_TOML=${PANTS_TOML:-pants.toml} + +PANTS_BIN_NAME="${PANTS_BIN_NAME:-$0}" + +PANTS_SETUP_CACHE="${PANTS_SETUP_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/pants/setup}" +# If given a relative path, we fix it to be absolute. +if [[ "$PANTS_SETUP_CACHE" != /* ]]; then + PANTS_SETUP_CACHE="${PWD}/${PANTS_SETUP_CACHE}" +fi + +PANTS_BOOTSTRAP="${PANTS_SETUP_CACHE}/bootstrap-$(uname -s)-$(uname -m)" + +PEX_VERSION=2.1.42 +PEX_URL="https://github.com/pantsbuild/pex/releases/download/v${PEX_VERSION}/pex" +PEX_EXPECTED_SHA256="69d6b1b1009b00dd14a3a9f19b72cff818a713ca44b3186c9b12074b2a31e51f" + +VIRTUALENV_VERSION=20.4.7 +VIRTUALENV_REQUIREMENTS=$( +cat << EOF +virtualenv==${VIRTUALENV_VERSION} --hash sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76 +filelock==3.0.12 --hash sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836 +six==1.16.0 --hash sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 +distlib==0.3.2 --hash sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c +appdirs==1.4.4 --hash sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 +importlib-resources==5.1.4; python_version < "3.7" --hash sha256:e962bff7440364183203d179d7ae9ad90cb1f2b74dcb84300e88ecc42dca3351 +importlib-metadata==4.5.0; python_version < "3.8" --hash sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00 +zipp==3.4.1; python_version < "3.10" --hash sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098 +typing-extensions==3.10.0.0; python_version < "3.8" --hash sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 +EOF +) + +COLOR_RED="\x1b[31m" +COLOR_GREEN="\x1b[32m" +COLOR_RESET="\x1b[0m" + +function log() { + echo -e "$@" 1>&2 +} + +function die() { + (($# > 0)) && log "${COLOR_RED}$*${COLOR_RESET}" + exit 1 +} + +function green() { + (($# > 0)) && log "${COLOR_GREEN}$*${COLOR_RESET}" +} + +function tempdir { + mkdir -p "$1" + mktemp -d "$1"/pants.XXXXXX +} + +function get_exe_path_or_die { + local exe="$1" + if ! command -v "${exe}"; then + die "Could not find ${exe}. Please ensure ${exe} is on your PATH." + fi +} + +function get_pants_config_value { + local config_key="$1" + local optional_space="[[:space:]]*" + local prefix="^${config_key}${optional_space}=${optional_space}" + local raw_value + raw_value="$(sed -ne "/${prefix}/ s#${prefix}##p" "${PANTS_TOML}")" + echo "${raw_value}" | tr -d \"\' && return 0 + return 0 +} + +function get_python_major_minor_version { + local python_exe="$1" + "$python_exe" <&1 > /dev/null)" == "pyenv: python${version}"* ]]; then + continue + fi + if [[ -n "$(check_python_exe_compatible_version "${interpreter_path}")" ]]; then + echo "${interpreter_path}" && return 0 + fi + done +} + +function determine_python_exe { + local pants_version="$1" + set_supported_python_versions "${pants_version}" + local requirement_str="For \`pants_version = \"${pants_version}\"\`, Pants requires Python ${supported_message} to run." + + local python_exe + if [[ "${PYTHON_BIN_NAME}" != 'unspecified' ]]; then + python_exe="$(get_exe_path_or_die "${PYTHON_BIN_NAME}")" || exit 1 + if [[ -z "$(check_python_exe_compatible_version "${python_exe}")" ]]; then + die "Invalid Python interpreter version for ${python_exe}. ${requirement_str}" + fi + else + python_exe="$(determine_default_python_exe)" + if [[ -z "${python_exe}" ]]; then + die "No valid Python interpreter found. ${requirement_str} Please check that a valid interpreter is installed and on your \$PATH." + fi + fi + echo "${python_exe}" +} + +function compute_sha256 { + local python="$1" + local path="$2" + + "$python" <&2 || exit 1 + fi + echo "${bootstrapped}" +} + +function bootstrap_virtualenv { + local python="$1" + local bootstrapped="${PANTS_BOOTSTRAP}/virtualenv-${VIRTUALENV_VERSION}/virtualenv.pex" + if [[ ! -f "${bootstrapped}" ]]; then + ( + green "Creating the virtualenv PEX." + pex_path="$(bootstrap_pex "${python}")" || exit 1 + mkdir -p "${PANTS_BOOTSTRAP}" + local staging_dir + staging_dir=$(tempdir "${PANTS_BOOTSTRAP}") + cd "${staging_dir}" + echo "${VIRTUALENV_REQUIREMENTS}" > requirements.txt + "${python}" "${pex_path}" -r requirements.txt -c virtualenv -o virtualenv.pex + mkdir -p "$(dirname "${bootstrapped}")" + mv -f "${staging_dir}/virtualenv.pex" "${bootstrapped}" + rm -rf "${staging_dir}" + ) 1>&2 || exit 1 + fi + echo "${bootstrapped}" +} + +function find_links_url { + local pants_version="$1" + local pants_sha="$2" + echo -n "https://binaries.pantsbuild.org/wheels/pantsbuild.pants/${pants_sha}/${pants_version/+/%2B}/index.html" +} + +function get_version_for_sha { + local sha="$1" + + # Retrieve the Pants version associated with this commit. + local pants_version + pants_version="$(curl --fail -sL "https://raw.githubusercontent.com/pantsbuild/pants/${sha}/src/python/pants/VERSION")" + + # Construct the version as the release version from src/python/pants/VERSION, plus the string `+gitXXXXXXXX`, + # where the XXXXXXXX is the first 8 characters of the SHA. + echo "${pants_version}+git${sha:0:8}" +} + +function bootstrap_pants { + local pants_version="$1" + local python="$2" + local pants_sha="${3:-}" + + local pants_requirement="pantsbuild.pants==${pants_version}" + local maybe_find_links + if [[ -z "${pants_sha}" ]]; then + maybe_find_links="" + else + maybe_find_links="--find-links=$(find_links_url "${pants_version}" "${pants_sha}")" + fi + local python_major_minor_version + python_major_minor_version="$(get_python_major_minor_version "${python}")" + local target_folder_name="${pants_version}_py${python_major_minor_version}" + local bootstrapped="${PANTS_BOOTSTRAP}/${target_folder_name}" + + if [[ ! -d "${bootstrapped}" ]]; then + ( + green "Bootstrapping Pants using ${python}" + local staging_dir + staging_dir=$(tempdir "${PANTS_BOOTSTRAP}") + local virtualenv_path + virtualenv_path="$(bootstrap_virtualenv "${python}")" || exit 1 + green "Installing ${pants_requirement} into a virtual environment at ${bootstrapped}" + # shellcheck disable=SC2086 + "${python}" "${virtualenv_path}" --no-download "${staging_dir}/install" && \ + "${staging_dir}/install/bin/pip" install -U pip && \ + "${staging_dir}/install/bin/pip" install ${maybe_find_links} --progress-bar off "${pants_requirement}" && \ + ln -s "${staging_dir}/install" "${staging_dir}/${target_folder_name}" && \ + mv "${staging_dir}/${target_folder_name}" "${bootstrapped}" && \ + green "New virtual environment successfully created at ${bootstrapped}." + ) 1>&2 || exit 1 + fi + echo "${bootstrapped}" +} + +# Ensure we operate from the context of the ./pants buildroot. +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +pants_version="$(determine_pants_version)" +python="$(determine_python_exe "${pants_version}")" +pants_dir="$(bootstrap_pants "${pants_version}" "${python}" "${PANTS_SHA:-}")" || exit 1 + +pants_python="${pants_dir}/bin/python" +pants_binary="${pants_dir}/bin/pants" +pants_extra_args="" +if [[ -n "${PANTS_SHA:-}" ]]; then + pants_extra_args="${pants_extra_args} --python-repos-repos=$(find_links_url "$pants_version" "$PANTS_SHA")" +fi + +# shellcheck disable=SC2086 +exec "${pants_python}" "${pants_binary}" ${pants_extra_args} \ + --pants-bin-name="${PANTS_BIN_NAME}" --pants-version=${pants_version} "$@" diff --git a/pants.toml b/pants.toml new file mode 100644 index 0000000..f052ede --- /dev/null +++ b/pants.toml @@ -0,0 +1,32 @@ +[GLOBAL] +pants_version = "2.6.1rc2" +backend_packages = [ + "pants.backend.python", + "pants.backend.python.lint.black", + "pants.backend.python.lint.isort", + "pants.backend.python.lint.flake8", + "pants.backend.python.typecheck.mypy", +] + +[anonymous-telemetry] +enabled = true +repo_id = "0b797772-e5c1-4b91-81c5-069b5578acbe" + +[python-infer] +inits = true + +[python-setup] +interpreter_constraints = ["CPython==3.9.*"] + +[flake8] +config = "setup.cfg" + +[isort] +config = "setup.cfg" + +[mypy] +config = "setup.cfg" +version = "mypy==0.910" + +[black] +version = "black==21.8b0" diff --git a/users/requirements.txt b/requirements.txt similarity index 58% rename from users/requirements.txt rename to requirements.txt index cd33944..7bf28b9 100644 --- a/users/requirements.txt +++ b/requirements.txt @@ -2,22 +2,17 @@ sqlalchemy==1.4.11 asyncpg==0.22.0 pyhumps==1.6.1 alembic==1.5.8 -pydantic[email]==1.8.1 +pydantic[email]@ https://github.com/samuelcolvin/pydantic/archive/b26d6f925b5b0e5077895334a203e539be105d30.tar.gz#egg=pydantic[email] passlib[bcrypt]==1.7.4 python-jose==3.2.0 python-multipart==0.0.5 fastapi==0.63.0 - -arq==0.20.0 +arq==0.22.0 uvloop==0.15.2 - -# TODO(Marcelo): Add dev requirements. +uvicorn==0.15.0 pytest==6.2.3 pytest-asyncio==0.15.0 pytest-cov==2.11.1 -flake8==3.9.1 -isort==5.8.0 -black==20.8b1 -mypy==0.812 httpx==0.17.1 asgi-lifespan==1.0.1 +gunicorn==20.1.0 diff --git a/services/users/Dockerfile b/services/users/Dockerfile new file mode 100644 index 0000000..0999328 --- /dev/null +++ b/services/users/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim as user-base + +RUN useradd --create-home user +WORKDIR /home/user +USER user + +COPY ./dist/src.python.users/binary.pex /home/user/users.pex + +CMD [ "./users.pex"] diff --git a/users/alembic.ini b/services/users/alembic.ini similarity index 100% rename from users/alembic.ini rename to services/users/alembic.ini diff --git a/users/migrations/README b/services/users/migrations/README similarity index 100% rename from users/migrations/README rename to services/users/migrations/README diff --git a/users/migrations/env.py b/services/users/migrations/env.py similarity index 96% rename from users/migrations/env.py rename to services/users/migrations/env.py index fa9e200..18646e5 100644 --- a/users/migrations/env.py +++ b/services/users/migrations/env.py @@ -6,8 +6,8 @@ from sqlalchemy import engine_from_config, pool from sqlalchemy.ext.asyncio import AsyncEngine -from app import models # noqa: F401 -from app.models.base import Base # noqa: F401 +from users import models # noqa: F401 +from users.models.base import Base # noqa: F401 # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/users/migrations/script.py.mako b/services/users/migrations/script.py.mako similarity index 100% rename from users/migrations/script.py.mako rename to services/users/migrations/script.py.mako diff --git a/users/migrations/versions/.gitkeep b/services/users/migrations/versions/.gitkeep similarity index 100% rename from users/migrations/versions/.gitkeep rename to services/users/migrations/versions/.gitkeep diff --git a/users/migrations/versions/dc304df8db88_first_migration.py.py b/services/users/migrations/versions/dc304df8db88_first_migration.py.py similarity index 100% rename from users/migrations/versions/dc304df8db88_first_migration.py.py rename to services/users/migrations/versions/dc304df8db88_first_migration.py.py diff --git a/users/scripts/initial_data.py b/services/users/scripts/initial_data.py similarity index 68% rename from users/scripts/initial_data.py rename to services/users/scripts/initial_data.py index 61fa3aa..f3d9736 100644 --- a/users/scripts/initial_data.py +++ b/services/users/scripts/initial_data.py @@ -5,18 +5,18 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core.config import settings -from app.core.database import SessionLocal -from app.core.security import get_password_hash -from app.models.users import User +from users.core.config import settings +from users.core.database import SessionLocal +from users.core.security import get_password_hash +from users.models.users import User logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) async def create_first_user(session: AsyncSession) -> None: - email = settings.FIRST_USER_EMAIL - password = get_password_hash(settings.FIRST_USER_PASSWORD.get_secret_value()) + email = settings().FIRST_USER_EMAIL + password = get_password_hash(settings().FIRST_USER_PASSWORD.get_secret_value()) result = await session.execute(select(User).where(User.email == email)) user: Optional[User] = result.scalars().first() if user is None: @@ -32,5 +32,4 @@ async def main(): if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/users/scripts/prestart.sh b/services/users/scripts/prestart.sh similarity index 100% rename from users/scripts/prestart.sh rename to services/users/scripts/prestart.sh diff --git a/services/worker/Dockerfile b/services/worker/Dockerfile new file mode 100644 index 0000000..8031d2b --- /dev/null +++ b/services/worker/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim as user-base + +RUN useradd --create-home user +WORKDIR /home/user +USER user + +COPY ./dist/src.python.worker/binary.pex /home/user/worker.pex + +CMD [ "./worker.pex"] diff --git a/setup.cfg b/setup.cfg index df9a235..8fb348b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,16 +1,19 @@ [isort] profile = black -known_first_party = app +known_first_party = users, worker, tests [flake8] -statistics = True max-line-length = 88 ignore = E203, E501, W503 -per-file-ignores = - __init__.py:F401 +per-file-ignores = __init__.py:F401 [mypy] +ignore_missing_imports = True follow_imports = skip +strict_optional = True +# disallow_untyped_calls = True +# disallow_untyped_defs = True + # Reference: https://github.com/nedbat/coveragepy/issues/1082 [coverage:run] diff --git a/src/python/users/BUILD b/src/python/users/BUILD new file mode 100644 index 0000000..8e90fef --- /dev/null +++ b/src/python/users/BUILD @@ -0,0 +1,7 @@ +python_library() + +pex_binary( + name="binary", + dependencies=['//:asyncpg'], + entry_point="__main__.py", +) diff --git a/users/app/__init__.py b/src/python/users/__init__.py similarity index 100% rename from users/app/__init__.py rename to src/python/users/__init__.py diff --git a/src/python/users/__main__.py b/src/python/users/__main__.py new file mode 100644 index 0000000..4d54d9c --- /dev/null +++ b/src/python/users/__main__.py @@ -0,0 +1,51 @@ +import multiprocessing +from typing import Any, Callable, Dict, Protocol, Type + +import uvicorn +from gunicorn.app.base import BaseApplication + +from users.core.config import settings +from users.main import app + + +class ASGI3Protocol(Protocol): + async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None: + ... + + +ASGI3Application = Type[ASGI3Protocol] + + +def number_of_workers() -> int: + return (multiprocessing.cpu_count() * 2) + 1 + + +class StandaloneApplication(BaseApplication): + def __init__(self, application: ASGI3Application, options: Dict[str, Any] = None): + self.options = options or {} + self.application = application + super().__init__() + + def load_config(self) -> None: + config = { + key: value + for key, value in self.options.items() + if key in self.cfg.settings and value is not None + } + for key, value in config.items(): + self.cfg.set(key.lower(), value) + + def load(self) -> ASGI3Application: + return self.application + + +if __name__ == "__main__": + if settings().ENV == "prod": + options = { + "bind": "%s:%s" % ("127.0.0.1", "8000"), + "workers": number_of_workers(), + "worker_class": "uvicorn.workers.UvicornWorker", + } + StandaloneApplication(app, options).run() + else: + uvicorn.run("main:app", reload=True) diff --git a/src/python/users/api/BUILD b/src/python/users/api/BUILD new file mode 100644 index 0000000..d80ecb5 --- /dev/null +++ b/src/python/users/api/BUILD @@ -0,0 +1 @@ +python_library() diff --git a/users/app/api/__init__.py b/src/python/users/api/__init__.py similarity index 58% rename from users/app/api/__init__.py rename to src/python/users/api/__init__.py index 6f560a0..be94b41 100644 --- a/users/app/api/__init__.py +++ b/src/python/users/api/__init__.py @@ -1,7 +1,7 @@ from fastapi import APIRouter -from app.api.health import router as health_router -from app.api.v1 import router as v1_router +from users.api.health import router as health_router +from users.api.v1 import router as v1_router router = APIRouter(prefix="/api") router.include_router(v1_router) diff --git a/users/app/api/deps.py b/src/python/users/api/deps.py similarity index 76% rename from users/app/api/deps.py rename to src/python/users/api/deps.py index 5efed24..f0ea904 100644 --- a/users/app/api/deps.py +++ b/src/python/users/api/deps.py @@ -4,24 +4,24 @@ from pydantic import ValidationError from sqlalchemy.ext.asyncio import AsyncSession -from app.core.config import settings -from app.core.database import SessionLocal -from app.core.security import ALGORITHM -from app.crud.users import crud_user -from app.models.users import User -from app.schemas.token import TokenPayload +from users.core.config import settings +from users.core.database import SessionLocal +from users.core.security import ALGORITHM +from users.crud.users import crud_user +from users.models.users import User +from users.schemas.token import TokenPayload oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login") -async def get_session(): +async def get_session() -> AsyncSession: async with SessionLocal() as session: yield session def get_token_data(token: str = Depends(oauth2)) -> TokenPayload: try: - secret_key = settings.SECRET_KEY.get_secret_value() + secret_key = settings().SECRET_KEY.get_secret_value() payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM]) token_data = TokenPayload(**payload) except (jwt.JWTError, ValidationError): @@ -30,9 +30,9 @@ def get_token_data(token: str = Depends(oauth2)) -> TokenPayload: async def get_current_user( - token: str = Depends(get_token_data), + token: TokenPayload = Depends(get_token_data), session: AsyncSession = Depends(get_session), -): +) -> User: user = await crud_user.get(session, id=token.user_id) if user is None: raise HTTPException(status_code=404, detail="User not found") diff --git a/users/app/api/health.py b/src/python/users/api/health.py similarity index 80% rename from users/app/api/health.py rename to src/python/users/api/health.py index 5c09163..4d8b0cd 100644 --- a/users/app/api/health.py +++ b/src/python/users/api/health.py @@ -5,13 +5,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from starlette.responses import Response -from app.api.deps import get_session +from users.api.deps import get_session router = APIRouter(prefix="/health", tags=["Health"]) @router.get("/", status_code=204) -async def health(session: AsyncSession = Depends(get_session)): +async def health(session: AsyncSession = Depends(get_session)) -> Response: try: await asyncio.wait_for(session.execute("SELECT 1"), timeout=1) except (asyncio.TimeoutError, socket.gaierror): diff --git a/src/python/users/api/v1/BUILD b/src/python/users/api/v1/BUILD new file mode 100644 index 0000000..d80ecb5 --- /dev/null +++ b/src/python/users/api/v1/BUILD @@ -0,0 +1 @@ +python_library() diff --git a/src/python/users/api/v1/__init__.py b/src/python/users/api/v1/__init__.py new file mode 100644 index 0000000..8b0c2a0 --- /dev/null +++ b/src/python/users/api/v1/__init__.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +from users.api.v1.home import router as home_router +from users.api.v1.login import router as login_router +from users.api.v1.tasks import router as tasks_router +from users.api.v1.users import router as users_router + +router = APIRouter(prefix="/v1") +router.include_router(home_router) +router.include_router(login_router) +router.include_router(tasks_router) +router.include_router(users_router) diff --git a/users/app/api/v1/home.py b/src/python/users/api/v1/home.py similarity index 68% rename from users/app/api/v1/home.py rename to src/python/users/api/v1/home.py index 5c84266..2c84d06 100644 --- a/users/app/api/v1/home.py +++ b/src/python/users/api/v1/home.py @@ -1,15 +1,16 @@ from fastapi import APIRouter, Depends -from app.api.deps import get_token_data +from users.api.deps import get_token_data router = APIRouter(prefix="/home", tags=["Home"]) @router.get("/", dependencies=[Depends(get_token_data)]) -async def home(): +async def home() -> str: return "Hello World!" @router.get("/another/") -async def another(): +async def another() -> str: + print("hi") return "Another Hello World!" diff --git a/users/app/api/v1/login.py b/src/python/users/api/v1/login.py similarity index 83% rename from users/app/api/v1/login.py rename to src/python/users/api/v1/login.py index 96f3782..592fa30 100644 --- a/users/app/api/v1/login.py +++ b/src/python/users/api/v1/login.py @@ -2,9 +2,9 @@ from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import get_session -from app.core.security import authenticate, create_access_token -from app.schemas.token import Token +from users.api.deps import get_session +from users.core.security import authenticate, create_access_token +from users.schemas.token import Token router = APIRouter(tags=["Login"]) diff --git a/users/app/api/v1/tasks.py b/src/python/users/api/v1/tasks.py similarity index 87% rename from users/app/api/v1/tasks.py rename to src/python/users/api/v1/tasks.py index c82e9da..5ae66c0 100644 --- a/users/app/api/v1/tasks.py +++ b/src/python/users/api/v1/tasks.py @@ -1,8 +1,8 @@ from arq.jobs import Job as ArqJob from fastapi import APIRouter -from app.core import redis -from app.schemas.job import Job +from users.core import redis +from users.schemas.job import Job router = APIRouter(prefix="/tasks", tags=["Tasks"]) diff --git a/users/app/api/v1/users.py b/src/python/users/api/v1/users.py similarity index 89% rename from users/app/api/v1/users.py rename to src/python/users/api/v1/users.py index 6337fd5..96e8f11 100644 --- a/users/app/api/v1/users.py +++ b/src/python/users/api/v1/users.py @@ -1,19 +1,20 @@ from typing import List from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import Response from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import ( +from users.api.deps import ( get_current_superuser, get_current_user, get_session, on_superuser, ) -from app.core.security import get_password_hash -from app.crud.users import crud_user -from app.models.users import User -from app.schemas.user import UserCreate, UserInDB, UserOut, UserUpdate +from users.core.security import get_password_hash +from users.crud.users import crud_user +from users.models.users import User +from users.schemas.user import UserCreate, UserInDB, UserOut, UserUpdate router = APIRouter(prefix="/users", tags=["Users"]) @@ -21,7 +22,7 @@ @router.get("/", response_model=List[UserOut], dependencies=[Depends(on_superuser)]) async def read_users( offset: int = 0, limit: int = 100, session: AsyncSession = Depends(get_session) -): +) -> List[User]: """ Retrieve users. """ @@ -32,7 +33,7 @@ async def read_users( @router.post("/", response_model=UserOut, dependencies=[Depends(on_superuser)]) async def create_user( user_in: UserCreate, session: AsyncSession = Depends(get_session) -): +) -> User: """ Create new user. """ @@ -53,7 +54,7 @@ async def read_user( user_id: int, current_user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), -): +) -> User: """ Get a specific user by id. """ @@ -73,7 +74,7 @@ async def read_user( @router.put("/{user_id}/", response_model=UserOut, dependencies=[Depends(on_superuser)]) async def update_user( user_id: int, user_in: UserUpdate, session: AsyncSession = Depends(get_session) -): +) -> User: user = await crud_user.get(session, id=user_id) if user is None: raise HTTPException( @@ -101,10 +102,11 @@ async def delete_user( user_id: int, current_user: User = Depends(get_current_superuser), session: AsyncSession = Depends(get_session), -): +) -> Response: user = await crud_user.get(session, id=user_id) if user is None: raise HTTPException(status_code=404, detail="User not found") if current_user.id == user_id: raise HTTPException(status_code=403, detail="User can't delete itself") await crud_user.delete(session, db_obj=user) + return Response(status_code=204) diff --git a/src/python/users/core/BUILD b/src/python/users/core/BUILD new file mode 100644 index 0000000..d80ecb5 --- /dev/null +++ b/src/python/users/core/BUILD @@ -0,0 +1 @@ +python_library() diff --git a/users/app/core/__init__.py b/src/python/users/core/__init__.py similarity index 100% rename from users/app/core/__init__.py rename to src/python/users/core/__init__.py diff --git a/users/app/core/config.py b/src/python/users/core/config.py similarity index 72% rename from users/app/core/config.py rename to src/python/users/core/config.py index 5614cc5..b44e4b0 100644 --- a/users/app/core/config.py +++ b/src/python/users/core/config.py @@ -1,16 +1,17 @@ +from functools import cache from typing import Any, Dict, Optional -from pydantic import BaseSettings, EmailStr, SecretStr, validator +from pydantic import BaseSettings, EmailStr, PostgresDsn, SecretStr, validator class Settings(BaseSettings): - PROJECT_NAME: str + PROJECT_NAME: str = "Users" + ENV: str = "dev" POSTGRES_DB: str POSTGRES_HOST: str POSTGRES_USER: str POSTGRES_PASSWORD: SecretStr - # TODO(Marcelo): Change type once https://github.com/samuelcolvin/pydantic/pull/2567 is merged. POSTGRES_URI: Optional[str] = None @validator("POSTGRES_URI", pre=True) @@ -18,12 +19,12 @@ def validate_postgres_conn(cls, v: Optional[str], values: Dict[str, Any]) -> str if isinstance(v, str): return v password: SecretStr = values.get("POSTGRES_PASSWORD", SecretStr("")) - return "{scheme}://{user}:{password}@{host}/{db}".format( + return PostgresDsn.build( scheme="postgresql+asyncpg", user=values.get("POSTGRES_USER"), password=password.get_secret_value(), host=values.get("POSTGRES_HOST"), - db=values.get("POSTGRES_DB"), + path=f"/{values.get('POSTGRES_DB') or ''}", ) FIRST_USER_EMAIL: EmailStr @@ -36,4 +37,6 @@ def validate_postgres_conn(cls, v: Optional[str], values: Dict[str, Any]) -> str REDIS_PORT: int -settings = Settings() +@cache +def settings() -> Settings: + return Settings() diff --git a/users/app/core/database.py b/src/python/users/core/database.py similarity index 67% rename from users/app/core/database.py rename to src/python/users/core/database.py index 6abc750..9aad7d0 100644 --- a/users/app/core/database.py +++ b/src/python/users/core/database.py @@ -1,7 +1,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker -from app.core.config import settings +from users.core.config import settings -engine = create_async_engine(settings.POSTGRES_URI) +engine = create_async_engine(settings().POSTGRES_URI) SessionLocal = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) diff --git a/users/app/core/redis.py b/src/python/users/core/redis.py similarity index 100% rename from users/app/core/redis.py rename to src/python/users/core/redis.py diff --git a/users/app/core/security.py b/src/python/users/core/security.py similarity index 77% rename from users/app/core/security.py rename to src/python/users/core/security.py index 91941aa..99d3ed7 100644 --- a/users/app/core/security.py +++ b/src/python/users/core/security.py @@ -6,9 +6,9 @@ from pydantic import EmailStr from sqlalchemy.ext.asyncio import AsyncSession -from app.core.config import settings -from app.crud.users import crud_user -from app.models.users import User +from users.core.config import settings +from users.crud.users import crud_user +from users.models.users import User pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -16,10 +16,12 @@ def create_access_token(user: User) -> str: - expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + expire = datetime.utcnow() + timedelta( + minutes=settings().ACCESS_TOKEN_EXPIRE_MINUTES + ) return jwt.encode( {"exp": expire, "user_id": str(user.id)}, - key=settings.SECRET_KEY.get_secret_value(), + key=settings().SECRET_KEY.get_secret_value(), algorithm=ALGORITHM, ) diff --git a/src/python/users/crud/BUILD b/src/python/users/crud/BUILD new file mode 100644 index 0000000..d80ecb5 --- /dev/null +++ b/src/python/users/crud/BUILD @@ -0,0 +1 @@ +python_library() diff --git a/users/app/crud/__init__.py b/src/python/users/crud/__init__.py similarity index 100% rename from users/app/crud/__init__.py rename to src/python/users/crud/__init__.py diff --git a/users/app/crud/base.py b/src/python/users/crud/base.py similarity index 98% rename from users/app/crud/base.py rename to src/python/users/crud/base.py index dd6906f..b31d68e 100644 --- a/users/app/crud/base.py +++ b/src/python/users/crud/base.py @@ -65,7 +65,7 @@ async def update( async def delete( self, session: AsyncSession, *args, db_obj: Optional[ModelType] = None, **kwargs - ) -> ModelType: + ) -> Optional[ModelType]: db_obj = db_obj or await self.get(session, *args, **kwargs) await session.delete(db_obj) await session.commit() diff --git a/src/python/users/crud/users.py b/src/python/users/crud/users.py new file mode 100644 index 0000000..187ce4e --- /dev/null +++ b/src/python/users/crud/users.py @@ -0,0 +1,6 @@ +from users.crud.base import CRUDBase +from users.models.users import User +from users.schemas.user import UserInDB, UserUpdateDB + +CRUDUser = CRUDBase[User, UserInDB, UserUpdateDB] +crud_user = CRUDBase(User) diff --git a/users/app/main.py b/src/python/users/main.py similarity index 57% rename from users/app/main.py rename to src/python/users/main.py index c59e21e..2c06263 100644 --- a/users/app/main.py +++ b/src/python/users/main.py @@ -2,23 +2,23 @@ from arq.connections import RedisSettings from fastapi import FastAPI -from app.api import router -from app.core import redis -from app.core.config import settings +from users.api import router +from users.core import redis +from users.core.config import settings -async def create_redis_pool(): +async def create_redis_pool() -> None: redis.pool = await create_pool( - RedisSettings(host=settings.REDIS_HOST, port=settings.REDIS_PORT) + RedisSettings(host=settings().REDIS_HOST, port=settings().REDIS_PORT) ) -async def close_redis_pool(): +async def close_redis_pool() -> None: redis.pool.close() def create_application() -> FastAPI: - application = FastAPI(title=settings.PROJECT_NAME) + application = FastAPI(title=settings().PROJECT_NAME) application.include_router(router) application.add_event_handler("startup", create_redis_pool) application.add_event_handler("shutdown", close_redis_pool) diff --git a/src/python/users/models/BUILD b/src/python/users/models/BUILD new file mode 100644 index 0000000..d80ecb5 --- /dev/null +++ b/src/python/users/models/BUILD @@ -0,0 +1 @@ +python_library() diff --git a/src/python/users/models/__init__.py b/src/python/users/models/__init__.py new file mode 100644 index 0000000..7011eae --- /dev/null +++ b/src/python/users/models/__init__.py @@ -0,0 +1,2 @@ +from users.models.items import Item +from users.models.users import User diff --git a/users/app/models/base.py b/src/python/users/models/base.py similarity index 100% rename from users/app/models/base.py rename to src/python/users/models/base.py diff --git a/users/app/models/items.py b/src/python/users/models/items.py similarity index 91% rename from users/app/models/items.py rename to src/python/users/models/items.py index c78fd53..9d1c287 100644 --- a/users/app/models/items.py +++ b/src/python/users/models/items.py @@ -1,7 +1,7 @@ from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship -from app.models.base import Base +from users.models.base import Base class Item(Base): diff --git a/users/app/models/users.py b/src/python/users/models/users.py similarity index 93% rename from users/app/models/users.py rename to src/python/users/models/users.py index c636a66..a6b5ece 100644 --- a/users/app/models/users.py +++ b/src/python/users/models/users.py @@ -1,7 +1,7 @@ from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy.orm import relationship -from app.models.base import Base +from users.models.base import Base class User(Base): diff --git a/src/python/users/schemas/BUILD b/src/python/users/schemas/BUILD new file mode 100644 index 0000000..d80ecb5 --- /dev/null +++ b/src/python/users/schemas/BUILD @@ -0,0 +1 @@ +python_library() diff --git a/users/app/schemas/__init__.py b/src/python/users/schemas/__init__.py similarity index 100% rename from users/app/schemas/__init__.py rename to src/python/users/schemas/__init__.py diff --git a/users/app/schemas/job.py b/src/python/users/schemas/job.py similarity index 100% rename from users/app/schemas/job.py rename to src/python/users/schemas/job.py diff --git a/users/app/schemas/token.py b/src/python/users/schemas/token.py similarity index 100% rename from users/app/schemas/token.py rename to src/python/users/schemas/token.py diff --git a/users/app/schemas/user.py b/src/python/users/schemas/user.py similarity index 100% rename from users/app/schemas/user.py rename to src/python/users/schemas/user.py diff --git a/src/python/users/tests/BUILD b/src/python/users/tests/BUILD new file mode 100644 index 0000000..d0ebc81 --- /dev/null +++ b/src/python/users/tests/BUILD @@ -0,0 +1,5 @@ +python_library() + +python_tests( + name="tests0", +) diff --git a/users/tests/__init__.py b/src/python/users/tests/__init__.py similarity index 100% rename from users/tests/__init__.py rename to src/python/users/tests/__init__.py diff --git a/users/tests/conftest.py b/src/python/users/tests/conftest.py similarity index 61% rename from users/tests/conftest.py rename to src/python/users/tests/conftest.py index 3728bcc..b038f54 100644 --- a/users/tests/conftest.py +++ b/src/python/users/tests/conftest.py @@ -1,37 +1,37 @@ import asyncio -from typing import Dict +from typing import AsyncIterator, Dict, Iterator import pytest from asgi_lifespan import LifespanManager from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession -from app.api.deps import get_session -from app.core.config import settings -from app.core.database import engine -from app.main import app +from users.api.deps import get_session +from users.core.config import settings +from users.core.database import engine +from users.main import app @pytest.fixture() -async def connection(): +async def connection() -> AsyncConnection: async with engine.begin() as conn: yield conn await conn.rollback() @pytest.fixture() -async def session(connection: AsyncConnection): +async def session(connection: AsyncConnection) -> AsyncIterator[AsyncSession]: async with AsyncSession(connection, expire_on_commit=False) as _session: yield _session @pytest.fixture(autouse=True) -async def override_dependency(session: AsyncSession): +async def override_dependency(session: AsyncSession) -> None: app.dependency_overrides[get_session] = lambda: session @pytest.fixture(scope="session", autouse=True) -def event_loop(): +def event_loop() -> Iterator[asyncio.AbstractEventLoop]: """Reference: https://github.com/pytest-dev/pytest-asyncio/issues/38#issuecomment-264418154""" loop = asyncio.get_event_loop_policy().new_event_loop() yield loop @@ -39,16 +39,16 @@ def event_loop(): @pytest.fixture() -async def client(): +async def client() -> AsyncClient: async with AsyncClient(app=app, base_url="http://test") as ac, LifespanManager(app): yield ac @pytest.fixture() -async def superuser_token_headers(client: AsyncClient) -> Dict[str, str]: +async def super_token_headers(client: AsyncClient) -> Dict[str, str]: login_data = { - "username": settings.FIRST_USER_EMAIL, - "password": settings.FIRST_USER_PASSWORD.get_secret_value(), + "username": settings().FIRST_USER_EMAIL, + "password": settings().FIRST_USER_PASSWORD.get_secret_value(), } res = await client.post("/api/v1/login/", data=login_data) access_token = res.json()["access_token"] diff --git a/users/tests/test_home.py b/src/python/users/tests/test_home.py similarity index 51% rename from users/tests/test_home.py rename to src/python/users/tests/test_home.py index 71ef38f..9296c3a 100644 --- a/users/tests/test_home.py +++ b/src/python/users/tests/test_home.py @@ -5,7 +5,7 @@ @pytest.mark.asyncio -async def test_home(client: AsyncClient, superuser_token_headers: Dict[str, str]): - res = await client.get("/api/v1/home", headers=superuser_token_headers) +async def test_home(client: AsyncClient, super_token_headers: Dict[str, str]) -> None: + res = await client.get("/api/v1/home", headers=super_token_headers) assert res.status_code == 200 assert res.json() == "Hello World!" diff --git a/users/tests/test_login.py b/src/python/users/tests/test_login.py similarity index 69% rename from users/tests/test_login.py rename to src/python/users/tests/test_login.py index 45f8645..a1713d5 100644 --- a/users/tests/test_login.py +++ b/src/python/users/tests/test_login.py @@ -1,14 +1,14 @@ import pytest from sqlalchemy.ext.asyncio import AsyncSession -from app.core.config import settings +from users.core.config import settings @pytest.mark.asyncio() async def test_login(client: AsyncSession): login_data = { - "username": settings.FIRST_USER_EMAIL, - "password": settings.FIRST_USER_PASSWORD.get_secret_value(), + "username": settings().FIRST_USER_EMAIL, + "password": settings().FIRST_USER_PASSWORD.get_secret_value(), } res = await client.post("/api/v1/login/", data=login_data) assert res.status_code == 200, res.json() diff --git a/src/python/worker/BUILD b/src/python/worker/BUILD new file mode 100644 index 0000000..340e231 --- /dev/null +++ b/src/python/worker/BUILD @@ -0,0 +1,6 @@ +python_library() + +pex_binary( + name="binary", + entry_point="__main__.py" +) diff --git a/src/python/worker/__init__.py b/src/python/worker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/python/worker/__main__.py b/src/python/worker/__main__.py new file mode 100644 index 0000000..95fe738 --- /dev/null +++ b/src/python/worker/__main__.py @@ -0,0 +1,6 @@ +from arq.worker import run_worker + +from worker.main import WorkerSettings + +if __name__ == "__main__": + run_worker(WorkerSettings) diff --git a/src/python/worker/config.py b/src/python/worker/config.py new file mode 100644 index 0000000..96d4e4a --- /dev/null +++ b/src/python/worker/config.py @@ -0,0 +1,13 @@ +from functools import cache + +from pydantic import BaseSettings + + +class Settings(BaseSettings): + REDIS_HOST: str = "localhost" + REDIS_PORT: int = 6379 + + +@cache +def settings() -> Settings: + return Settings() diff --git a/users/app/worker.py b/src/python/worker/main.py similarity index 51% rename from users/app/worker.py rename to src/python/worker/main.py index a6f5a08..7c546f1 100644 --- a/users/app/worker.py +++ b/src/python/worker/main.py @@ -1,32 +1,29 @@ import asyncio -import os import uvloop from arq.connections import RedisSettings -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) +from worker.config import settings -# NOTE(Marcelo): Do we want to have the same environment variables on worker and app? -REDIS_HOST = os.getenv("REDIS_HOST", "localhost") -REDIS_PORT = os.getenv("REDIS_PORT", 6379) +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -async def test_task(ctx, word: str): +async def test_task(ctx: dict, word: str) -> str: await asyncio.sleep(10) return f"test task return {word}" -async def startup(ctx): +async def startup(ctx: dict) -> None: print("start") -async def shutdown(ctx): +async def shutdown(ctx: dict) -> None: print("end") class WorkerSettings: functions = [test_task] - redis_settings = RedisSettings(host=REDIS_HOST, port=REDIS_PORT) + redis_settings = RedisSettings(settings().REDIS_HOST, port=settings().REDIS_PORT) on_startup = startup on_shutdown = shutdown handle_signals = False diff --git a/users/app/api/v1/__init__.py b/users/app/api/v1/__init__.py deleted file mode 100644 index 5602dd4..0000000 --- a/users/app/api/v1/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import APIRouter - -from app.api.v1.home import router as home_router -from app.api.v1.login import router as login_router -from app.api.v1.tasks import router as tasks_router -from app.api.v1.users import router as users_router - -router = APIRouter(prefix="/v1") -router.include_router(home_router) -router.include_router(login_router) -router.include_router(tasks_router) -router.include_router(users_router) diff --git a/users/app/crud/users.py b/users/app/crud/users.py deleted file mode 100644 index 1cabefb..0000000 --- a/users/app/crud/users.py +++ /dev/null @@ -1,6 +0,0 @@ -from app.crud.base import CRUDBase -from app.models.users import User -from app.schemas.user import UserInDB, UserUpdateDB - -CRUDUser = CRUDBase[User, UserInDB, UserUpdateDB] -crud_user = CRUDBase(User) diff --git a/users/app/models/__init__.py b/users/app/models/__init__.py deleted file mode 100644 index d7dba90..0000000 --- a/users/app/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from app.models.items import Item -from app.models.users import User diff --git a/users/docker/backend.dockerfile b/users/docker/backend.dockerfile deleted file mode 100644 index e5b8717..0000000 --- a/users/docker/backend.dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8-slim - -WORKDIR /app -ENV PYTHONPATH /app -ENV PRE_START_PATH /app/scripts/prestart.sh -ENV WEB_CONCURRENCY 3 - -COPY ./requirements.txt /app/requirements.txt -RUN pip install --upgrade pip && \ - pip install -r requirements.txt && \ - rm /app/requirements.txt - -COPY ./scripts /app/scripts -COPY ./alembic.ini /app/alembic.ini -COPY ./migrations /app/migrations -COPY ./tests /app/tests -COPY ./app /app/app diff --git a/users/docker/worker.dockerfile b/users/docker/worker.dockerfile deleted file mode 100644 index 0b57012..0000000 --- a/users/docker/worker.dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.8-slim - -ENV PYTHONUNBUFFERED1=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=off \ - PIP_DISABLE_PIP_VERSION_CHECK=on - -WORKDIR /app -ENV PYTHONPATH /app - -COPY ./requirements.txt /app/requirements.txt -RUN pip install --upgrade pip --no-cache && \ - pip install -r requirements.txt --no-cache && \ - rm /app/requirements.txt - -COPY ./scripts /app/scripts -COPY ./app /app/app - -CMD ["arq", "app.worker.WorkerSettings"]