Залежності з yield¶
🌐 Переклад ШІ та людьми
Цей переклад виконано ШІ під керівництвом людей. 🤝
Можливі помилки через неправильне розуміння початкового змісту або неприродні формулювання тощо. 🤖
Ви можете покращити цей переклад, допомігши нам краще спрямовувати AI LLM.
FastAPI підтримує залежності, які виконують деякі додаткові кроки після завершення.
Щоб це зробити, використовуйте yield замість return і напишіть додаткові кроки (код) після нього.
Порада
Переконайтесь, що ви використовуєте yield лише один раз на залежність.
Технічні деталі
Будь-яка функція, яку можна використовувати з:
буде придатною як залежність у FastAPI.
Насправді FastAPI використовує ці два декоратори внутрішньо.
Залежність бази даних з yield¶
Наприклад, ви можете використати це, щоб створити сесію бази даних і закрити її після завершення.
Перед створенням відповіді виконується лише код до і включно з оператором yield:
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
Значення, передане yield, впроваджується в операції шляху та інші залежності:
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
Код після оператора yield виконується після відповіді:
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
Порада
Можете використовувати як async, так і звичайні функції.
FastAPI зробить усе правильно з кожною з них, так само як і зі звичайними залежностями.
Залежність з yield та try¶
Якщо ви використовуєте блок try в залежності з yield, ви отримаєте будь-який виняток, який був згенерований під час використання залежності.
Наприклад, якщо якийсь код десь посередині, в іншій залежності або в операції шляху, зробив «rollback» транзакції бази даних або створив будь-який інший виняток, ви отримаєте цей виняток у своїй залежності.
Тож ви можете обробити цей конкретний виняток усередині залежності за допомогою except SomeException.
Так само ви можете використовувати finally, щоб гарантувати виконання завершальних кроків незалежно від того, був виняток чи ні.
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
Підзалежності з yield¶
Ви можете мати підзалежності та «дерева» підзалежностей будь-якого розміру і форми, і будь-яка або всі з них можуть використовувати yield.
FastAPI гарантує, що «exit code» у кожній залежності з yield буде виконано в правильному порядку.
Наприклад, dependency_c може залежати від dependency_b, а dependency_b - від dependency_a:
from typing import Annotated
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
І всі вони можуть використовувати yield.
У цьому випадку dependency_c, щоб виконати свій завершальний код, потребує, щоб значення з dependency_b (тут dep_b) все ще було доступним.
І, у свою чергу, dependency_b потребує, щоб значення з dependency_a (тут dep_a) було доступним для свого завершального коду.
from typing import Annotated
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
Так само ви можете мати деякі залежності з yield, а інші - з return, і частина з них може залежати від інших.
І ви можете мати одну залежність, яка вимагає кілька інших залежностей з yield тощо.
Ви можете мати будь-які комбінації залежностей, які вам потрібні.
FastAPI подбає, щоб усе виконувалося в правильному порядку.
Технічні деталі
Це працює завдяки Менеджерам контексту Python.
FastAPI використовує їх внутрішньо, щоб досягти цього.
Залежності з yield та HTTPException¶
Ви бачили, що можна використовувати залежності з yield і мати блоки try, які намагаються виконати деякий код, а потім запускають завершальний код після finally.
Також можна використовувати except, щоб перехопити згенерований виняток і щось із ним зробити.
Наприклад, ви можете підняти інший виняток, як-от HTTPException.
Порада
Це доволі просунута техніка, і в більшості випадків вона вам не знадобиться, адже ви можете піднімати винятки (включно з HTTPException) всередині іншого коду вашого застосунку, наприклад, у функції операції шляху.
Але вона є, якщо вам це потрібно. 🤓
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
Якщо ви хочете перехоплювати винятки та створювати на їх основі користувацьку відповідь, створіть Користувацький обробник винятків.
Залежності з yield та except¶
Якщо ви перехоплюєте виняток за допомогою except у залежності з yield і не піднімаєте його знову (або не піднімаєте новий виняток), FastAPI не зможе помітити, що стався виняток, так само як це було б у звичайному Python:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
У цьому випадку клієнт побачить відповідь HTTP 500 Internal Server Error, як і має бути, з огляду на те, що ми не піднімаємо HTTPException або подібний виняток, але на сервері не буде жодних логів чи інших ознак того, що це була за помилка. 😱
Завжди використовуйте raise у залежностях з yield та except¶
Якщо ви перехоплюєте виняток у залежності з yield, якщо тільки ви не піднімаєте інший HTTPException або подібний, вам слід повторно підняти початковий виняток.
Ви можете повторно підняти той самий виняток, використовуючи raise:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
Тепер клієнт отримає ту саму відповідь HTTP 500 Internal Server Error, але сервер матиме наш користувацький InternalError у логах. 😎
Виконання залежностей з yield¶
Послідовність виконання приблизно така, як на цій діаграмі. Час тече зверху вниз. І кожна колонка - це одна з частин, що взаємодіють або виконують код.
sequenceDiagram
participant client as Client
participant handler as Exception handler
participant dep as Dep with yield
participant operation as Path Operation
participant tasks as Background tasks
Note over client,operation: Can raise exceptions, including HTTPException
client ->> dep: Start request
Note over dep: Run code up to yield
opt raise Exception
dep -->> handler: Raise Exception
handler -->> client: HTTP error response
end
dep ->> operation: Run dependency, e.g. DB session
opt raise
operation -->> dep: Raise Exception (e.g. HTTPException)
opt handle
dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception
end
handler -->> client: HTTP error response
end
operation ->> client: Return response to client
Note over client,operation: Response is already sent, can't change it anymore
opt Tasks
operation -->> tasks: Send background tasks
end
opt Raise other exception
tasks -->> tasks: Handle exceptions in the background task code
end
Інформація
Лише одна відповідь буде надіслана клієнту. Це може бути одна з помилкових відповідей або відповідь від операції шляху.
Після відправлення однієї з цих відповідей іншу відправити не можна.
Порада
Якщо ви піднімаєте будь-який виняток у коді з функції операції шляху, він буде переданий у залежності з yield, включно з HTTPException. У більшості випадків ви захочете повторно підняти той самий виняток або новий із залежності з yield, щоб переконатися, що його коректно оброблено.
Ранній вихід і scope¶
Зазвичай завершальний код залежностей з yield виконується після того, як відповідь надіслано клієнту.
Але якщо ви знаєте, що вам не потрібно використовувати залежність після повернення з функції операції шляху, ви можете використати Depends(scope="function"), щоб сказати FastAPI, що слід закрити залежність після повернення з функції операції шляху, але до надсилання відповіді.
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_username():
try:
yield "Rick"
finally:
print("Cleanup up before response is sent")
@app.get("/users/me")
def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]):
return username
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI
app = FastAPI()
def get_username():
try:
yield "Rick"
finally:
print("Cleanup up before response is sent")
@app.get("/users/me")
def get_user_me(username: str = Depends(get_username, scope="function")):
return username
Depends() приймає параметр scope, який може бути:
"function": запустити залежність перед функцією операції шляху, що обробляє запит, завершити залежність після завершення функції операції шляху, але до того, як відповідь буде відправлена клієнту. Тобто функція залежності буде виконуватися навколо функції операції шляху."request": запустити залежність перед функцією операції шляху, що обробляє запит (подібно до"function"), але завершити після того, як відповідь буде відправлена клієнту. Тобто функція залежності буде виконуватися навколо циклу запиту та відповіді.
Якщо не вказано, і залежність має yield, за замовчуванням scope дорівнює "request".
scope для підзалежностей¶
Коли ви оголошуєте залежність із scope="request" (за замовчуванням), будь-яка підзалежність також має мати scope рівний "request".
Але залежність з scope рівним "function" може мати залежності з scope "function" і scope "request".
Це тому, що будь-яка залежність має бути здатною виконати свій завершальний код раніше за підзалежності, оскільки вона може все ще потребувати їх під час свого завершального коду.
sequenceDiagram
participant client as Client
participant dep_req as Dep scope="request"
participant dep_func as Dep scope="function"
participant operation as Path Operation
client ->> dep_req: Start request
Note over dep_req: Run code up to yield
dep_req ->> dep_func: Pass dependency
Note over dep_func: Run code up to yield
dep_func ->> operation: Run path operation with dependency
operation ->> dep_func: Return from path operation
Note over dep_func: Run code after yield
Note over dep_func: ✅ Dependency closed
dep_func ->> client: Send response to client
Note over client: Response sent
Note over dep_req: Run code after yield
Note over dep_req: ✅ Dependency closed
Залежності з yield, HTTPException, except і фоновими задачами¶
Залежності з yield еволюціонували з часом, щоб покрити різні сценарії та виправити деякі проблеми.
Якщо ви хочете дізнатися, що змінювалося в різних версіях FastAPI, прочитайте про це в просунутому посібнику користувача: Розширені залежності - Залежності з yield, HTTPException, except і фоновими задачами.
Менеджери контексту¶
Що таке «Менеджери контексту»¶
«Менеджери контексту» - це будь-які Python-об'єкти, які можна використовувати в операторі with.
Наприклад, можна використати with, щоб прочитати файл:
with open("./somefile.txt") as f:
contents = f.read()
print(contents)
Під капотом open("./somefile.txt") створює об'єкт, який називається «Менеджер контексту».
Коли блок with завершується, він гарантує закриття файлу, навіть якщо були винятки.
Коли ви створюєте залежність з yield, FastAPI внутрішньо створить для неї менеджер контексту й поєднає його з іншими пов'язаними інструментами.
Використання менеджерів контексту в залежностях з yield¶
Попередження
Це, загалом, «просунута» ідея.
Якщо ви тільки починаєте з FastAPI, можливо, варто наразі пропустити це.
У Python ви можете створювати Менеджери контексту, створивши клас із двома методами: __enter__() і __exit__().
Ви також можете використовувати їх усередині залежностей FastAPI з yield, використовуючи
with або async with у середині функції залежності:
class MySuperContextManager:
def __init__(self):
self.db = DBSession()
def __enter__(self):
return self.db
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
async def get_db():
with MySuperContextManager() as db:
yield db
Порада
Інший спосіб створити менеджер контексту:
використовуючи їх для декорування функції з одним yield.
Саме це FastAPI використовує внутрішньо для залежностей з yield.
Але вам не потрібно використовувати ці декоратори для залежностей FastAPI (і не варто).
FastAPI зробить це за вас внутрішньо.