├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── apps │ │ ├── __init__.py │ │ ├── admin_app.py │ │ └── other_app.py │ └── endpoints │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── other_api.py │ │ ├── tasks.py │ │ ├── develop.py │ │ ├── auth.py │ │ ├── users.py │ │ └── todos.py ├── core │ ├── __init__.py │ ├── logger │ │ ├── __init__.py │ │ └── logger.py │ ├── utils.py │ ├── config.py │ ├── auth.py │ └── database.py ├── commands │ ├── __init__.py │ ├── __set_base_path__.py │ └── user_creation.py ├── exceptions │ ├── __init__.py │ ├── exception_handlers.py │ ├── core.py │ └── error_messages.py ├── models │ ├── __init__.py │ ├── tags.py │ ├── todos_tags.py │ ├── todos.py │ ├── users.py │ └── base.py ├── crud │ ├── __init__.py │ ├── tag.py │ ├── user.py │ ├── todo.py │ └── base.py ├── crud_v2 │ ├── __init__.py │ ├── tag.py │ ├── user.py │ ├── todo.py │ └── base.py ├── schemas │ ├── request_info.py │ ├── token.py │ ├── __init__.py │ ├── tag.py │ ├── user.py │ ├── language_analyzer.py │ ├── todo.py │ └── core.py ├── logger_config.yaml ├── app_manager.py └── main.py ├── tests ├── __init__.py ├── todos │ ├── __init__.py │ ├── conftest.py │ └── test_todos.py ├── pydantic_performances │ ├── __init__.py │ └── test_performances.py ├── base.py ├── testing_utils.py └── conftest.py ├── .python-version ├── runtime.txt ├── .dockerignore ├── alembic ├── README ├── script.py.mako ├── env.py └── versions │ └── 20230131-0237_.py ├── docs ├── docs │ ├── install.md │ └── index.md └── mkdocs.yml ├── frontend_sample ├── src │ ├── api_clients │ │ ├── .openapi-generator │ │ │ ├── VERSION │ │ │ └── FILES │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── index.ts │ │ ├── client.ts │ │ ├── .openapi-generator-ignore │ │ ├── base.ts │ │ ├── git_push.sh │ │ ├── configuration.ts │ │ └── common.ts │ ├── config.ts │ ├── types │ │ └── api │ │ │ └── index.ts │ ├── pages │ │ ├── todos │ │ │ ├── list.tsx │ │ │ ├── create.tsx │ │ │ └── edit.tsx │ │ ├── _app.tsx │ │ └── index.tsx │ ├── styles │ │ ├── globals.css │ │ └── Home.module.css │ ├── lib │ │ └── hooks │ │ │ └── api │ │ │ ├── todos.ts │ │ │ └── index.ts │ └── components │ │ └── templates │ │ └── todos │ │ ├── TodoCreateTemplate │ │ └── TodoCreateTemplate.tsx │ │ ├── TodoUpdateTemplate │ │ └── TodoUpdateTemplate.tsx │ │ └── TodosListTemplate │ │ └── TodosListTemplate.tsx ├── .eslintrc.json ├── public │ ├── favicon.ico │ └── vercel.svg ├── next.config.js ├── .gitignore ├── package.json ├── tsconfig.json └── README.md ├── elasticsearch ├── readme.md ├── elasticsearch │ └── Dockerfile.es ├── logstash │ ├── Dockerfile │ └── pipeline │ │ └── main.conf └── docker-compose.yml ├── .flake8 ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── database-client-config.json ├── Procfile ├── .env.test ├── .env.example ├── bak_flake8.ini ├── docker-compose.ecs.yml ├── .docker └── docker-compose.ecs.yml ├── Dockerfile ├── seeder ├── seeds_json │ ├── todos.json │ └── users.json └── run.py ├── Dockerfile.lambda ├── LICENSE.md ├── mypy.ini ├── .aws └── ecs-task-definition.json ├── Makefile ├── requirements.lock ├── requirements-dev.lock ├── .github └── workflows │ ├── test.yml │ ├── push_ecr.yml │ └── aws.yml ├── docker-compose.es.yml ├── docker-compose.yml ├── .pre-commit-config.yaml ├── .devcontainer └── devcontainer.json ├── pyproject.toml ├── alembic.ini ├── .gitignore └── readme.md /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/todos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11.3 2 | -------------------------------------------------------------------------------- /app/api/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.11.3 2 | -------------------------------------------------------------------------------- /tests/pydantic_performances/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/.venv 3 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. 2 | -------------------------------------------------------------------------------- /docs/docs/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | インストール、環境構築について説明します。 3 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 7.0.0-SNAPSHOT 2 | -------------------------------------------------------------------------------- /frontend_sample/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /elasticsearch/readme.md: -------------------------------------------------------------------------------- 1 | wsl -d docker-desktop 2 | sysctl -w vm.max_map_count=262144 3 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/*.js 2 | node_modules 3 | typings 4 | dist 5 | -------------------------------------------------------------------------------- /app/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * # noqa 2 | from .error_messages import * # noqa 3 | -------------------------------------------------------------------------------- /app/core/logger/__init__.py: -------------------------------------------------------------------------------- 1 | from .logger import get_logger, init_gunicorn_uvicorn_logger, init_logger 2 | -------------------------------------------------------------------------------- /frontend_sample/src/config.ts: -------------------------------------------------------------------------------- 1 | const API_HOST = process.env.NEXT_PUBLIC_API_HOST; 2 | export { API_HOST }; 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 119 3 | exclude = __init__.py,alembic,tests 4 | ignore = E203,W503,W504 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "matangover.mypy", 4 | "charliermarsh.ruff" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /frontend_sample/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takashi-yoneya/fastapi-template/HEAD/frontend_sample/public/favicon.ico -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/.npmignore: -------------------------------------------------------------------------------- 1 | # empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm 2 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .tags import Tag 2 | from .todos import Todo 3 | from .todos_tags import TodoTag 4 | from .users import User 5 | -------------------------------------------------------------------------------- /app/commands/__set_base_path__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | sys.path.append(str(Path(__file__).absolute().parent.parent)) 5 | -------------------------------------------------------------------------------- /app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .tag import * # noqa 3 | from .todo import * # noqa 4 | from .user import * # noqa 5 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT:-5000} --log-config=./app/logger_config.yaml --proxy-headers --forwarded-allow-ips='*' 2 | -------------------------------------------------------------------------------- /app/crud_v2/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .tag import * # noqa 3 | from .todo import * # noqa 4 | from .user import * # noqa 5 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .npmignore 3 | api.ts 4 | base.ts 5 | common.ts 6 | configuration.ts 7 | git_push.sh 8 | index.ts 9 | -------------------------------------------------------------------------------- /frontend_sample/src/types/api/index.ts: -------------------------------------------------------------------------------- 1 | interface ErrorResponse { 2 | errorCode?: string; 3 | errorMessage?: string; 4 | } 5 | 6 | export type { ErrorResponse }; 7 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | ENV=local 2 | DEBUG=false 3 | SENTRY_SDK_DNS="" 4 | API_GATEWAY_STAGE_PATH="" 5 | 6 | DB_HOST=db 7 | DB_PORT=3306 8 | DB_NAME=test 9 | DB_USER_NAME=root 10 | DB_PASSWORD= 11 | -------------------------------------------------------------------------------- /app/api/apps/admin_app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from app.api.endpoints import admin 4 | 5 | app = FastAPI() 6 | app.include_router(admin.router, prefix="/admin", tags=["admin"]) 7 | -------------------------------------------------------------------------------- /frontend_sample/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ENV=local 2 | DEBUG=false 3 | SENTRY_SDK_DNS="" 4 | API_GATEWAY_STAGE_PATH="" 5 | DB_HOST=db 6 | DB_PORT=3306 7 | DB_NAME=docker 8 | DB_USER_NAME=docker 9 | DB_PASSWORD=docker 10 | -------------------------------------------------------------------------------- /app/api/apps/other_app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from app.api.endpoints import other_api 4 | 5 | app = FastAPI() 6 | app.include_router(other_api.router, prefix="/other", tags=["other"]) 7 | -------------------------------------------------------------------------------- /elasticsearch/elasticsearch/Dockerfile.es: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/elasticsearch/elasticsearch:7.11.0 2 | RUN elasticsearch-plugin install analysis-kuromoji 3 | RUN elasticsearch-plugin install analysis-icu 4 | -------------------------------------------------------------------------------- /bak_flake8.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = "__init__.py,alembic,tests,.git,__pycache__,.mypy_cache,.pytest_cache" 4 | ignore = "E203,W503,W504,E701" 5 | max-line-length = 88 6 | per-file-ignores = "__init__.py:F401" 7 | -------------------------------------------------------------------------------- /app/schemas/request_info.py: -------------------------------------------------------------------------------- 1 | from pydantic import ConfigDict 2 | 3 | from app.schemas.core import BaseSchema 4 | 5 | 6 | class RequestInfoResponse(BaseSchema): 7 | model_config = ConfigDict(from_attributes=True) 8 | ip_address: str | None 9 | host: str | None 10 | -------------------------------------------------------------------------------- /app/api/endpoints/admin.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.core.logger import get_logger 4 | 5 | logger = get_logger(__name__) 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.get("/hoge", operation_id="get_admin_hoge") 11 | async def get_admin_hoge() -> dict[str, str]: 12 | return {"response": "hoge"} 13 | -------------------------------------------------------------------------------- /app/api/endpoints/other_api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.core.logger import get_logger 4 | 5 | logger = get_logger(__name__) 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.get("/hoge", operation_id="get_other_hoge") 11 | async def get_other_hoge() -> dict[str, str]: 12 | return {"response": "hoge"} 13 | -------------------------------------------------------------------------------- /frontend_sample/src/pages/todos/list.tsx: -------------------------------------------------------------------------------- 1 | import TodosListTemplate from "components/templates/todos/TodosListTemplate/TodosListTemplate"; 2 | import { NextPage } from "next"; 3 | 4 | const TodosListPage: NextPage = () => { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default TodosListPage; 13 | -------------------------------------------------------------------------------- /frontend_sample/src/pages/todos/create.tsx: -------------------------------------------------------------------------------- 1 | import TodoCreateTemplate from "components/templates/todos/TodoCreateTemplate/TodoCreateTemplate"; 2 | import { NextPage } from "next"; 3 | 4 | const TodoUpdatePage: NextPage = () => { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default TodoUpdatePage; 13 | -------------------------------------------------------------------------------- /app/schemas/token.py: -------------------------------------------------------------------------------- 1 | from pydantic import ConfigDict 2 | 3 | from app.schemas.core import BaseSchema 4 | 5 | 6 | class Token(BaseSchema): 7 | model_config = ConfigDict(alias_generator=None, populate_by_name=False, from_attributes=True) 8 | access_token: str 9 | token_type: str 10 | 11 | 12 | class TokenPayload(BaseSchema): 13 | sub: str | None 14 | -------------------------------------------------------------------------------- /docker-compose.ecs.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | web: 5 | image: 589800662540.dkr.ecr.us-west-1.amazonaws.com/fastapi-sample-backend:latest 6 | build: 7 | context: ./backend 8 | dockerfile: Dockerfile 9 | ports: 10 | - 80:80 11 | volumes: 12 | - ./backend:/backend 13 | restart: always 14 | tty: true 15 | -------------------------------------------------------------------------------- /.docker/docker-compose.ecs.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | web: 5 | image: 589800662540.dkr.ecr.us-west-1.amazonaws.com/fastapi-sample-backend:latest 6 | build: 7 | context: ../ 8 | dockerfile: Dockerfile 9 | ports: 10 | - 80:80 11 | # volumes: 12 | # - ./backend:/backend 13 | restart: always 14 | tty: true 15 | -------------------------------------------------------------------------------- /app/core/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | import ulid 4 | from fastapi import Request 5 | 6 | 7 | def get_ulid() -> str: 8 | return ulid.new().str 9 | 10 | 11 | def get_request_info(request: Request) -> str: 12 | return request.client.host 13 | 14 | 15 | def get_host_by_ip_address(ip_address: str) -> str: 16 | return socket.gethostbyaddr(ip_address)[0] 17 | -------------------------------------------------------------------------------- /elasticsearch/logstash/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM logstash:8.1.3 2 | 3 | USER root 4 | RUN apt-get update && apt-get install -y gcc libmariadb-dev curl 5 | 6 | USER logstash 7 | RUN rm -rf /usr/share/logstash/pipeline 8 | COPY ./pipeline pipeline 9 | RUN curl -o mysql-connector-java-8.0.29.jar -L https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.29/mysql-connector-java-8.0.29.jar 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "FastAPI Sample", 6 | "type": "python", 7 | "request": "launch", 8 | "module": "uvicorn", 9 | "args": ["app.main:app", "--host", "0.0.0.0", "--port", "81", "--reload", "--log-config", "./app/logger_config.yaml"], 10 | "jinja": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.enabled": true, 3 | "python.linting.pylintEnabled": false, 4 | // "python.linting.flake8Enabled": true, 5 | "python.linting.mypyEnabled": true, 6 | "python.formatting.provider": null, 7 | "python.linting.lintOnSave": true, 8 | "editor.formatOnSave": true, 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend_sample/src/pages/todos/edit.tsx: -------------------------------------------------------------------------------- 1 | import TodoUpdateTemplate from "components/templates/todos/TodoUpdateTemplate/TodoUpdateTemplate"; 2 | import { NextPage } from "next"; 3 | // import TodoUpdateTemplate from "components/templates/todos/TodoUpdateTemplate/TodosUpdateTemplate"; 4 | 5 | const TodoUpdatePage: NextPage = () => { 6 | return ( 7 | <> 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default TodoUpdatePage; 14 | -------------------------------------------------------------------------------- /app/exceptions/exception_handlers.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from urllib.request import Request 3 | 4 | from fastapi.responses import PlainTextResponse 5 | 6 | from app.core.logger import get_logger 7 | 8 | logger = get_logger(__name__) 9 | 10 | 11 | async def http_exception_handler(request: Request, exc: Any) -> PlainTextResponse: # noqa: ARG001 12 | """HTTPリクエストに起因したExceptionエラー発生時のフック処理.""" 13 | logger.exception(str(exc)) 14 | return PlainTextResponse("Server Error: " + str(exc), status_code=500) 15 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * FastAPI Sample 5 | * Apps link
/docs
/admin/docs
/other/docs 6 | * 7 | * The version of the OpenAPI document: 0.0.1 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | export * from "./api"; 16 | export * from "./configuration"; 17 | -------------------------------------------------------------------------------- /frontend_sample/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/client.ts: -------------------------------------------------------------------------------- 1 | import { API_HOST } from "config"; 2 | import { TodosApi, UsersApi } from "./api"; 3 | import { Configuration } from "./configuration"; 4 | 5 | const authConfig = new Configuration({ 6 | basePath: API_HOST, 7 | baseOptions: { withCredentials: true }, 8 | }); 9 | const nonAuthConfig = new Configuration({ 10 | basePath: API_HOST, 11 | baseOptions: { withCredentials: false }, 12 | }); 13 | 14 | export const UsersApiClient = new UsersApi(nonAuthConfig); 15 | export const TodosApiClient = new TodosApi(nonAuthConfig); 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-bookworm 2 | 3 | ENV LANG C.UTF-8 4 | ENV TZ UTC 5 | ENV PYTHONUNBUFFERED 1 6 | ENV PYTHONPATH /backend/app:/backend 7 | ENV TERM xterm-256color 8 | 9 | RUN apt-get update && apt-get install -y \ 10 | git \ 11 | gcc \ 12 | libmariadb-dev \ 13 | curl 14 | 15 | WORKDIR /backend 16 | COPY ./requirements.lock /backend/ 17 | 18 | # ryeがrequirements.lockを常に最新化していることを前提とした暫定的な対応 19 | RUN sed '/-e/d' requirements.lock > requirements.txt 20 | RUN pip install -r requirements.txt 21 | 22 | COPY . /backend/ 23 | -------------------------------------------------------------------------------- /app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import BaseSchema, PagingMeta, PagingQueryIn, SortQueryIn 2 | from .language_analyzer import AnalyzedLanguage, AnalyzedlanguageToken 3 | from .request_info import RequestInfoResponse 4 | from .tag import TagCreate, TagResponse, TagsPagedResponse, TagUpdate 5 | from .todo import ( 6 | TodoCreate, 7 | TodoResponse, 8 | TodoSortQueryIn, 9 | TodosPagedResponse, 10 | TodoUpdate, 11 | ) 12 | from .token import Token, TokenPayload 13 | from .user import UserCreate, UserResponse, UsersPagedResponse, UserUpdate 14 | -------------------------------------------------------------------------------- /frontend_sample/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to MkDocs 2 | 3 | For full documentation visit [mkdocs.org](https://www.mkdocs.org). 4 | 5 | ## Commands 6 | 7 | * `mkdocs new [dir-name]` - Create a new project. 8 | * `mkdocs serve` - Start the live-reloading docs server. 9 | * `mkdocs build` - Build the documentation site. 10 | * `mkdocs -h` - Print help message and exit. 11 | 12 | ## Project layout 13 | 14 | mkdocs.yml # The configuration file. 15 | docs/ 16 | index.md # The documentation homepage. 17 | ... # Other markdown pages, images and other files. 18 | -------------------------------------------------------------------------------- /frontend_sample/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { QueryClient, QueryClientProvider } from "react-query"; 4 | 5 | const queryClient = new QueryClient({ 6 | defaultOptions: { 7 | queries: { 8 | retry: false, 9 | refetchOnWindowFocus: false, 10 | }, 11 | }, 12 | }); 13 | 14 | export default function App({ Component, pageProps }: AppProps) { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/commands/user_creation.py: -------------------------------------------------------------------------------- 1 | import fire 2 | 3 | from app import crud, models, schemas 4 | from app.core.database import get_db 5 | 6 | from . import __set_base_path__ # noqa 7 | 8 | 9 | def create_user(email: str, full_name: str, password: str) -> models.User: 10 | db = next(get_db()) 11 | user = crud.user.create( 12 | db, 13 | obj_in=schemas.UserCreate(full_name=full_name, email=email, password=password), 14 | ) 15 | print(user.to_dict()) 16 | 17 | return user 18 | 19 | 20 | if __name__ == "__main__": 21 | fire.Fire(create_user) 22 | -------------------------------------------------------------------------------- /app/models/tags.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String 2 | from sqlalchemy.orm import Mapped, relationship 3 | 4 | from app.models.base import Base, ModelBaseMixin 5 | 6 | 7 | class Tag(ModelBaseMixin, Base): 8 | __tablename__ = "tags" 9 | mysql_charset = ("utf8mb4",) 10 | mysql_collate = "utf8mb4_unicode_ci" 11 | 12 | name: Mapped[str] = Column(String(100), unique=True, index=True) 13 | 14 | todos: Mapped[list] = relationship( 15 | "Todo", 16 | secondary="todos_tags", 17 | back_populates="tags", 18 | lazy="joined", 19 | ) 20 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: FastAPI Sample Docs 2 | nav: 3 | - Home: index.md 4 | - Install: install.md 5 | - Demos: demos.md 6 | - Features: features.md 7 | - Settings: settings.md 8 | - Logging: logging.md 9 | - ErrorExceptions: errors.md 10 | - Testing: testing.md 11 | - Migration: migration.md 12 | - CI/CD: ci-cd.md 13 | - PackageManagement: package-management.md 14 | - CRUD: crud.md 15 | - Models: models.md 16 | - Endpoints: endpoints.md 17 | - Schemas: schemas.md 18 | - PermissionScopes: permission-scopes.md 19 | - Docker: docker.md 20 | - Deployment: deployment.md 21 | -------------------------------------------------------------------------------- /frontend_sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "18.11.10", 13 | "@types/react": "18.0.26", 14 | "@types/react-dom": "18.0.9", 15 | "axios": "^1.2.0", 16 | "eslint": "8.29.0", 17 | "eslint-config-next": "13.0.6", 18 | "next": "13.0.6", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "react-query": "^3.39.2", 22 | "typescript": "4.9.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend_sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "src" 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /app/schemas/tag.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pydantic import ConfigDict 4 | 5 | from app.schemas.core import BaseSchema, PagingMeta 6 | 7 | 8 | class TagBase(BaseSchema): 9 | name: str | None 10 | 11 | 12 | class TagResponse(TagBase): 13 | model_config = ConfigDict(from_attributes=True) 14 | id: str 15 | created_at: datetime.datetime | None 16 | updated_at: datetime.datetime | None 17 | deleted_at: datetime.datetime | None 18 | 19 | 20 | class TagCreate(TagBase): 21 | name: str 22 | 23 | 24 | class TagUpdate(TagBase): 25 | pass 26 | 27 | 28 | class TagsPagedResponse(BaseSchema): 29 | data: list[TagResponse] | None 30 | meta: PagingMeta | None 31 | -------------------------------------------------------------------------------- /app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import ConfigDict, EmailStr 2 | 3 | from app.schemas.core import BaseSchema, PagingMeta 4 | 5 | 6 | class UserBase(BaseSchema): 7 | full_name: str | None = None 8 | 9 | 10 | class UserCreate(UserBase): 11 | email: EmailStr 12 | password: str 13 | 14 | 15 | # Properties to receive via API on update 16 | class UserUpdate(UserBase): 17 | password: str | None = None 18 | 19 | 20 | class UserResponse(UserBase): 21 | model_config = ConfigDict(from_attributes=True) 22 | id: str 23 | email: EmailStr 24 | email_verified: bool 25 | 26 | 27 | class UsersPagedResponse(BaseSchema): 28 | data: list[UserResponse] | None 29 | meta: PagingMeta | None 30 | -------------------------------------------------------------------------------- /app/schemas/language_analyzer.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class AnalyzedlanguageToken(BaseModel): 5 | surface: str = Field(..., description="表層形式(入力文字のまま)") 6 | dictionaly_form: str = Field(..., description="辞書形式") 7 | reading_form: str = Field(..., description="読みカナ") 8 | normalized_form: str = Field(..., description="正規化済の形式") 9 | part_of_speech: tuple[str] = Field(..., description="品詞") 10 | begin_pos: int = Field(..., description="開始文字番号") 11 | end_pos: int = Field(..., description="終了文字番号") 12 | 13 | 14 | class AnalyzedLanguage(BaseModel): 15 | raw_text: str 16 | tokens: list[AnalyzedlanguageToken] = [] 17 | excluded_tokens: list[AnalyzedlanguageToken] = [] 18 | during_time: float 19 | -------------------------------------------------------------------------------- /app/models/todos_tags.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey, Integer, UniqueConstraint 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from app.models.base import Base, ModelBaseMixinWithoutDeletedAt 5 | 6 | 7 | class TodoTag(ModelBaseMixinWithoutDeletedAt, Base): 8 | __tablename__ = "todos_tags" 9 | mysql_charset = ("utf8mb4",) 10 | mysql_collate = "utf8mb4_unicode_ci" 11 | __table_args__ = (UniqueConstraint("todo_id", "tag_id", name="ix_todos_tags_todo_id_tag_id"),) 12 | 13 | id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True) 14 | todo_id: Mapped[str] = mapped_column(ForeignKey("todos.id"), index=True) 15 | tag_id: Mapped[str] = mapped_column(ForeignKey("tags.id"), index=True) 16 | -------------------------------------------------------------------------------- /tests/todos/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | 5 | import pytest_asyncio 6 | from sqlalchemy.orm import Session 7 | 8 | from app import models 9 | 10 | 11 | @pytest_asyncio.fixture 12 | async def data_set(db: Session) -> None: 13 | await insert_todos(db) 14 | 15 | 16 | async def insert_todos(db: Session) -> None: 17 | now = datetime.datetime.now(tz=datetime.timezone.utc) 18 | data = [ 19 | models.Todo( 20 | id=str(i), 21 | title=f"test-title-{i}", 22 | description=f"test-description-{i}", 23 | created_at=now - datetime.timedelta(days=i), 24 | ) 25 | for i in range(1, 25) 26 | ] 27 | db.add_all(data) 28 | await db.commit() 29 | -------------------------------------------------------------------------------- /seeder/seeds_json/todos.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": "app.models.Todo", 3 | "data": [ 4 | { 5 | "title": "TestTodo1", 6 | "description": "TestDescription1" 7 | }, 8 | { 9 | "title": "TestTodo2", 10 | "description": "TestDescription2" 11 | }, 12 | { 13 | "title": "TestTodo3", 14 | "description": "TestDescription3" 15 | }, 16 | { 17 | "title": "TestTodo4", 18 | "description": "TestDescription4" 19 | }, 20 | { 21 | "title": "TestTodo5", 22 | "description": "TestDescription5" 23 | }, 24 | { 25 | "title": "TestTodo6", 26 | "description": "TestDescription6" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /app/models/todos.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import DateTime, String, Text 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from app.models.base import Base, ModelBaseMixin 7 | 8 | 9 | class Todo(ModelBaseMixin, Base): 10 | __tablename__ = "todos" 11 | mysql_charset = ("utf8mb4",) 12 | mysql_collate = "utf8mb4_unicode_ci" 13 | 14 | title: Mapped[str] = mapped_column(String(100), index=True) 15 | description: Mapped[str] = mapped_column(Text, nullable=True) 16 | completed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True) 17 | 18 | tags: Mapped[list] = relationship( 19 | "Tag", 20 | secondary="todos_tags", 21 | back_populates="todos", 22 | lazy="joined", 23 | ) 24 | -------------------------------------------------------------------------------- /seeder/seeds_json/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": "app.models.User", 3 | "data": [ 4 | { 5 | "id": "1", 6 | "email": "test-user1@example.com", 7 | "hashed_password": "$2b$12$mV0czmMSlSBR66ZqiJZ9HuE9z0VKhMqwdH.t2a3TZeXhwtWCuU/gi", 8 | "full_name": "user1" 9 | }, 10 | { 11 | "id": "2", 12 | "email": "test-user2@example.com", 13 | "hashed_password": "$2b$12$mV0czmMSlSBR66ZqiJZ9HuE9z0VKhMqwdH.t2a3TZeXhwtWCuU/gi", 14 | "full_name": "user2" 15 | }, 16 | { 17 | "id": "3", 18 | "email": "test-user3@example.com", 19 | "hashed_password": "$2b$12$mV0czmMSlSBR66ZqiJZ9HuE9z0VKhMqwdH.t2a3TZeXhwtWCuU/gi", 20 | "full_name": "user3" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /app/models/users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, String, Text 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from app.models.base import Base, ModelBaseMixin 5 | 6 | 7 | class User(ModelBaseMixin, Base): 8 | __tablename__ = "users" 9 | mysql_charset = ("utf8mb4",) 10 | mysql_collate = "utf8mb4_unicode_ci" 11 | 12 | full_name: Mapped[str] = mapped_column(String(64), index=True) 13 | email: Mapped[str] = mapped_column( 14 | String(200), 15 | unique=True, 16 | index=True, 17 | nullable=False, 18 | ) 19 | email_verified: Mapped[bool] = mapped_column( 20 | Boolean, 21 | nullable=False, 22 | server_default="0", 23 | ) 24 | hashed_password: Mapped[str] = mapped_column(Text, nullable=False) 25 | scopes: Mapped[str] = mapped_column(Text, nullable=True) 26 | -------------------------------------------------------------------------------- /frontend_sample/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | import styles from "../styles/Home.module.css"; 6 | 7 | export default function Home() { 8 | const router = useRouter(); 9 | return ( 10 |
11 | 12 | Fastapi Sample Frontend 13 | 14 | 15 |

Fastapi Sample Frontend

16 | 17 |
18 |
19 | 22 |
23 | Todos List 24 |
25 |
26 | Todos Create 27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/api/endpoints/tasks.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from fastapi import APIRouter, BackgroundTasks 5 | 6 | from app.core.logger import get_logger 7 | 8 | logger = get_logger(__name__) 9 | 10 | router = APIRouter() 11 | 12 | 13 | def long_process_thread(id: str) -> None: 14 | for i in range(100): 15 | logger.info(f"[long_process_thread(id={id})] {i+1}sec") 16 | time.sleep(1) 17 | 18 | 19 | async def long_process_async(id: str) -> None: 20 | for i in range(100): 21 | logger.info(f"[long_process_asyncio(id={id})] {i+1}sec") 22 | await asyncio.sleep(1) 23 | 24 | 25 | @router.post("/long-process/thread") 26 | def exec_long_process_thread( 27 | id: str, 28 | background_tasks: BackgroundTasks, 29 | ) -> None: 30 | background_tasks.add_task(long_process_thread, id) 31 | 32 | 33 | @router.post("/long-process/async") 34 | async def exec_long_process_async( 35 | id: str, 36 | background_tasks: BackgroundTasks, 37 | ) -> None: 38 | background_tasks.add_task(long_process_async, id) 39 | -------------------------------------------------------------------------------- /app/exceptions/core.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from fastapi import HTTPException, status 4 | 5 | 6 | class APIException(HTTPException): 7 | """API例外.""" 8 | 9 | default_status_code = status.HTTP_400_BAD_REQUEST 10 | 11 | def __init__( 12 | self, 13 | error: Any, 14 | status_code: int = default_status_code, 15 | headers: dict[str, Any] | None = None, 16 | ) -> None: 17 | self.headers = headers 18 | try: 19 | error_obj = error() 20 | except Exception: 21 | error_obj = error 22 | 23 | try: 24 | message = error_obj.text.format(error_obj.param) 25 | except Exception: 26 | message = error_obj.text 27 | 28 | try: 29 | self.status_code = error_obj.status_code 30 | except Exception: 31 | self.status_code = status_code 32 | 33 | self.detail = {"error_code": str(error_obj), "error_msg": message} 34 | print(self.detail) 35 | super().__init__(self.status_code, self.detail) 36 | -------------------------------------------------------------------------------- /frontend_sample/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile.lambda: -------------------------------------------------------------------------------- 1 | ARG FUNCTION_DIR="/function" 2 | FROM python:3.11-bookworm 3 | 4 | ENV LANG C.UTF-8 5 | ENV TZ UTC 6 | ENV PYTHONUNBUFFERED 1 7 | ENV PYTHONPATH ${FUNCTION_DIR}/backend 8 | ENV TERM xterm-256color 9 | # EXPOSE 80 10 | 11 | RUN apt-get update && apt-get install -y \ 12 | git \ 13 | gcc \ 14 | libmariadb-dev \ 15 | curl 16 | RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \ 17 | cd /usr/local/bin && \ 18 | ln -s /opt/poetry/bin/poetry && \ 19 | poetry config virtualenvs.create false 20 | 21 | # Include global arg in this stage of the build 22 | ARG FUNCTION_DIR 23 | # Create function directory 24 | RUN mkdir -p ${FUNCTION_DIR} 25 | WORKDIR ${FUNCTION_DIR}/backend 26 | COPY ./pyproject.toml ./poetry.lock ${FUNCTION_DIR}/backend/ 27 | 28 | RUN poetry install --no-root 29 | RUN pip install awslambdaric 30 | 31 | COPY . ${FUNCTION_DIR}/backend/ 32 | 33 | ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] 34 | CMD ["app/main.handler"] 35 | # CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--reload"] 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 takashi.yoneya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /elasticsearch/logstash/pipeline/main.conf: -------------------------------------------------------------------------------- 1 | input { 2 | jdbc { 3 | jdbc_driver_library => "/usr/share/logstash/mysql-connector-java-8.0.29.jar" 4 | jdbc_driver_class => "com.mysql.cj.jdbc.Driver" 5 | jdbc_connection_string => "jdbc:mysql://db:3306/docker" 6 | jdbc_default_timezone => "Asia/Tokyo" 7 | jdbc_user => "docker" 8 | jdbc_password => "docker" 9 | jdbc_default_timezone => "Asia/Tokyo" 10 | statement => "SELECT * FROM jobs where updated_at > :sql_last_value" 11 | tracking_column => "updated_at" 12 | schedule => "*/1 * * * *" 13 | } 14 | } 15 | 16 | filter { 17 | mutate { 18 | copy => { "id" => "[@metadata][_id]"} 19 | remove_field => ["@version", "@timestamp"] 20 | } 21 | if [deleted_at] { 22 | mutate { 23 | add_field => {"[@metadata][action]" => "delete"} 24 | } 25 | } else { 26 | mutate { 27 | add_field => {"[@metadata][action]" => "index"} 28 | } 29 | } 30 | } 31 | 32 | output { 33 | elasticsearch { 34 | hosts => ["http://es01:9200"] 35 | index => "jobs" 36 | action => "%{[@metadata][action]}" 37 | document_id => "%{[@metadata][_id]}" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/pydantic_performances/test_performances.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timezone 3 | 4 | import pytest 5 | from pydantic import BaseModel 6 | 7 | 8 | class Tag(BaseModel): 9 | id: int 10 | tag: str 11 | 12 | 13 | class Data(BaseModel): 14 | name: str 15 | age: int 16 | address: str 17 | point: float 18 | tags: list[Tag] 19 | created_at: datetime 20 | updated_at: datetime 21 | 22 | 23 | @pytest.mark.skip(reason="pydantic v2のパフォーマンス検証用のため、通常のテストでは使用しない") 24 | def test_check_performances() -> None: 25 | """pydantic v2のパフォーマンス検証用""" 26 | start = time.time() 27 | for i in range(1000000): 28 | data = Data( 29 | name=f"John{i}", 30 | age=20, 31 | address="New York", 32 | point=1.23, 33 | tags=[ 34 | Tag(id=1, tag="tag1"), 35 | Tag(id=2, tag="tag2"), 36 | Tag(id=3, tag="tag3"), 37 | ], 38 | created_at=datetime.now(tz=timezone.utc), 39 | updated_at=datetime.now(tz=timezone.utc), 40 | ) 41 | data.model_dump_json() 42 | 43 | print(f"Time taken: {time.time() - start}") 44 | -------------------------------------------------------------------------------- /app/api/endpoints/develop.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | 3 | from app import schemas 4 | from app.core import utils 5 | from app.core.logger import get_logger 6 | from app.exceptions.core import APIException 7 | from app.exceptions.error_messages import ErrorMessage 8 | from app.schemas.core import BaseSchema 9 | 10 | logger = get_logger(__name__) 11 | 12 | router = APIRouter() 13 | 14 | 15 | @router.get("/error") 16 | def exec_error() -> None: 17 | logger.error("debug test") 18 | print(1 / 0) 19 | raise APIException(ErrorMessage.NOT_FOUND("デバックテストID")) 20 | 21 | 22 | @router.get("/error2") 23 | def exec_error2() -> None: 24 | raise APIException(ErrorMessage.INTERNAL_SERVER_ERROR) 25 | 26 | 27 | class RequestInfoResponse(BaseSchema): 28 | ip_address: str | None 29 | host: str | None 30 | 31 | class Config: 32 | orm_mode = True 33 | 34 | 35 | @router.get("/request-info") 36 | def get_request_info(request: Request) -> schemas.RequestInfoResponse: 37 | ip_address = utils.get_request_info(request) 38 | host = utils.get_host_by_ip_address(ip_address) 39 | 40 | return schemas.RequestInfoResponse(ip_address=ip_address, host=host) 41 | -------------------------------------------------------------------------------- /app/logger_config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | disable_existing_loggers: false 3 | 4 | formatters: 5 | json: 6 | format: "%(asctime)s %(name)s %(levelname)s %(message)s %(filename)s %(module)s %(funcName)s %(lineno)d" 7 | class: pythonjsonlogger.jsonlogger.JsonFormatter 8 | normal: 9 | format: "%(asctime)s %(name)s %(levelname)s %(message)s %(filename)s %(module)s %(funcName)s %(lineno)d" 10 | 11 | handlers: 12 | console: 13 | class: logging.StreamHandler 14 | level: INFO 15 | formatter: normal # json形式にしたい場合はjsonと記述する 16 | stream: ext://sys.stdout 17 | 18 | loggers: 19 | backend: 20 | level: INFO 21 | handlers: [console] 22 | propagate: false 23 | 24 | # gunicorn.error: 25 | # level: DEBUG 26 | # handlers: [console] 27 | # propagate: false 28 | 29 | uvicorn.access: 30 | level: INFO 31 | handlers: [console] 32 | propagate: false 33 | 34 | # sqlalchemy.engine: 35 | # level: DEBUG 36 | # handlers: [console] 37 | # propagate: false 38 | 39 | alembic.runtime.migration: 40 | level: INFO 41 | handlers: [console] 42 | propagate: false 43 | 44 | root: 45 | level: INFO 46 | handlers: [console] 47 | -------------------------------------------------------------------------------- /app/crud/tag.py: -------------------------------------------------------------------------------- 1 | from fastapi.encoders import jsonable_encoder 2 | from sqlalchemy.dialects.mysql import insert 3 | from sqlalchemy.orm import Session 4 | 5 | from app import models, schemas 6 | 7 | from .base import CRUDBase 8 | 9 | 10 | class CRUDTag( 11 | CRUDBase[ 12 | models.Tag, 13 | schemas.TagResponse, 14 | schemas.TagCreate, 15 | schemas.TagUpdate, 16 | schemas.TagsPagedResponse, 17 | ], 18 | ): 19 | def upsert_tags( 20 | self, 21 | db: Session, 22 | tags_in: list[schemas.TagCreate], 23 | ) -> list[models.Tag]: 24 | tags_in_list = jsonable_encoder(tags_in) 25 | insert_stmt = insert(models.Tag).values(tags_in_list) 26 | insert_stmt = insert_stmt.on_duplicate_key_update( 27 | name=insert_stmt.inserted.name, 28 | ) 29 | db.execute(insert_stmt) 30 | 31 | tag_names = (x.name for x in tags_in) 32 | tags = db.query(models.Tag).filter(models.Tag.name.in_(tag_names)).all() 33 | 34 | return tags 35 | 36 | 37 | tag = CRUDTag( 38 | models.Tag, 39 | response_schema_class=schemas.TagResponse, 40 | list_response_class=schemas.TagsPagedResponse, 41 | ) 42 | -------------------------------------------------------------------------------- /app/schemas/todo.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | 4 | from fastapi import Query 5 | from pydantic import ConfigDict 6 | 7 | from app import schemas 8 | from app.schemas.core import BaseSchema, PagingMeta 9 | from app.schemas.tag import TagResponse 10 | 11 | 12 | class TodoBase(BaseSchema): 13 | title: str | None = None 14 | description: str | None = None 15 | completed_at: datetime.datetime | None = None 16 | 17 | 18 | class TodoResponse(TodoBase): 19 | id: str 20 | tags: list[TagResponse] | None = [] 21 | created_at: datetime.datetime | None = None 22 | updated_at: datetime.datetime | None = None 23 | 24 | model_config = ConfigDict(from_attributes=True) 25 | 26 | 27 | class TodoCreate(TodoBase): 28 | title: str 29 | description: str | None = None 30 | 31 | 32 | class TodoUpdate(TodoBase): 33 | pass 34 | 35 | 36 | class TodosPagedResponse(BaseSchema): 37 | data: list[TodoResponse] | None = [] 38 | meta: PagingMeta | None = None 39 | 40 | 41 | class TodoSortFieldEnum(Enum): 42 | created_at = "created_at" 43 | title = "title" 44 | 45 | 46 | class TodoSortQueryIn(schemas.SortQueryIn): 47 | sort_field: TodoSortFieldEnum | None = Query(TodoSortFieldEnum.created_at) 48 | -------------------------------------------------------------------------------- /app/crud_v2/tag.py: -------------------------------------------------------------------------------- 1 | from fastapi.encoders import jsonable_encoder 2 | from sqlalchemy.dialects.mysql import insert 3 | from sqlalchemy.orm import Session 4 | from sqlalchemy.sql import select 5 | 6 | from app import models, schemas 7 | 8 | from .base import CRUDV2Base 9 | 10 | 11 | class CRUDTag( 12 | CRUDV2Base[ 13 | models.Tag, 14 | schemas.TagResponse, 15 | schemas.TagCreate, 16 | schemas.TagUpdate, 17 | schemas.TagsPagedResponse, 18 | ], 19 | ): 20 | def upsert_tags( 21 | self, 22 | db: Session, 23 | tags_in: list[schemas.TagCreate], 24 | ) -> list[models.Tag]: 25 | tags_in_list = jsonable_encoder(tags_in) 26 | insert_stmt = insert(models.Tag).values(tags_in_list) 27 | insert_stmt = insert_stmt.on_duplicate_key_update( 28 | name=insert_stmt.inserted.name, 29 | ) 30 | db.execute(insert_stmt) 31 | 32 | tag_names = (x.name for x in tags_in) 33 | stmt = select(models.Tag).where(models.Tag.name.in_(tag_names)) 34 | tags = db.execute(stmt).scalars().all() 35 | 36 | return tags 37 | 38 | 39 | tag = CRUDTag( 40 | models.Tag, 41 | response_schema_class=schemas.TagResponse, 42 | list_response_class=schemas.TagsPagedResponse, 43 | ) 44 | -------------------------------------------------------------------------------- /app/api/endpoints/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from fastapi import APIRouter, Depends 4 | from fastapi.security import OAuth2PasswordRequestForm 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from app import crud_v2, schemas 8 | from app.core import auth 9 | from app.core.config import settings 10 | from app.core.database import get_async_db 11 | from app.exceptions.core import APIException 12 | from app.exceptions.error_messages import ErrorMessage 13 | 14 | router = APIRouter() 15 | 16 | 17 | @router.post("/login") 18 | async def login_access_token( 19 | db: AsyncSession = Depends(get_async_db), 20 | form_data: OAuth2PasswordRequestForm = Depends(), 21 | ) -> schemas.Token: 22 | """OAuth2 compatible token login, get an access token for future requests.""" 23 | user = await crud_v2.user.authenticate( 24 | db, 25 | email=form_data.username, 26 | password=form_data.password, 27 | ) 28 | if not user: 29 | raise APIException(ErrorMessage.FAILURE_LOGIN) 30 | 31 | access_token_expires = datetime.timedelta( 32 | minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES, 33 | ) 34 | access_token = auth.create_access_token(user.id, expires_delta=access_token_expires) 35 | return schemas.Token( 36 | access_token=access_token, 37 | token_type="bearer", 38 | ) 39 | -------------------------------------------------------------------------------- /app/app_manager.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | 4 | class FastAPIAppManager: 5 | def __init__(self, root_app: FastAPI) -> None: 6 | self.app_path_list: list[str] = [""] 7 | self.root_app: FastAPI = root_app 8 | self.apps: list[FastAPI] = [root_app] 9 | 10 | def add_app(self, app: FastAPI, path: str) -> None: 11 | self.apps.append(app) 12 | if not path.startswith("/"): 13 | path = f"/{path}" 14 | else: 15 | path = path 16 | self.app_path_list.append(path) 17 | app.title = f"{self.root_app.title}({path})" 18 | app.version = self.root_app.version 19 | app.debug = self.root_app.debug 20 | self.root_app.mount(path=path, app=app) 21 | 22 | def setup_apps_docs_link(self) -> None: 23 | """他のAppへのリンクがopenapiに表示されるようにセットする""" 24 | for app, path in zip(self.apps, self.app_path_list, strict=True): 25 | app.description = self._make_app_docs_link_html(path) 26 | 27 | def _make_app_docs_link_html(self, current_path: str) -> str: 28 | # openapiの上部に表示する各Appへのリンクを生成する 29 | descriptions = [ 30 | f"{path}/docs" if path != current_path else f"{path}/docs" 31 | for path in self.app_path_list 32 | ] 33 | descriptions.insert(0, "Apps link") 34 | return "
".join(descriptions) 35 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.11 3 | platform = linux 4 | plugins = pydantic.mypy,sqlalchemy.ext.mypy.plugin 5 | 6 | # Import discovery 7 | namespace_packages=True 8 | ignore_missing_imports = True 9 | # follow_imports=error 10 | # follow_imports_for_stubs=True 11 | no_site_packages = True 12 | no_silence_site_packages=True 13 | 14 | # Disallow dynamic typing 15 | # disallow_any_unimported=True 16 | # disallow_any_expr=True 17 | # disallow_any_decorated=True 18 | # disallow_any_explicit=True 19 | disallow_any_generics=True 20 | # disallow_subclassing_any=True 21 | 22 | # Untyped definitions and calls 23 | disallow_untyped_calls = True 24 | disallow_untyped_defs = True 25 | disallow_incomplete_defs = True 26 | check_untyped_defs = True 27 | # disallow_untyped_decorators = True 28 | 29 | # None and Optional handling 30 | no_implicit_optional = True 31 | 32 | # Configuring warnings 33 | warn_redundant_casts = True 34 | warn_unused_ignores = True 35 | # warn_return_any = True 36 | warn_unreachable = True 37 | 38 | # Miscellaneous strictness flags 39 | strict_equality = True 40 | 41 | # Configuring error messages 42 | show_error_context = True 43 | show_column_numbers = True 44 | show_error_codes = True 45 | # pretty = True 46 | 47 | # Miscellaneous 48 | warn_unused_configs = True 49 | 50 | [pydantic-mypy] 51 | init_forbid_extra = True 52 | init_typed = True 53 | warn_required_dynamic_aliases = True 54 | warn_untyped_fields = True 55 | -------------------------------------------------------------------------------- /.aws/ecs-task-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "fastapi-sample-backend", 3 | "networkMode": "awsvpc", 4 | "executionRoleArn": "arn:aws:iam::589800662540:role/fastapi-sample-backend-task-execution-role", 5 | "taskRoleArn": "arn:aws:iam::589800662540:role/fastapi-sample-backend-task-role", 6 | "containerDefinitions": [ 7 | { 8 | "name": "fastapi-sample-backend", 9 | "image": "589800662540.dkr.ecr.us-west-1.amazonaws.com/fastapi-sample-backend:latest", 10 | "linuxParameters": { 11 | "initProcessEnabled": true 12 | }, 13 | "logConfiguration": { 14 | "logDriver": "awslogs", 15 | "options": { 16 | "awslogs-group": "/aws/ecs/fastapi-sample-backend", 17 | "awslogs-region": "us-west-1", 18 | "awslogs-stream-prefix": "container-stdout" 19 | } 20 | }, 21 | "command": [ 22 | "uvicorn", 23 | "main:app", 24 | "--host", 25 | "0.0.0.0", 26 | "--port", 27 | "80" 28 | ], 29 | "portMappings": [ 30 | { 31 | "hostPort": 80, 32 | "protocol": "tcp", 33 | "containerPort": 80 34 | } 35 | ] 36 | } 37 | ], 38 | "requiresCompatibilities": [ 39 | "FARGATE" 40 | ], 41 | "cpu": "256", 42 | "memory": "512" 43 | } 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # コンテナ内に入る 2 | .PHONY: docker-run 3 | docker-run: 4 | @docker compose run --rm web bash 5 | 6 | # Docker内でファイルを作成・更新した場合に、ローカルで権限の問題になる場合の対応(ubuntuの場合) 7 | .PHONY: chown 8 | chown: 9 | @sudo chown -hR ${USER}:${USER} . 10 | 11 | # フロントエンド用のAPIClientを生成 12 | .PHONY: openapi-generator 13 | openapi-generator: 14 | @docker compose run --rm openapi-generator 15 | 16 | # 全てのファイルに対してpre-commitを実行 17 | .PHONY: pre-commit-all 18 | pre-commit-all: 19 | @pre-commit run --all-files 20 | 21 | # pre-commitで指定されているパッケージのバージョンを更新 22 | .PHONY: pre-commit-update 23 | pre-commit-update: 24 | @pre-commit autoupdate 25 | 26 | # pytestでテストを実行 27 | .PHONY: test 28 | test: 29 | @docker compose run --rm web bash -c "pytest tests/ --durations=5 -v" 30 | 31 | # マイグレーションファイルを作成 32 | # m: マイグレーションファイルの名前 33 | .PHONY: makemigrations 34 | makemigrations: 35 | @docker compose run --rm web bash -c "alembic revision --autogenerate -m ${m}" 36 | 37 | # DBのマイグレーション 38 | .PHONY: migrate 39 | migrate: 40 | @docker compose run --rm web bash -c "alembic upgrade heads" 41 | 42 | # Seedデータの投入 43 | .PHONY: seeder 44 | seeder: 45 | @docker compose run --rm web bash -c "python seeder/run.py import_seed" 46 | 47 | # 全てのテーブルの削除 48 | .PHONY: drop-all-tables 49 | drop-all-tables: 50 | @docker compose run --rm web bash -c "python seeder/run.py drop_all_tables" 51 | 52 | # dbの初期化 53 | .PHONY: init-db 54 | init-db: 55 | @make drop-all-tables 56 | @make migrate 57 | @make seeder 58 | -------------------------------------------------------------------------------- /.vscode/database-client-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://release-1256208672.cos.ap-guangzhou.myqcloud.com/dbclient/schema.json", 3 | "database": { 4 | "1651840710641": { 5 | "key": "1651840710641", 6 | "host": "localhost", 7 | "port": "3307", 8 | "user": "docker", 9 | "authType": "default", 10 | "password": "docker", 11 | "encoding": "utf8", 12 | "useSSL": false, 13 | "useClearText": false, 14 | "usingSSH": false, 15 | "showHidden": true, 16 | "dbType": "MySQL", 17 | "encrypt": true, 18 | "hideSystemSchema": true, 19 | "readonly": false, 20 | "savePassword": "Forever", 21 | "sort": 10, 22 | "isCluster": false, 23 | "srv": false, 24 | "esAuth": "none", 25 | "global": false, 26 | "ssh": { 27 | "host": "", 28 | "privateKeyPath": "", 29 | "port": 22, 30 | "username": "root", 31 | "type": "password", 32 | "connectTimeout": 10000, 33 | "algorithms": { 34 | "cipher": [] 35 | } 36 | }, 37 | "connectionKey": "database", 38 | "name": "fastapi-db-local", 39 | "tooltip": "Host: localhost, Port: 3307" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend_sample/src/lib/hooks/api/todos.ts: -------------------------------------------------------------------------------- 1 | import type { UseGetResult, UsePostResult } from "."; 2 | import { useGet, usePost } from "."; 3 | import { ErrorResponse } from "types/api"; 4 | import internal from "stream"; 5 | import { 6 | TodoCreate, 7 | TodoResponse, 8 | TodosPagedResponse, 9 | TodoUpdate, 10 | } from "api_clients"; 11 | import { TodosApiClient } from "api_clients/client"; 12 | 13 | export const useGetTodoById = (id?: string): UseGetResult => 14 | useGet([`/todos`, id], async () => 15 | id ? TodosApiClient.getTodoById(id) : Promise.reject(), 16 | ); 17 | 18 | type GetJobsProps = { 19 | q?: string; 20 | page?: number; 21 | perPage?: number; 22 | }; 23 | 24 | export const useGetTodos = ( 25 | props: GetJobsProps, 26 | ): UseGetResult => { 27 | const { q, page, perPage } = props; 28 | return useGet([`/todos`, q, page, perPage], async () => 29 | TodosApiClient.getTodos(q, page, perPage), 30 | ); 31 | }; 32 | 33 | export const useCreateTodo = (): UsePostResult< 34 | TodoResponse, 35 | ErrorResponse, 36 | TodoCreate 37 | > => 38 | usePost([`/todos`], async (request: TodoCreate) => 39 | TodosApiClient.createTodo(request), 40 | ); 41 | 42 | export const useUpdateTodo = ( 43 | id?: string, 44 | ): UsePostResult => { 45 | return usePost([`/todos`, id], async (request: TodoUpdate) => 46 | id ? TodosApiClient.updateTodo(id, request) : Promise.reject(), 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | aiomysql==0.1.1 11 | alembic==1.11.1 12 | annotated-types==0.5.0 13 | anyio==3.7.0 14 | bcrypt==4.0.1 15 | certifi==2023.5.7 16 | click==8.1.3 17 | dnspython==2.3.0 18 | ecdsa==0.18.0 19 | email-validator==2.0.0.post2 20 | fastapi==0.100.0 21 | fastapi-debug-toolbar==0.5.0 22 | fire==0.5.0 23 | h11==0.14.0 24 | httpcore==0.17.2 25 | httpx==0.24.1 26 | humps==0.2.2 27 | idna==3.4 28 | iniconfig==2.0.0 29 | jinja2==3.1.2 30 | mako==1.2.4 31 | mangum==0.17.0 32 | markupsafe==2.1.2 33 | mirakuru==2.5.1 34 | mysql-connector-python==8.0.33 35 | mysqlclient==2.1.1 36 | packaging==23.1 37 | passlib==1.7.4 38 | pluggy==1.0.0 39 | port-for==0.6.3 40 | protobuf==3.20.3 41 | psutil==5.9.5 42 | pyasn1==0.5.0 43 | pydantic==2.0.2 44 | pydantic-core==2.1.2 45 | pydantic-extra-types==2.0.0 46 | pydantic-settings==2.0.0 47 | pyinstrument==4.5.1 48 | pymysql==1.0.3 49 | pytest==7.3.1 50 | pytest-asyncio==0.21.0 51 | pytest-mysql==2.4.2 52 | python-dotenv==1.0.0 53 | python-jose==3.3.0 54 | python-json-logger==2.0.7 55 | python-multipart==0.0.6 56 | pyyaml==6.0 57 | rsa==4.9 58 | sentry-sdk==1.24.0 59 | six==1.16.0 60 | sniffio==1.3.0 61 | sqlalchemy==2.0.15 62 | sqlalchemyseed==2.0.0 63 | sqlparse==0.4.4 64 | starlette==0.27.0 65 | termcolor==2.3.0 66 | toml==0.10.2 67 | typing-extensions==4.6.2 68 | ulid-py==1.1.0 69 | urllib3==1.26.16 70 | uvicorn==0.22.0 71 | -------------------------------------------------------------------------------- /app/core/logger/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | from logging import Logger, getLogger 3 | from logging.config import dictConfig 4 | 5 | import yaml 6 | 7 | 8 | def init_logger(filepath: str) -> None: 9 | with open(filepath) as f: 10 | config = yaml.safe_load(f) 11 | dictConfig(config) 12 | 13 | 14 | def init_gunicorn_uvicorn_logger(filepath: str) -> None: 15 | import logging 16 | 17 | from fastapi.logger import logger as fastapi_logger 18 | 19 | if "gunicorn" in os.environ.get("SERVER_SOFTWARE", ""): 20 | """ 21 | gunicornで起動した場合のみ、loggerを切り替える必要がある 22 | """ 23 | init_logger(filepath) 24 | gunicorn_logger = logging.getLogger("gunicorn") 25 | log_level = gunicorn_logger.level 26 | 27 | root_logger = logging.getLogger() 28 | gunicorn_error_logger = logging.getLogger("gunicorn.error") 29 | uvicorn_access_logger = logging.getLogger("uvicorn.access") 30 | 31 | # Use gunicorn error handlers for root, uvicorn, and fastapi loggers 32 | root_logger.handlers = gunicorn_error_logger.handlers 33 | uvicorn_access_logger.handlers = gunicorn_error_logger.handlers 34 | fastapi_logger.handlers = gunicorn_error_logger.handlers 35 | 36 | # Pass on logging levels for root, uvicorn, and fastapi loggers 37 | root_logger.setLevel(log_level) 38 | uvicorn_access_logger.setLevel(log_level) 39 | fastapi_logger.setLevel(log_level) 40 | 41 | 42 | def get_logger(name: str) -> Logger: 43 | return getLogger(name) 44 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | aiomysql==0.1.1 11 | alembic==1.11.1 12 | annotated-types==0.5.0 13 | anyio==3.7.0 14 | bcrypt==4.0.1 15 | certifi==2023.5.7 16 | click==8.1.3 17 | dnspython==2.3.0 18 | ecdsa==0.18.0 19 | email-validator==2.0.0.post2 20 | fastapi==0.100.0 21 | fastapi-debug-toolbar==0.5.0 22 | fire==0.5.0 23 | h11==0.14.0 24 | httpcore==0.17.2 25 | httpx==0.24.1 26 | humps==0.2.2 27 | idna==3.4 28 | iniconfig==2.0.0 29 | jinja2==3.1.2 30 | mako==1.2.4 31 | mangum==0.17.0 32 | markupsafe==2.1.2 33 | mirakuru==2.5.1 34 | mysql-connector-python==8.0.33 35 | mysqlclient==2.1.1 36 | packaging==23.1 37 | passlib==1.7.4 38 | pluggy==1.0.0 39 | port-for==0.6.3 40 | protobuf==3.20.3 41 | psutil==5.9.5 42 | pyasn1==0.5.0 43 | pydantic==2.0.2 44 | pydantic-core==2.1.2 45 | pydantic-extra-types==2.0.0 46 | pydantic-settings==2.0.0 47 | pyinstrument==4.5.1 48 | pymysql==1.0.3 49 | pytest==7.3.1 50 | pytest-asyncio==0.21.0 51 | pytest-mysql==2.4.2 52 | python-dotenv==1.0.0 53 | python-jose==3.3.0 54 | python-json-logger==2.0.7 55 | python-multipart==0.0.6 56 | pyyaml==6.0 57 | rsa==4.9 58 | ruff==0.0.270 59 | sentry-sdk==1.24.0 60 | six==1.16.0 61 | sniffio==1.3.0 62 | sqlalchemy==2.0.15 63 | sqlalchemyseed==2.0.0 64 | sqlparse==0.4.4 65 | starlette==0.27.0 66 | termcolor==2.3.0 67 | toml==0.10.2 68 | typing-extensions==4.6.2 69 | ulid-py==1.1.0 70 | urllib3==1.26.16 71 | uvicorn==0.22.0 72 | -------------------------------------------------------------------------------- /seeder/run.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import fire 5 | from sqlalchemy import text 6 | from sqlalchemy.orm import Session 7 | from sqlalchemyseed import Seeder, load_entities_from_json 8 | 9 | from app.core import database 10 | from app.core.config import settings 11 | from app.core.logger.logger import init_logger 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | # TODO: できれば、Seederの処理と連動させたい 17 | def truncate_tables(db: Session) -> None: 18 | TABLES = ["users"] 19 | 20 | db.execute(text("SET FOREIGN_KEY_CHECKS = 0")) 21 | 22 | for table in TABLES: 23 | db.execute(text(f"truncate {table}")) 24 | logger.info(f"truncate table={table}") 25 | 26 | db.execute(text("SET FOREIGN_KEY_CHECKS = 1")) 27 | 28 | 29 | def drop_all_tables() -> None: 30 | database.drop_all_tables() 31 | 32 | 33 | def import_seed() -> None: 34 | logger.info("start: import_seed") 35 | seeds_json_files = list(Path(__file__).parent.glob("seeds_json/*.json")) 36 | db: Session = database.session_factory() 37 | try: 38 | truncate_tables(db) 39 | 40 | entities = [] 41 | for file in seeds_json_files: 42 | logger.info(f"load seed file={file!s}") 43 | entities.append(load_entities_from_json(str(file))) 44 | 45 | seeder = Seeder(db) 46 | seeder.seed(entities) 47 | db.commit() 48 | logger.info("end: seeds import completed") 49 | except Exception as e: 50 | db.rollback() 51 | logger.error(f"end: seeds import failed. detail={e}") 52 | 53 | 54 | if __name__ == "__main__": 55 | init_logger(settings.LOGGER_CONFIG_PATH) 56 | fire.Fire() 57 | -------------------------------------------------------------------------------- /frontend_sample/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 20 12 | services: 13 | # テスト用のDBコンテナを起動 14 | db: 15 | image: mysql:8.0 16 | env: 17 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 18 | MYSQL_ROOT_PASSWORD: 19 | MYSQL_DATABASE: docker 20 | MYSQL_USER: docker 21 | MYSQL_PASSWORD: docker 22 | ports: 23 | - 3306:3306 24 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: set up python 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: "3.10.12" 32 | 33 | - name: update path 34 | shell: bash 35 | run: echo "$HOME/.local/bin" >> $GITHUB_PATH 36 | 37 | - name: set up .venv cache 38 | uses: actions/cache@v2 39 | id: cache 40 | with: 41 | path: ~/.cache/pip 42 | key: pip-${{ hashFiles('**/requirements-dev.lock') }} 43 | restore-keys: | 44 | pip- 45 | 46 | - name: install packages 47 | run: | 48 | sed '/-e/d' requirements-dev.lock > requirements.txt 49 | pip install -r requirements.txt 50 | 51 | - name: create .env 52 | shell: bash 53 | run: | 54 | cp ".env.test" ".env" 55 | 56 | - name: test 57 | shell: bash 58 | # serviceコンテナへは127.0.0.1でアクセスする必要があるため環境変数を変更 59 | run: | 60 | DB_HOST=127.0.0.1 pytest tests/ --durations=3 -v 61 | timeout-minutes: 10 62 | -------------------------------------------------------------------------------- /elasticsearch/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | es01: 5 | build: 6 | context: ./elasticsearch 7 | dockerfile: Dockerfile.es 8 | container_name: es01 9 | environment: 10 | - node.name=es01 11 | - discovery.seed_hosts=es02 12 | - cluster.initial_master_nodes=es01,es02 13 | - cluster.name=docker-cluster 14 | - bootstrap.memory_lock=true 15 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 16 | ulimits: 17 | memlock: 18 | soft: -1 19 | hard: -1 20 | volumes: 21 | - esdata01:/usr/share/elasticsearch/data 22 | ports: 23 | - 9200:9200 24 | networks: 25 | - fastapi_network 26 | es02: 27 | build: 28 | context: ./elasticsearch 29 | dockerfile: Dockerfile.es 30 | container_name: es02 31 | environment: 32 | - node.name=es02 33 | - discovery.seed_hosts=es01 34 | - cluster.initial_master_nodes=es01,es02 35 | - cluster.name=docker-cluster 36 | - bootstrap.memory_lock=true 37 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 38 | ulimits: 39 | memlock: 40 | soft: -1 41 | hard: -1 42 | volumes: 43 | - esdata02:/usr/share/elasticsearch/data 44 | networks: 45 | - fastapi_network 46 | 47 | # logstash: 48 | # build: 49 | # context: ./logstash 50 | # dockerfile: Dockerfile 51 | # ports: 52 | # - 5044:5044 53 | # - 9600:9600 54 | # # volumes: 55 | # # - ./logstash:/logstash 56 | # tty: true 57 | # networks: 58 | # - fastapi_network 59 | 60 | volumes: 61 | esdata01: 62 | driver: local 63 | esdata02: 64 | driver: local 65 | 66 | networks: 67 | fastapi_network: 68 | external: true 69 | -------------------------------------------------------------------------------- /docker-compose.es.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | es01: 5 | build: 6 | context: ./elasticsearch/elasticsearch 7 | dockerfile: Dockerfile.es 8 | container_name: es01 9 | environment: 10 | - node.name=es01 11 | - discovery.seed_hosts=es02 12 | - cluster.initial_master_nodes=es01,es02 13 | - cluster.name=docker-cluster 14 | - bootstrap.memory_lock=true 15 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 16 | ulimits: 17 | memlock: 18 | soft: -1 19 | hard: -1 20 | volumes: 21 | - esdata01:/usr/share/elasticsearch/data 22 | ports: 23 | - 9200:9200 24 | networks: 25 | - fastapi_network 26 | es02: 27 | build: 28 | context: ./elasticsearch/elasticsearch 29 | dockerfile: Dockerfile.es 30 | container_name: es02 31 | environment: 32 | - node.name=es02 33 | - discovery.seed_hosts=es01 34 | - cluster.initial_master_nodes=es01,es02 35 | - cluster.name=docker-cluster 36 | - bootstrap.memory_lock=true 37 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 38 | ulimits: 39 | memlock: 40 | soft: -1 41 | hard: -1 42 | volumes: 43 | - esdata02:/usr/share/elasticsearch/data 44 | networks: 45 | - fastapi_network 46 | 47 | # logstash: 48 | # build: 49 | # context: ./logstash 50 | # dockerfile: Dockerfile 51 | # ports: 52 | # - 5044:5044 53 | # - 9600:9600 54 | # # volumes: 55 | # # - ./logstash:/logstash 56 | # tty: true 57 | # networks: 58 | # - fastapi_network 59 | 60 | volumes: 61 | esdata01: 62 | driver: local 63 | esdata02: 64 | driver: local 65 | 66 | networks: 67 | fastapi_network: 68 | external: true 69 | -------------------------------------------------------------------------------- /.github/workflows/push_ecr.yml: -------------------------------------------------------------------------------- 1 | name: Push ECR 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | AWS_REGION: ap-northeast-1 # set this to your preferred AWS region, e.g. us-west-1 10 | ECR_REPOSITORY: fastapi-sample-backend # set this to your Amazon ECR repository name 11 | 12 | jobs: 13 | push_ecr: 14 | name: Push ECR 15 | runs-on: ubuntu-latest 16 | environment: production 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Configure AWS credentials 23 | uses: aws-actions/configure-aws-credentials@v1 24 | with: 25 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 26 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 27 | aws-region: ${{ env.AWS_REGION }} 28 | 29 | - name: Login to Amazon ECR 30 | id: login-ecr 31 | uses: aws-actions/amazon-ecr-login@v1 32 | 33 | - name: Build, tag, and push image to Amazon ECR 34 | id: build-image 35 | env: 36 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 37 | IMAGE_TAG: ${{ github.sha }} 38 | run: | 39 | # Build a docker container and 40 | # push it to ECR so that it can 41 | # be deployed to ECS. 42 | docker build -f Dockerfile.lambda --cache-from=$ECR_REGISTRY/$ECR_REPOSITORY:latest --build-arg BUILDKIT_INLINE_CACHE=1 -t $ECR_REPOSITORY:$IMAGE_TAG . 43 | docker tag $ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest 44 | docker tag $ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 45 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest 46 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 47 | echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" 48 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/base.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * FastAPI Sample 5 | * Apps link
/docs
/admin/docs
/other/docs 6 | * 7 | * The version of the OpenAPI document: 0.0.1 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import type { Configuration } from "./configuration"; 16 | // Some imports not used depending on template conditions 17 | // @ts-ignore 18 | import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios"; 19 | import globalAxios from "axios"; 20 | 21 | export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); 22 | 23 | /** 24 | * 25 | * @export 26 | */ 27 | export const COLLECTION_FORMATS = { 28 | csv: ",", 29 | ssv: " ", 30 | tsv: "\t", 31 | pipes: "|", 32 | }; 33 | 34 | /** 35 | * 36 | * @export 37 | * @interface RequestArgs 38 | */ 39 | export interface RequestArgs { 40 | url: string; 41 | options: AxiosRequestConfig; 42 | } 43 | 44 | /** 45 | * 46 | * @export 47 | * @class BaseAPI 48 | */ 49 | export class BaseAPI { 50 | protected configuration: Configuration | undefined; 51 | 52 | constructor( 53 | configuration?: Configuration, 54 | protected basePath: string = BASE_PATH, 55 | protected axios: AxiosInstance = globalAxios, 56 | ) { 57 | if (configuration) { 58 | this.configuration = configuration; 59 | this.basePath = configuration.basePath || this.basePath; 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * 66 | * @export 67 | * @class RequiredError 68 | * @extends {Error} 69 | */ 70 | export class RequiredError extends Error { 71 | constructor(public field: string, msg?: string) { 72 | super(msg); 73 | this.name = "RequiredError"; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/api/endpoints/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Security 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from app import crud_v2, models, schemas 5 | from app.core.auth import get_current_user 6 | from app.core.database import get_async_db 7 | from app.exceptions.core import APIException 8 | from app.exceptions.error_messages import ErrorMessage 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.get("/me") 14 | async def get_user_me( 15 | current_user: models.User = Depends(get_current_user), 16 | ) -> schemas.UserResponse: 17 | return current_user 18 | 19 | 20 | @router.get( 21 | "/{id}", 22 | dependencies=[Security(get_current_user, scopes=["admin"])], 23 | ) 24 | async def get_user( 25 | id: str, 26 | db: AsyncSession = Depends(get_async_db), 27 | ) -> schemas.UserResponse: 28 | user = await crud_v2.user.get_db_obj_by_id(db, id=id) 29 | if not user: 30 | raise APIException(ErrorMessage.ID_NOT_FOUND) 31 | return user 32 | 33 | 34 | @router.post("") 35 | async def create_user( 36 | data_in: schemas.UserCreate, 37 | db: AsyncSession = Depends(get_async_db), 38 | ) -> schemas.UserResponse: 39 | user = await crud_v2.user.get_by_email(db, email=data_in.email) 40 | if user: 41 | raise APIException(ErrorMessage.ALREADY_REGISTED_EMAIL) 42 | return await crud_v2.user.create(db, obj_in=data_in) 43 | 44 | 45 | @router.put( 46 | "/{id}", 47 | dependencies=[Security(get_current_user, scopes=["admin"])], 48 | ) 49 | async def update_user( 50 | id: str, 51 | data_in: schemas.UserUpdate, 52 | db: AsyncSession = Depends(get_async_db), 53 | ) -> schemas.UserResponse: 54 | user = await crud_v2.user.get_db_obj_by_id(db, id=id) 55 | if not user: 56 | raise APIException(ErrorMessage.ID_NOT_FOUND) 57 | return await crud_v2.user.update(db, db_obj=user, obj_in=data_in) 58 | -------------------------------------------------------------------------------- /app/core/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import lru_cache 3 | from pathlib import Path 4 | 5 | from pydantic_settings import BaseSettings, SettingsConfigDict 6 | 7 | 8 | class Settings(BaseSettings): 9 | # NOTE: .envファイルや環境変数が同名の変数にセットされる 10 | TITLE: str = "FastAPI Sample" 11 | ENV: str = "" 12 | DEBUG: bool = False 13 | VERSION: str = "0.0.1" 14 | CORS_ORIGINS: list[str] = [ 15 | "http://localhost:8000", 16 | "http://127.0.0.1:8000", 17 | "http://localhost:3000", 18 | "http://localhost:3333", 19 | ] 20 | BASE_DIR_PATH: str = str(Path(__file__).parent.parent.absolute()) 21 | ROOT_DIR_PATH: str = str(Path(__file__).parent.parent.parent.absolute()) 22 | DB_HOST: str 23 | DB_PORT: str 24 | DB_NAME: str 25 | DB_USER_NAME: str 26 | DB_PASSWORD: str 27 | API_GATEWAY_STAGE_PATH: str = "" 28 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 29 | SECRET_KEY: str = "secret" 30 | LOGGER_CONFIG_PATH: str = os.path.join(BASE_DIR_PATH, "logger_config.yaml") 31 | SENTRY_SDK_DNS: str = "" 32 | MIGRATIONS_DIR_PATH: str = os.path.join(ROOT_DIR_PATH, "alembic") 33 | 34 | def get_database_url(self, is_async: bool = False) -> str: 35 | if is_async: 36 | return ( 37 | "mysql+aiomysql://" 38 | f"{self.DB_USER_NAME}:{self.DB_PASSWORD}@" 39 | f"{self.DB_HOST}/{self.DB_NAME}?charset=utf8mb4" 40 | ) 41 | else: 42 | return ( 43 | "mysql://" f"{self.DB_USER_NAME}:{self.DB_PASSWORD}@" f"{self.DB_HOST}/{self.DB_NAME}?charset=utf8mb4" 44 | ) 45 | 46 | model_config = SettingsConfigDict(env_file=".env") 47 | 48 | def get_app_title(self, app_name: str) -> str: 49 | return f"[{self.ENV}]{self.TITLE}({app_name=})" 50 | 51 | 52 | @lru_cache 53 | def get_settings() -> Settings: 54 | return Settings() 55 | 56 | 57 | settings = get_settings() 58 | -------------------------------------------------------------------------------- /app/crud/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from app import models, schemas 4 | from app.core.auth import get_password_hash, verify_password 5 | from app.crud.base import CRUDBase 6 | 7 | 8 | class CRUDUser( 9 | CRUDBase[ 10 | models.User, 11 | schemas.UserResponse, 12 | schemas.UserCreate, 13 | schemas.UserUpdate, 14 | schemas.UserResponse, 15 | ], 16 | ): 17 | def get_by_email(self, db: Session, *, email: str) -> models.User | None: 18 | return db.query(models.User).filter(models.User.email == email).first() 19 | 20 | def create(self, db: Session, obj_in: schemas.UserCreate) -> models.User: 21 | db_obj = models.User( 22 | email=obj_in.email, 23 | hashed_password=get_password_hash(obj_in.password), 24 | full_name=obj_in.full_name, 25 | ) 26 | db.add(db_obj) 27 | db.flush() 28 | db.refresh(db_obj) 29 | return db_obj 30 | 31 | def update( # type: ignore[override] 32 | self, 33 | db: Session, 34 | *, 35 | db_obj: models.User, 36 | obj_in: schemas.UserUpdate, 37 | ) -> models.User: 38 | if obj_in.password: 39 | hashed_password = get_password_hash(obj_in.password) 40 | db_obj.hashed_password = hashed_password 41 | return super().update(db, db_obj=db_obj, update_schema=obj_in) 42 | 43 | def authenticate( 44 | self, 45 | db: Session, 46 | *, 47 | email: str, 48 | password: str, 49 | ) -> models.User | None: 50 | user = self.get_by_email(db, email=email) 51 | if not user: 52 | return None 53 | if not verify_password(password, user.hashed_password): 54 | return None 55 | return user 56 | 57 | 58 | user = CRUDUser( 59 | models.User, 60 | response_schema_class=schemas.UserResponse, 61 | list_response_class=schemas.UsersPagedResponse, 62 | ) 63 | -------------------------------------------------------------------------------- /frontend_sample/src/api_clients/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /app/crud/todo.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import or_ 2 | from sqlalchemy.orm import Session, contains_eager 3 | 4 | from app import crud, models, schemas 5 | 6 | from .base import CRUDBase 7 | 8 | 9 | class CRUDTodo( 10 | CRUDBase[ 11 | models.Todo, 12 | schemas.TodoResponse, 13 | schemas.TodoCreate, 14 | schemas.TodoUpdate, 15 | schemas.TodosPagedResponse, 16 | ], 17 | ): 18 | def get_paged_list( # type: ignore[override] 19 | self, 20 | db: Session, 21 | paging_query_in: schemas.PagingQueryIn, 22 | q: str | None = None, 23 | sort_query_in: schemas.SortQueryIn | None = None, 24 | ) -> schemas.TodosPagedResponse: 25 | where_clause = ( 26 | [ 27 | or_( 28 | models.Todo.title.ilike(f"%{q}%"), 29 | models.Todo.description.ilike(f"%{q}%"), 30 | ), 31 | ] 32 | if q 33 | else [] 34 | ) 35 | return super().get_paged_list( 36 | db, 37 | paging_query_in=paging_query_in, 38 | where_clause=where_clause, 39 | sort_query_in=sort_query_in, 40 | ) 41 | 42 | def add_tags_to_todo( 43 | self, 44 | db: Session, 45 | todo: models.Todo, 46 | tags_in: list[schemas.TagCreate], 47 | ) -> models.Todo: 48 | tags = crud.tag.upsert_tags(db, tags_in=tags_in) 49 | for tag in tags: 50 | todo.tags.append(tag) # noqa: PERF402 51 | db.flush() 52 | 53 | # one-to-many joined response one query 54 | todo = ( 55 | db.query(models.Todo) 56 | .outerjoin(models.Todo.tags) 57 | .options(contains_eager(models.Todo.tags)) 58 | .filter(models.Todo.id == todo.id) 59 | .first() 60 | ) 61 | 62 | return todo 63 | 64 | 65 | todo = CRUDTodo( 66 | models.Todo, 67 | response_schema_class=schemas.TodoResponse, 68 | list_response_class=schemas.TodosPagedResponse, 69 | ) 70 | -------------------------------------------------------------------------------- /app/exceptions/error_messages.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from starlette import status 4 | 5 | 6 | class BaseMessage: 7 | """メッセージクラスのベース.""" 8 | 9 | text: str 10 | status_code: int = status.HTTP_400_BAD_REQUEST 11 | 12 | def __init__(self, param: Any | None = None) -> None: 13 | self.param = param 14 | 15 | def __str__(self) -> str: 16 | return self.__class__.__name__ 17 | 18 | 19 | class ErrorMessage: 20 | """エラーメッセージクラス. 21 | 22 | Notes 23 | ----- 24 | BaseMessagを継承することで 25 | Class呼び出し時にClass名がエラーコードになり、.textでエラーメッセージも取得できるため 26 | エラーコードと、メッセージの管理が直感的に行える。 27 | 28 | """ 29 | 30 | # 共通 31 | class INTERNAL_SERVER_ERROR(BaseMessage): 32 | status_code = status.HTTP_500_INTERNAL_SERVER_ERROR 33 | text = "システムエラーが発生しました、管理者に問い合わせてください" 34 | 35 | class FAILURE_LOGIN(BaseMessage): 36 | text = "ログインが失敗しました" 37 | 38 | class NOT_FOUND(BaseMessage): 39 | text = "{}が見つかりません" 40 | 41 | class ID_NOT_FOUND(BaseMessage): 42 | status_code = status.HTTP_404_NOT_FOUND 43 | text = "このidは見つかりません" 44 | 45 | class PARAM_IS_NOT_SET(BaseMessage): 46 | text = "{}がセットされていません" 47 | 48 | class ALREADY_DELETED(BaseMessage): 49 | text = "既に削除済です" 50 | 51 | class SOFT_DELETE_NOT_SUPPORTED(BaseMessage): 52 | text = "論理削除には未対応です" 53 | 54 | class COLUMN_NOT_ALLOWED(BaseMessage): 55 | text = "このカラムは指定できません" 56 | 57 | # ユーザー 58 | class ALREADY_REGISTED_EMAIL(BaseMessage): 59 | text = "登録済のメールアドレスです" 60 | 61 | class INCORRECT_CURRENT_PASSWORD(BaseMessage): 62 | text = "現在のパスワードが間違っています" 63 | 64 | class INCORRECT_EMAIL_OR_PASSWORD(BaseMessage): 65 | status_code = status.HTTP_403_FORBIDDEN 66 | text = "メールアドレスまたはパスワードが正しくありません" 67 | 68 | class PERMISSION_ERROR(BaseMessage): 69 | text = "実行権限がありません" 70 | 71 | class CouldNotValidateCredentials(BaseMessage): 72 | status_code = status.HTTP_403_FORBIDDEN 73 | text = "認証エラー" 74 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from httpx import AsyncClient 4 | from starlette import status 5 | 6 | from tests.testing_utils import assert_dict_part 7 | 8 | 9 | async def assert_create( 10 | uri: str, 11 | client: AsyncClient, 12 | data_in: dict[str, Any], 13 | expected_status: int, 14 | expected_data: dict[str, Any] | None, 15 | ) -> None: 16 | res = await client.post(uri, json=data_in) 17 | assert res.status_code == expected_status 18 | if expected_status == status.HTTP_200_OK: 19 | res_data = res.json() 20 | assert_dict_part(res_data, expected_data) 21 | 22 | 23 | async def assert_update( 24 | uri: str, 25 | client: AsyncClient, 26 | id: str, 27 | data_in: dict[str, Any], 28 | expected_status: int, 29 | expected_data: dict[str, Any] | None, 30 | ) -> None: 31 | res = await client.patch(f"{uri}/{id}", json=data_in) 32 | assert res.status_code == expected_status 33 | if expected_status == status.HTTP_200_OK: 34 | res_data = res.json() 35 | assert_dict_part(res_data, expected_data) 36 | 37 | 38 | async def assert_get_by_id( 39 | uri: str, 40 | client: AsyncClient, 41 | id: str, 42 | expected_status: int, 43 | expected_data: dict[str, Any] | None, 44 | ) -> None: 45 | res = await client.get(f"{uri}/{id}") 46 | assert res.status_code == expected_status 47 | if expected_status == status.HTTP_200_OK: 48 | res_data = res.json() 49 | assert_dict_part(res_data, expected_data) 50 | 51 | 52 | async def assert_get_paged_list( 53 | uri: str, 54 | client: AsyncClient, 55 | params: dict[str, Any], 56 | expected_status: int, 57 | expected_first_data: dict[str, Any], 58 | expected_paging_meta: dict[str, Any], 59 | ) -> None: 60 | res = await client.get(uri, params=params) 61 | assert res.status_code == expected_status 62 | if expected_status == status.HTTP_200_OK: 63 | res_data = res.json() 64 | assert res_data["data"] 65 | assert_dict_part(res_data["data"][0], expected_first_data) 66 | assert_dict_part(res_data["meta"], expected_paging_meta) 67 | -------------------------------------------------------------------------------- /app/crud_v2/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import AsyncSession 2 | from sqlalchemy.sql import select 3 | 4 | from app import models, schemas 5 | from app.core.auth import get_password_hash, verify_password 6 | from app.crud_v2.base import CRUDV2Base 7 | 8 | 9 | class CRUDUser( 10 | CRUDV2Base[ 11 | models.User, 12 | schemas.UserResponse, 13 | schemas.UserCreate, 14 | schemas.UserUpdate, 15 | schemas.UserResponse, 16 | ] 17 | ): 18 | async def get_by_email(self, db: AsyncSession, *, email: str) -> models.User | None: 19 | stmt = select(models.User).where(models.User.email == email) 20 | return (await db.execute(stmt)).scalars().first() 21 | # return db.query(models.User).filter(models.User.email == email).first() 22 | 23 | async def create(self, db: AsyncSession, obj_in: schemas.UserCreate) -> models.User: 24 | db_obj = models.User( 25 | email=obj_in.email, 26 | hashed_password=get_password_hash(obj_in.password), 27 | full_name=obj_in.full_name, 28 | ) 29 | db.add(db_obj) 30 | await db.flush() 31 | await db.refresh(db_obj) 32 | return db_obj 33 | 34 | async def update( # type: ignore[override] 35 | self, db: AsyncSession, *, db_obj: models.User, obj_in: schemas.UserUpdate 36 | ) -> models.User: 37 | if obj_in.password: 38 | hashed_password = get_password_hash(obj_in.password) 39 | db_obj.hashed_password = hashed_password 40 | return await super().update(db, db_obj=db_obj, update_schema=obj_in) 41 | 42 | async def authenticate(self, db: AsyncSession, *, email: str, password: str) -> models.User | None: 43 | user = await self.get_by_email(db, email=email) 44 | if not user: 45 | return None 46 | if not verify_password(password, user.hashed_password): 47 | return None 48 | return user 49 | 50 | 51 | user = CRUDUser( 52 | models.User, 53 | response_schema_class=schemas.UserResponse, 54 | list_response_class=schemas.UsersPagedResponse, 55 | ) 56 | -------------------------------------------------------------------------------- /frontend_sample/src/components/templates/todos/TodoCreateTemplate/TodoCreateTemplate.tsx: -------------------------------------------------------------------------------- 1 | import { TodoCreate, TodoUpdate } from "api_clients"; 2 | import { 3 | useCreateTodo, 4 | useGetTodoById, 5 | useUpdateTodo, 6 | } from "lib/hooks/api/todos"; 7 | import { useRouter } from "next/router"; 8 | import { FC, useCallback, useEffect, useState } from "react"; 9 | 10 | const TodoCreateTemplate: FC = () => { 11 | const [requestData, setRequestData] = useState({ 12 | title: "", 13 | description: "", 14 | }); 15 | const [isSuccess, setIsSuccess] = useState(false); 16 | const router = useRouter(); 17 | const { mutateAsync: createMutateAsync, isLoading: isLoading } = 18 | useCreateTodo(); 19 | 20 | const handleChangeValue = useCallback( 21 | (e: React.ChangeEvent): void => { 22 | const { name, value } = e.target; 23 | setRequestData({ 24 | ...requestData, 25 | [name]: value, 26 | }); 27 | }, 28 | [setRequestData, requestData], 29 | ); 30 | 31 | const handleClickUpdateButton = async (): Promise => { 32 | if (!requestData) return; 33 | await createMutateAsync( 34 | requestData, 35 | (responseData) => { 36 | setIsSuccess(true); 37 | console.log("success.", responseData); 38 | }, 39 | (error) => { 40 | setIsSuccess(false); 41 | console.log("error.", error); 42 | }, 43 | ); 44 | }; 45 | return ( 46 |
47 |

Todo Create

48 |
49 | タイトル: 50 | 55 |
56 |
57 | 説明: 58 |