├── .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 |

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 |

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 |

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 |
9 | {% for blog in all_blogs %}
10 |
11 |
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 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Blog_Bootstrap/start/templates/new_blog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 새로운 블로그 생성
4 |
5 |
6 |
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 |
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 |
9 | {% for blog in all_blogs %}
10 |
11 |
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 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Blog_DB_Handling/final/templates/new_blog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 새로운 블로그 생성
4 |
5 |
6 |
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 |
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 |
9 | {% for blog in all_blogs %}
10 |
11 |
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 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Blog_MVC/final/templates/new_blog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 새로운 블로그 생성
4 |
5 |
6 |
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 |
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 |
9 | {% for blog in all_blogs %}
10 |
11 |
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 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Blog_MVC/start/templates/new_blog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 새로운 블로그 생성
4 |
5 |
6 |
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 |
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 |

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 |

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 |

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 |

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 |

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 |

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 |
9 |
10 |
--------------------------------------------------------------------------------
/Templates/final/templates/item_urlfor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Item Details
4 |
5 |
6 |
7 | Item id: {{ id }}
8 |
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 |
--------------------------------------------------------------------------------