├── .gitignore ├── AsyncDB_Handling ├── final │ ├── .env │ ├── async_cursor.py │ ├── async_db.py │ ├── async_pool.py │ ├── db │ │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ │ └── blog.py │ ├── schemas │ │ └── blog_schema.py │ ├── services │ │ └── blog_svc.py │ ├── static │ │ ├── default │ │ │ └── blog_default.png │ │ ├── sample_images │ │ │ ├── blog_default.png │ │ │ ├── irises.png │ │ │ ├── party.png │ │ │ └── starry_night.png │ │ └── uploads │ │ │ └── _blank_.txt │ ├── templates │ │ ├── index.html │ │ ├── layout │ │ │ ├── footer.html │ │ │ ├── main_layout.html │ │ │ └── navbar.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ └── show_blog.html │ └── utils │ │ └── util.py └── start │ ├── .env │ ├── async_cursor.py │ ├── async_db.py │ ├── async_pool.py │ ├── db │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ └── blog.py │ ├── schemas │ └── blog_schema.py │ ├── services │ └── blog_svc.py │ ├── static │ ├── default │ │ └── blog_default.png │ ├── sample_images │ │ ├── blog_default.png │ │ ├── irises.png │ │ ├── party.png │ │ └── starry_night.png │ └── uploads │ │ └── _blank_.txt │ ├── templates │ ├── index.html │ ├── layout │ │ ├── footer.html │ │ ├── main_layout.html │ │ └── navbar.html │ ├── modify_blog.html │ ├── new_blog.html │ └── show_blog.html │ └── utils │ └── util.py ├── Authentication ├── final │ ├── .env │ ├── db │ │ └── database.py │ ├── initial_blog_user.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ │ ├── auth.py │ │ └── blog.py │ ├── schemas │ │ ├── auth_schema.py │ │ └── blog_schema.py │ ├── services │ │ ├── auth_svc.py │ │ └── blog_svc.py │ ├── static │ │ ├── default │ │ │ └── blog_default.png │ │ ├── sample_images │ │ │ ├── blog_default.png │ │ │ ├── irises.png │ │ │ ├── party.png │ │ │ └── starry_night.png │ │ └── uploads │ │ │ └── 권철민 │ │ │ ├── irises_1724384870.png │ │ │ ├── party_1724306758.png │ │ │ └── party_1724384805.png │ ├── templates │ │ ├── http_error.html │ │ ├── index.html │ │ ├── layout │ │ │ ├── footer.html │ │ │ ├── main_layout.html │ │ │ └── navbar.html │ │ ├── login.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ ├── register_user.html │ │ ├── show_blog.html │ │ └── validation_error.html │ └── utils │ │ ├── common.py │ │ ├── exc_handler.py │ │ ├── middleware.py │ │ └── util.py └── start │ ├── .env │ ├── db │ ├── __pycache__ │ │ └── database.cpython-310.pyc │ └── database.py │ ├── initial_blog_user.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ └── blog.py │ ├── schemas │ └── blog_schema.py │ ├── services │ └── blog_svc.py │ ├── static │ ├── default │ │ └── blog_default.png │ ├── sample_images │ │ ├── blog_default.png │ │ ├── irises.png │ │ ├── party.png │ │ └── starry_night.png │ └── uploads │ │ └── 권철민 │ │ ├── irises_1724384870.png │ │ ├── party_1724306758.png │ │ └── party_1724384805.png │ ├── templates │ ├── http_error.html │ ├── index.html │ ├── layout │ │ ├── footer.html │ │ ├── main_layout.html │ │ └── navbar.html │ ├── modify_blog.html │ ├── new_blog.html │ ├── show_blog.html │ └── validation_error.html │ └── utils │ ├── common.py │ ├── exc_handler.py │ ├── middleware.py │ └── util.py ├── Blog_Bootstrap ├── final │ ├── .env │ ├── db │ │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ │ └── blog.py │ ├── schemas │ │ └── blog_schema.py │ ├── services │ │ └── blog_svc.py │ ├── static │ │ ├── default │ │ │ └── blog_default.png │ │ ├── sample_images │ │ │ ├── blog_default.png │ │ │ ├── irises.png │ │ │ ├── party.png │ │ │ └── starry_night.png │ │ └── uploads │ │ │ └── _blank_.txt │ ├── templates │ │ ├── index.html │ │ ├── layout │ │ │ ├── footer.html │ │ │ ├── main_layout.html │ │ │ └── navbar.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ └── show_blog.html │ └── utils │ │ └── util.py └── start │ ├── .env │ ├── db │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ └── blog.py │ ├── schemas │ └── blog_schema.py │ ├── services │ └── blog_svc.py │ ├── templates │ ├── index.html │ ├── modify_blog.html │ ├── new_blog.html │ └── show_blog.html │ └── utils │ └── util.py ├── Blog_DB_Handling ├── final │ ├── .env │ ├── db │ │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── routes │ │ └── blog.py │ ├── schemas │ │ └── blog_schema.py │ ├── templates │ │ ├── index.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ └── show_blog.html │ └── utils │ │ ├── __pycache__ │ │ └── util.cpython-310.pyc │ │ └── util.py └── start │ └── initial_data.sql ├── Blog_MVC ├── final │ ├── .env │ ├── db │ │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── routes │ │ └── blog.py │ ├── schemas │ │ └── blog_schema.py │ ├── services │ │ └── blog_svc.py │ ├── templates │ │ ├── index.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ └── show_blog.html │ └── utils │ │ └── util.py └── start │ ├── .env │ ├── db │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── routes │ └── blog.py │ ├── schemas │ └── blog_schema.py │ ├── templates │ ├── index.html │ ├── modify_blog.html │ ├── new_blog.html │ └── show_blog.html │ └── utils │ └── util.py ├── Bootstrap_Template ├── final │ ├── main.py │ └── templates │ │ ├── include │ │ ├── footer.html │ │ └── navbar.html │ │ ├── index.html │ │ ├── index_include.html │ │ ├── index_no_include.html │ │ └── layout │ │ ├── footer.html │ │ ├── main_layout.html │ │ └── navbar.html └── start │ ├── main.py │ └── templates │ └── index_no_include.html ├── DB_Fundamentals ├── bind_variable.py ├── context_practice.py ├── cursor_fetch.py ├── database.py ├── db_basic.py ├── db_config_data.sql ├── module_context.py ├── module_direct.py ├── pool_practice.py └── requirements.txt ├── Exception_Handler ├── final │ ├── .env │ ├── db │ │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ │ └── blog.py │ ├── schemas │ │ └── blog_schema.py │ ├── services │ │ └── blog_svc.py │ ├── static │ │ ├── default │ │ │ └── blog_default.png │ │ ├── sample_images │ │ │ ├── blog_default.png │ │ │ ├── irises.png │ │ │ ├── party.png │ │ │ └── starry_night.png │ │ └── uploads │ │ │ └── _blank_.txt │ ├── templates │ │ ├── http_error.html │ │ ├── index.html │ │ ├── layout │ │ │ ├── footer.html │ │ │ ├── main_layout.html │ │ │ └── navbar.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ ├── show_blog.html │ │ └── validation_error.html │ └── utils │ │ ├── common.py │ │ ├── exc_handler.py │ │ └── util.py └── start │ ├── .env │ ├── async_cursor.py │ ├── async_db.py │ ├── async_pool.py │ ├── db │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ └── blog.py │ ├── schemas │ └── blog_schema.py │ ├── services │ └── blog_svc.py │ ├── static │ ├── default │ │ └── blog_default.png │ ├── sample_images │ │ ├── blog_default.png │ │ ├── irises.png │ │ ├── party.png │ │ └── starry_night.png │ └── uploads │ │ └── _blank_.txt │ ├── templates │ ├── index.html │ ├── layout │ │ ├── footer.html │ │ ├── main_layout.html │ │ └── navbar.html │ ├── modify_blog.html │ ├── new_blog.html │ └── show_blog.html │ └── utils │ └── util.py ├── FastAPI_Async_Thread └── main.py ├── Middleware ├── final │ ├── .env │ ├── cors_request.html │ ├── db │ │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ │ └── blog.py │ ├── schemas │ │ └── blog_schema.py │ ├── services │ │ └── blog_svc.py │ ├── static │ │ ├── default │ │ │ └── blog_default.png │ │ ├── sample_images │ │ │ ├── blog_default.png │ │ │ ├── irises.png │ │ │ ├── party.png │ │ │ └── starry_night.png │ │ └── uploads │ │ │ └── 권철민 │ │ │ ├── irises_1724384870.png │ │ │ ├── party_1724306758.png │ │ │ └── party_1724384805.png │ ├── templates │ │ ├── http_error.html │ │ ├── index.html │ │ ├── layout │ │ │ ├── footer.html │ │ │ ├── main_layout.html │ │ │ └── navbar.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ ├── show_blog.html │ │ └── validation_error.html │ └── utils │ │ ├── common.py │ │ ├── exc_handler.py │ │ ├── middleware.py │ │ ├── middleware.py.bak │ │ └── util.py └── start │ ├── .env │ ├── db │ └── database.py │ ├── initial_data.sql │ ├── main.py │ ├── requirements.txt │ ├── routes │ └── blog.py │ ├── schemas │ └── blog_schema.py │ ├── services │ └── blog_svc.py │ ├── static │ ├── default │ │ └── blog_default.png │ ├── sample_images │ │ ├── blog_default.png │ │ ├── irises.png │ │ ├── party.png │ │ └── starry_night.png │ └── uploads │ │ └── 권철민 │ │ ├── party_1724306758.png │ │ └── party_1724311063.png │ ├── templates │ ├── http_error.html │ ├── index.html │ ├── layout │ │ ├── footer.html │ │ ├── main_layout.html │ │ └── navbar.html │ ├── modify_blog.html │ ├── new_blog.html │ ├── show_blog.html │ └── validation_error.html │ └── utils │ ├── common.py │ ├── exc_handler.py │ └── util.py ├── Pydantic ├── dataclass.py ├── main.py ├── pydantic_01.py ├── pydantic_02.py ├── pydantic_03.py ├── pydantic_04.py └── pydantic_05.py ├── Requests ├── main_form.py ├── main_opt_type.py ├── main_path.py ├── main_query.py ├── main_rbody.py ├── main_rbody_js.py ├── main_request.py ├── main_test.py ├── rbody_01.json ├── rbody_02.json ├── rbody_03.json └── static │ └── rbody.html ├── Responses ├── main_response.py └── rbody_01.json ├── Router ├── final │ ├── main.py │ ├── main_org.py │ └── routes │ │ ├── item.py │ │ └── user.py └── start │ └── main_org.py ├── Session_Redis ├── final │ ├── .env │ ├── db │ │ └── database.py │ ├── initial_blog_user.sql │ ├── main.py │ ├── main_cookie.py │ ├── main_sessionredis.py │ ├── main_sessmiddle.py │ ├── redis_test.py │ ├── requirements.txt │ ├── routes │ │ ├── auth.py │ │ └── blog.py │ ├── schemas │ │ ├── auth_schema.py │ │ └── blog_schema.py │ ├── services │ │ ├── auth_svc.py │ │ └── blog_svc.py │ ├── static │ │ ├── default │ │ │ └── blog_default.png │ │ └── sample_images │ │ │ ├── blog_default.png │ │ │ ├── irises.png │ │ │ ├── party.png │ │ │ └── starry_night.png │ ├── templates │ │ ├── http_error.html │ │ ├── index.html │ │ ├── layout │ │ │ ├── footer.html │ │ │ ├── main_layout.html │ │ │ └── navbar.html │ │ ├── login.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ ├── register_user.html │ │ ├── show_blog.html │ │ └── validation_error.html │ └── utils │ │ ├── common.py │ │ ├── exc_handler.py │ │ ├── middleware.py │ │ └── util.py └── start │ ├── .env │ ├── db │ └── database.py │ ├── initial_blog_user.sql │ ├── main.py │ ├── main_cookie.py │ ├── main_sessionredis.py │ ├── main_sessmiddle.py │ ├── redis_test.py │ ├── requirements.txt │ ├── routes │ ├── auth.py │ └── blog.py │ ├── schemas │ ├── auth_schema.py │ └── blog_schema.py │ ├── services │ ├── auth_svc.py │ └── blog_svc.py │ ├── static │ ├── default │ │ └── blog_default.png │ └── sample_images │ │ ├── blog_default.png │ │ ├── irises.png │ │ ├── party.png │ │ └── starry_night.png │ ├── templates │ ├── http_error.html │ ├── index.html │ ├── layout │ │ ├── footer.html │ │ ├── main_layout.html │ │ └── navbar.html │ ├── login.html │ ├── modify_blog.html │ ├── new_blog.html │ ├── register_user.html │ ├── show_blog.html │ └── validation_error.html │ └── utils │ ├── common.py │ ├── exc_handler.py │ ├── middleware.py │ └── util.py ├── Signed_Cookie ├── final │ ├── .env │ ├── create_key.py │ ├── db │ │ └── database.py │ ├── initial_blog_user.sql │ ├── main.py │ ├── main_cookie.py │ ├── main_sessmiddle.py │ ├── requirements.txt │ ├── routes │ │ ├── auth.py │ │ └── blog.py │ ├── schemas │ │ ├── auth_schema.py │ │ └── blog_schema.py │ ├── services │ │ ├── auth_svc.py │ │ └── blog_svc.py │ ├── static │ │ ├── default │ │ │ └── blog_default.png │ │ └── sample_images │ │ │ ├── blog_default.png │ │ │ ├── irises.png │ │ │ ├── party.png │ │ │ └── starry_night.png │ ├── templates │ │ ├── http_error.html │ │ ├── index.html │ │ ├── layout │ │ │ ├── footer.html │ │ │ ├── main_layout.html │ │ │ └── navbar.html │ │ ├── login.html │ │ ├── modify_blog.html │ │ ├── new_blog.html │ │ ├── register_user.html │ │ ├── show_blog.html │ │ └── validation_error.html │ └── utils │ │ ├── common.py │ │ ├── exc_handler.py │ │ ├── middleware.py │ │ └── util.py └── start │ ├── .env │ ├── db │ └── database.py │ ├── initial_blog_user.sql │ ├── main.py │ ├── main_cookie.py │ ├── main_sessmiddle.py │ ├── requirements.txt │ ├── routes │ ├── auth.py │ └── blog.py │ ├── schemas │ ├── auth_schema.py │ └── blog_schema.py │ ├── services │ ├── auth_svc.py │ └── blog_svc.py │ ├── static │ ├── default │ │ └── blog_default.png │ ├── sample_images │ │ ├── blog_default.png │ │ ├── irises.png │ │ ├── party.png │ │ └── starry_night.png │ └── uploads │ │ └── 권철민 │ │ ├── irises_1724384870.png │ │ ├── party_1724306758.png │ │ └── party_1724384805.png │ ├── templates │ ├── http_error.html │ ├── index.html │ ├── layout │ │ ├── footer.html │ │ ├── main_layout.html │ │ └── navbar.html │ ├── login.html │ ├── modify_blog.html │ ├── new_blog.html │ ├── register_user.html │ ├── show_blog.html │ └── validation_error.html │ └── utils │ ├── common.py │ ├── exc_handler.py │ ├── middleware.py │ └── util.py ├── Templates ├── final │ ├── main.py │ ├── main_static.py │ ├── static │ │ ├── css │ │ │ └── styles.css │ │ └── link_tp.html │ └── templates │ │ ├── item.html │ │ ├── item_all.html │ │ ├── item_gubun.html │ │ ├── item_static.html │ │ ├── item_urlfor.html │ │ └── read_safe.html └── start │ ├── main.py │ └── main_static.py └── Welcome └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/async_cursor.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | 6 | async def execute_query(): 7 | conn = await direct_get_conn() 8 | query = "select * from blog" 9 | stmt = text(query) 10 | # SQL 호출하여 CursorResult 반환. 11 | result = await conn.execute(stmt) 12 | # 아래는 오류를 발생. 13 | rows = await result.fetchone() 14 | print(rows) 15 | result.close() 16 | await conn.rollback() 17 | await conn.close() 18 | await engine.dispose() 19 | 20 | async def stream_query(): 21 | conn = await direct_get_conn() 22 | query = "select * from blog" 23 | stmt = text(query) 24 | # connection의 execute()가 아닌 stream()을 호출해야 함. 25 | async_result = await conn.stream(stmt) 26 | # async로 iteration 수행. 27 | async for row in async_result: 28 | print(row) 29 | 30 | await async_result.close() 31 | await conn.rollback() 32 | await conn.close() 33 | await engine.dispose() 34 | 35 | async def main(): 36 | await stream_query() 37 | #await execute_query() 38 | 39 | if __name__ == "__main__": 40 | asyncio.run(main()) -------------------------------------------------------------------------------- /AsyncDB_Handling/final/async_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | 6 | async def execute_query(): 7 | conn = await direct_get_conn() 8 | print("conn type:", type(conn)) 9 | query = "select * from blog" 10 | stmt = text(query) 11 | # SQL 호출하여 CursorResult 반환. 12 | result = await conn.execute(stmt) 13 | 14 | rows = result.fetchall() 15 | print(rows) 16 | result.close() 17 | await conn.rollback() 18 | await conn.close() 19 | await engine.dispose() 20 | 21 | async def main(): 22 | await execute_query() 23 | 24 | if __name__ == "__main__": 25 | asyncio.run(main()) 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/async_pool.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | async def execute_sleep_query(): 6 | for ind in range(10): 7 | print("loop index:", ind) 8 | conn = await direct_get_conn() 9 | query = "select sleep(5)" 10 | stmt = text(query) 11 | result = await conn.execute(stmt) 12 | await conn.close() 13 | await engine.dispose() 14 | 15 | async def main(): 16 | await execute_sleep_query() 17 | 18 | if __name__ == "__main__": 19 | asyncio.run(main()) 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.staticfiles import StaticFiles 3 | from routes import blog 4 | from contextlib import asynccontextmanager 5 | from db.database import engine 6 | 7 | @asynccontextmanager 8 | async def lifespan(app: FastAPI): 9 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 10 | print("Starting up...") 11 | yield 12 | 13 | #FastAPI 인스턴스 종료시 필요한 작업 수행 14 | print("Shutting down...") 15 | await engine.dispose() 16 | 17 | app = FastAPI(lifespan=lifespan) 18 | 19 | app.mount("/static", StaticFiles(directory="static"), name="static") 20 | app.include_router(blog.router) 21 | 22 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql==1.1.1 2 | aiomysql==0.2.0 3 | aiofiles==24.1.0 -------------------------------------------------------------------------------- /AsyncDB_Handling/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /AsyncDB_Handling/final/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/final/static/default/blog_default.png -------------------------------------------------------------------------------- /AsyncDB_Handling/final/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/final/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /AsyncDB_Handling/final/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/final/static/sample_images/irises.png -------------------------------------------------------------------------------- /AsyncDB_Handling/final/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/final/static/sample_images/party.png -------------------------------------------------------------------------------- /AsyncDB_Handling/final/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/final/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /AsyncDB_Handling/final/static/uploads/_blank_.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/final/static/uploads/_blank_.txt -------------------------------------------------------------------------------- /AsyncDB_Handling/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /AsyncDB_Handling/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/async_cursor.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | 6 | async def execute_query(): 7 | conn = await direct_get_conn() 8 | query = "select * from blog" 9 | stmt = text(query) 10 | # SQL 호출하여 CursorResult 반환. 11 | result = await conn.execute(stmt) 12 | # 아래는 오류를 발생. 13 | rows = await result.fetchone() 14 | print(rows) 15 | result.close() 16 | await conn.rollback() 17 | await conn.close() 18 | await engine.dispose() 19 | 20 | async def stream_query(): 21 | conn = await direct_get_conn() 22 | query = "select * from blog" 23 | stmt = text(query) 24 | # connection의 execute()가 아닌 stream()을 호출해야 함. 25 | async_result = await conn.stream(stmt) 26 | # async로 iteration 수행. 27 | async for row in async_result: 28 | print(row) 29 | 30 | await async_result.close() 31 | await conn.rollback() 32 | await conn.close() 33 | await engine.dispose() 34 | 35 | async def main(): 36 | await stream_query() 37 | #await execute_query() 38 | 39 | if __name__ == "__main__": 40 | asyncio.run(main()) -------------------------------------------------------------------------------- /AsyncDB_Handling/start/async_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | 6 | async def execute_query(): 7 | conn = await direct_get_conn() 8 | print("conn type:", type(conn)) 9 | query = "select * from blog" 10 | stmt = text(query) 11 | # SQL 호출하여 CursorResult 반환. 12 | result = await conn.execute(stmt) 13 | 14 | rows = result.fetchall() 15 | print(rows) 16 | result.close() 17 | await conn.rollback() 18 | await conn.close() 19 | await engine.dispose() 20 | 21 | async def main(): 22 | await execute_query() 23 | 24 | if __name__ == "__main__": 25 | asyncio.run(main()) 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/async_pool.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | async def execute_sleep_query(): 6 | for ind in range(10): 7 | print("loop index:", ind) 8 | conn = await direct_get_conn() 9 | query = "select sleep(5)" 10 | stmt = text(query) 11 | result = await conn.execute(stmt) 12 | await conn.close() 13 | await engine.dispose() 14 | 15 | async def main(): 16 | await execute_sleep_query() 17 | 18 | if __name__ == "__main__": 19 | asyncio.run(main()) 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.staticfiles import StaticFiles 3 | from routes import blog 4 | 5 | app = FastAPI() 6 | 7 | app.mount("/static", StaticFiles(directory="static"), name="static") 8 | app.include_router(blog.router) 9 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql==1.1.1 2 | aiomysql==0.2.0 3 | aiofiles==24.1.0 -------------------------------------------------------------------------------- /AsyncDB_Handling/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /AsyncDB_Handling/start/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/start/static/default/blog_default.png -------------------------------------------------------------------------------- /AsyncDB_Handling/start/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/start/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /AsyncDB_Handling/start/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/start/static/sample_images/irises.png -------------------------------------------------------------------------------- /AsyncDB_Handling/start/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/start/static/sample_images/party.png -------------------------------------------------------------------------------- /AsyncDB_Handling/start/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/start/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /AsyncDB_Handling/start/static/uploads/_blank_.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/AsyncDB_Handling/start/static/uploads/_blank_.txt -------------------------------------------------------------------------------- /AsyncDB_Handling/start/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /AsyncDB_Handling/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Authentication/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Authentication/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.exceptions import RequestValidationError 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from routes import blog, auth 7 | from utils.common import lifespan 8 | from utils import exc_handler, middleware 9 | 10 | app = FastAPI(lifespan=lifespan) 11 | 12 | app.mount("/static", StaticFiles(directory="static"), name="static") 13 | 14 | app.add_middleware(CORSMiddleware, 15 | allow_origins=["*"], 16 | allow_methods=["*"], 17 | allow_headers=["*"], 18 | allow_credentials=True, 19 | max_age=-1) 20 | # app.add_middleware(middleware.DummyMiddleware) 21 | app.add_middleware(middleware.MethodOverrideMiddlware) 22 | 23 | app.include_router(blog.router) 24 | app.include_router(auth.router) 25 | 26 | app.add_exception_handler(StarletteHTTPException, exc_handler.custom_http_exception_handler) 27 | app.add_exception_handler(RequestValidationError, exc_handler.validation_exception_handler) -------------------------------------------------------------------------------- /Authentication/final/requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==4.0.1 2 | passlib[bcrypt]==1.7.4 3 | itsdangerous==2.2.0 -------------------------------------------------------------------------------- /Authentication/final/schemas/auth_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class UserData(BaseModel): 4 | id: int 5 | name: str 6 | email: str 7 | 8 | class UserDataPASS(UserData): 9 | hashed_password: str 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Authentication/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Authentication/final/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/default/blog_default.png -------------------------------------------------------------------------------- /Authentication/final/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Authentication/final/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/sample_images/irises.png -------------------------------------------------------------------------------- /Authentication/final/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/sample_images/party.png -------------------------------------------------------------------------------- /Authentication/final/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Authentication/final/static/uploads/권철민/irises_1724384870.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/uploads/권철민/irises_1724384870.png -------------------------------------------------------------------------------- /Authentication/final/static/uploads/권철민/party_1724306758.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/uploads/권철민/party_1724306758.png -------------------------------------------------------------------------------- /Authentication/final/static/uploads/권철민/party_1724384805.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/final/static/uploads/권철민/party_1724384805.png -------------------------------------------------------------------------------- /Authentication/final/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Authentication/final/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Authentication/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Authentication/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Authentication/final/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Authentication/final/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Authentication/final/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Authentication/final/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Authentication/final/utils/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | 4 | class DummyMiddleware(BaseHTTPMiddleware): 5 | async def dispatch(self, request: Request, call_next): 6 | print("### request info:", request.url, request.method) 7 | print("### request type:", type(request)) 8 | 9 | response = await call_next(request) 10 | return response 11 | 12 | class MethodOverrideMiddlware(BaseHTTPMiddleware): 13 | async def dispatch(self, request: Request, call_next): 14 | # print("#### request url, query_params, method", 15 | # request.url, request.query_params, request.method) 16 | if request.method == "POST": 17 | query = request.query_params 18 | if query: 19 | method_override = query["_method"] 20 | if method_override: 21 | method_override = method_override.upper() 22 | if method_override in ("PUT", "DELETE"): 23 | request.scope["method"] = method_override 24 | 25 | response = await call_next(request) 26 | return response 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Authentication/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Authentication/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Authentication/start/db/__pycache__/database.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/db/__pycache__/database.cpython-310.pyc -------------------------------------------------------------------------------- /Authentication/start/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.exceptions import RequestValidationError 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from routes import blog 7 | from utils.common import lifespan 8 | from utils import exc_handler, middleware 9 | 10 | app = FastAPI(lifespan=lifespan) 11 | 12 | app.mount("/static", StaticFiles(directory="static"), name="static") 13 | 14 | app.add_middleware(CORSMiddleware, 15 | allow_origins=["*"], 16 | allow_methods=["*"], 17 | allow_headers=["*"], 18 | allow_credentials=True, 19 | max_age=-1) 20 | # app.add_middleware(middleware.DummyMiddleware) 21 | app.add_middleware(middleware.MethodOverrideMiddlware) 22 | 23 | app.include_router(blog.router) 24 | 25 | app.add_exception_handler(StarletteHTTPException, exc_handler.custom_http_exception_handler) 26 | app.add_exception_handler(RequestValidationError, exc_handler.validation_exception_handler) -------------------------------------------------------------------------------- /Authentication/start/requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==4.0.1 2 | passlib[bcrypt]==1.7.4 3 | itsdangerous==2.2.0 -------------------------------------------------------------------------------- /Authentication/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Authentication/start/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/default/blog_default.png -------------------------------------------------------------------------------- /Authentication/start/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Authentication/start/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/sample_images/irises.png -------------------------------------------------------------------------------- /Authentication/start/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/sample_images/party.png -------------------------------------------------------------------------------- /Authentication/start/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Authentication/start/static/uploads/권철민/irises_1724384870.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/uploads/권철민/irises_1724384870.png -------------------------------------------------------------------------------- /Authentication/start/static/uploads/권철민/party_1724306758.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/uploads/권철민/party_1724306758.png -------------------------------------------------------------------------------- /Authentication/start/static/uploads/권철민/party_1724384805.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Authentication/start/static/uploads/권철민/party_1724384805.png -------------------------------------------------------------------------------- /Authentication/start/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Authentication/start/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Authentication/start/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Authentication/start/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Authentication/start/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Authentication/start/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Authentication/start/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Authentication/start/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Authentication/start/utils/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | 4 | class DummyMiddleware(BaseHTTPMiddleware): 5 | async def dispatch(self, request: Request, call_next): 6 | print("### request info:", request.url, request.method) 7 | print("### request type:", type(request)) 8 | 9 | response = await call_next(request) 10 | return response 11 | 12 | class MethodOverrideMiddlware(BaseHTTPMiddleware): 13 | async def dispatch(self, request: Request, call_next): 14 | # print("#### request url, query_params, method", 15 | # request.url, request.query_params, request.method) 16 | if request.method == "POST": 17 | query = request.query_params 18 | if query: 19 | method_override = query["_method"] 20 | if method_override: 21 | method_override = method_override.upper() 22 | if method_override in ("PUT", "DELETE"): 23 | request.scope["method"] = method_override 24 | 25 | response = await call_next(request) 26 | return response 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Authentication/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Blog_Bootstrap/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Blog_Bootstrap/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.staticfiles import StaticFiles 3 | from routes import blog 4 | 5 | app = FastAPI() 6 | 7 | app.mount("/static", StaticFiles(directory="static"), name="static") 8 | app.include_router(blog.router) 9 | -------------------------------------------------------------------------------- /Blog_Bootstrap/final/requirements.txt: -------------------------------------------------------------------------------- 1 | python-multipart==0.0.9 -------------------------------------------------------------------------------- /Blog_Bootstrap/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Blog_Bootstrap/final/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Blog_Bootstrap/final/static/default/blog_default.png -------------------------------------------------------------------------------- /Blog_Bootstrap/final/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Blog_Bootstrap/final/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Blog_Bootstrap/final/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Blog_Bootstrap/final/static/sample_images/irises.png -------------------------------------------------------------------------------- /Blog_Bootstrap/final/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Blog_Bootstrap/final/static/sample_images/party.png -------------------------------------------------------------------------------- /Blog_Bootstrap/final/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Blog_Bootstrap/final/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Blog_Bootstrap/final/static/uploads/_blank_.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Blog_Bootstrap/final/static/uploads/_blank_.txt -------------------------------------------------------------------------------- /Blog_Bootstrap/final/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Blog_Bootstrap/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Blog_Bootstrap/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Blog_Bootstrap/final/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Blog_Bootstrap/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Blog_Bootstrap/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 2 | -------------------------------------------------------------------------------- /Blog_Bootstrap/start/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from routes import blog 3 | 4 | app = FastAPI() 5 | app.include_router(blog.router) 6 | -------------------------------------------------------------------------------- /Blog_Bootstrap/start/requirements.txt: -------------------------------------------------------------------------------- 1 | python-multipart==0.0.9 -------------------------------------------------------------------------------- /Blog_Bootstrap/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Blog_Bootstrap/start/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |

모든 블로그 리스트

8 |
새로운 Blog 생성
9 | {% for blog in all_blogs %} 10 |
11 |

{{ blog.title }}

12 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

13 |

{{ blog.content }}

14 |
15 | {% endfor %} 16 | 17 | -------------------------------------------------------------------------------- /Blog_Bootstrap/start/templates/modify_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 블로그 수정 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_Bootstrap/start/templates/new_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 새로운 블로그 생성 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_Bootstrap/start/templates/show_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |
8 | 9 |

{{ blog.title }}

10 |
Posted on {{ blog.modified_dt }} by {{ blog.author }}

11 |

{{ blog.content | safe }}

12 |
13 |
14 | Home으로 돌아가기 15 | 수정하기 16 |
17 | 18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /Blog_Bootstrap/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None -------------------------------------------------------------------------------- /Blog_DB_Handling/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 2 | -------------------------------------------------------------------------------- /Blog_DB_Handling/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from routes import blog 3 | 4 | app = FastAPI() 5 | app.include_router(blog.router) 6 | -------------------------------------------------------------------------------- /Blog_DB_Handling/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Blog_DB_Handling/final/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |

모든 블로그 리스트

8 |
새로운 Blog 생성
9 | {% for blog in all_blogs %} 10 |
11 |

{{ blog.title }}

12 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

13 |

{{ blog.content }}

14 |
15 | {% endfor %} 16 | 17 | -------------------------------------------------------------------------------- /Blog_DB_Handling/final/templates/modify_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 블로그 수정 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_DB_Handling/final/templates/new_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 새로운 블로그 생성 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_DB_Handling/final/templates/show_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |
8 | 9 |

{{ blog.title }}

10 |
Posted on {{ blog.modified_dt }} by {{ blog.author }}

11 |

{{ blog.content | safe }}

12 |
13 |
14 | Home으로 돌아가기 15 | 수정하기 16 |
17 | 18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /Blog_DB_Handling/final/utils/__pycache__/util.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Blog_DB_Handling/final/utils/__pycache__/util.cpython-310.pyc -------------------------------------------------------------------------------- /Blog_DB_Handling/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None -------------------------------------------------------------------------------- /Blog_MVC/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 2 | -------------------------------------------------------------------------------- /Blog_MVC/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from routes import blog 3 | 4 | app = FastAPI() 5 | app.include_router(blog.router) 6 | -------------------------------------------------------------------------------- /Blog_MVC/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Blog_MVC/final/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |

모든 블로그 리스트

8 |
새로운 Blog 생성
9 | {% for blog in all_blogs %} 10 |
11 |

{{ blog.title }}

12 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

13 |

{{ blog.content }}

14 |
15 | {% endfor %} 16 | 17 | -------------------------------------------------------------------------------- /Blog_MVC/final/templates/modify_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 블로그 수정 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_MVC/final/templates/new_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 새로운 블로그 생성 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_MVC/final/templates/show_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |
8 | 9 |

{{ blog.title }}

10 |
Posted on {{ blog.modified_dt }} by {{ blog.author }}

11 |

{{ blog.content | safe }}

12 |
13 |
14 | Home으로 돌아가기 15 | 수정하기 16 |
17 | 18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /Blog_MVC/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None -------------------------------------------------------------------------------- /Blog_MVC/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 2 | -------------------------------------------------------------------------------- /Blog_MVC/start/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from routes import blog 3 | 4 | app = FastAPI() 5 | app.include_router(blog.router) 6 | -------------------------------------------------------------------------------- /Blog_MVC/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Blog_MVC/start/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |

모든 블로그 리스트

8 |
새로운 Blog 생성
9 | {% for blog in all_blogs %} 10 |
11 |

{{ blog.title }}

12 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

13 |

{{ blog.content }}

14 |
15 | {% endfor %} 16 | 17 | -------------------------------------------------------------------------------- /Blog_MVC/start/templates/modify_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 블로그 수정 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_MVC/start/templates/new_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 새로운 블로그 생성 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /Blog_MVC/start/templates/show_blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 블로그에 오신걸 환영합니다 5 | 6 | 7 |
8 | 9 |

{{ blog.title }}

10 |
Posted on {{ blog.modified_dt }} by {{ blog.author }}

11 |

{{ blog.content | safe }}

12 |
13 |
14 | Home으로 돌아가기 15 | 수정하기 16 |
17 | 18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /Blog_MVC/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None -------------------------------------------------------------------------------- /Bootstrap_Template/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import HTMLResponse 3 | from fastapi.templating import Jinja2Templates 4 | from pydantic import BaseModel 5 | 6 | app = FastAPI() 7 | 8 | # jinja2 Template 생성. 인자로 directory 입력 9 | templates = Jinja2Templates(directory="templates") 10 | 11 | class Item(BaseModel): 12 | name: str 13 | description: str 14 | 15 | @app.get("/all_items", response_class=HTMLResponse) 16 | async def read_all_items(request: Request): 17 | all_items = [Item(name="테스트_상품명_" +str(i), 18 | description="테스트 내용입니다. 인덱스는 " + str(i)) for i in range(5) ] 19 | print("all_items:", all_items) 20 | return templates.TemplateResponse( 21 | request=request, 22 | #name="index_no_include.html", 23 | #name="index_include.html", 24 | name="index.html", 25 | context={"all_items": all_items} 26 | ) -------------------------------------------------------------------------------- /Bootstrap_Template/final/templates/include/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Bootstrap_Template/final/templates/include/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Bootstrap_Template/final/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 | {% for item in all_items %} 8 |
9 |

Item: {{ item.name }}

10 |

{{ item.description }}

11 | Learn More 12 |
13 | {% endfor %} 14 |
15 |
16 |
17 |
18 | {% endblock %} -------------------------------------------------------------------------------- /Bootstrap_Template/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Bootstrap_Template/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Item UI 7 | 8 | 9 | 10 | 11 | {% include '/layout/navbar.html' %} 12 | 13 | 14 | 15 | {% block content %} 16 | {% endblock %} 17 | 18 | 19 | 20 | {% include '/layout/footer.html' %} 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Bootstrap_Template/final/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Bootstrap_Template/start/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import HTMLResponse 3 | from fastapi.templating import Jinja2Templates 4 | from pydantic import BaseModel 5 | 6 | app = FastAPI() 7 | 8 | # jinja2 Template 생성. 인자로 directory 입력 9 | templates = Jinja2Templates(directory="templates") 10 | 11 | 12 | class Item(BaseModel): 13 | name: str 14 | description: str 15 | 16 | @app.get("/all_items", response_class=HTMLResponse) 17 | async def read_all_items(request: Request): 18 | all_items = [Item(name="테스트_상품명_" +str(i), 19 | description="테스트 내용입니다. 인덱스는 " + str(i)) for i in range(5) ] 20 | print("all_items:", all_items) 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="index_no_include.html", 24 | #name="index_include.html", 25 | #name="index.html", 26 | context={"all_items": all_items} 27 | ) -------------------------------------------------------------------------------- /DB_Fundamentals/bind_variable.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text, Connection 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from database import direct_get_conn 4 | from datetime import datetime 5 | 6 | try: 7 | # Connection 얻기 8 | conn = direct_get_conn() 9 | 10 | # SQL 선언 및 text로 감싸기 11 | # 1, 2, 3, 4 | '둘리', '길동' 12 | query = "select id, title, author from blog where id = :id and author = :author \ 13 | and modified_dt < :modified_dt" 14 | stmt = text(query) 15 | bind_stmt = stmt.bindparams(id=1, author='둘리', modified_dt=datetime.now()) 16 | 17 | # SQL 호출하여 CursorResult 반환. 18 | result = conn.execute(bind_stmt) 19 | rows = result.fetchall() # row Set을 개별 원소로 가지는 List로 반환. 20 | print(rows) 21 | result.close() 22 | except SQLAlchemyError as e: 23 | print("############# ", e) 24 | #raise e 25 | finally: 26 | # close() 메소드 호출하여 connection 반환. 27 | conn.close() -------------------------------------------------------------------------------- /DB_Fundamentals/context_practice.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine, text 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from sqlalchemy.pool import QueuePool, NullPool 4 | 5 | # database connection URL 6 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 7 | 8 | engine = create_engine(DATABASE_CONN, 9 | poolclass=QueuePool, 10 | #poolclass=NullPool, 11 | pool_size=10, max_overflow=0) 12 | 13 | def context_execute_sleep(): 14 | with engine.connect() as conn: 15 | query = "select sleep(5)" 16 | result = conn.execute(text(query)) 17 | result.close() 18 | #conn.close() 19 | 20 | for ind in range(20): 21 | print("loop index:", ind) 22 | context_execute_sleep() 23 | 24 | print("end of loop") -------------------------------------------------------------------------------- /DB_Fundamentals/cursor_fetch.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text, Connection 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from database import direct_get_conn 4 | 5 | try: 6 | # Connection 얻기 7 | conn = direct_get_conn() 8 | 9 | # SQL 선언 및 text로 감싸기 10 | query = "select id, title from blog" 11 | stmt = text(query) 12 | 13 | # SQL 호출하여 CursorResult 반환. 14 | result = conn.execute(stmt) 15 | rows = result.fetchall() # row Set을 개별 원소로 가지는 List로 반환. 16 | #rows = result.fetchone() # row Set 단일 원소 반환 17 | #rows = result.fetchmany(2) # row Set을 개별 원소로 가지는 List로 반환. 18 | # rows = [row for row in result] # List Comprehension으로 row Set을 개별 원소로 가지는 List로 반환 19 | print(rows) 20 | print(type(rows)) 21 | 22 | 23 | # 개별 row를 컬럼명를 key로 가지는 dict로 반환하기 24 | # row_dict = result.mappings().fetchall() 25 | # print(row_dict) 26 | 27 | # 코드레벨에서 컬럼명 명시화 28 | # row = result.fetchone() 29 | # print(row._key_to_index) 30 | # rows = [(row.id, row.title) for row in result] 31 | # print(rows) 32 | 33 | result.close() 34 | except SQLAlchemyError as e: 35 | print("############# ", e) 36 | #raise e 37 | finally: 38 | # close() 메소드 호출하여 connection 반환. 39 | conn.close() -------------------------------------------------------------------------------- /DB_Fundamentals/db_basic.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine, text 2 | from sqlalchemy.pool import QueuePool 3 | from sqlalchemy.exc import SQLAlchemyError 4 | 5 | # database connection URL 6 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@127.0.0.1:3306/blog_db" 7 | # Engine 생성 8 | engine = create_engine(DATABASE_CONN, poolclass=QueuePool, 9 | pool_size=10, max_overflow=0) 10 | 11 | try: 12 | # Connection 얻기 13 | conn = engine.connect() 14 | # SQL 선언 및 text로 감싸기 15 | query = "select id, title from blog" 16 | stmt = text(query) 17 | 18 | # SQL 호출하여 CursorResult 반환. 19 | result = conn.execute(stmt) 20 | print("type result:", result) 21 | 22 | rows = result.fetchall() 23 | print(rows) 24 | 25 | # print(type(rows[0])) 26 | # print(rows[0].id, rows[0].title) 27 | # print(rows[0][0], rows[0][1]) 28 | # print(rows[0]._key_to_index) 29 | 30 | result.close() 31 | except SQLAlchemyError as e: 32 | print(e) 33 | finally: 34 | # close() 메소드 호출하여 connection 반환. 35 | conn.close() 36 | -------------------------------------------------------------------------------- /DB_Fundamentals/db_config_data.sql: -------------------------------------------------------------------------------- 1 | /* sample db 및 데이터 생성 */ 2 | 3 | DROP DATABASE if exists blog_db; 4 | 5 | CREATE DATABASE blog_db; 6 | 7 | DROP TABLE if exists blog; 8 | 9 | CREATE TABLE blog ( 10 | id int(11) NOT NULL AUTO_INCREMENT, 11 | title varchar(200) NOT NULL, 12 | author varchar(100) NOT NULL, 13 | content varchar(4000) NOT NULL, 14 | image_loc varchar(300) DEFAULT NULL, 15 | modified_dt datetime NOT NULL, 16 | PRIMARY KEY (id) 17 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 18 | 19 | INSERT INTO blog_db.blog(title, author, content, modified_dt) values('테스트 title 1', '둘리', '테스트 컨텐츠 1', now()); 20 | INSERT INTO blog_db.blog(title, author, content, modified_dt) values('테스트 title 2', '길동', '테스트 컨텐츠 2', now()); 21 | INSERT INTO blog_db.blog(title, author, content, modified_dt) values('테스트 title 3', '도넛', '테스트 컨텐츠 3', now()); 22 | INSERT INTO blog_db.blog(title, author, content, modified_dt) values('테스트 title 4', '희동', '테스트 컨텐츠 4', now()); 23 | 24 | COMMIT; 25 | 26 | /* connection 모니터링 스크립트. root로 수행 필요. */ 27 | select * from sys.session where db='blog_db' order by conn_id; 28 | -------------------------------------------------------------------------------- /DB_Fundamentals/module_direct.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text, Connection 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from database import direct_get_conn 4 | 5 | def execute_query(conn: Connection): 6 | query = "select * from blog" 7 | stmt = text(query) 8 | # SQL 호출하여 CursorResult 반환. 9 | result = conn.execute(stmt) 10 | 11 | rows = result.fetchall() 12 | print(rows) 13 | result.close() 14 | 15 | def execute_sleep(conn: Connection): 16 | query = "select sleep(5)" 17 | result = conn.execute(text(query)) 18 | result.close() 19 | 20 | for ind in range(20): 21 | try: 22 | conn = direct_get_conn() 23 | execute_sleep(conn) 24 | print("loop index:", ind) 25 | except SQLAlchemyError as e: 26 | print(e) 27 | finally: 28 | conn.close() 29 | print("connection is closed inside finally") 30 | 31 | print("end of loop") 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /DB_Fundamentals/pool_practice.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine, text 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from sqlalchemy.pool import QueuePool, NullPool 4 | 5 | # database connection URL 6 | DATABASE_CONN = "mysql+mysqlconnector://root:root1234@localhost:3306/blog_db" 7 | 8 | # engine = create_engine(DATABASE_CONN) 9 | engine = create_engine(DATABASE_CONN, 10 | poolclass=QueuePool, 11 | #poolclass=NullPool, # Connection Pool 사용하지 않음. 12 | pool_size=10, max_overflow=2 13 | ) 14 | print("#### engine created") 15 | 16 | def direct_execute_sleep(is_close: bool = False): 17 | conn = engine.connect() 18 | query = "select sleep(5)" 19 | result = conn.execute(text(query)) 20 | # rows = result.fetchall() 21 | # print(rows) 22 | result.close() 23 | 24 | # 인자로 is_close가 True일 때만 connection close() 25 | if is_close: 26 | conn.close() 27 | print("conn closed") 28 | 29 | for ind in range(20): 30 | print("loop index:", ind) 31 | direct_execute_sleep(is_close=True) 32 | 33 | 34 | print("end of loop") 35 | 36 | 37 | -------------------------------------------------------------------------------- /DB_Fundamentals/requirements.txt: -------------------------------------------------------------------------------- 1 | SQLAlchemy==2.0.32 2 | mysql-connector-python==9.0.0 -------------------------------------------------------------------------------- /Exception_Handler/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Exception_Handler/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.exceptions import RequestValidationError 5 | from routes import blog 6 | from db.database import engine 7 | from utils.common import lifespan 8 | from utils import exc_handler 9 | 10 | app = FastAPI(lifespan=lifespan) 11 | 12 | app.mount("/static", StaticFiles(directory="static"), name="static") 13 | 14 | app.include_router(blog.router) 15 | 16 | app.add_exception_handler(StarletteHTTPException, exc_handler.custom_http_exception_handler) 17 | app.add_exception_handler(RequestValidationError, exc_handler.validation_exception_handler) -------------------------------------------------------------------------------- /Exception_Handler/final/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql==1.1.1 2 | aiomysql==0.2.0 3 | aiofiles==24.1.0 -------------------------------------------------------------------------------- /Exception_Handler/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Exception_Handler/final/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/final/static/default/blog_default.png -------------------------------------------------------------------------------- /Exception_Handler/final/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/final/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Exception_Handler/final/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/final/static/sample_images/irises.png -------------------------------------------------------------------------------- /Exception_Handler/final/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/final/static/sample_images/party.png -------------------------------------------------------------------------------- /Exception_Handler/final/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/final/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Exception_Handler/final/static/uploads/_blank_.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/final/static/uploads/_blank_.txt -------------------------------------------------------------------------------- /Exception_Handler/final/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Exception_Handler/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Exception_Handler/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Exception_Handler/final/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Exception_Handler/final/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Exception_Handler/final/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Exception_Handler/final/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Exception_Handler/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Exception_Handler/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Exception_Handler/start/async_cursor.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | 6 | async def execute_query(): 7 | conn = await direct_get_conn() 8 | query = "select * from blog" 9 | stmt = text(query) 10 | # SQL 호출하여 CursorResult 반환. 11 | result = await conn.execute(stmt) 12 | # 아래는 오류를 발생. 13 | rows = await result.fetchone() 14 | print(rows) 15 | result.close() 16 | await conn.rollback() 17 | await conn.close() 18 | await engine.dispose() 19 | 20 | async def stream_query(): 21 | conn = await direct_get_conn() 22 | query = "select * from blog" 23 | stmt = text(query) 24 | # connection의 execute()가 아닌 stream()을 호출해야 함. 25 | async_result = await conn.stream(stmt) 26 | # async로 iteration 수행. 27 | async for row in async_result: 28 | print(row) 29 | 30 | await async_result.close() 31 | await conn.rollback() 32 | await conn.close() 33 | await engine.dispose() 34 | 35 | async def main(): 36 | await stream_query() 37 | #await execute_query() 38 | 39 | if __name__ == "__main__": 40 | asyncio.run(main()) -------------------------------------------------------------------------------- /Exception_Handler/start/async_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | 6 | async def execute_query(): 7 | conn = await direct_get_conn() 8 | print("conn type:", type(conn)) 9 | query = "select * from blog" 10 | stmt = text(query) 11 | # SQL 호출하여 CursorResult 반환. 12 | result = await conn.execute(stmt) 13 | 14 | rows = result.fetchall() 15 | print(rows) 16 | result.close() 17 | await conn.rollback() 18 | await conn.close() 19 | await engine.dispose() 20 | 21 | async def main(): 22 | await execute_query() 23 | 24 | if __name__ == "__main__": 25 | asyncio.run(main()) 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Exception_Handler/start/async_pool.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import text 2 | from db.database import direct_get_conn, engine 3 | import asyncio 4 | 5 | async def execute_sleep_query(): 6 | for ind in range(10): 7 | print("loop index:", ind) 8 | conn = await direct_get_conn() 9 | query = "select sleep(5)" 10 | stmt = text(query) 11 | result = await conn.execute(stmt) 12 | await conn.close() 13 | await engine.dispose() 14 | 15 | async def main(): 16 | await execute_sleep_query() 17 | 18 | if __name__ == "__main__": 19 | asyncio.run(main()) 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Exception_Handler/start/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.staticfiles import StaticFiles 3 | from routes import blog 4 | from contextlib import asynccontextmanager 5 | from db.database import engine 6 | 7 | @asynccontextmanager 8 | async def lifespan(app: FastAPI): 9 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 10 | print("Starting up...") 11 | yield 12 | 13 | #FastAPI 인스턴스 종료시 필요한 작업 수행 14 | print("Shutting down...") 15 | await engine.dispose() 16 | 17 | app = FastAPI(lifespan=lifespan) 18 | 19 | app.mount("/static", StaticFiles(directory="static"), name="static") 20 | app.include_router(blog.router) 21 | 22 | -------------------------------------------------------------------------------- /Exception_Handler/start/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql==1.1.1 2 | aiomysql==0.2.0 3 | aiofiles==24.1.0 -------------------------------------------------------------------------------- /Exception_Handler/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Exception_Handler/start/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/start/static/default/blog_default.png -------------------------------------------------------------------------------- /Exception_Handler/start/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/start/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Exception_Handler/start/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/start/static/sample_images/irises.png -------------------------------------------------------------------------------- /Exception_Handler/start/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/start/static/sample_images/party.png -------------------------------------------------------------------------------- /Exception_Handler/start/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/start/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Exception_Handler/start/static/uploads/_blank_.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Exception_Handler/start/static/uploads/_blank_.txt -------------------------------------------------------------------------------- /Exception_Handler/start/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Exception_Handler/start/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Exception_Handler/start/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Exception_Handler/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /FastAPI_Async_Thread/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | import asyncio 3 | import time 4 | 5 | app = FastAPI() 6 | 7 | # long-running I/O-bound 작업 시뮬레이션 8 | async def long_running_task(): 9 | # 특정 초동안 수행 시뮬레이션 10 | await asyncio.sleep(20) 11 | return {"status": "long_running task completed"} 12 | 13 | @app.get("/task") 14 | async def run_task(): 15 | result = await long_running_task() 16 | return result 17 | 18 | # @app.get("/task") 19 | # async def run_task(): 20 | # time.sleep(20) 21 | # return {"status": "long_running task completed"} 22 | 23 | @app.get("/quick") 24 | async def quick_response(): 25 | return {"status": "quick response"} -------------------------------------------------------------------------------- /Middleware/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Middleware/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.exceptions import RequestValidationError 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from routes import blog 7 | from utils.common import lifespan 8 | from utils import exc_handler, middleware 9 | 10 | app = FastAPI(lifespan=lifespan) 11 | 12 | app.mount("/static", StaticFiles(directory="static"), name="static") 13 | 14 | app.add_middleware(CORSMiddleware, 15 | allow_origins=["*"], 16 | allow_methods=["*"], 17 | allow_headers=["*"], 18 | allow_credentials=True, 19 | max_age=-1) 20 | # app.add_middleware(middleware.DummyMiddleware) 21 | app.add_middleware(middleware.MethodOverrideMiddlware) 22 | 23 | app.include_router(blog.router) 24 | 25 | app.add_exception_handler(StarletteHTTPException, exc_handler.custom_http_exception_handler) 26 | app.add_exception_handler(RequestValidationError, exc_handler.validation_exception_handler) -------------------------------------------------------------------------------- /Middleware/final/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql==1.1.1 2 | aiomysql==0.2.0 3 | aiofiles==24.1.0 -------------------------------------------------------------------------------- /Middleware/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Middleware/final/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/default/blog_default.png -------------------------------------------------------------------------------- /Middleware/final/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Middleware/final/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/sample_images/irises.png -------------------------------------------------------------------------------- /Middleware/final/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/sample_images/party.png -------------------------------------------------------------------------------- /Middleware/final/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Middleware/final/static/uploads/권철민/irises_1724384870.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/uploads/권철민/irises_1724384870.png -------------------------------------------------------------------------------- /Middleware/final/static/uploads/권철민/party_1724306758.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/uploads/권철민/party_1724306758.png -------------------------------------------------------------------------------- /Middleware/final/static/uploads/권철민/party_1724384805.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/final/static/uploads/권철민/party_1724384805.png -------------------------------------------------------------------------------- /Middleware/final/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Middleware/final/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Middleware/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Middleware/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Middleware/final/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Middleware/final/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Middleware/final/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Middleware/final/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Middleware/final/utils/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | 4 | class DummyMiddleware(BaseHTTPMiddleware): 5 | async def dispatch(self, request: Request, call_next): 6 | print("### request info:", request.url, request.method) 7 | print("### request type:", type(request)) 8 | 9 | response = await call_next(request) 10 | return response 11 | 12 | class MethodOverrideMiddlware(BaseHTTPMiddleware): 13 | async def dispatch(self, request: Request, call_next): 14 | # print("#### request url, query_params, method", request.url, request.query_params, request.method) 15 | if request.method == "POST": 16 | query = request.query_params 17 | if query: 18 | method_override = query["_method"] 19 | if method_override: 20 | method_override = method_override.upper() 21 | if method_override in ("PUT", "DELETE"): 22 | request.scope["method"] = method_override 23 | 24 | response = await call_next(request) 25 | return response 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Middleware/final/utils/middleware.py.bak: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | 4 | class DummyMiddleware(BaseHTTPMiddleware): 5 | async def dispatch(self, request: Request, call_next): 6 | print("### request info:", request.url, request.method) 7 | print("### request type:", type(request)) 8 | 9 | response = await call_next(request) 10 | return response 11 | 12 | class MethodOverrideMiddlware(BaseHTTPMiddleware): 13 | async def dispatch(self, request: Request, call_next): 14 | print("#### request url, query_params, method", 15 | request.url, request.query_params, request.method) 16 | if request.method == "POST": 17 | query = request.query_params 18 | if query: 19 | method_override = query["_method"] 20 | if method_override: 21 | method_override = method_override.upper() 22 | if method_override in ("PUT", "DELETE"): 23 | request.scope["method"] = method_override 24 | 25 | response = await call_next(request) 26 | return response 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Middleware/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Middleware/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Middleware/start/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.exceptions import RequestValidationError 5 | from routes import blog 6 | from db.database import engine 7 | from utils.common import lifespan 8 | from utils import exc_handler 9 | 10 | app = FastAPI(lifespan=lifespan) 11 | 12 | app.mount("/static", StaticFiles(directory="static"), name="static") 13 | 14 | app.include_router(blog.router) 15 | 16 | app.add_exception_handler(StarletteHTTPException, exc_handler.custom_http_exception_handler) 17 | app.add_exception_handler(RequestValidationError, exc_handler.validation_exception_handler) -------------------------------------------------------------------------------- /Middleware/start/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql==1.1.1 2 | aiomysql==0.2.0 3 | aiofiles==24.1.0 -------------------------------------------------------------------------------- /Middleware/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Middleware/start/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/start/static/default/blog_default.png -------------------------------------------------------------------------------- /Middleware/start/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/start/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Middleware/start/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/start/static/sample_images/irises.png -------------------------------------------------------------------------------- /Middleware/start/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/start/static/sample_images/party.png -------------------------------------------------------------------------------- /Middleware/start/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/start/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Middleware/start/static/uploads/권철민/party_1724306758.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/start/static/uploads/권철민/party_1724306758.png -------------------------------------------------------------------------------- /Middleware/start/static/uploads/권철민/party_1724311063.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Middleware/start/static/uploads/권철민/party_1724311063.png -------------------------------------------------------------------------------- /Middleware/start/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Middleware/start/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Middleware/start/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Middleware/start/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Middleware/start/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Middleware/start/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Middleware/start/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Middleware/start/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Middleware/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Pydantic/pydantic_02.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ValidationError, ConfigDict, Field, Strict 2 | from typing import List, Annotated 3 | 4 | class Address(BaseModel): 5 | street: str 6 | city: str 7 | country: str 8 | 9 | class User(BaseModel): 10 | # 문자열->숫자값 자동 파싱을 허용하지 않을 경우 Strict 모드로 설정. 11 | #model_config = ConfigDict(strict=True) 12 | 13 | id: int 14 | name: str 15 | email: str 16 | addresses: List[Address] 17 | age: int | None = None # Optional[int] = None 18 | #개별 속성에 Strict 모드 설정 시 Field나 Annotated 이용. None 적용 시 Optional 19 | #age: int = Field(None, strict=True) 20 | #age: Annotated[int, Strict()] = None 21 | 22 | #Pydantic Model 객체화 시 자동으로 검증 수행 수행하고, 검증 오류 시 ValidationError raise 23 | try: 24 | user = User( 25 | id=123, 26 | name="John Doe", 27 | email="john.doe@example.com", 28 | addresses=[{"street": "123 Main St", "city": "Hometown", "country": "USA"}], 29 | age="29" # 문자열 값을 자동으로 int 로 파싱함. 30 | ) 31 | print(user) 32 | except ValidationError as e: 33 | print("validation error happened") 34 | print(e) 35 | 36 | -------------------------------------------------------------------------------- /Requests/main_path.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from enum import Enum 3 | 4 | app = FastAPI() 5 | 6 | # http://localhost:8081/items/3 7 | # decorator에 path값으로 들어오는 문자열중에 8 | # format string { }로 지정된 변수가 path parameter 9 | @app.get("/items/{item_id}") 10 | # 수행 함수 인자로 path parameter가 입력됨. 11 | # 함수 인자의 타입을 지정하여 path parameter 타입 지정. 12 | async def read_item(item_id: int): 13 | return {"item_id": item_id} 14 | 15 | # Path parameter값과 특정 지정 Path가 충돌되지 않도록 endpoint 작성 코드 위치에 주의 16 | @app.get("/items/all") 17 | # 수행 함수 인자로 path parameter가 입력됨. 함수 인자의 타입을 지정하여 path parameter 타입 지정. 18 | async def read_all_items(): 19 | return {"message": "all items"} 20 | 21 | 22 | -------------------------------------------------------------------------------- /Requests/main_test.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from enum import Enum 3 | 4 | 5 | app = FastAPI() 6 | 7 | from pydantic import BaseModel 8 | 9 | class Item(BaseModel): 10 | name: str 11 | description: str | None = None 12 | price: float 13 | tax: float | None = None 14 | 15 | @app.post("/items/") 16 | async def create_item(item: Item): 17 | return item -------------------------------------------------------------------------------- /Requests/rbody_01.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Foo", 3 | "description": "An optional description", 4 | "price": 45.2, 5 | "tax": 3.5 6 | } -------------------------------------------------------------------------------- /Requests/rbody_02.json: -------------------------------------------------------------------------------- 1 | { 2 | "item": { 3 | "name": "Foo", 4 | "description": "The pretender", 5 | "price": 42.0, 6 | "tax": 3.2 7 | }, 8 | "user": { 9 | "username": "dave", 10 | "full_name": "Dave Grohl" 11 | } 12 | } -------------------------------------------------------------------------------- /Requests/rbody_03.json: -------------------------------------------------------------------------------- 1 | { 2 | "item": { 3 | "name": "Foo", 4 | "description": "The pretender", 5 | "price": 42.0, 6 | "tax": 3.2 7 | }, 8 | "user": { 9 | "username": "dave", 10 | "full_name": "Dave Grohl" 11 | }, 12 | "importance": 5 13 | } -------------------------------------------------------------------------------- /Responses/rbody_01.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Foo", 3 | "description": "An optional description", 4 | "price": 45.2, 5 | "tax": 3.5 6 | } -------------------------------------------------------------------------------- /Router/final/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from routes import item, user 3 | 4 | app = FastAPI() 5 | 6 | app.include_router(item.router) 7 | app.include_router(user.router) 8 | -------------------------------------------------------------------------------- /Router/final/main_org.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | 4 | app = FastAPI() 5 | 6 | class Item(BaseModel): 7 | name: str 8 | description: str = None 9 | price: float 10 | tax: float = None 11 | 12 | @app.get("/item/{item_id}") 13 | async def read_item(item_id: int): 14 | return {"item_id": item_id} 15 | 16 | @app.post("/item") 17 | async def create_item(item: Item): 18 | return item 19 | 20 | @app.put("/item/{item_id}") 21 | async def update_item(item_id: int, item: Item): 22 | return {"item_id": item_id, "item": item} 23 | 24 | @app.get("/users/") 25 | async def read_users(): 26 | return [{"username": "Rickie"}, {"username": "Martin"}] 27 | 28 | 29 | @app.get("/users/me") 30 | async def read_user_me(): 31 | return {"username": "currentuser"} 32 | 33 | 34 | @app.get("/users/{username}") 35 | async def read_user(username: str): 36 | return {"username": username} -------------------------------------------------------------------------------- /Router/final/routes/item.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from pydantic import BaseModel 3 | 4 | router = APIRouter(prefix="/item", tags=["item"]) 5 | 6 | class Item(BaseModel): 7 | name: str 8 | description: str = None 9 | price: float 10 | tax: float = None 11 | 12 | @router.get("/{item_id}") 13 | async def read_item(item_id: int): 14 | return {"item_id": item_id} 15 | 16 | @router.post("/") 17 | async def create_item(item: Item): 18 | return item 19 | 20 | @router.put("/{item_id}") 21 | async def update_item(item_id: int, item: Item): 22 | return {"item_id": item_id, "item": item} -------------------------------------------------------------------------------- /Router/final/routes/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter(prefix="/user", tags=["user"]) 4 | 5 | @router.get("/") 6 | async def read_users(): 7 | return [{"username": "Rickie"}, {"username": "Martin"}] 8 | 9 | @router.get("/me") 10 | async def read_user_me(): 11 | return {"username": "currentuser"} 12 | 13 | @router.get("/{username}", tags=["users"]) 14 | async def read_user(username: str): 15 | return {"username": username} -------------------------------------------------------------------------------- /Router/start/main_org.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | 4 | app = FastAPI() 5 | 6 | class Item(BaseModel): 7 | name: str 8 | description: str = None 9 | price: float 10 | tax: float = None 11 | 12 | @app.get("/item/{item_id}") 13 | async def read_item(item_id: int): 14 | return {"item_id": item_id} 15 | 16 | @app.post("/item") 17 | async def create_item(item: Item): 18 | return item 19 | 20 | @app.put("/item/{item_id}") 21 | async def update_item(item_id: int, item: Item): 22 | return {"item_id": item_id, "item": item} 23 | 24 | @app.get("/users/") 25 | async def read_users(): 26 | return [{"username": "Rickie"}, {"username": "Martin"}] 27 | 28 | 29 | @app.get("/users/me") 30 | async def read_user_me(): 31 | return {"username": "currentuser"} 32 | 33 | 34 | @app.get("/users/{username}") 35 | async def read_user(username: str): 36 | return {"username": username} -------------------------------------------------------------------------------- /Session_Redis/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | SECRET_KEY = "your_unique_secret_key" 4 | -------------------------------------------------------------------------------- /Session_Redis/final/redis_test.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | #localhost, post=6379, db=0으로 접속 (default port is 6379) and use database 0 4 | r_client = redis.Redis(host='localhost', port=6379, db=0) 5 | 6 | # db=0 key와 value 설정한 뒤 해당 key값으로 값 추출. 7 | r_client.set('name', 'FastAPI') 8 | name_value = r_client.get('name') 9 | print(f"Value for 'name' in db 0: {name_value.decode('utf-8')}") 10 | 11 | # db1에 접속 12 | r_client_db1 = redis.Redis(host='localhost', port=6379, db=1) 13 | 14 | # db=1 key와 value 설정한 뒤 해당 key값으로 값 추출. 15 | r_client_db1.set('name', 'Redis') 16 | name_value_db1 = r_client_db1.get('name') 17 | print(f"Value for 'name' in db 1: {name_value_db1.decode('utf-8')}") 18 | 19 | # Connection Pool 기반 access 20 | redis_pool = redis.ConnectionPool(host='localhost', port=6379, db=0, max_connections=10) 21 | redis_client_pool = redis.Redis(connection_pool=redis_pool) 22 | 23 | redis_client_pool.set('name', 'FastAPI') 24 | name_value = redis_client_pool.get('name') 25 | print(f"Value for 'name' in db 0: {name_value.decode('utf-8')}") -------------------------------------------------------------------------------- /Session_Redis/final/requirements.txt: -------------------------------------------------------------------------------- 1 | pip install redis==5.0.8 -------------------------------------------------------------------------------- /Session_Redis/final/schemas/auth_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class UserData(BaseModel): 4 | id: int 5 | name: str 6 | email: str 7 | 8 | class UserDataPASS(UserData): 9 | hashed_password: str 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Session_Redis/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | class BlogData(BaseModel): 18 | id: int 19 | title: str 20 | author_id: int 21 | author: str | None = None 22 | email: str | None = None 23 | content: str 24 | modified_dt: datetime 25 | image_loc: str | None = None 26 | -------------------------------------------------------------------------------- /Session_Redis/final/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/final/static/default/blog_default.png -------------------------------------------------------------------------------- /Session_Redis/final/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/final/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Session_Redis/final/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/final/static/sample_images/irises.png -------------------------------------------------------------------------------- /Session_Redis/final/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/final/static/sample_images/party.png -------------------------------------------------------------------------------- /Session_Redis/final/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/final/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Session_Redis/final/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Session_Redis/final/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Session_Redis/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Session_Redis/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Session_Redis/final/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Session_Redis/final/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Session_Redis/final/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Session_Redis/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Session_Redis/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | SECRET_KEY = "your_unique_secret_key" 4 | -------------------------------------------------------------------------------- /Session_Redis/start/redis_test.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | #localhost, post=6379, db=0으로 접속 (default port is 6379) and use database 0 4 | r_client = redis.Redis(host='localhost', port=6379, db=0) 5 | 6 | # db=0 key와 value 설정한 뒤 해당 key값으로 값 추출. 7 | r_client.set('name', 'FastAPI') 8 | name_value = r_client.get('name') 9 | print(f"Value for 'name' in db 0: {name_value.decode('utf-8')}") 10 | 11 | # db1에 접속 12 | r_client_db1 = redis.Redis(host='localhost', port=6379, db=1) 13 | 14 | # db=1 key와 value 설정한 뒤 해당 key값으로 값 추출. 15 | r_client_db1.set('name', 'Redis') 16 | name_value_db1 = r_client_db1.get('name') 17 | print(f"Value for 'name' in db 1: {name_value_db1.decode('utf-8')}") 18 | 19 | # Connection Pool 기반 access 20 | redis_pool = redis.ConnectionPool(host='localhost', port=6379, db=0, max_connections=10) 21 | redis_client_pool = redis.Redis(connection_pool=redis_pool) 22 | 23 | redis_client_pool.set('name', 'FastAPI') 24 | name_value = redis_client_pool.get('name') 25 | print(f"Value for 'name' in db 0: {name_value.decode('utf-8')}") -------------------------------------------------------------------------------- /Session_Redis/start/requirements.txt: -------------------------------------------------------------------------------- 1 | pip install redis==5.0.8 -------------------------------------------------------------------------------- /Session_Redis/start/schemas/auth_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class UserData(BaseModel): 4 | id: int 5 | name: str 6 | email: str 7 | 8 | class UserDataPASS(UserData): 9 | hashed_password: str 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Session_Redis/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | class BlogData(BaseModel): 18 | id: int 19 | title: str 20 | author_id: int 21 | author: str | None = None 22 | email: str | None = None 23 | content: str 24 | modified_dt: datetime 25 | image_loc: str | None = None 26 | -------------------------------------------------------------------------------- /Session_Redis/start/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/start/static/default/blog_default.png -------------------------------------------------------------------------------- /Session_Redis/start/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/start/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Session_Redis/start/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/start/static/sample_images/irises.png -------------------------------------------------------------------------------- /Session_Redis/start/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/start/static/sample_images/party.png -------------------------------------------------------------------------------- /Session_Redis/start/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Session_Redis/start/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Session_Redis/start/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Session_Redis/start/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Session_Redis/start/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Session_Redis/start/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Session_Redis/start/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Session_Redis/start/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Session_Redis/start/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Session_Redis/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Signed_Cookie/final/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | SECRET_KEY = "your_unique_secret_key" 4 | -------------------------------------------------------------------------------- /Signed_Cookie/final/create_key.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | 3 | # Generate a secure random key (e.g., 32 bytes) 4 | secret_key = secrets.token_hex(32) 5 | print(f"Your secret key: {secret_key}") -------------------------------------------------------------------------------- /Signed_Cookie/final/requirements.txt: -------------------------------------------------------------------------------- 1 | itsdangerous==2.2.0 -------------------------------------------------------------------------------- /Signed_Cookie/final/schemas/auth_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class UserData(BaseModel): 4 | id: int 5 | name: str 6 | email: str 7 | 8 | class UserDataPASS(UserData): 9 | hashed_password: str 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Signed_Cookie/final/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | class BlogData(BaseModel): 18 | id: int 19 | title: str 20 | author_id: int 21 | author: str | None = None 22 | email: str | None = None 23 | content: str 24 | modified_dt: datetime 25 | image_loc: str | None = None 26 | -------------------------------------------------------------------------------- /Signed_Cookie/final/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/final/static/default/blog_default.png -------------------------------------------------------------------------------- /Signed_Cookie/final/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/final/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Signed_Cookie/final/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/final/static/sample_images/irises.png -------------------------------------------------------------------------------- /Signed_Cookie/final/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/final/static/sample_images/party.png -------------------------------------------------------------------------------- /Signed_Cookie/final/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/final/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Signed_Cookie/final/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Signed_Cookie/final/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Signed_Cookie/final/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Signed_Cookie/final/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Signed_Cookie/final/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Signed_Cookie/final/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Signed_Cookie/final/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Signed_Cookie/final/utils/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | 4 | class DummyMiddleware(BaseHTTPMiddleware): 5 | async def dispatch(self, request: Request, call_next): 6 | print("### request info:", request.url, request.method) 7 | print("### request type:", type(request)) 8 | 9 | response = await call_next(request) 10 | return response 11 | 12 | class MethodOverrideMiddlware(BaseHTTPMiddleware): 13 | async def dispatch(self, request: Request, call_next): 14 | # print("#### request url, query_params, method", 15 | # request.url, request.query_params, request.method) 16 | if request.method == "POST": 17 | query = request.query_params 18 | if query: 19 | method_override = query["_method"] 20 | if method_override: 21 | method_override = method_override.upper() 22 | if method_override in ("PUT", "DELETE"): 23 | request.scope["method"] = method_override 24 | 25 | response = await call_next(request) 26 | return response 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Signed_Cookie/final/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Signed_Cookie/start/.env: -------------------------------------------------------------------------------- 1 | DATABASE_CONN = "mysql+aiomysql://root:root1234@localhost:3306/blog_db" 2 | UPLOAD_DIR = "./static/uploads" 3 | -------------------------------------------------------------------------------- /Signed_Cookie/start/requirements.txt: -------------------------------------------------------------------------------- 1 | itsdangerous==2.2.0 -------------------------------------------------------------------------------- /Signed_Cookie/start/schemas/auth_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class UserData(BaseModel): 4 | id: int 5 | name: str 6 | email: str 7 | 8 | class UserDataPASS(UserData): 9 | hashed_password: str 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Signed_Cookie/start/schemas/blog_schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from datetime import datetime 3 | from typing import Optional, Annotated 4 | from pydantic.dataclasses import dataclass 5 | 6 | class BlogInput(BaseModel): 7 | title: str = Field(..., min_length=2, max_length=200) 8 | author: str = Field(..., max_length=100) 9 | content: str = Field(..., min_length=2, max_length=4000) 10 | image_loc: Optional[str] = Field(None, max_length=400) 11 | #image_loc: Annotated[str, Field(None, max_length=400)] = None 12 | 13 | class Blog(BlogInput): 14 | id: int 15 | modified_dt: datetime 16 | 17 | @dataclass 18 | class BlogData: 19 | id: int 20 | title: str 21 | author: str 22 | content: str 23 | modified_dt: datetime 24 | image_loc: str | None = None -------------------------------------------------------------------------------- /Signed_Cookie/start/static/default/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/default/blog_default.png -------------------------------------------------------------------------------- /Signed_Cookie/start/static/sample_images/blog_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/sample_images/blog_default.png -------------------------------------------------------------------------------- /Signed_Cookie/start/static/sample_images/irises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/sample_images/irises.png -------------------------------------------------------------------------------- /Signed_Cookie/start/static/sample_images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/sample_images/party.png -------------------------------------------------------------------------------- /Signed_Cookie/start/static/sample_images/starry_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/sample_images/starry_night.png -------------------------------------------------------------------------------- /Signed_Cookie/start/static/uploads/권철민/irises_1724384870.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/uploads/권철민/irises_1724384870.png -------------------------------------------------------------------------------- /Signed_Cookie/start/static/uploads/권철민/party_1724306758.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/uploads/권철민/party_1724306758.png -------------------------------------------------------------------------------- /Signed_Cookie/start/static/uploads/권철민/party_1724384805.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulminkw/fastapi_pguide/8f947dcb88c90f0022fc6ee72f003adf5005b670/Signed_Cookie/start/static/uploads/권철민/party_1724384805.png -------------------------------------------------------------------------------- /Signed_Cookie/start/templates/http_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 블로그 Home으로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Signed_Cookie/start/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "/layout/main_layout.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 | 7 |
8 | {% for blog in all_blogs %} 9 |
10 |

{{ blog.title }}

11 |

Posted on {{ blog.modified_dt }} by {{ blog.author }}

12 |
13 |
14 |

{{ blog.content | safe }}

15 |
16 |
17 | Blog Image 18 |
19 |
20 | Read More 21 |
22 | {% endfor %} 23 |
24 |
25 |
26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /Signed_Cookie/start/templates/layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Signed_Cookie/start/templates/layout/main_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Blog UI 7 | 8 | 9 | 10 | {% include '/layout/navbar.html' %} 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | {% include '/layout/footer.html' %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Signed_Cookie/start/templates/layout/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 23 | -------------------------------------------------------------------------------- /Signed_Cookie/start/templates/validation_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Page 7 | 8 | 9 | 10 | 11 | 12 |
13 |

{{ status_code }}

14 |

{{ title_message }}

15 |

16 | {{ detail }} 17 |

18 | 이전 페이지로 돌아가기 19 |
20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Signed_Cookie/start/utils/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from db.database import engine 3 | from contextlib import asynccontextmanager 4 | 5 | @asynccontextmanager 6 | async def lifespan(app: FastAPI): 7 | # FastAPI 인스턴스 기동시 필요한 작업 수행. 8 | print("Starting up...") 9 | yield 10 | 11 | #FastAPI 인스턴스 종료시 필요한 작업 수행 12 | print("Shutting down...") 13 | await engine.dispose() 14 | -------------------------------------------------------------------------------- /Signed_Cookie/start/utils/exc_handler.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, status 2 | from starlette.exceptions import HTTPException as StarletteHTTPException 3 | from fastapi.templating import Jinja2Templates 4 | from fastapi.exceptions import RequestValidationError 5 | 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | async def custom_http_exception_handler(request: Request, exc: StarletteHTTPException): 9 | return templates.TemplateResponse( 10 | request = request, 11 | name="http_error.html", 12 | context={ 13 | "status_code": exc.status_code, 14 | "title_message": "불편을 드려 죄송합니다.", 15 | "detail": exc.detail 16 | }, 17 | status_code=exc.status_code 18 | ) 19 | 20 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 21 | return templates.TemplateResponse( 22 | request=request, 23 | name="validation_error.html", 24 | context = { 25 | "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, 26 | "title_message": "잘못된 값을 입력하였습니다. 제목은 최소 2자 이상, 200자 미만. 내용은 최소 2자 이상, 4000자 미만입니다.", 27 | "detail": exc.errors() 28 | }, 29 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY 30 | ) 31 | -------------------------------------------------------------------------------- /Signed_Cookie/start/utils/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | 4 | class DummyMiddleware(BaseHTTPMiddleware): 5 | async def dispatch(self, request: Request, call_next): 6 | print("### request info:", request.url, request.method) 7 | print("### request type:", type(request)) 8 | 9 | response = await call_next(request) 10 | return response 11 | 12 | class MethodOverrideMiddlware(BaseHTTPMiddleware): 13 | async def dispatch(self, request: Request, call_next): 14 | # print("#### request url, query_params, method", 15 | # request.url, request.query_params, request.method) 16 | if request.method == "POST": 17 | query = request.query_params 18 | if query: 19 | method_override = query["_method"] 20 | if method_override: 21 | method_override = method_override.upper() 22 | if method_override in ("PUT", "DELETE"): 23 | request.scope["method"] = method_override 24 | 25 | response = await call_next(request) 26 | return response 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Signed_Cookie/start/utils/util.py: -------------------------------------------------------------------------------- 1 | def truncate_text(text, limit=150) -> str: 2 | if text is not None: 3 | if len(text) > limit: 4 | truncated_text = text[:limit] + "...." 5 | else: 6 | truncated_text = text 7 | return truncated_text 8 | return None 9 | 10 | def newline_to_br(text_newline: str) -> str: 11 | if text_newline is not None: 12 | return text_newline.replace('\n', '
') 13 | return None 14 | 15 | def none_to_null(text, is_squote=False): 16 | if text is None: 17 | return "Null" 18 | else: 19 | if is_squote: 20 | return f"'{text}'" 21 | else: 22 | return text 23 | -------------------------------------------------------------------------------- /Templates/final/main_static.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import HTMLResponse 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.templating import Jinja2Templates 5 | 6 | app = FastAPI() 7 | 8 | # /static은 url path, StaticFiles의 directory는 directory명, name은 url_for등에서 참조하는 이름 9 | app.mount("/static", StaticFiles(directory="static"), name="static") 10 | 11 | # jinja2 Template 생성. 인자로 directory 입력 12 | templates = Jinja2Templates(directory="templates") 13 | 14 | @app.get("/items/{id}", response_class=HTMLResponse) 15 | async def read_item(request: Request, id: str, q: str | None = None): 16 | html_name = "item_static.html" 17 | #html_name = "item_urlfor.html" 18 | return templates.TemplateResponse( 19 | request=request, name=html_name, context={"id": id, "q_str": q} 20 | ) 21 | -------------------------------------------------------------------------------- /Templates/final/static/css/styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: green; 3 | } -------------------------------------------------------------------------------- /Templates/final/static/link_tp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | link tp 4 | 5 | 6 | 7 | 8 |

Another Result

9 | 10 | -------------------------------------------------------------------------------- /Templates/final/templates/item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Item Details 4 | 5 | 6 |

Item id: {{ id }}

7 |

query: {{ q_str}}

8 |

{{item}}

9 |
item name: {{item.name}}, item price: {{item.price}}
10 |

item_dict[name]: {{item_dict['name']}}

11 | 12 | -------------------------------------------------------------------------------- /Templates/final/templates/item_all.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Item Details 4 | 5 | 6 | {% for item in all_items %} 7 |

item name:{{ item.name }} item price: {{ item.price }}

8 | {% endfor %} 9 | 10 | -------------------------------------------------------------------------------- /Templates/final/templates/item_gubun.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Item Details 4 | 5 | 6 | {% if gubun == "admin" %} 7 |

이것은 어드민용 item입니다.

8 | {% else %} 9 |

이것은 일반용 item 입니다.

10 | {% endif %} 11 |

{{item}}

12 | 13 | -------------------------------------------------------------------------------- /Templates/final/templates/item_static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Item Details 4 | 5 | 6 | 7 |

Item id: {{ id }}

8 |

another link

9 | 10 | -------------------------------------------------------------------------------- /Templates/final/templates/item_urlfor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Item Details 4 | 5 | 6 | 7 |

Item id: {{ id }}

8 |

another link

9 | 10 | -------------------------------------------------------------------------------- /Templates/final/templates/read_safe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

우리 상품은

4 | {{ html_str | safe }} 5 | 6 | -------------------------------------------------------------------------------- /Templates/start/main_static.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import HTMLResponse 3 | from fastapi.staticfiles import StaticFiles 4 | 5 | from fastapi.templating import Jinja2Templates 6 | 7 | app = FastAPI() 8 | # /static은 url path, StaticFiles의 directory는 directory명, name은 url_for등에서 참조하는 이름 9 | app.mount("/static", StaticFiles(directory="static_other"), name="static") 10 | 11 | # jinja2 Template 생성. 인자로 directory 입력 12 | templates = Jinja2Templates(directory="templates") 13 | 14 | @app.get("/items/{id}", response_class=HTMLResponse) 15 | async def read_item(request: Request, id: str, q: str | None = None): 16 | html_name = "item_static.html" 17 | #html_name = "item_urlfor.html" 18 | return templates.TemplateResponse( 19 | request=request, name=html_name, context={"id": id, "q_str": q} 20 | ) 21 | -------------------------------------------------------------------------------- /Welcome/main.py: -------------------------------------------------------------------------------- 1 | # FastAPI import 2 | from fastapi import FastAPI 3 | 4 | # FastAPI instance 생성. 5 | app = FastAPI() 6 | 7 | # Path 오퍼레이션 생성. Path는 도메인명을 제외하고 / 로 시작하는 URL 부분 8 | # 만약 url이 https://example.com/items/foo 라면 path는 /items/foo 9 | # Operation은 GET, POST, PUT/PATCH, DELETE등의 HTTP 메소드임. 10 | @app.get("/") 11 | async def root(): 12 | return {"message": "Hello World"} 13 | 14 | --------------------------------------------------------------------------------