From d784a90207e6545a62d86b57c7a09a0c90e29bce Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 5 Jan 2025 14:44:44 +0000 Subject: [PATCH 001/123] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20Peewee?= =?UTF-8?q?=20tutorial=20files=20(#13158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql_databases_peewee/sql_app/__init__.py | 0 docs_src/sql_databases_peewee/sql_app/crud.py | 30 ------- .../sql_databases_peewee/sql_app/database.py | 24 ------ docs_src/sql_databases_peewee/sql_app/main.py | 79 ------------------- .../sql_databases_peewee/sql_app/models.py | 21 ----- .../sql_databases_peewee/sql_app/schemas.py | 49 ------------ 6 files changed, 203 deletions(-) delete mode 100644 docs_src/sql_databases_peewee/sql_app/__init__.py delete mode 100644 docs_src/sql_databases_peewee/sql_app/crud.py delete mode 100644 docs_src/sql_databases_peewee/sql_app/database.py delete mode 100644 docs_src/sql_databases_peewee/sql_app/main.py delete mode 100644 docs_src/sql_databases_peewee/sql_app/models.py delete mode 100644 docs_src/sql_databases_peewee/sql_app/schemas.py diff --git a/docs_src/sql_databases_peewee/sql_app/__init__.py b/docs_src/sql_databases_peewee/sql_app/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs_src/sql_databases_peewee/sql_app/crud.py b/docs_src/sql_databases_peewee/sql_app/crud.py deleted file mode 100644 index 56c587559..000000000 --- a/docs_src/sql_databases_peewee/sql_app/crud.py +++ /dev/null @@ -1,30 +0,0 @@ -from . import models, schemas - - -def get_user(user_id: int): - return models.User.filter(models.User.id == user_id).first() - - -def get_user_by_email(email: str): - return models.User.filter(models.User.email == email).first() - - -def get_users(skip: int = 0, limit: int = 100): - return list(models.User.select().offset(skip).limit(limit)) - - -def create_user(user: schemas.UserCreate): - fake_hashed_password = user.password + "notreallyhashed" - db_user = models.User(email=user.email, hashed_password=fake_hashed_password) - db_user.save() - return db_user - - -def get_items(skip: int = 0, limit: int = 100): - return list(models.Item.select().offset(skip).limit(limit)) - - -def create_user_item(item: schemas.ItemCreate, user_id: int): - db_item = models.Item(**item.dict(), owner_id=user_id) - db_item.save() - return db_item diff --git a/docs_src/sql_databases_peewee/sql_app/database.py b/docs_src/sql_databases_peewee/sql_app/database.py deleted file mode 100644 index 6938fe826..000000000 --- a/docs_src/sql_databases_peewee/sql_app/database.py +++ /dev/null @@ -1,24 +0,0 @@ -from contextvars import ContextVar - -import peewee - -DATABASE_NAME = "test.db" -db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} -db_state = ContextVar("db_state", default=db_state_default.copy()) - - -class PeeweeConnectionState(peewee._ConnectionState): - def __init__(self, **kwargs): - super().__setattr__("_state", db_state) - super().__init__(**kwargs) - - def __setattr__(self, name, value): - self._state.get()[name] = value - - def __getattr__(self, name): - return self._state.get()[name] - - -db = peewee.SqliteDatabase(DATABASE_NAME, check_same_thread=False) - -db._state = PeeweeConnectionState() diff --git a/docs_src/sql_databases_peewee/sql_app/main.py b/docs_src/sql_databases_peewee/sql_app/main.py deleted file mode 100644 index 8fbd2075d..000000000 --- a/docs_src/sql_databases_peewee/sql_app/main.py +++ /dev/null @@ -1,79 +0,0 @@ -import time -from typing import List - -from fastapi import Depends, FastAPI, HTTPException - -from . import crud, database, models, schemas -from .database import db_state_default - -database.db.connect() -database.db.create_tables([models.User, models.Item]) -database.db.close() - -app = FastAPI() - -sleep_time = 10 - - -async def reset_db_state(): - database.db._state._state.set(db_state_default.copy()) - database.db._state.reset() - - -def get_db(db_state=Depends(reset_db_state)): - try: - database.db.connect() - yield - finally: - if not database.db.is_closed(): - database.db.close() - - -@app.post("/users/", response_model=schemas.User, dependencies=[Depends(get_db)]) -def create_user(user: schemas.UserCreate): - db_user = crud.get_user_by_email(email=user.email) - if db_user: - raise HTTPException(status_code=400, detail="Email already registered") - return crud.create_user(user=user) - - -@app.get("/users/", response_model=List[schemas.User], dependencies=[Depends(get_db)]) -def read_users(skip: int = 0, limit: int = 100): - users = crud.get_users(skip=skip, limit=limit) - return users - - -@app.get( - "/users/{user_id}", response_model=schemas.User, dependencies=[Depends(get_db)] -) -def read_user(user_id: int): - db_user = crud.get_user(user_id=user_id) - if db_user is None: - raise HTTPException(status_code=404, detail="User not found") - return db_user - - -@app.post( - "/users/{user_id}/items/", - response_model=schemas.Item, - dependencies=[Depends(get_db)], -) -def create_item_for_user(user_id: int, item: schemas.ItemCreate): - return crud.create_user_item(item=item, user_id=user_id) - - -@app.get("/items/", response_model=List[schemas.Item], dependencies=[Depends(get_db)]) -def read_items(skip: int = 0, limit: int = 100): - items = crud.get_items(skip=skip, limit=limit) - return items - - -@app.get( - "/slowusers/", response_model=List[schemas.User], dependencies=[Depends(get_db)] -) -def read_slow_users(skip: int = 0, limit: int = 100): - global sleep_time - sleep_time = max(0, sleep_time - 1) - time.sleep(sleep_time) # Fake long processing request - users = crud.get_users(skip=skip, limit=limit) - return users diff --git a/docs_src/sql_databases_peewee/sql_app/models.py b/docs_src/sql_databases_peewee/sql_app/models.py deleted file mode 100644 index 46bdcd009..000000000 --- a/docs_src/sql_databases_peewee/sql_app/models.py +++ /dev/null @@ -1,21 +0,0 @@ -import peewee - -from .database import db - - -class User(peewee.Model): - email = peewee.CharField(unique=True, index=True) - hashed_password = peewee.CharField() - is_active = peewee.BooleanField(default=True) - - class Meta: - database = db - - -class Item(peewee.Model): - title = peewee.CharField(index=True) - description = peewee.CharField(index=True) - owner = peewee.ForeignKeyField(User, backref="items") - - class Meta: - database = db diff --git a/docs_src/sql_databases_peewee/sql_app/schemas.py b/docs_src/sql_databases_peewee/sql_app/schemas.py deleted file mode 100644 index d8775cb30..000000000 --- a/docs_src/sql_databases_peewee/sql_app/schemas.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Any, List, Union - -import peewee -from pydantic import BaseModel -from pydantic.utils import GetterDict - - -class PeeweeGetterDict(GetterDict): - def get(self, key: Any, default: Any = None): - res = getattr(self._obj, key, default) - if isinstance(res, peewee.ModelSelect): - return list(res) - return res - - -class ItemBase(BaseModel): - title: str - description: Union[str, None] = None - - -class ItemCreate(ItemBase): - pass - - -class Item(ItemBase): - id: int - owner_id: int - - class Config: - orm_mode = True - getter_dict = PeeweeGetterDict - - -class UserBase(BaseModel): - email: str - - -class UserCreate(UserBase): - password: str - - -class User(UserBase): - id: int - is_active: bool - items: List[Item] = [] - - class Config: - orm_mode = True - getter_dict = PeeweeGetterDict From 6e60d0a0563497f134088b9d161611367b95d21d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 5 Jan 2025 14:45:08 +0000 Subject: [PATCH 002/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9b6cae978..cb0eda45d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 🔥 Remove unused Peewee tutorial files. PR [#13158](https://github.com/fastapi/fastapi/pull/13158) by [@alejsdev](https://github.com/alejsdev). * 📝 Update image in body-nested-model docs. PR [#11063](https://github.com/fastapi/fastapi/pull/11063) by [@untilhamza](https://github.com/untilhamza). * 📝 Update `fastapi-cli` UI examples in docs. PR [#13107](https://github.com/fastapi/fastapi/pull/13107) by [@Zhongheng-Cheng](https://github.com/Zhongheng-Cheng). * 👷 Add new GitHub Action to update contributors, translators, and translation reviewers. PR [#13136](https://github.com/fastapi/fastapi/pull/13136) by [@tiangolo](https://github.com/tiangolo). From b0e70cb37e2fe9446f51443d88cb053da91748ce Mon Sep 17 00:00:00 2001 From: Kinuax Date: Mon, 6 Jan 2025 12:24:17 +0100 Subject: [PATCH 003/123] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Update=20Strawberr?= =?UTF-8?q?y=20integration=20docs=20(#13155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/how-to/graphql.md | 2 +- docs_src/graphql/tutorial001.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/how-to/graphql.md b/docs/en/docs/how-to/graphql.md index a6219e481..361010736 100644 --- a/docs/en/docs/how-to/graphql.md +++ b/docs/en/docs/how-to/graphql.md @@ -35,7 +35,7 @@ Depending on your use case, you might prefer to use a different library, but if Here's a small preview of how you could integrate Strawberry with FastAPI: -{* ../../docs_src/graphql/tutorial001.py hl[3,22,25:26] *} +{* ../../docs_src/graphql/tutorial001.py hl[3,22,25] *} You can learn more about Strawberry in the Strawberry documentation. diff --git a/docs_src/graphql/tutorial001.py b/docs_src/graphql/tutorial001.py index 3b4ca99cf..e92b2d71c 100644 --- a/docs_src/graphql/tutorial001.py +++ b/docs_src/graphql/tutorial001.py @@ -1,6 +1,6 @@ import strawberry from fastapi import FastAPI -from strawberry.asgi import GraphQL +from strawberry.fastapi import GraphQLRouter @strawberry.type @@ -19,8 +19,7 @@ class Query: schema = strawberry.Schema(query=Query) -graphql_app = GraphQL(schema) +graphql_app = GraphQLRouter(schema) app = FastAPI() -app.add_route("/graphql", graphql_app) -app.add_websocket_route("/graphql", graphql_app) +app.include_router(graphql_app, prefix="/graphql") From d7bd68979f9db9dc3b6a59dc3b8ba46b47580dbc Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 6 Jan 2025 11:24:40 +0000 Subject: [PATCH 004/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index cb0eda45d..520b0f9e6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* ✏️ Update Strawberry integration docs. PR [#13155](https://github.com/fastapi/fastapi/pull/13155) by [@kinuax](https://github.com/kinuax). * 🔥 Remove unused Peewee tutorial files. PR [#13158](https://github.com/fastapi/fastapi/pull/13158) by [@alejsdev](https://github.com/alejsdev). * 📝 Update image in body-nested-model docs. PR [#11063](https://github.com/fastapi/fastapi/pull/11063) by [@untilhamza](https://github.com/untilhamza). * 📝 Update `fastapi-cli` UI examples in docs. PR [#13107](https://github.com/fastapi/fastapi/pull/13107) by [@Zhongheng-Cheng](https://github.com/Zhongheng-Cheng). From 4cd5a7ac1c8f37951cec2caf4ad150aa75c112d8 Mon Sep 17 00:00:00 2001 From: Yaroslav Luchinsky <61277193+Yarous@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:26:39 +0300 Subject: [PATCH 005/123] =?UTF-8?q?=F0=9F=8C=90=20Update=20Russian=20trans?= =?UTF-8?q?lation=20for=20`docs/ru/docs/tutorial/security/first-steps.md`?= =?UTF-8?q?=20(#13159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/security/first-steps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ru/docs/tutorial/security/first-steps.md b/docs/ru/docs/tutorial/security/first-steps.md index e55f48b89..375c2d7f6 100644 --- a/docs/ru/docs/tutorial/security/first-steps.md +++ b/docs/ru/docs/tutorial/security/first-steps.md @@ -182,7 +182,7 @@ oauth2_scheme(some, parameters) Если он не видит заголовка `Authorization` или значение не имеет токена `Bearer`, то в ответ будет выдана ошибка с кодом состояния 401 (`UNAUTHORIZED`). -Для возврата ошибки даже не нужно проверять, существует ли токен. Вы можете быть уверены, что если ваша функция будет выполнилась, то в этом токене есть `строка`. +Для возврата ошибки даже не нужно проверять, существует ли токен. Вы можете быть уверены, что если ваша функция была выполнена, то в этом токене есть `строка`. Проверить это можно уже сейчас в интерактивной документации: From 144f09ea146b2cc026bf317f730aa0e0dbc3de24 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 6 Jan 2025 18:27:10 +0000 Subject: [PATCH 006/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 520b0f9e6..e3e07afb7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -27,6 +27,7 @@ hide: ### Translations +* 🌐 Update Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#13159](https://github.com/fastapi/fastapi/pull/13159) by [@Yarous](https://github.com/Yarous). * ✏️ Delete unnecessary backspace in `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#12238](https://github.com/fastapi/fastapi/pull/12238) by [@FakeDocument](https://github.com/FakeDocument). * 🌐 Update Chinese translation for `docs/zh/docs/fastapi-cli.md`. PR [#13102](https://github.com/fastapi/fastapi/pull/13102) by [@Zhongheng-Cheng](https://github.com/Zhongheng-Cheng). * 🌐 Add new Spanish translations for all docs with new LLM-assisted system using PydanticAI. PR [#13122](https://github.com/fastapi/fastapi/pull/13122) by [@tiangolo](https://github.com/tiangolo). From 44adb29ce18b5a720898ca69c2b0995170542da2 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:23:42 +0000 Subject: [PATCH 007/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20ba?= =?UTF-8?q?ckground=5Ftasks=20(#13166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_background_tasks/test_tutorial002.py | 23 ++++++++++++++++--- .../test_tutorial002_an.py | 19 --------------- .../test_tutorial002_an_py310.py | 21 ----------------- .../test_tutorial002_an_py39.py | 21 ----------------- .../test_tutorial002_py310.py | 21 ----------------- 5 files changed, 20 insertions(+), 85 deletions(-) delete mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_an.py delete mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py delete mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py delete mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002.py b/tests/test_tutorial/test_background_tasks/test_tutorial002.py index 74de1314b..d5ef51ee2 100644 --- a/tests/test_tutorial/test_background_tasks/test_tutorial002.py +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002.py @@ -1,14 +1,31 @@ +import importlib import os from pathlib import Path +import pytest from fastapi.testclient import TestClient -from docs_src.background_tasks.tutorial002 import app +from ...utils import needs_py39, needs_py310 -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + "tutorial002_an", + pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.background_tasks.{request.param}") -def test(): + client = TestClient(mod.app) + return client + + +def test(client: TestClient): log = Path("log.txt") if log.is_file(): os.remove(log) # pragma: no cover diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py deleted file mode 100644 index af682ecff..000000000 --- a/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py +++ /dev/null @@ -1,19 +0,0 @@ -import os -from pathlib import Path - -from fastapi.testclient import TestClient - -from docs_src.background_tasks.tutorial002_an import app - -client = TestClient(app) - - -def test(): - log = Path("log.txt") - if log.is_file(): - os.remove(log) # pragma: no cover - response = client.post("/send-notification/foo@example.com?q=some-query") - assert response.status_code == 200, response.text - assert response.json() == {"message": "Message sent"} - with open("./log.txt") as f: - assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py deleted file mode 100644 index 067b2787e..000000000 --- a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -from pathlib import Path - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@needs_py310 -def test(): - from docs_src.background_tasks.tutorial002_an_py310 import app - - client = TestClient(app) - log = Path("log.txt") - if log.is_file(): - os.remove(log) # pragma: no cover - response = client.post("/send-notification/foo@example.com?q=some-query") - assert response.status_code == 200, response.text - assert response.json() == {"message": "Message sent"} - with open("./log.txt") as f: - assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py deleted file mode 100644 index 06b5a2f57..000000000 --- a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -from pathlib import Path - -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@needs_py39 -def test(): - from docs_src.background_tasks.tutorial002_an_py39 import app - - client = TestClient(app) - log = Path("log.txt") - if log.is_file(): - os.remove(log) # pragma: no cover - response = client.post("/send-notification/foo@example.com?q=some-query") - assert response.status_code == 200, response.text - assert response.json() == {"message": "Message sent"} - with open("./log.txt") as f: - assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py deleted file mode 100644 index 6937c8fab..000000000 --- a/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -from pathlib import Path - -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@needs_py310 -def test(): - from docs_src.background_tasks.tutorial002_py310 import app - - client = TestClient(app) - log = Path("log.txt") - if log.is_file(): - os.remove(log) # pragma: no cover - response = client.post("/send-notification/foo@example.com?q=some-query") - assert response.status_code == 200, response.text - assert response.json() == {"message": "Message sent"} - with open("./log.txt") as f: - assert "found query: some-query\nmessage to foo@example.com" in f.read() From afd150228352ffc87d04ed841870dcab6936b4f1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Jan 2025 19:24:05 +0000 Subject: [PATCH 008/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e3e07afb7..bf95dbea9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for additional_status_codes. PR [#13149](https://github.com/fastapi/fastapi/pull/13149) by [@tiangolo](https://github.com/tiangolo). ### Docs From 5d3f45c2d47f04a7a8c793c13e0349803394e86b Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:25:01 +0000 Subject: [PATCH 009/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20bi?= =?UTF-8?q?gger=5Fapplications=20(#13167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_bigger_applications/test_main.py | 19 +- .../test_bigger_applications/test_main_an.py | 715 ----------------- .../test_main_an_py39.py | 742 ------------------ 3 files changed, 15 insertions(+), 1461 deletions(-) delete mode 100644 tests/test_tutorial/test_bigger_applications/test_main_an.py delete mode 100644 tests/test_tutorial/test_bigger_applications/test_main_an_py39.py diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index 35fdfa4a6..fe40fad7d 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,13 +1,24 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.bigger_applications.app.main import app +@pytest.fixture( + name="client", + params=[ + "app_an.main", + pytest.param("app_an_py39.main", marks=needs_py39), + "app.main", + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.bigger_applications.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py deleted file mode 100644 index 4e2e3e74d..000000000 --- a/tests/test_tutorial/test_bigger_applications/test_main_an.py +++ /dev/null @@ -1,715 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.bigger_applications.app_an.main import app - - client = TestClient(app) - return client - - -def test_users_token_jessica(client: TestClient): - response = client.get("/users?token=jessica") - assert response.status_code == 200 - assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] - - -def test_users_with_no_token(client: TestClient): - response = client.get("/users") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_users_foo_token_jessica(client: TestClient): - response = client.get("/users/foo?token=jessica") - assert response.status_code == 200 - assert response.json() == {"username": "foo"} - - -def test_users_foo_with_no_token(client: TestClient): - response = client.get("/users/foo") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_users_me_token_jessica(client: TestClient): - response = client.get("/users/me?token=jessica") - assert response.status_code == 200 - assert response.json() == {"username": "fakecurrentuser"} - - -def test_users_me_with_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_users_token_monica_with_no_jessica(client: TestClient): - response = client.get("/users?token=monica") - assert response.status_code == 400 - assert response.json() == {"detail": "No Jessica token provided"} - - -def test_items_token_jessica(client: TestClient): - response = client.get( - "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200 - assert response.json() == { - "plumbus": {"name": "Plumbus"}, - "gun": {"name": "Portal Gun"}, - } - - -def test_items_with_no_token_jessica(client: TestClient): - response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_items_plumbus_token_jessica(client: TestClient): - response = client.get( - "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200 - assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} - - -def test_items_bar_token_jessica(client: TestClient): - response = client.get( - "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 404 - assert response.json() == {"detail": "Item not found"} - - -def test_items_plumbus_with_no_token(client: TestClient): - response = client.get( - "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_items_with_invalid_token(client: TestClient): - response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) - assert response.status_code == 400 - assert response.json() == {"detail": "X-Token header invalid"} - - -def test_items_bar_with_invalid_token(client: TestClient): - response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) - assert response.status_code == 400 - assert response.json() == {"detail": "X-Token header invalid"} - - -def test_items_with_missing_x_token_header(client: TestClient): - response = client.get("/items?token=jessica") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_items_plumbus_with_missing_x_token_header(client: TestClient): - response = client.get("/items/plumbus?token=jessica") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_root_token_jessica(client: TestClient): - response = client.get("/?token=jessica") - assert response.status_code == 200 - assert response.json() == {"message": "Hello Bigger Applications!"} - - -def test_root_with_no_token(client: TestClient): - response = client.get("/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_put_no_header(client: TestClient): - response = client.put("/items/foo") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_put_invalid_header(client: TestClient): - response = client.put("/items/foo", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -def test_put(client: TestClient): - response = client.put( - "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} - - -def test_put_forbidden(client: TestClient): - response = client.put( - "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "You can only update the item: plumbus"} - - -def test_admin(client: TestClient): - response = client.post( - "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"message": "Admin getting schwifty"} - - -def test_admin_invalid_header(client: TestClient): - response = client.post("/admin/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/me": { - "get": { - "tags": ["users"], - "summary": "Read User Me", - "operationId": "read_user_me_users_me_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{username}": { - "get": { - "tags": ["users"], - "summary": "Read User", - "operationId": "read_user_users__username__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Username", "type": "string"}, - "name": "username", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "tags": ["items"], - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "tags": ["items", "custom"], - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "403": {"description": "Operation forbidden"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/admin/": { - "post": { - "tags": ["admin"], - "summary": "Update Admin", - "operationId": "update_admin_admin__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "418": {"description": "I'm a teapot"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py deleted file mode 100644 index 8c9e976df..000000000 --- a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py +++ /dev/null @@ -1,742 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.bigger_applications.app_an_py39.main import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_users_token_jessica(client: TestClient): - response = client.get("/users?token=jessica") - assert response.status_code == 200 - assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] - - -@needs_py39 -def test_users_with_no_token(client: TestClient): - response = client.get("/users") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_users_foo_token_jessica(client: TestClient): - response = client.get("/users/foo?token=jessica") - assert response.status_code == 200 - assert response.json() == {"username": "foo"} - - -@needs_py39 -def test_users_foo_with_no_token(client: TestClient): - response = client.get("/users/foo") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_users_me_token_jessica(client: TestClient): - response = client.get("/users/me?token=jessica") - assert response.status_code == 200 - assert response.json() == {"username": "fakecurrentuser"} - - -@needs_py39 -def test_users_me_with_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_users_token_monica_with_no_jessica(client: TestClient): - response = client.get("/users?token=monica") - assert response.status_code == 400 - assert response.json() == {"detail": "No Jessica token provided"} - - -@needs_py39 -def test_items_token_jessica(client: TestClient): - response = client.get( - "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200 - assert response.json() == { - "plumbus": {"name": "Plumbus"}, - "gun": {"name": "Portal Gun"}, - } - - -@needs_py39 -def test_items_with_no_token_jessica(client: TestClient): - response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_items_plumbus_token_jessica(client: TestClient): - response = client.get( - "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200 - assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} - - -@needs_py39 -def test_items_bar_token_jessica(client: TestClient): - response = client.get( - "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 404 - assert response.json() == {"detail": "Item not found"} - - -@needs_py39 -def test_items_plumbus_with_no_token(client: TestClient): - response = client.get( - "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_items_with_invalid_token(client: TestClient): - response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) - assert response.status_code == 400 - assert response.json() == {"detail": "X-Token header invalid"} - - -@needs_py39 -def test_items_bar_with_invalid_token(client: TestClient): - response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) - assert response.status_code == 400 - assert response.json() == {"detail": "X-Token header invalid"} - - -@needs_py39 -def test_items_with_missing_x_token_header(client: TestClient): - response = client.get("/items?token=jessica") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_items_plumbus_with_missing_x_token_header(client: TestClient): - response = client.get("/items/plumbus?token=jessica") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_root_token_jessica(client: TestClient): - response = client.get("/?token=jessica") - assert response.status_code == 200 - assert response.json() == {"message": "Hello Bigger Applications!"} - - -@needs_py39 -def test_root_with_no_token(client: TestClient): - response = client.get("/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_put_no_header(client: TestClient): - response = client.put("/items/foo") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_put_invalid_header(client: TestClient): - response = client.put("/items/foo", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -@needs_py39 -def test_put(client: TestClient): - response = client.put( - "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} - - -@needs_py39 -def test_put_forbidden(client: TestClient): - response = client.put( - "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "You can only update the item: plumbus"} - - -@needs_py39 -def test_admin(client: TestClient): - response = client.post( - "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"message": "Admin getting schwifty"} - - -@needs_py39 -def test_admin_invalid_header(client: TestClient): - response = client.post("/admin/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/me": { - "get": { - "tags": ["users"], - "summary": "Read User Me", - "operationId": "read_user_me_users_me_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{username}": { - "get": { - "tags": ["users"], - "summary": "Read User", - "operationId": "read_user_users__username__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Username", "type": "string"}, - "name": "username", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "tags": ["items"], - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "tags": ["items", "custom"], - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "403": {"description": "Operation forbidden"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/admin/": { - "post": { - "tags": ["admin"], - "summary": "Update Admin", - "operationId": "update_admin_admin__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "418": {"description": "I'm a teapot"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } From a8447c15e5193eb304cadaef114d2af3df25d2ce Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Jan 2025 19:25:29 +0000 Subject: [PATCH 010/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bf95dbea9..559ddc5d7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for bigger_applications. PR [#13167](https://github.com/fastapi/fastapi/pull/13167) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for additional_status_codes. PR [#13149](https://github.com/fastapi/fastapi/pull/13149) by [@tiangolo](https://github.com/tiangolo). From 0cc031f4779fbaf26e3bfa0394d95711f5675394 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:26:16 +0000 Subject: [PATCH 011/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20bo?= =?UTF-8?q?dy=20(#13168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_body/test_tutorial001.py | 17 +- .../test_body/test_tutorial001_py310.py | 498 ------------------ 2 files changed, 13 insertions(+), 502 deletions(-) delete mode 100644 tests/test_tutorial/test_body/test_tutorial001_py310.py diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 0d55d73eb..f8b5aee8d 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -1,15 +1,24 @@ +import importlib from unittest.mock import patch import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py310 -@pytest.fixture -def client(): - from docs_src.body.tutorial001 import app - client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py deleted file mode 100644 index 4b9c12806..000000000 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ /dev/null @@ -1,498 +0,0 @@ -from unittest.mock import patch - -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture -def client(): - from docs_src.body.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_body_float(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "price": 50.5}) - assert response.status_code == 200 - assert response.json() == { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - } - - -@needs_py310 -def test_post_with_str_float(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) - assert response.status_code == 200 - assert response.json() == { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - } - - -@needs_py310 -def test_post_with_str_float_description(client: TestClient): - response = client.post( - "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} - ) - assert response.status_code == 200 - assert response.json() == { - "name": "Foo", - "price": 50.5, - "description": "Some Foo", - "tax": None, - } - - -@needs_py310 -def test_post_with_str_float_description_tax(client: TestClient): - response = client.post( - "/items/", - json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, - ) - assert response.status_code == 200 - assert response.json() == { - "name": "Foo", - "price": 50.5, - "description": "Some Foo", - "tax": 0.3, - } - - -@needs_py310 -def test_post_with_only_name(client: TestClient): - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {"name": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py310 -def test_post_with_only_name_price(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "float_parsing", - "loc": ["body", "price"], - "msg": "Input should be a valid number, unable to parse string as a number", - "input": "twenty", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } - ) - - -@needs_py310 -def test_post_with_no_data(client: TestClient): - response = client.post("/items/", json={}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "name"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "price"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_post_with_none(client: TestClient): - response = client.post("/items/", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py310 -def test_post_broken_body(client: TestClient): - response = client.post( - "/items/", - headers={"content-type": "application/json"}, - content="{some broken json}", - ) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "json_invalid", - "loc": ["body", 1], - "msg": "JSON decode error", - "input": {}, - "ctx": { - "error": "Expecting property name enclosed in double quotes" - }, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } - ) - - -@needs_py310 -def test_post_form_for_json(client: TestClient): - response = client.post("/items/", data={"name": "Foo", "price": 50.5}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": "name=Foo&price=50.5", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) - - -@needs_py310 -def test_explicit_content_type(client: TestClient): - response = client.post( - "/items/", - content='{"name": "Foo", "price": 50.5}', - headers={"Content-Type": "application/json"}, - ) - assert response.status_code == 200, response.text - - -@needs_py310 -def test_geo_json(client: TestClient): - response = client.post( - "/items/", - content='{"name": "Foo", "price": 50.5}', - headers={"Content-Type": "application/geo+json"}, - ) - assert response.status_code == 200, response.text - - -@needs_py310 -def test_no_content_type_is_json(client: TestClient): - response = client.post( - "/items/", - content='{"name": "Foo", "price": 50.5}', - ) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "price": 50.5, - "tax": None, - } - - -@needs_py310 -def test_wrong_headers(client: TestClient): - data = '{"name": "Foo", "price": 50.5}' - response = client.post( - "/items/", content=data, headers={"Content-Type": "text/plain"} - ) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) - - response = client.post( - "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} - ) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) - response = client.post( - "/items/", content=data, headers={"Content-Type": "application/not-really-json"} - ) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "model_attributes_type", - "loc": ["body"], - "msg": "Input should be a valid dictionary or object to extract fields from", - "input": '{"name": "Foo", "price": 50.5}', - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - ) - - -@needs_py310 -def test_other_exceptions(client: TestClient): - with patch("json.loads", side_effect=Exception): - response = client.post("/items/", json={"test": "test2"}) - assert response.status_code == 400, response.text - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From fe4b25e2d7c01925fc1b9bd4504b99452f436139 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Jan 2025 19:26:39 +0000 Subject: [PATCH 012/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 559ddc5d7..4897b4022 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for body. PR [#13168](https://github.com/fastapi/fastapi/pull/13168) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for bigger_applications. PR [#13167](https://github.com/fastapi/fastapi/pull/13167) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for additional_status_codes. PR [#13149](https://github.com/fastapi/fastapi/pull/13149) by [@tiangolo](https://github.com/tiangolo). From 9b88c7c18a0019a729945b3d5c7e1bf6f0b1e139 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:28:44 +0000 Subject: [PATCH 013/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20bo?= =?UTF-8?q?dy=5Ffields=20(#13169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_body_fields/test_tutorial001.py | 21 +- .../test_body_fields/test_tutorial001_an.py | 203 ----------------- .../test_tutorial001_an_py310.py | 209 ------------------ .../test_tutorial001_an_py39.py | 209 ------------------ .../test_tutorial001_py310.py | 209 ------------------ 5 files changed, 17 insertions(+), 834 deletions(-) delete mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py delete mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_py310.py diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index fd6139eb9..fb68f2868 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -1,13 +1,26 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39, needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_fields.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_fields.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py deleted file mode 100644 index 72c18c1f7..000000000 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py +++ /dev/null @@ -1,203 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_fields.tutorial001_an import app - - client = TestClient(app) - return client - - -def test_items_5(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - } - - -def test_items_6(client: TestClient): - response = client.put( - "/items/6", - json={ - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", - } - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - } - - -def test_invalid_price(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", "item", "price"], - "msg": "Input should be greater than 0", - "input": -3.0, - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "The description of the item", - "anyOf": [ - {"maxLength": 300, "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - } - ), - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py deleted file mode 100644 index 1bc62868f..000000000 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py +++ /dev/null @@ -1,209 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_fields.tutorial001_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_items_5(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - } - - -@needs_py310 -def test_items_6(client: TestClient): - response = client.put( - "/items/6", - json={ - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", - } - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - } - - -@needs_py310 -def test_invalid_price(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", "item", "price"], - "msg": "Input should be greater than 0", - "input": -3.0, - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "The description of the item", - "anyOf": [ - {"maxLength": 300, "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - } - ), - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py deleted file mode 100644 index 3c5557a1b..000000000 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py +++ /dev/null @@ -1,209 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_fields.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_items_5(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - } - - -@needs_py39 -def test_items_6(client: TestClient): - response = client.put( - "/items/6", - json={ - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", - } - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - } - - -@needs_py39 -def test_invalid_price(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", "item", "price"], - "msg": "Input should be greater than 0", - "input": -3.0, - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "The description of the item", - "anyOf": [ - {"maxLength": 300, "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - } - ), - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py deleted file mode 100644 index 8c1386aa6..000000000 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py +++ /dev/null @@ -1,209 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_fields.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_items_5(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - } - - -@needs_py310 -def test_items_6(client: TestClient): - response = client.put( - "/items/6", - json={ - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", - } - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - } - - -@needs_py310 -def test_invalid_price(client: TestClient): - response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "greater_than", - "loc": ["body", "item", "price"], - "msg": "Input should be greater than 0", - "input": -3.0, - "ctx": {"gt": 0.0}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "The description of the item", - "anyOf": [ - {"maxLength": 300, "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - } - ), - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From 3d2ef237edf99cff95e68ff9245f8b823d322a69 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Jan 2025 19:29:11 +0000 Subject: [PATCH 014/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4897b4022..3245bd8c4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for body_fields. PR [#13169](https://github.com/fastapi/fastapi/pull/13169) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body. PR [#13168](https://github.com/fastapi/fastapi/pull/13168) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for bigger_applications. PR [#13167](https://github.com/fastapi/fastapi/pull/13167) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). From 9d293b7086064e2e20b2fff0c3c472eba79137bd Mon Sep 17 00:00:00 2001 From: nillvitor <32855855+nillvitor@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:41:07 -0300 Subject: [PATCH 015/123] =?UTF-8?q?=F0=9F=8C=90=20Update=20Portuguese=20tr?= =?UTF-8?q?anslations=20(#13156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/deployment/manually.md | 66 +++++++---------- docs/pt/docs/deployment/server-workers.md | 87 ++++++++++------------- docs/pt/docs/tutorial/first-steps.md | 72 ++++++++----------- docs/pt/docs/tutorial/index.md | 62 +++++++++------- 4 files changed, 130 insertions(+), 157 deletions(-) diff --git a/docs/pt/docs/deployment/manually.md b/docs/pt/docs/deployment/manually.md index 237f4f8b9..46e580807 100644 --- a/docs/pt/docs/deployment/manually.md +++ b/docs/pt/docs/deployment/manually.md @@ -7,45 +7,33 @@ Em resumo, utilize o comando `fastapi run` para inicializar sua aplicação Fast
```console -$ fastapi run main.py -INFO Using path main.py -INFO Resolved absolute path /home/user/code/awesomeapp/main.py -INFO Searching for package file structure from directories with __init__.py files -INFO Importing from /home/user/code/awesomeapp - - ╭─ Python module file ─╮ - │ │ - │ 🐍 main.py │ - │ │ - ╰──────────────────────╯ - -INFO Importing module main -INFO Found importable FastAPI app - - ╭─ Importable FastAPI app ─╮ - │ │ - │ from main import app │ - │ │ - ╰──────────────────────────╯ - -INFO Using import string main:app - - ╭─────────── FastAPI CLI - Production mode ───────────╮ - │ │ - │ Serving at: http://0.0.0.0:8000 │ - │ │ - │ API docs: http://0.0.0.0:8000/docs │ - │ │ - │ Running in production mode, for development use: │ - │ │ - fastapi dev - │ │ - ╰─────────────────────────────────────────────────────╯ - -INFO: Started server process [2306215] -INFO: Waiting for application startup. -INFO: Application startup complete. -INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) +$ fastapi run main.py + + FastAPI Starting production server 🚀 + + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp + + module 🐍 main.py + + code Importing the FastAPI app object from the module with + the following code: + + from main import app + + app Using import string: main:app + + server Server started at http://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Started server process [2306215] + INFO Waiting for application startup. + INFO Application startup complete. + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C + to quit) ```
diff --git a/docs/pt/docs/deployment/server-workers.md b/docs/pt/docs/deployment/server-workers.md index 63eda56b4..a0db1bea4 100644 --- a/docs/pt/docs/deployment/server-workers.md +++ b/docs/pt/docs/deployment/server-workers.md @@ -36,56 +36,43 @@ Se você usar o comando `fastapi`:
```console -$
 fastapi run --workers 4 main.py
-INFO     Using path main.py
-INFO     Resolved absolute path /home/user/code/awesomeapp/main.py
-INFO     Searching for package file structure from directories with __init__.py files
-INFO     Importing from /home/user/code/awesomeapp
-
- ╭─ Python module file ─╮
- │                      │
- │  🐍 main.py          │
- │                      │
- ╰──────────────────────╯
-
-INFO     Importing module main
-INFO     Found importable FastAPI app
-
- ╭─ Importable FastAPI app ─╮
- │                          │
- │  from main import app    │
- │                          │
- ╰──────────────────────────╯
-
-INFO     Using import string main:app
-
- ╭─────────── FastAPI CLI - Production mode ───────────╮
- │                                                     │
- │  Serving at: http://0.0.0.0:8000                    │
- │                                                     │
- │  API docs: http://0.0.0.0:8000/docs                 │
- │                                                     │
- │  Running in production mode, for development use:   │
- │                                                     │
- fastapi dev
- │                                                     │
- ╰─────────────────────────────────────────────────────╯
-
-INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
-INFO:     Started parent process [27365]
-INFO:     Started server process [27368]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-INFO:     Started server process [27369]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-INFO:     Started server process [27370]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-INFO:     Started server process [27367]
-INFO:     Waiting for application startup.
-INFO:     Application startup complete.
-
+$ fastapi run --workers 4 main.py + + FastAPI Starting production server 🚀 + + Searching for package file structure from directories with + __init__.py files + Importing from /home/user/code/awesomeapp + + module 🐍 main.py + + code Importing the FastAPI app object from the module with the + following code: + + from main import app + + app Using import string: main:app + + server Server started at http://0.0.0.0:8000 + server Documentation at http://0.0.0.0:8000/docs + + Logs: + + INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to + quit) + INFO Started parent process [27365] + INFO Started server process [27368] + INFO Started server process [27369] + INFO Started server process [27370] + INFO Started server process [27367] + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Waiting for application startup. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. + INFO Application startup complete. ```
diff --git a/docs/pt/docs/tutorial/first-steps.md b/docs/pt/docs/tutorial/first-steps.md index 523b8f05c..5184d2d5f 100644 --- a/docs/pt/docs/tutorial/first-steps.md +++ b/docs/pt/docs/tutorial/first-steps.md @@ -11,26 +11,42 @@ Execute o servidor:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` + FastAPI Starting development server 🚀 -
+ Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp -/// note | Nota + module 🐍 main.py -O comando `uvicorn main:app` se refere a: + code Importing the FastAPI app object from the module with + the following code: -* `main`: o arquivo `main.py` (o "módulo" Python). -* `app`: o objeto criado no arquivo `main.py` com a linha `app = FastAPI()`. -* `--reload`: faz o servidor reiniciar após mudanças de código. Use apenas para desenvolvimento. + from main import app -/// + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: + + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. +``` + + Na saída, temos: @@ -151,34 +167,6 @@ Aqui, a variável `app` será uma "instância" da classe `FastAPI`. Este será o principal ponto de interação para criar toda a sua API. -Este `app` é o mesmo referenciado por `uvicorn` no comando: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -Se você criar a sua aplicação como: - -{* ../../docs_src/first_steps/tutorial002.py hl[3] *} - -E colocar em um arquivo `main.py`, você iria chamar o `uvicorn` assim: - -
- -```console -$ uvicorn main:my_awesome_api --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- ### Passo 3: crie uma *rota* #### Rota diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md index 4e6293bb0..7c04b17f2 100644 --- a/docs/pt/docs/tutorial/index.md +++ b/docs/pt/docs/tutorial/index.md @@ -4,9 +4,7 @@ Esse tutorial mostra como usar o **FastAPI** com a maior parte de seus recursos, Cada seção constrói, gradualmente, sobre as anteriores, mas sua estrutura são tópicos separados, para que você possa ir a qualquer um específico e resolver suas necessidades específicas de API. -Ele também foi feito como referência futura. - -Então você poderá voltar e ver exatamente o que precisar. +Ele também foi construído para servir como uma referência futura, então você pode voltar e ver exatamente o que você precisa. ## Rode o código @@ -17,13 +15,39 @@ Para rodar qualquer um dos exemplos, copie o codigo para um arquivo `main.py`, e
```console -$ uvicorn main:app --reload +$ fastapi dev main.py + + FastAPI Starting development server 🚀 + + Searching for package file structure from directories + with __init__.py files + Importing from /home/user/code/awesomeapp + + module 🐍 main.py + + code Importing the FastAPI app object from the module with + the following code: + + from main import app + + app Using import string: main:app + + server Server started at http://127.0.0.1:8000 + server Documentation at http://127.0.0.1:8000/docs + + tip Running in development mode, for production use: + fastapi run + + Logs: -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. + INFO Will watch for changes in these directories: + ['/home/user/code/awesomeapp'] + INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C + to quit) + INFO Started reloader process [383138] using WatchFiles + INFO Started server process [383153] + INFO Waiting for application startup. + INFO Application startup complete. ```
@@ -43,32 +67,18 @@ Para o tutorial, você deve querer instalá-lo com todas as dependências e recu
```console -$ pip install "fastapi[all]" +$ pip install "fastapi[standard]" ---> 100% ```
-...isso também inclui o `uvicorn`, que você pode usar como o servidor que rodará seu código. - /// note | Nota -Você também pode instalar parte por parte. - -Isso é provavelmente o que você faria quando você quisesse lançar sua aplicação em produção: - -``` -pip install fastapi -``` - -Também instale o `uvicorn` para funcionar como servidor: - -``` -pip install "uvicorn[standard]" -``` +Quando você instala com pip install "fastapi[standard]", ele vem com algumas dependências opcionais padrão. -E o mesmo para cada dependência opcional que você quiser usar. +Se você não quiser ter essas dependências opcionais, pode instalar pip install fastapi em vez disso. /// From 837e94573db9a56b9de4ef004fb96eedf031421e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Jan 2025 20:41:29 +0000 Subject: [PATCH 016/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3245bd8c4..f1e033da3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Translations +* 🌐 Update Portuguese translations. PR [#13156](https://github.com/fastapi/fastapi/pull/13156) by [@nillvitor](https://github.com/nillvitor). * 🌐 Update Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#13159](https://github.com/fastapi/fastapi/pull/13159) by [@Yarous](https://github.com/Yarous). * ✏️ Delete unnecessary backspace in `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#12238](https://github.com/fastapi/fastapi/pull/12238) by [@FakeDocument](https://github.com/FakeDocument). * 🌐 Update Chinese translation for `docs/zh/docs/fastapi-cli.md`. PR [#13102](https://github.com/fastapi/fastapi/pull/13102) by [@Zhongheng-Cheng](https://github.com/Zhongheng-Cheng). From e54cc8ffa3e0d1d3f55515887f89b45df7d60258 Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Fri, 10 Jan 2025 10:32:37 -0300 Subject: [PATCH 017/123] =?UTF-8?q?=F0=9F=8C=90=20Remove=20Wrong=20Portugu?= =?UTF-8?q?ese=20translations=20location=20for=20`docs/pt/docs/advanced/be?= =?UTF-8?q?nchmarks.md`=20(#13187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/advanced/benchmarks.md | 34 ----------------------------- 1 file changed, 34 deletions(-) delete mode 100644 docs/pt/docs/advanced/benchmarks.md diff --git a/docs/pt/docs/advanced/benchmarks.md b/docs/pt/docs/advanced/benchmarks.md deleted file mode 100644 index 72ef1e444..000000000 --- a/docs/pt/docs/advanced/benchmarks.md +++ /dev/null @@ -1,34 +0,0 @@ -# Benchmarks - -Benchmarks independentes da TechEmpower mostram que aplicações **FastAPI** rodando com o Uvicorn como um dos frameworks Python mais rápidos disponíveis, ficando atrás apenas do Starlette e Uvicorn (utilizado internamente pelo FastAPI). - -Porém, ao verificar benchmarks e comparações você deve prestar atenção ao seguinte: - -## Benchmarks e velocidade - -Quando você verifica os benchmarks, é comum ver diversas ferramentas de diferentes tipos comparados como se fossem equivalentes. - -Especificamente, para ver o Uvicorn, Starlette e FastAPI comparados entre si (entre diversas outras ferramentas). - -Quanto mais simples o problema resolvido pela ferramenta, melhor será a performance. E a maioria das análises não testa funcionalidades adicionais que são oferecidas pela ferramenta. - -A hierarquia é: - -* **Uvicorn**: um servidor ASGI - * **Starlette**: (utiliza Uvicorn) um microframework web - * **FastAPI**: (utiliza Starlette) um microframework para APIs com diversas funcionalidades adicionais para a construção de APIs, com validação de dados, etc. - -* **Uvicorn**: - * Terá a melhor performance, pois não possui muito código além do próprio servidor. - * Você não escreveria uma aplicação utilizando o Uvicorn diretamente. Isso significaria que o seu código teria que incluir pelo menos todo o código fornecido pelo Starlette (ou o **FastAPI**). E caso você fizesse isso, a sua aplicação final teria a mesma sobrecarga que teria se utilizasse um framework, minimizando o código e os bugs. - * Se você está comparando o Uvicorn, compare com os servidores de aplicação Daphne, Hypercorn, uWSGI, etc. -* **Starlette**: - * Terá o melhor desempenho, depois do Uvicorn. Na verdade, o Starlette utiliza o Uvicorn para rodar. Portanto, ele pode ficar mais "devagar" que o Uvicorn apenas por ter que executar mais código. - * Mas ele fornece as ferramentas para construir aplicações web simples, com roteamento baseado em caminhos, etc. - * Se você está comparando o Starlette, compare-o com o Sanic, Flask, Django, etc. Frameworks web (ou microframeworks). -* **FastAPI**: - * Da mesma forma que o Starlette utiliza o Uvicorn e não consegue ser mais rápido que ele, o **FastAPI** utiliza o Starlette, portanto, ele não consegue ser mais rápido que ele. - * O FastAPI provê mais funcionalidades em cima do Starlette. Funcionalidades que você quase sempre precisará quando estiver construindo APIs, como validação de dados e serialização. E ao utilizá-lo, você obtém documentação automática sem custo nenhum (a documentação automática sequer adiciona sobrecarga nas aplicações rodando, pois ela é gerada na inicialização). - * Caso você não utilize o FastAPI e faz uso do Starlette diretamente (ou outra ferramenta, como o Sanic, Flask, Responder, etc) você mesmo teria que implementar toda a validação de dados e serialização. Então, a sua aplicação final ainda teria a mesma sobrecarga caso estivesse usando o FastAPI. E em muitos casos, validação de dados e serialização é a maior parte do código escrito em aplicações. - * Então, ao utilizar o FastAPI, você está economizando tempo de programação, evitando bugs, linhas de código, e provavelmente terá a mesma performance (ou até melhor) do que teria caso você não o utilizasse (já que você teria que implementar tudo no seu código). - * Se você está comparando o FastAPI, compare-o com frameworks de aplicações web (ou conjunto de ferramentas) que oferecem validação de dados, serialização e documentação, como por exemplo o Flask-apispec, NestJS, Molten, etc. Frameworks que possuem validação integrada de dados, serialização e documentação. From f36927d0a63c5be349e68862c98759d006d515aa Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Jan 2025 13:33:14 +0000 Subject: [PATCH 018/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f1e033da3..489849903 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Translations +* 🌐 Remove Wrong Portuguese translations location for `docs/pt/docs/advanced/benchmarks.md`. PR [#13187](https://github.com/fastapi/fastapi/pull/13187) by [@ceb10n](https://github.com/ceb10n). * 🌐 Update Portuguese translations. PR [#13156](https://github.com/fastapi/fastapi/pull/13156) by [@nillvitor](https://github.com/nillvitor). * 🌐 Update Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#13159](https://github.com/fastapi/fastapi/pull/13159) by [@Yarous](https://github.com/Yarous). * ✏️ Delete unnecessary backspace in `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#12238](https://github.com/fastapi/fastapi/pull/12238) by [@FakeDocument](https://github.com/FakeDocument). From e69182940ea3205886656d9ebd2061cca2132fba Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Fri, 10 Jan 2025 10:33:35 -0300 Subject: [PATCH 019/123] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20`docs/pt/docs/tutorial/security/get-current-user?= =?UTF-8?q?.md`=20(#13188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/security/get-current-user.md | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/pt/docs/tutorial/security/get-current-user.md diff --git a/docs/pt/docs/tutorial/security/get-current-user.md b/docs/pt/docs/tutorial/security/get-current-user.md new file mode 100644 index 000000000..1a2badb83 --- /dev/null +++ b/docs/pt/docs/tutorial/security/get-current-user.md @@ -0,0 +1,105 @@ +# Obter Usuário Atual + +No capítulo anterior, o sistema de segurança (que é baseado no sistema de injeção de dependências) estava fornecendo à *função de operação de rota* um `token` como uma `str`: + +{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} + +Mas isso ainda não é tão útil. + +Vamos fazer com que ele nos forneça o usuário atual. + +## Criar um modelo de usuário + +Primeiro, vamos criar um modelo de usuário com Pydantic. + +Da mesma forma que usamos o Pydantic para declarar corpos, podemos usá-lo em qualquer outro lugar: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} + +## Criar uma dependência `get_current_user` + +Vamos criar uma dependência chamada `get_current_user`. + +Lembra que as dependências podem ter subdependências? + +`get_current_user` terá uma dependência com o mesmo `oauth2_scheme` que criamos antes. + +Da mesma forma que estávamos fazendo antes diretamente na *operação de rota*, a nossa nova dependência `get_current_user` receberá um `token` como uma `str` da subdependência `oauth2_scheme`: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} + +## Obter o usuário + +`get_current_user` usará uma função utilitária (falsa) que criamos, que recebe um token como uma `str` e retorna nosso modelo Pydantic `User`: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} + +## Injetar o usuário atual + +Então agora nós podemos usar o mesmo `Depends` com nosso `get_current_user` na *operação de rota*: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} + +Observe que nós declaramos o tipo de `current_user` como o modelo Pydantic `User`. + +Isso nos ajudará dentro da função com todo o preenchimento automático e verificações de tipo. + +/// tip | Dica + +Você pode se lembrar que corpos de requisição também são declarados com modelos Pydantic. + +Aqui, o **FastAPI** não ficará confuso porque você está usando `Depends`. + +/// + +/// check | Verifique + +A forma como esse sistema de dependências foi projetado nos permite ter diferentes dependências (diferentes "dependables") que retornam um modelo `User`. + +Não estamos restritos a ter apenas uma dependência que possa retornar esse tipo de dado. + +/// + +## Outros modelos + +Agora você pode obter o usuário atual diretamente nas *funções de operação de rota* e lidar com os mecanismos de segurança no nível da **Injeção de Dependências**, usando `Depends`. + +E você pode usar qualquer modelo ou dado para os requisitos de segurança (neste caso, um modelo Pydantic `User`). + +Mas você não está restrito a usar um modelo de dados, classe ou tipo específico. + +Você quer ter apenas um `id` e `email`, sem incluir nenhum `username` no modelo? Claro. Você pode usar essas mesmas ferramentas. + +Você quer ter apenas uma `str`? Ou apenas um `dict`? Ou uma instância de modelo de classe de banco de dados diretamente? Tudo funciona da mesma forma. + +Na verdade, você não tem usuários que fazem login no seu aplicativo, mas sim robôs, bots ou outros sistemas, que possuem apenas um token de acesso? Novamente, tudo funciona da mesma forma. + +Apenas use qualquer tipo de modelo, qualquer tipo de classe, qualquer tipo de banco de dados que você precise para a sua aplicação. O **FastAPI** cobre tudo com o sistema de injeção de dependências. + +## Tamanho do código + +Este exemplo pode parecer verboso. Lembre-se de que estamos misturando segurança, modelos de dados, funções utilitárias e *operações de rota* no mesmo arquivo. + +Mas aqui está o ponto principal. + +O código relacionado à segurança e à injeção de dependências é escrito apenas uma vez. + +E você pode torná-lo tão complexo quanto quiser. E ainda assim, tê-lo escrito apenas uma vez, em um único lugar. Com toda a flexibilidade. + +Mas você pode ter milhares de endpoints (*operações de rota*) usando o mesmo sistema de segurança. + +E todos eles (ou qualquer parte deles que você desejar) podem aproveitar o reuso dessas dependências ou de quaisquer outras dependências que você criar. + +E todos esses milhares de *operações de rota* podem ter apenas 3 linhas: + +{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} + +## Recapitulação + +Agora você pode obter o usuário atual diretamente na sua *função de operação de rota*. + +Já estamos na metade do caminho. + +Só precisamos adicionar uma *operação de rota* para que o usuário/cliente realmente envie o `username` e `password`. + +Isso vem a seguir. From 62be4a1600b29d3bcf717fa0b14a55ab1d5b7e57 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Jan 2025 13:33:58 +0000 Subject: [PATCH 020/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 489849903..9bd4dbf72 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/get-current-user.md`. PR [#13188](https://github.com/fastapi/fastapi/pull/13188) by [@ceb10n](https://github.com/ceb10n). * 🌐 Remove Wrong Portuguese translations location for `docs/pt/docs/advanced/benchmarks.md`. PR [#13187](https://github.com/fastapi/fastapi/pull/13187) by [@ceb10n](https://github.com/ceb10n). * 🌐 Update Portuguese translations. PR [#13156](https://github.com/fastapi/fastapi/pull/13156) by [@nillvitor](https://github.com/nillvitor). * 🌐 Update Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#13159](https://github.com/fastapi/fastapi/pull/13159) by [@Yarous](https://github.com/Yarous). From cda85623fbd58b10c7670557094588eaebef34d8 Mon Sep 17 00:00:00 2001 From: Guspan Tanadi <36249910+guspan-tanadi@users.noreply.github.com> Date: Sat, 11 Jan 2025 03:31:13 +0700 Subject: [PATCH 021/123] =?UTF-8?q?=F0=9F=8C=90=20Add=20Indonesian=20trans?= =?UTF-8?q?lation=20for=20`docs/id/docs/tutorial/static-files.md`=20(#1309?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/id/docs/tutorial/static-files.md | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/id/docs/tutorial/static-files.md diff --git a/docs/id/docs/tutorial/static-files.md b/docs/id/docs/tutorial/static-files.md new file mode 100644 index 000000000..b55f31394 --- /dev/null +++ b/docs/id/docs/tutorial/static-files.md @@ -0,0 +1,40 @@ +# Berkas Statis + +Anda dapat menyajikan berkas statis secara otomatis dari sebuah direktori menggunakan `StaticFiles`. + +## Penggunaan `StaticFiles` + +* Mengimpor `StaticFiles`. +* "Mount" representatif `StaticFiles()` di jalur spesifik. + +{* ../../docs_src/static_files/tutorial001.py hl[2,6] *} + +/// note | Detail Teknis + +Anda dapat pula menggunakan `from starlette.staticfiles import StaticFiles`. + +**FastAPI** menyediakan `starlette.staticfiles` sama seperti `fastapi.staticfiles` sebagai kemudahan pada Anda, yaitu para pengembang. Tetapi ini asli berasal langsung dari Starlette. + +/// + +### Apa itu "Mounting" + +"Mounting" dimaksud menambah aplikasi "independen" secara lengkap di jalur spesifik, kemudian menangani seluruh sub-jalur. + +Hal ini berbeda dari menggunakan `APIRouter` karena aplikasi yang dimount benar-benar independen. OpenAPI dan dokumentasi dari aplikasi utama Anda tak akan menyertakan apa pun dari aplikasi yang dimount, dst. + +Anda dapat mempelajari mengenai ini dalam [Panduan Pengguna Lanjutan](../advanced/index.md){.internal-link target=_blank}. + +## Detail + +Terhadap `"/static"` pertama mengacu pada sub-jalur yang akan menjadi tempat "sub-aplikasi" ini akan "dimount". Maka, jalur apa pun yang dimulai dengan `"/static"` akan ditangani oleh sub-jalur tersebut. + +Terhadap `directory="static"` mengacu pada nama direktori yang berisi berkas statis Anda. + +Terhadap `name="static"` ialah nama yang dapat digunakan secara internal oleh **FastAPI**. + +Seluruh parameter ini dapat berbeda dari sekadar "`static`", sesuaikan parameter dengan keperluan dan detail spesifik akan aplikasi Anda. + +## Info lanjutan + +Sebagai detail dan opsi tambahan lihat dokumentasi Starlette perihal Berkas Statis. From 09a0295bf3bd604eb0acd7eb1f9d9cd4852f3805 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Jan 2025 20:31:39 +0000 Subject: [PATCH 022/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9bd4dbf72..3aabd1fc0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Translations +* 🌐 Add Indonesian translation for `docs/id/docs/tutorial/static-files.md`. PR [#13092](https://github.com/fastapi/fastapi/pull/13092) by [@guspan-tanadi](https://github.com/guspan-tanadi). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/get-current-user.md`. PR [#13188](https://github.com/fastapi/fastapi/pull/13188) by [@ceb10n](https://github.com/ceb10n). * 🌐 Remove Wrong Portuguese translations location for `docs/pt/docs/advanced/benchmarks.md`. PR [#13187](https://github.com/fastapi/fastapi/pull/13187) by [@ceb10n](https://github.com/ceb10n). * 🌐 Update Portuguese translations. PR [#13156](https://github.com/fastapi/fastapi/pull/13156) by [@nillvitor](https://github.com/nillvitor). From 5d18ae0d902111bca76fabe6d136adafce2b4daf Mon Sep 17 00:00:00 2001 From: Gerry Sabar Date: Mon, 13 Jan 2025 20:36:23 +0700 Subject: [PATCH 023/123] =?UTF-8?q?=F0=9F=8C=90=20Add=20Indonesian=20trans?= =?UTF-8?q?lation=20for=20`docs/id/docs/index.md`=20(#13191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/id/docs/index.md | 495 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 docs/id/docs/index.md diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md new file mode 100644 index 000000000..7fdd1cc7a --- /dev/null +++ b/docs/id/docs/index.md @@ -0,0 +1,495 @@ +# FastAPI + + + +

+ FastAPI +

+

+ FastAPI, framework performa tinggi, mudah dipelajari, cepat untuk coding, siap untuk pengembangan +

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Dokumentasi**: https://fastapi.tiangolo.com + +**Kode Sumber**: https://github.com/fastapi/fastapi + +--- + +FastAPI adalah *framework* *web* moderen, cepat (performa-tinggi) untuk membangun API dengan Python berdasarkan tipe petunjuk Python. + +Fitur utama FastAPI: + +* **Cepat**: Performa sangat tinggi, setara **NodeJS** dan **Go** (berkat Starlette dan Pydantic). [Salah satu *framework* Python tercepat yang ada](#performa). +* **Cepat untuk coding**: Meningkatkan kecepatan pengembangan fitur dari 200% sampai 300%. * +* **Sedikit bug**: Mengurangi hingga 40% kesalahan dari manusia (pemrogram). * +* **Intuitif**: Dukungan editor hebat. Penyelesaian di mana pun. Lebih sedikit *debugging*. +* **Mudah**: Dibuat mudah digunakan dan dipelajari. Sedikit waktu membaca dokumentasi. +* **Ringkas**: Mengurasi duplikasi kode. Beragam fitur dari setiap deklarasi parameter. Lebih sedikit *bug*. +* **Handal**: Dapatkan kode siap-digunakan. Dengan dokumentasi otomatis interaktif. +* **Standar-resmi**: Berdasarkan (kompatibel dengan ) standar umum untuk API: OpenAPI (sebelumnya disebut Swagger) dan JSON Schema. + +* estimasi berdasarkan pengujian tim internal pengembangan applikasi siap pakai. + +## Sponsor + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Sponsor lainnya + +## Opini + +"_[...] Saya banyak menggunakan **FastAPI** sekarang ini. [...] Saya berencana menggunakannya di semua tim servis ML Microsoft. Beberapa dari mereka sudah mengintegrasikan dengan produk inti *Windows** dan sebagian produk **Office**._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_Kami adopsi library **FastAPI** untuk membuat server **REST** yang melakukan kueri untuk menghasilkan **prediksi**. [untuk Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** dengan bangga mengumumkan rilis open-source orkestrasi framework **manajemen krisis** : **Dispatch**! [dibuat dengan **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_Saya sangat senang dengan **FastAPI**. Sangat menyenangkan!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Jujur, apa yang anda buat sangat solid dan berkualitas. Ini adalah yang saya inginkan di **Hug** - sangat menginspirasi melihat seseorang membuat ini._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_Jika anda ingin mempelajari **framework moderen** untuk membangun REST API, coba **FastAPI** [...] cepat, mudah digunakan dan dipelajari [...]_" + +"_Kami sudah pindah ke **FastAPI** untuk **API** kami [...] Saya pikir kamu juga akan suka [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- +"_Jika anda ingin membuat API Python siap pakai, saya merekomendasikan **FastAPI**. FastAPI **didesain indah**, **mudah digunakan** dan **sangat scalable**, FastAPI adalah **komponen kunci** di strategi pengembangan API pertama kami dan mengatur banyak otomatisasi dan service seperti TAC Engineer kami._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, CLI FastAPI + + + +Jika anda mengembangkan app CLI yang digunakan di terminal bukan sebagai API web, kunjungi **Typer**. + +**Typer** adalah saudara kecil FastAPI. Dan ditujukan sebagai **CLI FastAPI**. ⌨️ 🚀 + +## Prayarat + +FastAPI berdiri di pundak raksasa: + +* Starlette untuk bagian web. +* Pydantic untuk bagian data. + +## Instalasi + +Buat dan aktifkan virtual environment kemudian *install* FastAPI: + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +**Catatan**: Pastikan anda menulis `"fastapi[standard]"` dengan tanda petik untuk memastikan bisa digunakan di semua *terminal*. + +## Contoh + +### Buat app + +* Buat file `main.py` dengan: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Atau gunakan async def... + +Jika kode anda menggunakan `async` / `await`, gunakan `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Catatan**: + +Jika anda tidak paham, kunjungi _"Panduan cepat"_ bagian `async` dan `await` di dokumentasi. + +
+ +### Jalankan + +Jalankan *server* dengan: + +
+ +```console +$ fastapi dev main.py + + ╭────────── FastAPI CLI - Development mode ───────────╮ + │ │ + │ Serving at: http://127.0.0.1:8000 │ + │ │ + │ API docs: http://127.0.0.1:8000/docs │ + │ │ + │ Running in development mode, for production use: │ + │ │ + │ fastapi run │ + │ │ + ╰─────────────────────────────────────────────────────╯ + +INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [2248755] using WatchFiles +INFO: Started server process [2248757] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+Mengenai perintah fastapi dev main.py... + +Perintah `fastapi dev` membaca file `main.py`, memeriksa app **FastAPI** di dalamnya, dan menjalan server dengan Uvicorn. + +Secara otomatis, `fastapi dev` akan mengaktifkan *auto-reload* untuk pengembangan lokal. + +Informasi lebih lanjut kunjungi Dokumen FastAPI CLI. + +
+ +### Periksa + +Buka *browser* di http://127.0.0.1:8000/items/5?q=somequery. + +Anda akan melihat respon JSON berikut: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Anda telah membuat API: + +* Menerima permintaan HTTP di _path_ `/` dan `/items/{item_id}`. +* Kedua _paths_ menerima operasi `GET` (juga disebut _metode_ HTTP). +* _path_ `/items/{item_id}` memiliki _parameter path_ `item_id` yang harus berjenis `int`. +* _path_ `/items/{item_id}` memiliki _query parameter_ `q` berjenis `str`. + +### Dokumentasi API interaktif + +Sekarang kunjungi http://127.0.0.1:8000/docs. + +Anda akan melihat dokumentasi API interaktif otomatis (dibuat oleh Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Dokumentasi API alternatif + +Kemudian kunjungi http://127.0.0.1:8000/redoc. + +Anda akan melihat dokumentasi alternatif otomatis (dibuat oleh ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Contoh upgrade + +Sekarang ubah `main.py` untuk menerima struktur permintaan `PUT`. + +Deklarasikan struktur menggunakan tipe standar Python, berkat Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +Server `fastapi dev` akan otomatis memuat kembali. + +### Upgrade dokumentasi API interaktif + +Kunjungi http://127.0.0.1:8000/docs. + +* Dokumentasi API interaktif akan otomatis diperbarui, termasuk kode yang baru: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Klik tombol "Try it out", anda dapat mengisi parameter dan langsung berinteraksi dengan API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Kemudian klik tombol "Execute", tampilan pengguna akan berkomunikasi dengan API, mengirim parameter, mendapatkan dan menampilkan hasil ke layar: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Upgrade dokumentasi API alternatif + +Kunjungi http://127.0.0.1:8000/redoc. + +* Dokumentasi alternatif akan menampilkan parameter *query* dan struktur *request*: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Ringkasan + +Singkatnya, anda mendeklarasikan **sekali** jenis parameter, struktur, dll. sebagai parameter fungsi. + +Anda melakukannya dengan tipe standar moderen Python. + +Anda tidak perlu belajar sintaksis, metode, *classs* baru dari *library* tertentu, dll. + +Cukup **Python** standar. + +Sebagai contoh untuk `int`: + +```Python +item_id: int +``` + +atau untuk model lebih rumit `Item`: + +```Python +item: Item +``` + +...dengan sekali deklarasi anda mendapatkan: + +* Dukungan editor, termasuk: + * Pelengkapan kode. + * Pengecekan tipe. +* Validasi data: + * Kesalahan otomatis dan jelas ketika data tidak sesuai. + * Validasi hingga untuk object JSON bercabang mendalam. +* Konversi input data: berasal dari jaringan ke data dan tipe Python. Membaca dari: + * JSON. + * Parameter path. + * Parameter query. + * Cookie. + * Header. + * Form. + * File. +* Konversi output data: konversi data Python ke tipe jaringan data (seperti JSON): + * Konversi tipe Python (`str`, `int`, `float`, `bool`, `list`, dll). + * Objek `datetime`. + * Objek `UUID`. + * Model database. + * ...dan banyak lagi. +* Dokumentasi interaktif otomatis, termasuk 2 alternatif tampilan pengguna: + * Swagger UI. + * ReDoc. + +--- + +Kembali ke kode contoh sebelumnya, **FastAPI** akan: + +* Validasi apakah terdapat `item_id` di *path* untuk permintaan `GET` dan `PUT` requests. +* Validasi apakah `item_id` berjenit `int` untuk permintaan `GET` dan `PUT`. + * Jika tidak, klien akan melihat pesan kesalahan jelas. +* Periksa jika ada parameter *query* opsional bernama `q` (seperti `http://127.0.0.1:8000/items/foo?q=somequery`) untuk permintaan `GET`. + * Karena parameter `q` dideklarasikan dengan `= None`, maka bersifat opsional. + * Tanpa `None` maka akan menjadi wajib ada (seperti struktur di kondisi dengan `PUT`). +* Untuk permintaan `PUT` `/items/{item_id}`, membaca struktur sebagai JSON: + * Memeriksa terdapat atribut wajib `name` harus berjenis `str`. + * Memeriksa terdapat atribut wajib`price` harus berjenis `float`. + * Memeriksa atribut opsional `is_offer`, harus berjenis `bool`, jika ada. + * Semua ini juga sama untuk objek json yang bersarang mendalam. +* Konversi dari dan ke JSON secara otomatis. +* Dokumentasi segalanya dengan OpenAPI, dengan menggunakan: + * Sistem dokumentasi interaktif. + * Sistem otomatis penghasil kode, untuk banyak bahasa. +* Menyediakan 2 tampilan dokumentasi web interaktif dengan langsung. + +--- + +Kita baru menyentuh permukaannya saja, tetapi anda sudah mulai paham gambaran besar cara kerjanya. + +Coba ubah baris: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...dari: + +```Python + ... "item_name": item.name ... +``` + +...menjadi: + +```Python + ... "item_price": item.price ... +``` + +...anda akan melihat kode editor secara otomatis melengkapi atributnya dan tahu tipe nya: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Untuk contoh lengkap termasuk fitur lainnya, kunjungi Tutorial - Panduan Pengguna. + +**Peringatan spoiler**: tutorial - panduan pengguna termasuk: + +* Deklarasi **parameter** dari tempat berbeda seperti: **header**, **cookie**, **form field** and **file**. +* Bagaimana mengatur **batasan validasi** seperti `maximum_length`atau `regex`. +* Sistem **Dependency Injection** yang hebat dan mudah digunakan. +* Keamanan dan autentikasi, termasuk dukungan ke **OAuth2** dengan **JWT token** dan autentikasi **HTTP Basic**. +* Teknik lebih aju (tetapi mudah dipakai untuk deklarasi **model JSON bersarang ke dalam** (berkat Pydantic). +* Integrasi **GraphQL** dengan Strawberry dan library lainnya. +* Fitur lainnya (berkat Starlette) seperti: + * **WebSocket** + * Test yang sangat mudah berdasarkan HTTPX dan `pytest` + * **CORS** + * **Cookie Session** + * ...dan lainnya. + +## Performa + +Tolok ukur Independent TechEmpower mendapati aplikasi **FastAPI** berjalan menggunakan Uvicorn sebagai salah satu framework Python tercepat yang ada, hanya di bawah Starlette dan Uvicorn itu sendiri (digunakan di internal FastAPI). (*) + +Penjelasan lebih lanjut, lihat bagian Tolok ukur. + +## Dependensi + +FastAPI bergantung pada Pydantic dan Starlette. + +### Dependensi `standar` + +Ketika anda meng-*install* FastAPI dengan `pip install "fastapi[standard]"`, maka FastAPI akan menggunakan sekumpulan dependensi opsional `standar`: + +Digunakan oleh Pydantic: + +* email-validator - untuk validasi email. + +Digunakan oleh Starlette: + +* httpx - Dibutuhkan jika anda menggunakan `TestClient`. +* jinja2 - Dibutuhkan jika anda menggunakan konfigurasi template bawaan. +* python-multipart - Dibutuhkan jika anda menggunakan form dukungan "parsing", dengan `request.form()`. + +Digunakan oleh FastAPI / Starlette: + +* uvicorn - untuk server yang memuat dan melayani aplikasi anda. Termasuk `uvicorn[standard]`, yang memasukan sejumlah dependensi (misal `uvloop`) untuk needed melayani dengan performa tinggi. +* `fastapi-cli` - untuk menyediakan perintah `fastapi`. + +### Tanpda dependensi `standard` + +Jika anda tidak ingin menambahkan dependensi opsional `standard`, anda dapat menggunakan `pip install fastapi` daripada `pip install "fastapi[standard]"`. + +### Dependensi Opsional Tambahan + +Ada beberapa dependensi opsional yang bisa anda install. + +Dependensi opsional tambahan Pydantic: + +* pydantic-settings - untuk manajemen setting. +* pydantic-extra-types - untuk tipe tambahan yang digunakan dengan Pydantic. + +Dependensi tambahan opsional FastAPI: + +* orjson - Diperlukan jika anda akan menggunakan`ORJSONResponse`. +* ujson - Diperlukan jika anda akan menggunakan `UJSONResponse`. + +## Lisensi + +Project terlisensi dengan lisensi MIT. From 2612fa3e9d17fe74c97029b55b5d64be2d38400f Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 13 Jan 2025 13:36:46 +0000 Subject: [PATCH 024/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3aabd1fc0..9393b1156 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Translations +* 🌐 Add Indonesian translation for `docs/id/docs/index.md`. PR [#13191](https://github.com/fastapi/fastapi/pull/13191) by [@gerry-sabar](https://github.com/gerry-sabar). * 🌐 Add Indonesian translation for `docs/id/docs/tutorial/static-files.md`. PR [#13092](https://github.com/fastapi/fastapi/pull/13092) by [@guspan-tanadi](https://github.com/guspan-tanadi). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/get-current-user.md`. PR [#13188](https://github.com/fastapi/fastapi/pull/13188) by [@ceb10n](https://github.com/ceb10n). * 🌐 Remove Wrong Portuguese translations location for `docs/pt/docs/advanced/benchmarks.md`. PR [#13187](https://github.com/fastapi/fastapi/pull/13187) by [@ceb10n](https://github.com/ceb10n). From 16199c4a138acbdac838124cfd54fb257dc52d8f Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Wed, 15 Jan 2025 17:16:13 -0300 Subject: [PATCH 025/123] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20trans?= =?UTF-8?q?lation=20for=20`docs/pt/docs/tutorial/security/oauth2-jwt.md`?= =?UTF-8?q?=20(#13205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/security/oauth2-jwt.md | 274 +++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 docs/pt/docs/tutorial/security/oauth2-jwt.md diff --git a/docs/pt/docs/tutorial/security/oauth2-jwt.md b/docs/pt/docs/tutorial/security/oauth2-jwt.md new file mode 100644 index 000000000..4b99c4c59 --- /dev/null +++ b/docs/pt/docs/tutorial/security/oauth2-jwt.md @@ -0,0 +1,274 @@ +# OAuth2 com Senha (e hashing), Bearer com tokens JWT + +Agora que temos todo o fluxo de segurança, vamos tornar a aplicação realmente segura, usando tokens JWT e hashing de senhas seguras. + +Este código é algo que você pode realmente usar na sua aplicação, salvar os hashes das senhas no seu banco de dados, etc. + +Vamos começar de onde paramos no capítulo anterior e incrementá-lo. + +## Sobre o JWT + +JWT significa "JSON Web Tokens". + +É um padrão para codificar um objeto JSON em uma string longa e densa sem espaços. Ele se parece com isso: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +Ele não é criptografado, então qualquer pessoa pode recuperar as informações do seu conteúdo. + +Mas ele é assinado. Assim, quando você recebe um token que você emitiu, você pode verificar que foi realmente você quem o emitiu. + +Dessa forma, você pode criar um token com um prazo de expiração, digamos, de 1 semana. E então, quando o usuário voltar no dia seguinte com o token, você sabe que ele ainda está logado no seu sistema. + +Depois de uma semana, o token expirará e o usuário não estará autorizado, precisando fazer login novamente para obter um novo token. E se o usuário (ou uma terceira parte) tentar modificar o token para alterar a expiração, você seria capaz de descobrir isso, pois as assinaturas não iriam corresponder. + +Se você quiser brincar com tokens JWT e ver como eles funcionam, visite https://jwt.io. + +## Instalar `PyJWT` + +Nós precisamos instalar o `PyJWT` para criar e verificar os tokens JWT em Python. + +Certifique-se de criar um [ambiente virtual](../../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar o `pyjwt`: + +
+ +```console +$ pip install pyjwt + +---> 100% +``` + +
+ +/// info | Informação + +Se você pretente utilizar algoritmos de assinatura digital como o RSA ou o ECDSA, você deve instalar a dependência da biblioteca de criptografia `pyjwt[crypto]`. + +Você pode ler mais sobre isso na documentação de instalação do PyJWT. + +/// + +## Hashing de senhas + +"Hashing" significa converter algum conteúdo (uma senha neste caso) em uma sequência de bytes (apenas uma string) que parece um monte de caracteres sem sentido. + +Sempre que você passar exatamente o mesmo conteúdo (exatamente a mesma senha), você obterá exatamente o mesmo resultado. + +Mas não é possível converter os caracteres sem sentido de volta para a senha original. + +### Por que usar hashing de senhas + +Se o seu banco de dados for roubado, o invasor não terá as senhas em texto puro dos seus usuários, apenas os hashes. + +Então, o invasor não poderá tentar usar essas senhas em outro sistema (como muitos usuários utilizam a mesma senha em vários lugares, isso seria perigoso). + +## Instalar o `passlib` + +O PassLib é uma excelente biblioteca Python para lidar com hashes de senhas. + +Ele suporta muitos algoritmos de hashing seguros e utilitários para trabalhar com eles. + +O algoritmo recomendado é o "Bcrypt". + +Certifique-se de criar um [ambiente virtual](../../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar o PassLib com Bcrypt: + +
+ +```console +$ pip install "passlib[bcrypt]" + +---> 100% +``` + +
+ +/// tip | Dica + +Com o `passlib`, você poderia até configurá-lo para ser capaz de ler senhas criadas pelo **Django**, um plug-in de segurança do **Flask** ou muitos outros. + +Assim, você poderia, por exemplo, compartilhar os mesmos dados de um aplicativo Django em um banco de dados com um aplicativo FastAPI. Ou migrar gradualmente uma aplicação Django usando o mesmo banco de dados. + +E seus usuários poderiam fazer login tanto pela sua aplicação Django quanto pela sua aplicação **FastAPI**, ao mesmo tempo. + +/// + +## Criar o hash e verificar as senhas + +Importe as ferramentas que nós precisamos de `passlib`. + +Crie um "contexto" do PassLib. Este será usado para criar o hash e verificar as senhas. + +/// tip | Dica + +O contexto do PassLib também possui funcionalidades para usar diferentes algoritmos de hashing, incluindo algoritmos antigos que estão obsoletos, apenas para permitir verificá-los, etc. + +Por exemplo, você poderia usá-lo para ler e verificar senhas geradas por outro sistema (como Django), mas criar o hash de novas senhas com um algoritmo diferente, como o Bcrypt. + +E ser compatível com todos eles ao mesmo tempo. + +/// + +Crie uma função utilitária para criar o hash de uma senha fornecida pelo usuário. + +E outra função utilitária para verificar se uma senha recebida corresponde ao hash armazenado. + +E outra para autenticar e retornar um usuário. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} + +/// note | Nota + +Se você verificar o novo banco de dados (falso) `fake_users_db`, você verá como o hash da senha se parece agora: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. + +/// + +## Manipular tokens JWT + +Importe os módulos instalados. + +Crie uma chave secreta aleatória que será usada para assinar os tokens JWT. + +Para gerar uma chave secreta aleatória e segura, use o comando: + +
+ +```console +$ openssl rand -hex 32 + +09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 +``` + +
+ +E copie a saída para a variável `SECRET_KEY` (não use a do exemplo). + +Crie uma variável `ALGORITHM` com o algoritmo usado para assinar o token JWT e defina como `"HS256"`. + +Crie uma variável para a expiração do token. + +Defina um modelo Pydantic que será usado no endpoint de token para a resposta. + +Crie uma função utilitária para gerar um novo token de acesso. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} + +## Atualize as dependências + +Atualize `get_current_user` para receber o mesmo token de antes, mas desta vez, usando tokens JWT. + +Decodifique o token recebido, verifique-o e retorne o usuário atual. + +Se o token for inválido, retorne um erro HTTP imediatamente. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} + +## Atualize a *operação de rota* `/token` + +Crie um `timedelta` com o tempo de expiração do token. + +Crie um token de acesso JWT real e o retorne. + +{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} + +### Detalhes técnicos sobre o "sujeito" `sub` do JWT + +A especificação JWT diz que existe uma chave `sub`, com o sujeito do token. + +É opcional usá-la, mas é onde você colocaria a identificação do usuário, então nós estamos usando aqui. + +O JWT pode ser usado para outras coisas além de identificar um usuário e permitir que ele execute operações diretamente na sua API. + +Por exemplo, você poderia identificar um "carro" ou uma "postagem de blog". + +Depois, você poderia adicionar permissões sobre essa entidade, como "dirigir" (para o carro) ou "editar" (para o blog). + +E então, poderia dar esse token JWT para um usuário (ou bot), e ele poderia usá-lo para realizar essas ações (dirigir o carro ou editar o blog) sem sequer precisar ter uma conta, apenas com o token JWT que sua API gerou para isso. + +Usando essas ideias, o JWT pode ser usado para cenários muito mais sofisticados. + +Nesses casos, várias dessas entidades poderiam ter o mesmo ID, digamos `foo` (um usuário `foo`, um carro `foo` e uma postagem de blog `foo`). + +Então, para evitar colisões de ID, ao criar o token JWT para o usuário, você poderia prefixar o valor da chave `sub`, por exemplo, com `username:`. Assim, neste exemplo, o valor de `sub` poderia ser: `username:johndoe`. + +O importante a se lembrar é que a chave `sub` deve ter um identificador único em toda a aplicação e deve ser uma string. + +## Testando + +Execute o servidor e vá para a documentação: http://127.0.0.1:8000/docs. + +Você verá a interface de usuário assim: + + + +Autorize a aplicação da mesma maneira que antes. + +Usando as credenciais: + +Username: `johndoe` +Password: `secret` + +/// check | Verifique + +Observe que em nenhuma parte do código está a senha em texto puro "`secret`", nós temos apenas o hash. + +/// + + + +Chame o endpoint `/users/me/`, você receberá o retorno como: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false +} +``` + + + +Se você abrir as ferramentas de desenvolvedor, poderá ver que os dados enviados incluem apenas o token. A senha é enviada apenas na primeira requisição para autenticar o usuário e obter o token de acesso, mas não é enviada nas próximas requisições: + + + +/// note | Nota + +Perceba que o cabeçalho `Authorization`, com o valor que começa com `Bearer `. + +/// + +## Uso avançado com `scopes` + +O OAuth2 tem a noção de "scopes" (escopos). + +Você pode usá-los para adicionar um conjunto específico de permissões a um token JWT. + +Então, você pode dar este token diretamente a um usuário ou a uma terceira parte para interagir com sua API com um conjunto de restrições. + +Você pode aprender como usá-los e como eles são integrados ao **FastAPI** mais adiante no **Guia Avançado do Usuário**. + + +## Recapitulação + +Com o que você viu até agora, você pode configurar uma aplicação **FastAPI** segura usando padrões como OAuth2 e JWT. + +Em quase qualquer framework, lidar com a segurança se torna rapidamente um assunto bastante complexo. + +Muitos pacotes que simplificam bastante isso precisam fazer muitas concessões com o modelo de dados, o banco de dados e os recursos disponíveis. E alguns desses pacotes que simplificam demais na verdade têm falhas de segurança subjacentes. + +--- + +O **FastAPI** não faz nenhuma concessão com nenhum banco de dados, modelo de dados ou ferramenta. + +Ele oferece toda a flexibilidade para você escolher as opções que melhor se ajustam ao seu projeto. + +E você pode usar diretamente muitos pacotes bem mantidos e amplamente utilizados, como `passlib` e `PyJWT`, porque o **FastAPI** não exige mecanismos complexos para integrar pacotes externos. + +Mas ele fornece as ferramentas para simplificar o processo o máximo possível, sem comprometer a flexibilidade, robustez ou segurança. + +E você pode usar e implementar protocolos padrão seguros, como o OAuth2, de uma maneira relativamente simples. + +Você pode aprender mais no **Guia Avançado do Usuário** sobre como usar os "scopes" do OAuth2 para um sistema de permissões mais refinado, seguindo esses mesmos padrões. O OAuth2 com scopes é o mecanismo usado por muitos provedores grandes de autenticação, como o Facebook, Google, GitHub, Microsoft, Twitter, etc. para autorizar aplicativos de terceiros a interagir com suas APIs em nome de seus usuários. From 5f49397d195293f6d5d948ae1765f5240c7b3a90 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 15 Jan 2025 20:16:38 +0000 Subject: [PATCH 026/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9393b1156..c09fcf950 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/oauth2-jwt.md`. PR [#13205](https://github.com/fastapi/fastapi/pull/13205) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Indonesian translation for `docs/id/docs/index.md`. PR [#13191](https://github.com/fastapi/fastapi/pull/13191) by [@gerry-sabar](https://github.com/gerry-sabar). * 🌐 Add Indonesian translation for `docs/id/docs/tutorial/static-files.md`. PR [#13092](https://github.com/fastapi/fastapi/pull/13092) by [@guspan-tanadi](https://github.com/guspan-tanadi). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/get-current-user.md`. PR [#13188](https://github.com/fastapi/fastapi/pull/13188) by [@ceb10n](https://github.com/ceb10n). From d9a640e06c924e40b29da274df26f82d3537393c Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Wed, 15 Jan 2025 17:17:23 -0300 Subject: [PATCH 027/123] =?UTF-8?q?=F0=9F=8C=90=20Update=20Portuguese=20tr?= =?UTF-8?q?anslation=20for=20`docs/pt/docs/advanced/settings.md`=20(#13209?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/advanced/settings.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/pt/docs/advanced/settings.md b/docs/pt/docs/advanced/settings.md index 00a39b0af..cdc6400ad 100644 --- a/docs/pt/docs/advanced/settings.md +++ b/docs/pt/docs/advanced/settings.md @@ -8,7 +8,7 @@ Por isso é comum prover essas configurações como variáveis de ambiente que s ## Variáveis de Ambiente -/// dica +/// tip | Dica Se você já sabe o que são variáveis de ambiente e como utilizá-las, sinta-se livre para avançar para o próximo tópico. @@ -67,7 +67,7 @@ name = os.getenv("MY_NAME", "World") print(f"Hello {name} from Python") ``` -/// dica +/// tip | Dica O segundo parâmetro em `os.getenv()` é o valor padrão para o retorno. @@ -124,7 +124,7 @@ Hello World from Python -/// dica +/// tip | Dica Você pode ler mais sobre isso em: The Twelve-Factor App: Configurações. @@ -196,7 +196,7 @@ Na versão 1 do Pydantic você importaria `BaseSettings` diretamente do módulo //// -/// dica +/// tip | Dica Se você quiser algo pronto para copiar e colar na sua aplicação, não use esse exemplo, mas sim o exemplo abaixo. @@ -226,7 +226,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p -/// dica +/// tip | Dica Para definir múltiplas variáveis de ambiente para um único comando basta separá-las utilizando espaços, e incluir todas elas antes do comando. @@ -250,7 +250,7 @@ E utilizar essa configuração em `main.py`: {* ../../docs_src/settings/app01/main.py hl[3,11:13] *} -/// dica +/// tip | Dica Você também precisa incluir um arquivo `__init__.py` como visto em [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=\_blank}. @@ -276,7 +276,7 @@ Agora criamos a dependência que retorna um novo objeto `config.Settings()`. {* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *} -/// dica +/// tip | Dica Vamos discutir sobre `@lru_cache` logo mais. @@ -304,7 +304,7 @@ Se você tiver muitas configurações que variem bastante, talvez em ambientes d Essa prática é tão comum que possui um nome, essas variáveis de ambiente normalmente são colocadas em um arquivo `.env`, e esse arquivo é chamado de "dotenv". -/// dica +/// tip | Dica Um arquivo iniciando com um ponto final (`.`) é um arquivo oculto em sistemas baseados em Unix, como Linux e MacOS. @@ -314,7 +314,7 @@ Mas um arquivo dotenv não precisa ter esse nome exato. Pydantic suporta a leitura desses tipos de arquivos utilizando uma biblioteca externa. Você pode ler mais em Pydantic Settings: Dotenv (.env) support. -/// dica +/// tip | Dica Para que isso funcione você precisa executar `pip install python-dotenv`. @@ -337,7 +337,7 @@ E então adicionar o seguinte código em `config.py`: {* ../../docs_src/settings/app03_an/config.py hl[9] *} -/// dica +/// tip | Dica O atributo `model_config` é usado apenas para configuração do Pydantic. Você pode ler mais em Pydantic Model Config. @@ -349,7 +349,7 @@ O atributo `model_config` é usado apenas para configuração do Pydantic. Você {* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *} -/// dica +/// tip | Dica A classe `Config` é usada apenas para configuração do Pydantic. Você pode ler mais em Pydantic Model Config. From e773d7e9193d221e0f9398de9b6dda5dfdb6d0b9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 15 Jan 2025 20:18:49 +0000 Subject: [PATCH 028/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c09fcf950..535973099 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Translations +* 🌐 Update Portuguese translation for `docs/pt/docs/advanced/settings.md`. PR [#13209](https://github.com/fastapi/fastapi/pull/13209) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/oauth2-jwt.md`. PR [#13205](https://github.com/fastapi/fastapi/pull/13205) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Indonesian translation for `docs/id/docs/index.md`. PR [#13191](https://github.com/fastapi/fastapi/pull/13191) by [@gerry-sabar](https://github.com/gerry-sabar). * 🌐 Add Indonesian translation for `docs/id/docs/tutorial/static-files.md`. PR [#13092](https://github.com/fastapi/fastapi/pull/13092) by [@guspan-tanadi](https://github.com/guspan-tanadi). From 9e0d4fa0efed6376a0e4066ddc59b8802fcd25b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Jan 2025 17:51:19 +0000 Subject: [PATCH 029/123] =?UTF-8?q?=F0=9F=91=B7=20Add=20independent=20CI?= =?UTF-8?q?=20automation=20for=20FastAPI=20People=20-=20Sponsors=20(#13221?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sponsors.yml | 53 ++++++++ scripts/sponsors.py | 220 +++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 .github/workflows/sponsors.yml create mode 100644 scripts/sponsors.py diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml new file mode 100644 index 000000000..590ac5487 --- /dev/null +++ b/.github/workflows/sponsors.yml @@ -0,0 +1,53 @@ +name: FastAPI People Sponsors + +on: + schedule: + - cron: "0 6 1 * *" + workflow_dispatch: + inputs: + debug_enabled: + description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" + required: false + default: "false" + +env: + UV_SYSTEM_PYTHON: 1 + +jobs: + job: + if: github.repository_owner == 'fastapi' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - name: Install Dependencies + run: uv pip install -r requirements-github-actions.txt + # Allow debugging with tmate + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} + with: + limit-access-to-actor: true + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} + - name: FastAPI People Sponsors + run: python ./scripts/sponsors.py + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/scripts/sponsors.py b/scripts/sponsors.py new file mode 100644 index 000000000..ed782a49e --- /dev/null +++ b/scripts/sponsors.py @@ -0,0 +1,220 @@ +import logging +import secrets +import subprocess +from collections import defaultdict +from pathlib import Path +from typing import Any + +import httpx +import yaml +from github import Github +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings + +github_graphql_url = "https://api.github.com/graphql" + + +sponsors_query = """ +query Q($after: String) { + user(login: "tiangolo") { + sponsorshipsAsMaintainer(first: 100, after: $after) { + edges { + cursor + node { + sponsorEntity { + ... on Organization { + login + avatarUrl + url + } + ... on User { + login + avatarUrl + url + } + } + tier { + name + monthlyPriceInDollars + } + } + } + } + } +} +""" + + +class SponsorEntity(BaseModel): + login: str + avatarUrl: str + url: str + + +class Tier(BaseModel): + name: str + monthlyPriceInDollars: float + + +class SponsorshipAsMaintainerNode(BaseModel): + sponsorEntity: SponsorEntity + tier: Tier + + +class SponsorshipAsMaintainerEdge(BaseModel): + cursor: str + node: SponsorshipAsMaintainerNode + + +class SponsorshipAsMaintainer(BaseModel): + edges: list[SponsorshipAsMaintainerEdge] + + +class SponsorsUser(BaseModel): + sponsorshipsAsMaintainer: SponsorshipAsMaintainer + + +class SponsorsResponseData(BaseModel): + user: SponsorsUser + + +class SponsorsResponse(BaseModel): + data: SponsorsResponseData + + +class Settings(BaseSettings): + github_token: SecretStr + github_repository: str + httpx_timeout: int = 30 + + +def get_graphql_response( + *, + settings: Settings, + query: str, + after: str | None = None, +) -> dict[str, Any]: + headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} + variables = {"after": after} + response = httpx.post( + github_graphql_url, + headers=headers, + timeout=settings.httpx_timeout, + json={"query": query, "variables": variables, "operationName": "Q"}, + ) + if response.status_code != 200: + logging.error(f"Response was not 200, after: {after}") + logging.error(response.text) + raise RuntimeError(response.text) + data = response.json() + if "errors" in data: + logging.error(f"Errors in response, after: {after}") + logging.error(data["errors"]) + logging.error(response.text) + raise RuntimeError(response.text) + return data + + +def get_graphql_sponsor_edges( + *, settings: Settings, after: str | None = None +) -> list[SponsorshipAsMaintainerEdge]: + data = get_graphql_response(settings=settings, query=sponsors_query, after=after) + graphql_response = SponsorsResponse.model_validate(data) + return graphql_response.data.user.sponsorshipsAsMaintainer.edges + + +def get_individual_sponsors( + settings: Settings, +) -> defaultdict[float, dict[str, SponsorEntity]]: + nodes: list[SponsorshipAsMaintainerNode] = [] + edges = get_graphql_sponsor_edges(settings=settings) + + while edges: + for edge in edges: + nodes.append(edge.node) + last_edge = edges[-1] + edges = get_graphql_sponsor_edges(settings=settings, after=last_edge.cursor) + + tiers: defaultdict[float, dict[str, SponsorEntity]] = defaultdict(dict) + for node in nodes: + tiers[node.tier.monthlyPriceInDollars][node.sponsorEntity.login] = ( + node.sponsorEntity + ) + return tiers + + +def update_content(*, content_path: Path, new_content: Any) -> bool: + old_content = content_path.read_text(encoding="utf-8") + + new_content = yaml.dump(new_content, sort_keys=False, width=200, allow_unicode=True) + if old_content == new_content: + logging.info(f"The content hasn't changed for {content_path}") + return False + content_path.write_text(new_content, encoding="utf-8") + logging.info(f"Updated {content_path}") + return True + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + settings = Settings() + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(settings.github_token.get_secret_value()) + repo = g.get_repo(settings.github_repository) + + tiers = get_individual_sponsors(settings=settings) + keys = list(tiers.keys()) + keys.sort(reverse=True) + sponsors = [] + for key in keys: + sponsor_group = [] + for login, sponsor in tiers[key].items(): + sponsor_group.append( + {"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url} + ) + sponsors.append(sponsor_group) + github_sponsors = { + "sponsors": sponsors, + } + + # For local development + # github_sponsors_path = Path("../docs/en/data/github_sponsors.yml") + github_sponsors_path = Path("./docs/en/data/github_sponsors.yml") + updated = update_content( + content_path=github_sponsors_path, new_content=github_sponsors + ) + + if not updated: + logging.info("The data hasn't changed, finishing.") + return + + logging.info("Setting up GitHub Actions git user") + subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run( + ["git", "config", "user.email", "github-actions@github.com"], check=True + ) + branch_name = f"fastapi-people-sponsors-{secrets.token_hex(4)}" + logging.info(f"Creating a new branch {branch_name}") + subprocess.run(["git", "checkout", "-b", branch_name], check=True) + logging.info("Adding updated file") + subprocess.run( + [ + "git", + "add", + str(github_sponsors_path), + ], + check=True, + ) + logging.info("Committing updated file") + message = "👥 Update FastAPI People - Sponsors" + subprocess.run(["git", "commit", "-m", message], check=True) + logging.info("Pushing branch") + subprocess.run(["git", "push", "origin", branch_name], check=True) + logging.info("Creating PR") + pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) + logging.info(f"Created PR: {pr.number}") + logging.info("Finished") + + +if __name__ == "__main__": + main() From db48d9cf0983752b155b93b882130da60d8a2636 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 17 Jan 2025 17:53:06 +0000 Subject: [PATCH 030/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 535973099..12cf637f5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -85,6 +85,7 @@ hide: ### Internal +* 👷 Add independent CI automation for FastAPI People - Sponsors. PR [#13221](https://github.com/fastapi/fastapi/pull/13221) by [@tiangolo](https://github.com/tiangolo). * 👷 Add retries to Smokeshow. PR [#13151](https://github.com/fastapi/fastapi/pull/13151) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Speakeasy sponsor graphic. PR [#13147](https://github.com/fastapi/fastapi/pull/13147) by [@chailandau](https://github.com/chailandau). * 👥 Update FastAPI GitHub topic repositories. PR [#13146](https://github.com/fastapi/fastapi/pull/13146) by [@tiangolo](https://github.com/tiangolo). From 35b24deef37e006ae732d9190ea39fab3001ca5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Jan 2025 22:33:38 +0000 Subject: [PATCH 031/123] =?UTF-8?q?=F0=9F=91=B7=20Update=20token=20for=20F?= =?UTF-8?q?astAPI=20People=20-=20Sponsors=20(#13225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sponsors.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 590ac5487..d2d845b27 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -45,9 +45,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - env: - GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} - name: FastAPI People Sponsors run: python ./scripts/sponsors.py env: - GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.SPONSORS }} From ea0cdd120c39b34aca6ea592832d7fa43944d34d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 17 Jan 2025 22:34:04 +0000 Subject: [PATCH 032/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 12cf637f5..5c628813c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -85,6 +85,7 @@ hide: ### Internal +* 👷 Update token for FastAPI People - Sponsors. PR [#13225](https://github.com/fastapi/fastapi/pull/13225) by [@tiangolo](https://github.com/tiangolo). * 👷 Add independent CI automation for FastAPI People - Sponsors. PR [#13221](https://github.com/fastapi/fastapi/pull/13221) by [@tiangolo](https://github.com/tiangolo). * 👷 Add retries to Smokeshow. PR [#13151](https://github.com/fastapi/fastapi/pull/13151) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Speakeasy sponsor graphic. PR [#13147](https://github.com/fastapi/fastapi/pull/13147) by [@chailandau](https://github.com/chailandau). From 2ee101fb815188313fdbe262388711bc238c1bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Jan 2025 12:58:36 +0000 Subject: [PATCH 033/123] =?UTF-8?q?=F0=9F=91=B7=20Refactor=20FastAPI=20Peo?= =?UTF-8?q?ple=20Sponsors=20to=20use=202=20tokens=20(#13228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sponsors.yml | 3 ++- scripts/sponsors.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index d2d845b27..a5230c834 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -48,4 +48,5 @@ jobs: - name: FastAPI People Sponsors run: python ./scripts/sponsors.py env: - GITHUB_TOKEN: ${{ secrets.SPONSORS }} + SPONSORS_TOKEN: ${{ secrets.SPONSORS_TOKEN }} + PR_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }} diff --git a/scripts/sponsors.py b/scripts/sponsors.py index ed782a49e..45e02bd62 100644 --- a/scripts/sponsors.py +++ b/scripts/sponsors.py @@ -83,7 +83,8 @@ class SponsorsResponse(BaseModel): class Settings(BaseSettings): - github_token: SecretStr + sponsors_token: SecretStr + pr_token: SecretStr github_repository: str httpx_timeout: int = 30 @@ -94,7 +95,7 @@ def get_graphql_response( query: str, after: str | None = None, ) -> dict[str, Any]: - headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} + headers = {"Authorization": f"token {settings.sponsors_token.get_secret_value()}"} variables = {"after": after} response = httpx.post( github_graphql_url, @@ -159,7 +160,7 @@ def main() -> None: logging.basicConfig(level=logging.INFO) settings = Settings() logging.info(f"Using config: {settings.model_dump_json()}") - g = Github(settings.github_token.get_secret_value()) + g = Github(settings.pr_token.get_secret_value()) repo = g.get_repo(settings.github_repository) tiers = get_individual_sponsors(settings=settings) From 2acdc13608592e184a1dba6f80fe81880eaa3af7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 18 Jan 2025 12:58:57 +0000 Subject: [PATCH 034/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5c628813c..404e3269a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -85,6 +85,7 @@ hide: ### Internal +* 👷 Refactor FastAPI People Sponsors to use 2 tokens. PR [#13228](https://github.com/fastapi/fastapi/pull/13228) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for FastAPI People - Sponsors. PR [#13225](https://github.com/fastapi/fastapi/pull/13225) by [@tiangolo](https://github.com/tiangolo). * 👷 Add independent CI automation for FastAPI People - Sponsors. PR [#13221](https://github.com/fastapi/fastapi/pull/13221) by [@tiangolo](https://github.com/tiangolo). * 👷 Add retries to Smokeshow. PR [#13151](https://github.com/fastapi/fastapi/pull/13151) by [@tiangolo](https://github.com/tiangolo). From af599c92ac4a2993416408e5c231e84c9971eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Jan 2025 13:10:13 +0000 Subject: [PATCH 035/123] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Sponsors=20(#13231)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 492 ++++++++++++++----------------- 1 file changed, 225 insertions(+), 267 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 5f0be61c2..55fe3dda9 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,57 +2,60 @@ sponsors: - - login: bump-sh avatarUrl: https://avatars.githubusercontent.com/u/33217836?v=4 url: https://github.com/bump-sh - - login: porter-dev - avatarUrl: https://avatars.githubusercontent.com/u/62078005?v=4 - url: https://github.com/porter-dev + - login: Nixtla + avatarUrl: https://avatars.githubusercontent.com/u/79945230?v=4 + url: https://github.com/Nixtla - login: andrew-propelauth avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=1188c27cb744bbec36447a2cfd4453126b2ddb5c&v=4 url: https://github.com/andrew-propelauth + - login: liblaber + avatarUrl: https://avatars.githubusercontent.com/u/100821118?v=4 + url: https://github.com/liblaber - login: zanfaruqui avatarUrl: https://avatars.githubusercontent.com/u/104461687?v=4 url: https://github.com/zanfaruqui - - login: Alek99 - avatarUrl: https://avatars.githubusercontent.com/u/38776361?u=bd6c163fe787c2de1a26c881598e54b67e2482dd&v=4 - url: https://github.com/Alek99 - - login: cryptapi - avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 - url: https://github.com/cryptapi - - login: Kong - avatarUrl: https://avatars.githubusercontent.com/u/962416?v=4 - url: https://github.com/Kong - - login: codacy - avatarUrl: https://avatars.githubusercontent.com/u/1834093?v=4 - url: https://github.com/codacy + - login: blockbee-io + avatarUrl: https://avatars.githubusercontent.com/u/115143449?u=1b8620c2d6567c4df2111a371b85a51f448f9b85&v=4 + url: https://github.com/blockbee-io + - login: zuplo + avatarUrl: https://avatars.githubusercontent.com/u/85497839?v=4 + url: https://github.com/zuplo + - login: render-sponsorships + avatarUrl: https://avatars.githubusercontent.com/u/189296666?v=4 + url: https://github.com/render-sponsorships + - login: porter-dev + avatarUrl: https://avatars.githubusercontent.com/u/62078005?v=4 + url: https://github.com/porter-dev - login: scalar avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 url: https://github.com/scalar - - login: ObliviousAI avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 url: https://github.com/ObliviousAI -- - login: databento - avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 - url: https://github.com/databento - - login: svix +- - login: svix avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 url: https://github.com/svix - - login: deepset-ai - avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 - url: https://github.com/deepset-ai - - login: mikeckennedy - avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=ce6165b799ea3164cb6f5ff54ea08042057442af&v=4 - url: https://github.com/mikeckennedy - - login: ndimares - avatarUrl: https://avatars.githubusercontent.com/u/6267663?u=cfb27efde7a7212be8142abb6c058a1aeadb41b1&v=4 - url: https://github.com/ndimares -- - login: takashi-yoneya - avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 - url: https://github.com/takashi-yoneya + - login: stainless-api + avatarUrl: https://avatars.githubusercontent.com/u/88061651?v=4 + url: https://github.com/stainless-api + - login: speakeasy-api + avatarUrl: https://avatars.githubusercontent.com/u/91446104?v=4 + url: https://github.com/speakeasy-api + - login: databento + avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 + url: https://github.com/databento +- - login: mercedes-benz + avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 + url: https://github.com/mercedes-benz - login: xoflare avatarUrl: https://avatars.githubusercontent.com/u/74335107?v=4 url: https://github.com/xoflare - login: marvin-robot avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=b9fcab402d0cd0aec738b6574fe60855cb0cd36d&v=4 url: https://github.com/marvin-robot + - login: Ponte-Energy-Partners + avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 + url: https://github.com/Ponte-Energy-Partners - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP @@ -62,42 +65,63 @@ sponsors: - - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie -- - login: americanair - avatarUrl: https://avatars.githubusercontent.com/u/12281813?v=4 - url: https://github.com/americanair +- - login: takashi-yoneya + avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 + url: https://github.com/takashi-yoneya +- - login: mainframeindustries + avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 + url: https://github.com/mainframeindustries - login: CanoaPBC avatarUrl: https://avatars.githubusercontent.com/u/64223768?v=4 url: https://github.com/CanoaPBC - - login: mainframeindustries - avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 - url: https://github.com/mainframeindustries - - login: mangualero - avatarUrl: https://avatars.githubusercontent.com/u/3422968?u=c59272d7b5a912d6126fd6c6f17db71e20f506eb&v=4 - url: https://github.com/mangualero - - login: birkjernstrom - avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 - url: https://github.com/birkjernstrom - login: yasyf avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 url: https://github.com/yasyf +- - login: genzou9201 + avatarUrl: https://avatars.githubusercontent.com/u/42960762?u=1ca6c18c59e8b327ae584c545b72de31ebc05275&v=4 + url: https://github.com/genzou9201 - - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb -- - login: jhundman - avatarUrl: https://avatars.githubusercontent.com/u/24263908?v=4 - url: https://github.com/jhundman - - login: upciti +- - login: upciti avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 url: https://github.com/upciti + - login: freddiev4 + avatarUrl: https://avatars.githubusercontent.com/u/8339018?u=1aad5b4f5a04cb750852b843d5e1d8f4ce339c2e&v=4 + url: https://github.com/freddiev4 - - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin - - login: Kludex - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex + - login: vincentkoc + avatarUrl: https://avatars.githubusercontent.com/u/25068?u=cbf098fc04c0473523d373b0dd2145b4ec99ef93&v=4 + url: https://github.com/vincentkoc + - login: ProteinQure + avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 + url: https://github.com/ProteinQure + - login: ddilidili + avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4 + url: https://github.com/ddilidili + - login: otosky + avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 + url: https://github.com/otosky + - login: mjohnsey + avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 + url: https://github.com/mjohnsey + - login: ashi-agrawal + avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 + url: https://github.com/ashi-agrawal + - login: sepsi77 + avatarUrl: https://avatars.githubusercontent.com/u/18682303?v=4 + url: https://github.com/sepsi77 + - login: RaamEEIL + avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 + url: https://github.com/RaamEEIL + - login: jhundman + avatarUrl: https://avatars.githubusercontent.com/u/24263908?v=4 + url: https://github.com/jhundman - login: b-rad-c avatarUrl: https://avatars.githubusercontent.com/u/25362581?u=5bb10629f4015b62bec1f9a366675d5085551af9&v=4 url: https://github.com/b-rad-c @@ -105,7 +129,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 url: https://github.com/ehaca - login: raphaellaude - avatarUrl: https://avatars.githubusercontent.com/u/28026311?u=9ae4b158c0d2cb29ebd46df6b6edb7de08a67566&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/28026311?u=28faad3e62250ef91a0c3c5d0faba39592d9ab39&v=4 url: https://github.com/raphaellaude - login: timlrx avatarUrl: https://avatars.githubusercontent.com/u/28362229?u=9a745ca31372ee324af682715ae88ce8522f9094&v=4 @@ -116,78 +140,51 @@ sponsors: - login: ygorpontelo avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 url: https://github.com/ygorpontelo - - login: ProteinQure - avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 - url: https://github.com/ProteinQure - - login: catherinenelson1 - avatarUrl: https://avatars.githubusercontent.com/u/11951946?u=e714b957185b8cf3d301cced7fc3ad2842122c6a&v=4 - url: https://github.com/catherinenelson1 - - login: jsoques - avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 - url: https://github.com/jsoques - - login: joeds13 - avatarUrl: https://avatars.githubusercontent.com/u/13631604?u=628eb122e08bef43767b3738752b883e8e7f6259&v=4 - url: https://github.com/joeds13 - - login: dannywade - avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 - url: https://github.com/dannywade - - login: khadrawy - avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 - url: https://github.com/khadrawy - - login: mjohnsey - avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 - url: https://github.com/mjohnsey - - login: ashi-agrawal - avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 - url: https://github.com/ashi-agrawal - - login: sepsi77 - avatarUrl: https://avatars.githubusercontent.com/u/18682303?v=4 - url: https://github.com/sepsi77 - - login: wedwardbeck - avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 - url: https://github.com/wedwardbeck - - login: RaamEEIL - avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 - url: https://github.com/RaamEEIL - - login: anthonycepeda - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 - url: https://github.com/anthonycepeda - - login: patricioperezv - avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 - url: https://github.com/patricioperezv + - login: chickenandstats + avatarUrl: https://avatars.githubusercontent.com/u/79477966?v=4 + url: https://github.com/chickenandstats - login: kaoru0310 avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 url: https://github.com/kaoru0310 - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare - - login: Eruditis + - login: Karine-Bauch + avatarUrl: https://avatars.githubusercontent.com/u/90465103?u=7feb1018abb1a5631cfd9a91fea723d1ceb5f49b&v=4 + url: https://github.com/Karine-Bauch + - login: eruditis avatarUrl: https://avatars.githubusercontent.com/u/95244703?v=4 - url: https://github.com/Eruditis + url: https://github.com/eruditis - login: jugeeem avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 url: https://github.com/jugeeem - - login: apitally - avatarUrl: https://avatars.githubusercontent.com/u/138365043?v=4 - url: https://github.com/apitally - login: logic-automation avatarUrl: https://avatars.githubusercontent.com/u/144732884?v=4 url: https://github.com/logic-automation - - login: ddilidili - avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4 - url: https://github.com/ddilidili + - login: Torqsight-Labs + avatarUrl: https://avatars.githubusercontent.com/u/169598176?v=4 + url: https://github.com/Torqsight-Labs - login: ramonalmeidam avatarUrl: https://avatars.githubusercontent.com/u/45269580?u=3358750b3a5854d7c3ed77aaca7dd20a0f529d32&v=4 url: https://github.com/ramonalmeidam + - login: roboflow + avatarUrl: https://avatars.githubusercontent.com/u/53104118?v=4 + url: https://github.com/roboflow - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 url: https://github.com/dudikbender - - login: prodhype - avatarUrl: https://avatars.githubusercontent.com/u/60444672?u=3f278cff25ea37ead487d7861d4a984795de819e&v=4 - url: https://github.com/prodhype - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia + - login: anthonycepeda + avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 + url: https://github.com/anthonycepeda + - login: patricioperezv + avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 + url: https://github.com/patricioperezv + - login: mintuhouse + avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 + url: https://github.com/mintuhouse - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith @@ -200,9 +197,6 @@ sponsors: - login: knallgelb avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 url: https://github.com/knallgelb - - login: johannquerne - avatarUrl: https://avatars.githubusercontent.com/u/2736484?u=9b3381546a25679913a2b08110e4373c98840821&v=4 - url: https://github.com/johannquerne - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 @@ -215,15 +209,18 @@ sponsors: - login: kennywakeland avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 url: https://github.com/kennywakeland - - login: simw - avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 - url: https://github.com/simw - - login: koconder - avatarUrl: https://avatars.githubusercontent.com/u/25068?u=582657b23622aaa3dfe68bd028a780f272f456fa&v=4 - url: https://github.com/koconder + - login: aacayaco + avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 + url: https://github.com/aacayaco + - login: anomaly + avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 + url: https://github.com/anomaly - login: jstanden avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 url: https://github.com/jstanden + - login: paulcwatts + avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4 + url: https://github.com/paulcwatts - login: andreaso avatarUrl: https://avatars.githubusercontent.com/u/285964?u=837265cc7562c0685f25b2d81cd9de0434fe107c&v=4 url: https://github.com/andreaso @@ -239,36 +236,36 @@ sponsors: - login: wshayes avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes + - login: gaetanBloch + avatarUrl: https://avatars.githubusercontent.com/u/583199?u=50c49e83d6b4feb78a091901ea02ead1462f442b&v=4 + url: https://github.com/gaetanBloch - login: koxudaxi avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 url: https://github.com/koxudaxi - login: falkben avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - - login: mintuhouse - avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 - url: https://github.com/mintuhouse - - login: Rehket - avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 - url: https://github.com/Rehket - - login: hiancdtrsnm - avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 - url: https://github.com/hiancdtrsnm - login: TrevorBenson - avatarUrl: https://avatars.githubusercontent.com/u/9167887?u=afdd1766fdb79e04e59094cc6a54cd011ee7f686&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9167887?u=dccbea3327a57750923333d8ebf1a0b3f1948949&v=4 url: https://github.com/TrevorBenson - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: aacayaco - avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 - url: https://github.com/aacayaco - - login: anomaly - avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 - url: https://github.com/anomaly - - login: jgreys - avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=096820d1ef89877d57d0f68e669ead8b0fde84df&v=4 - url: https://github.com/jgreys + - login: catherinenelson1 + avatarUrl: https://avatars.githubusercontent.com/u/11951946?u=fe11bc35d36b6038cd46a946e4e46ef8aa5688ab&v=4 + url: https://github.com/catherinenelson1 + - login: jsoques + avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 + url: https://github.com/jsoques + - login: joeds13 + avatarUrl: https://avatars.githubusercontent.com/u/13631604?u=628eb122e08bef43767b3738752b883e8e7f6259&v=4 + url: https://github.com/joeds13 + - login: dannywade + avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 + url: https://github.com/dannywade + - login: khadrawy + avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 + url: https://github.com/khadrawy - login: Ryandaydev avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=48f68868db8886fce31a1d802c1003914c6cd7c6&v=4 url: https://github.com/Ryandaydev @@ -290,108 +287,81 @@ sponsors: - login: FernandoCelmer avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=d29fff3fd862fda4ca752079f13f32e84c762ea4&v=4 url: https://github.com/FernandoCelmer -- - login: getsentry - avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 - url: https://github.com/getsentry + - login: simw + avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 + url: https://github.com/simw + - login: Rehket + avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 + url: https://github.com/Rehket + - login: hiancdtrsnm + avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 + url: https://github.com/hiancdtrsnm - - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy - - login: SebTota - avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 - url: https://github.com/SebTota - - login: nisutec - avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 - url: https://github.com/nisutec - - login: hoenie-ams - avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 - url: https://github.com/hoenie-ams - - login: joerambo - avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 - url: https://github.com/joerambo - - login: rlnchow - avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 - url: https://github.com/rlnchow - login: engineerjoe440 avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=db5e6f4f87836cad26c2aa90ce390ce49041c5a9&v=4 url: https://github.com/bnkc - - login: DevOpsKev - avatarUrl: https://avatars.githubusercontent.com/u/36336550?u=6ccd5978fdaab06f37e22f2a14a7439341df7f67&v=4 - url: https://github.com/DevOpsKev - login: petercool avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=81c525232bb35780945a68e88afd96bb2cdad9c4&v=4 url: https://github.com/petercool - - login: JimFawkes - avatarUrl: https://avatars.githubusercontent.com/u/12075115?u=dc58ecfd064d72887c34bf500ddfd52592509acd&v=4 - url: https://github.com/JimFawkes - - login: artempronevskiy - avatarUrl: https://avatars.githubusercontent.com/u/12235104?u=03df6e1e55c9c6fe5d230adabb8dd7d43d8bbe8f&v=4 - url: https://github.com/artempronevskiy - - login: TheR1D - avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 - url: https://github.com/TheR1D - - login: joshuatz - avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 - url: https://github.com/joshuatz - - login: jangia - avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 - url: https://github.com/jangia - - login: jackleeio - avatarUrl: https://avatars.githubusercontent.com/u/20477587?u=c5184dab6d021733d10c8f975b20e391856303d6&v=4 - url: https://github.com/jackleeio - - login: shuheng-liu - avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 - url: https://github.com/shuheng-liu - - login: pers0n4 - avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 - url: https://github.com/pers0n4 - - login: curegit - avatarUrl: https://avatars.githubusercontent.com/u/37978051?u=1733c322079118c0cdc573c03d92813f50a9faec&v=4 - url: https://github.com/curegit - - login: fernandosmither - avatarUrl: https://avatars.githubusercontent.com/u/66154723?u=f79753eb207d01cca5bbb91ac62db6123e7622d1&v=4 - url: https://github.com/fernandosmither - - login: PunRabbit - avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 - url: https://github.com/PunRabbit - - login: PelicanQ - avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 - url: https://github.com/PelicanQ - - login: tahmarrrr23 - avatarUrl: https://avatars.githubusercontent.com/u/138208610?u=465a46b0ff72a74252d3e3a71ac7d2f1919cda28&v=4 - url: https://github.com/tahmarrrr23 - - login: zk-Call - avatarUrl: https://avatars.githubusercontent.com/u/147117264?v=4 - url: https://github.com/zk-Call - - login: kristiangronberg - avatarUrl: https://avatars.githubusercontent.com/u/42678548?v=4 - url: https://github.com/kristiangronberg - - login: leonardo-holguin - avatarUrl: https://avatars.githubusercontent.com/u/43093055?u=b59013d52fb6c4e0954aaaabc0882bd844985b38&v=4 - url: https://github.com/leonardo-holguin - - login: arrrrrmin - avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=36a3880a6eb29309c19e6cadbb173bafbe91deb1&v=4 - url: https://github.com/arrrrrmin + - login: siavashyj + avatarUrl: https://avatars.githubusercontent.com/u/43583410?u=562005ddc7901cd27a1219a118a2363817b14977&v=4 + url: https://github.com/siavashyj - login: mobyw avatarUrl: https://avatars.githubusercontent.com/u/44370805?v=4 url: https://github.com/mobyw - login: ArtyomVancyan avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 url: https://github.com/ArtyomVancyan - - login: harol97 - avatarUrl: https://avatars.githubusercontent.com/u/49042862?u=2b18e115ab73f5f09a280be2850f93c58a12e3d2&v=4 - url: https://github.com/harol97 + - login: TheR1D + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 + url: https://github.com/TheR1D + - login: joshuatz + avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 + url: https://github.com/joshuatz + - login: SebTota + avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 + url: https://github.com/SebTota + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec + - login: hoenie-ams + avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 + url: https://github.com/hoenie-ams + - login: joerambo + avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 + url: https://github.com/joerambo + - login: rlnchow + avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 + url: https://github.com/rlnchow + - login: dvlpjrs + avatarUrl: https://avatars.githubusercontent.com/u/32254642?u=fbd6ad0324d4f1eb6231cf775be1c7bd4404e961&v=4 + url: https://github.com/dvlpjrs + - login: caviri + avatarUrl: https://avatars.githubusercontent.com/u/45425937?u=4e14bd64282bad8f385eafbdb004b5a279366d6e&v=4 + url: https://github.com/caviri - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=62c7ff3519858423579676cd0efbd7e3f1ffe63a&v=4 url: https://github.com/hgalytoby - login: conservative-dude avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4 url: https://github.com/conservative-dude - - login: Joaopcamposs - avatarUrl: https://avatars.githubusercontent.com/u/57376574?u=699d5ba5ee66af1d089df6b5e532b97169e73650&v=4 - url: https://github.com/Joaopcamposs + - login: CR1337 + avatarUrl: https://avatars.githubusercontent.com/u/62649536?u=57a6aab10d2421a497306da8bcded01b826c54ae&v=4 + url: https://github.com/CR1337 + - login: PunRabbit + avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 + url: https://github.com/PunRabbit + - login: PelicanQ + avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 + url: https://github.com/PelicanQ + - login: tochikuji + avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 + url: https://github.com/tochikuji - login: browniebroke avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke @@ -407,9 +377,12 @@ sponsors: - login: leobiscassi avatarUrl: https://avatars.githubusercontent.com/u/1977418?u=f9f82445a847ab479bd7223debd677fcac6c49a0&v=4 url: https://github.com/leobiscassi - - login: cbonoz - avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 - url: https://github.com/cbonoz + - login: Alisa-lisa + avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 + url: https://github.com/Alisa-lisa + - login: Graeme22 + avatarUrl: https://avatars.githubusercontent.com/u/4185684?u=498182a42300d7bcd4de1215190cb17eb501136c&v=4 + url: https://github.com/Graeme22 - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier @@ -419,33 +392,15 @@ sponsors: - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs - - login: adamghill - avatarUrl: https://avatars.githubusercontent.com/u/317045?u=f1349d5ffe84a19f324e204777859fbf69ddf633&v=4 - url: https://github.com/adamghill + - login: ceb10n + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n - login: eteq avatarUrl: https://avatars.githubusercontent.com/u/346587?v=4 url: https://github.com/eteq - - login: dmig - avatarUrl: https://avatars.githubusercontent.com/u/388564?v=4 - url: https://github.com/dmig - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy - - login: tochikuji - avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 - url: https://github.com/tochikuji - - login: KentShikama - avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 - url: https://github.com/KentShikama - - login: katnoria - avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 - url: https://github.com/katnoria - - login: harsh183 - avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 - url: https://github.com/harsh183 - - login: hcristea - avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 - url: https://github.com/hcristea - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 @@ -453,7 +408,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/9369632?u=8c988f1b008a3f601385a3616f9327820f66e3a5&v=4 url: https://github.com/msehnout - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=2ef1ede118a72c170805f50b9ad07341fd16a354&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 url: https://github.com/xncbf - login: DMantis avatarUrl: https://avatars.githubusercontent.com/u/9536869?v=4 @@ -464,9 +419,6 @@ sponsors: - login: supdann avatarUrl: https://avatars.githubusercontent.com/u/9986994?u=9671810f4ae9504c063227fee34fd47567ff6954&v=4 url: https://github.com/supdann - - login: satwikkansal - avatarUrl: https://avatars.githubusercontent.com/u/10217535?u=b12d6ef74ea297de9e46da6933b1a5b7ba9e6a61&v=4 - url: https://github.com/satwikkansal - login: mntolia avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 url: https://github.com/mntolia @@ -479,17 +431,14 @@ sponsors: - login: Zuzah avatarUrl: https://avatars.githubusercontent.com/u/10934846?u=1ef43e075ddc87bd1178372bf4d95ee6175cae27&v=4 url: https://github.com/Zuzah - - login: Alisa-lisa - avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 - url: https://github.com/Alisa-lisa - - login: Graeme22 - avatarUrl: https://avatars.githubusercontent.com/u/4185684?u=498182a42300d7bcd4de1215190cb17eb501136c&v=4 - url: https://github.com/Graeme22 + - login: artempronevskiy + avatarUrl: https://avatars.githubusercontent.com/u/12235104?u=03df6e1e55c9c6fe5d230adabb8dd7d43d8bbe8f&v=4 + url: https://github.com/artempronevskiy - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood - login: rangulvers - avatarUrl: https://avatars.githubusercontent.com/u/5235430?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 url: https://github.com/rangulvers - login: sdevkota avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 @@ -500,33 +449,42 @@ sponsors: - login: Baghdady92 avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 url: https://github.com/Baghdady92 - - login: jakeecolution - avatarUrl: https://avatars.githubusercontent.com/u/5884696?u=4a7c7883fb064b593b50cb6697b54687e6f7aafe&v=4 - url: https://github.com/jakeecolution - - login: stephane-rbn - avatarUrl: https://avatars.githubusercontent.com/u/5939522?u=eb7ffe768fa3bcbcd04de14fe4a47444cc00ec4c&v=4 - url: https://github.com/stephane-rbn -- - login: danburonline - avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=94935cccfbec58083ab1e535212d54f1bf2c978a&v=4 - url: https://github.com/danburonline - - login: AliYmn - avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=0de5a262e8b4dc0a08d065f30f7a39941e246530&v=4 - url: https://github.com/AliYmn - - login: sadikkuzu - avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 - url: https://github.com/sadikkuzu - - login: tran-hai-long - avatarUrl: https://avatars.githubusercontent.com/u/119793901?u=3b173a845dcf099b275bdc9713a69cbbc36040ce&v=4 - url: https://github.com/tran-hai-long + - login: KentShikama + avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 + url: https://github.com/KentShikama + - login: katnoria + avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 + url: https://github.com/katnoria + - login: harsh183 + avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 + url: https://github.com/harsh183 + - login: hcristea + avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 + url: https://github.com/hcristea +- - login: larsyngvelundin + avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 + url: https://github.com/larsyngvelundin + - login: andrecorumba + avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 + url: https://github.com/andrecorumba - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd + - login: sadikkuzu + avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 + url: https://github.com/sadikkuzu + - login: Olegt0rr + avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 + url: https://github.com/Olegt0rr + - login: FabulousCodingFox + avatarUrl: https://avatars.githubusercontent.com/u/78906517?u=924a27cbee3db7e0ece5cc1509921402e1445e74&v=4 + url: https://github.com/FabulousCodingFox + - login: anqorithm + avatarUrl: https://avatars.githubusercontent.com/u/61029571?u=468256fa4e2d9ce2870b608299724bebb7a33f18&v=4 + url: https://github.com/anqorithm - login: ssbarnea - avatarUrl: https://avatars.githubusercontent.com/u/102495?u=c2efbf6fea2737e21dfc6b1113c4edc9644e9eaa&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/102495?u=c7bd9ddf127785286fc939dd18cb02db0a453bce&v=4 url: https://github.com/ssbarnea - - login: yuawn - avatarUrl: https://avatars.githubusercontent.com/u/5111198?u=5315576f3fe1a70fd2d0f02181588f4eea5d353d&v=4 - url: https://github.com/yuawn - - login: dongzhenye - avatarUrl: https://avatars.githubusercontent.com/u/5765843?u=fe420c9a4c41e5b060faaf44029f5485616b470d&v=4 - url: https://github.com/dongzhenye + - login: andreagrandi + avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 + url: https://github.com/andreagrandi From 7e06c4c97f1c33431449dacf35c992170991c4b3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 18 Jan 2025 13:10:40 +0000 Subject: [PATCH 036/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 404e3269a..f87e89af9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -85,6 +85,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Sponsors. PR [#13231](https://github.com/fastapi/fastapi/pull/13231) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor FastAPI People Sponsors to use 2 tokens. PR [#13228](https://github.com/fastapi/fastapi/pull/13228) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for FastAPI People - Sponsors. PR [#13225](https://github.com/fastapi/fastapi/pull/13225) by [@tiangolo](https://github.com/tiangolo). * 👷 Add independent CI automation for FastAPI People - Sponsors. PR [#13221](https://github.com/fastapi/fastapi/pull/13221) by [@tiangolo](https://github.com/tiangolo). From 1cedd8becf0cb832d4fec05c2bb4babbca89b2a9 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:19:58 +0000 Subject: [PATCH 037/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20bo?= =?UTF-8?q?dy=5Fmultiple=5Fparams=20(#13170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_tutorial001.py | 21 +- .../test_tutorial001_an.py | 206 ----------------- .../test_tutorial001_an_py310.py | 213 ------------------ .../test_tutorial001_an_py39.py | 213 ------------------ .../test_tutorial001_py310.py | 213 ------------------ 5 files changed, 17 insertions(+), 849 deletions(-) delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 6275ebe95..142405595 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -1,13 +1,26 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39, needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py deleted file mode 100644 index 5cd3e2c4a..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py +++ /dev/null @@ -1,206 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial001_an import app - - client = TestClient(app) - return client - - -def test_post_body_q_bar_content(client: TestClient): - response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - } - - -def test_post_no_body_q_bar(client: TestClient): - response = client.put("/items/5?q=bar", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5, "q": "bar"} - - -def test_post_no_body(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5} - - -def test_post_id_foo(client: TestClient): - response = client.put("/items/foo", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Item"} - ) - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py deleted file mode 100644 index 0173ab21b..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py +++ /dev/null @@ -1,213 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial001_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_q_bar_content(client: TestClient): - response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - } - - -@needs_py310 -def test_post_no_body_q_bar(client: TestClient): - response = client.put("/items/5?q=bar", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5, "q": "bar"} - - -@needs_py310 -def test_post_no_body(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5} - - -@needs_py310 -def test_post_id_foo(client: TestClient): - response = client.put("/items/foo", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Item"} - ) - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py deleted file mode 100644 index cda19918a..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py +++ /dev/null @@ -1,213 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_q_bar_content(client: TestClient): - response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - } - - -@needs_py39 -def test_post_no_body_q_bar(client: TestClient): - response = client.put("/items/5?q=bar", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5, "q": "bar"} - - -@needs_py39 -def test_post_no_body(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5} - - -@needs_py39 -def test_post_id_foo(client: TestClient): - response = client.put("/items/foo", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Item"} - ) - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py deleted file mode 100644 index 663291933..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py +++ /dev/null @@ -1,213 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_q_bar_content(client: TestClient): - response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - } - - -@needs_py310 -def test_post_no_body_q_bar(client: TestClient): - response = client.put("/items/5?q=bar", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5, "q": "bar"} - - -@needs_py310 -def test_post_no_body(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 200 - assert response.json() == {"item_id": 5} - - -@needs_py310 -def test_post_id_foo(client: TestClient): - response = client.put("/items/foo", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["path", "item_id"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/Item"}, - {"type": "null"}, - ], - "title": "Item", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/Item"} - ) - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From 4191f4d33a11d3d9c4d996c6d92314f03b43d28e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:20:23 +0000 Subject: [PATCH 038/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f87e89af9..a0863e72f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for body_multiple_params. PR [#13170](https://github.com/fastapi/fastapi/pull/13170) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_fields. PR [#13169](https://github.com/fastapi/fastapi/pull/13169) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body. PR [#13168](https://github.com/fastapi/fastapi/pull/13168) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for bigger_applications. PR [#13167](https://github.com/fastapi/fastapi/pull/13167) by [@alejsdev](https://github.com/alejsdev). From 55ef9270b8821377402231064ba1da5cfe2a626e Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:20:41 +0000 Subject: [PATCH 039/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20bo?= =?UTF-8?q?dy=5Fnested=5Fmodels=20(#13171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_tutorial009.py | 18 ++- .../test_tutorial009_py39.py | 128 ------------------ 2 files changed, 14 insertions(+), 132 deletions(-) delete mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index 762073aea..38ba3c887 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -1,13 +1,23 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_nested_models.tutorial009 import app +@pytest.fixture( + name="client", + params=[ + "tutorial009", + pytest.param("tutorial009_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py deleted file mode 100644 index 24623cecc..000000000 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py +++ /dev/null @@ -1,128 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_nested_models.tutorial009_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body(client: TestClient): - data = {"2": 2.2, "3": 3.3} - response = client.post("/index-weights/", json=data) - assert response.status_code == 200, response.text - assert response.json() == data - - -@needs_py39 -def test_post_invalid_body(client: TestClient): - data = {"foo": 2.2, "3": 3.3} - response = client.post("/index-weights/", json=data) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "foo", "[key]"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "foo", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/index-weights/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Index Weights", - "operationId": "create_index_weights_index_weights__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Weights", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From f30dd4fe40ee3c70a906b2693f0d23832ab45508 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:21:05 +0000 Subject: [PATCH 040/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a0863e72f..3063be8d6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for body_nested_models. PR [#13171](https://github.com/fastapi/fastapi/pull/13171) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_multiple_params. PR [#13170](https://github.com/fastapi/fastapi/pull/13170) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_fields. PR [#13169](https://github.com/fastapi/fastapi/pull/13169) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body. PR [#13168](https://github.com/fastapi/fastapi/pull/13168) by [@alejsdev](https://github.com/alejsdev). From 0a882e926e30af050a9557721ec1f726500972ba Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:21:30 +0000 Subject: [PATCH 041/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20bo?= =?UTF-8?q?dy=5Fupdates=20(#13172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_body_updates/test_tutorial001.py | 19 +- .../test_tutorial001_py310.py | 317 ------------------ .../test_tutorial001_py39.py | 317 ------------------ 3 files changed, 14 insertions(+), 639 deletions(-) delete mode 100644 tests/test_tutorial/test_body_updates/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_body_updates/test_tutorial001_py39.py diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index e586534a0..f874dc9bd 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -1,14 +1,23 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv1, needs_pydanticv2 +from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_updates.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_updates.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py deleted file mode 100644 index 6bc969d43..000000000 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ /dev/null @@ -1,317 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_updates.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_get(client: TestClient): - response = client.get("/items/baz") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Baz", - "description": None, - "price": 50.2, - "tax": 10.5, - "tags": [], - } - - -@needs_py310 -def test_put(client: TestClient): - response = client.put( - "/items/bar", json={"name": "Barz", "price": 3, "description": None} - ) - assert response.json() == { - "name": "Barz", - "description": None, - "price": 3, - "tax": 10.5, - "tags": [], - } - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "type": "object", - "title": "Item", - "properties": { - "name": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - }, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "price": { - "anyOf": [{"type": "number"}, {"type": "null"}], - "title": "Price", - }, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_py310 -@needs_pydanticv1 -def test_openapi_schema_pv1(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py deleted file mode 100644 index a1edb3370..000000000 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ /dev/null @@ -1,317 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_updates.tutorial001_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get(client: TestClient): - response = client.get("/items/baz") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Baz", - "description": None, - "price": 50.2, - "tax": 10.5, - "tags": [], - } - - -@needs_py39 -def test_put(client: TestClient): - response = client.put( - "/items/bar", json={"name": "Barz", "price": 3, "description": None} - ) - assert response.json() == { - "name": "Barz", - "description": None, - "price": 3, - "tax": 10.5, - "tags": [], - } - - -@needs_py39 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "type": "object", - "title": "Item", - "properties": { - "name": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - }, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "price": { - "anyOf": [{"type": "number"}, {"type": "null"}], - "title": "Price", - }, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_py39 -@needs_pydanticv1 -def test_openapi_schema_pv1(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From c0fddaa9a9431c3d5292bb9728bdcb62cfc3a1c9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:21:52 +0000 Subject: [PATCH 042/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3063be8d6..2b46bb38e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for body_updates. PR [#13172](https://github.com/fastapi/fastapi/pull/13172) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_nested_models. PR [#13171](https://github.com/fastapi/fastapi/pull/13171) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_multiple_params. PR [#13170](https://github.com/fastapi/fastapi/pull/13170) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_fields. PR [#13169](https://github.com/fastapi/fastapi/pull/13169) by [@alejsdev](https://github.com/alejsdev). From 920df4d1ac3bba3ac14ac61ccd330d88975d7849 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:25:51 +0000 Subject: [PATCH 043/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20de?= =?UTF-8?q?pendencies=20(#13174)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_dependencies/test_tutorial001.py | 25 +- .../test_dependencies/test_tutorial001_an.py | 183 ------------ .../test_tutorial001_an_py310.py | 191 ------------- .../test_tutorial001_an_py39.py | 191 ------------- .../test_tutorial001_py310.py | 191 ------------- .../test_dependencies/test_tutorial004.py | 25 +- .../test_dependencies/test_tutorial004_an.py | 162 ----------- .../test_tutorial004_an_py310.py | 170 ----------- .../test_tutorial004_an_py39.py | 170 ----------- .../test_tutorial004_py310.py | 170 ----------- .../test_dependencies/test_tutorial006.py | 30 +- .../test_dependencies/test_tutorial006_an.py | 149 ---------- .../test_tutorial006_an_py39.py | 161 ----------- .../test_dependencies/test_tutorial008b.py | 26 +- .../test_dependencies/test_tutorial008b_an.py | 23 -- .../test_tutorial008b_an_py39.py | 33 --- .../test_dependencies/test_tutorial008c.py | 36 ++- .../test_dependencies/test_tutorial008c_an.py | 38 --- .../test_tutorial008c_an_py39.py | 44 --- .../test_dependencies/test_tutorial008d.py | 40 ++- .../test_dependencies/test_tutorial008d_an.py | 41 --- .../test_tutorial008d_an_py39.py | 47 ---- .../test_dependencies/test_tutorial012.py | 38 ++- .../test_dependencies/test_tutorial012_an.py | 250 ---------------- .../test_tutorial012_an_py39.py | 266 ------------------ 25 files changed, 162 insertions(+), 2538 deletions(-) delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial004_an.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial006_an.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008b_an.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008c_an.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008c_an_py39.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008d_an.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008d_an_py39.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial012_an.py delete mode 100644 tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index d1324a641..ed9944912 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -1,10 +1,27 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.dependencies.tutorial001 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -17,13 +34,13 @@ client = TestClient(app) ("/users", 200, {"q": None, "skip": 0, "limit": 100}), ], ) -def test_get(path, expected_status, expected_response): +def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py deleted file mode 100644 index 79c2a1e10..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py +++ /dev/null @@ -1,183 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.dependencies.tutorial001_an import app - -client = TestClient(app) - - -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ], -) -def test_get(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py deleted file mode 100644 index 7db55a1c5..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py +++ /dev/null @@ -1,191 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial001_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ], -) -def test_get(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py deleted file mode 100644 index 68c2dedb1..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py +++ /dev/null @@ -1,191 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ], -) -def test_get(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py deleted file mode 100644 index 381eecb63..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py +++ /dev/null @@ -1,191 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ], -) -def test_get(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index 5c5d34cfc..8221c83d4 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -1,10 +1,27 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.dependencies.tutorial004 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + "tutorial004_an", + pytest.param("tutorial004_an_py39", marks=needs_py39), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -55,13 +72,13 @@ client = TestClient(app) ), ], ) -def test_get(path, expected_status, expected_response): +def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py deleted file mode 100644 index c5c1a1fb8..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py +++ /dev/null @@ -1,162 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.dependencies.tutorial004_an import app - -client = TestClient(app) - - -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ] - }, - ), - ( - "/items?q=foo", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ], - "q": "foo", - }, - ), - ( - "/items?q=foo&skip=1", - 200, - {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, - ), - ( - "/items?q=bar&limit=2", - 200, - {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?q=bar&skip=1&limit=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?limit=1&q=bar&skip=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ], -) -def test_get(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py deleted file mode 100644 index 6fd093ddb..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial004_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ] - }, - ), - ( - "/items?q=foo", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ], - "q": "foo", - }, - ), - ( - "/items?q=foo&skip=1", - 200, - {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, - ), - ( - "/items?q=bar&limit=2", - 200, - {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?q=bar&skip=1&limit=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?limit=1&q=bar&skip=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ], -) -def test_get(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py deleted file mode 100644 index fbbe84cc9..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial004_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ] - }, - ), - ( - "/items?q=foo", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ], - "q": "foo", - }, - ), - ( - "/items?q=foo&skip=1", - 200, - {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, - ), - ( - "/items?q=bar&limit=2", - 200, - {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?q=bar&skip=1&limit=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?limit=1&q=bar&skip=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ], -) -def test_get(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py deleted file mode 100644 index 845b098e7..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial004_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ] - }, - ), - ( - "/items?q=foo", - 200, - { - "items": [ - {"item_name": "Foo"}, - {"item_name": "Bar"}, - {"item_name": "Baz"}, - ], - "q": "foo", - }, - ), - ( - "/items?q=foo&skip=1", - 200, - {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, - ), - ( - "/items?q=bar&limit=2", - 200, - {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?q=bar&skip=1&limit=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ( - "/items?limit=1&q=bar&skip=1", - 200, - {"items": [{"item_name": "Bar"}], "q": "bar"}, - ), - ], -) -def test_get(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Q", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Q", "type": "string"} - ), - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 5f14d9a3b..4530762f7 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -1,12 +1,28 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.dependencies.tutorial006 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial006", + "tutorial006_an", + pytest.param("tutorial006_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_get_no_headers(): +def test_get_no_headers(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -45,13 +61,13 @@ def test_get_no_headers(): ) -def test_get_invalid_one_header(): +def test_get_invalid_one_header(client: TestClient): response = client.get("/items/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_get_invalid_second_header(): +def test_get_invalid_second_header(client: TestClient): response = client.get( "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} ) @@ -59,7 +75,7 @@ def test_get_invalid_second_header(): assert response.json() == {"detail": "X-Key header invalid"} -def test_get_valid_headers(): +def test_get_valid_headers(client: TestClient): response = client.get( "/items/", headers={ @@ -71,7 +87,7 @@ def test_get_valid_headers(): assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py deleted file mode 100644 index a307ff808..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py +++ /dev/null @@ -1,149 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.dependencies.tutorial006_an import app - -client = TestClient(app) - - -def test_get_no_headers(): - response = client.get("/items/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_get_invalid_one_header(): - response = client.get("/items/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -def test_get_invalid_second_header(): - response = client.get( - "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Key header invalid"} - - -def test_get_valid_headers(): - response = client.get( - "/items/", - headers={ - "X-Token": "fake-super-secret-token", - "X-Key": "fake-super-secret-key", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py deleted file mode 100644 index b41b1537e..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py +++ /dev/null @@ -1,161 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial006_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get_no_headers(client: TestClient): - response = client.get("/items/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_get_invalid_one_header(client: TestClient): - response = client.get("/items/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -@needs_py39 -def test_get_invalid_second_header(client: TestClient): - response = client.get( - "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Key header invalid"} - - -@needs_py39 -def test_get_valid_headers(client: TestClient): - response = client.get( - "/items/", - headers={ - "X-Token": "fake-super-secret-token", - "X-Key": "fake-super-secret-key", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b.py b/tests/test_tutorial/test_dependencies/test_tutorial008b.py index 86acba9e4..4d7092265 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008b.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b.py @@ -1,23 +1,39 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.dependencies.tutorial008b import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial008b", + "tutorial008b_an", + pytest.param("tutorial008b_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_get_no_item(): +def test_get_no_item(client: TestClient): response = client.get("/items/foo") assert response.status_code == 404, response.text assert response.json() == {"detail": "Item not found"} -def test_owner_error(): +def test_owner_error(client: TestClient): response = client.get("/items/plumbus") assert response.status_code == 400, response.text assert response.json() == {"detail": "Owner error: Rick"} -def test_get_item(): +def test_get_item(client: TestClient): response = client.get("/items/portal-gun") assert response.status_code == 200, response.text assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py deleted file mode 100644 index 7f51fc52a..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.dependencies.tutorial008b_an import app - -client = TestClient(app) - - -def test_get_no_item(): - response = client.get("/items/foo") - assert response.status_code == 404, response.text - assert response.json() == {"detail": "Item not found"} - - -def test_owner_error(): - response = client.get("/items/plumbus") - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Owner error: Rick"} - - -def test_get_item(): - response = client.get("/items/portal-gun") - assert response.status_code == 200, response.text - assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py deleted file mode 100644 index 7d24809a8..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial008b_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get_no_item(client: TestClient): - response = client.get("/items/foo") - assert response.status_code == 404, response.text - assert response.json() == {"detail": "Item not found"} - - -@needs_py39 -def test_owner_error(client: TestClient): - response = client.get("/items/plumbus") - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Owner error: Rick"} - - -@needs_py39 -def test_get_item(client: TestClient): - response = client.get("/items/portal-gun") - assert response.status_code == 200, response.text - assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008c.py b/tests/test_tutorial/test_dependencies/test_tutorial008c.py index 27be8895a..11e96bf46 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008c.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008c.py @@ -1,38 +1,50 @@ +import importlib +from types import ModuleType + import pytest from fastapi.exceptions import FastAPIError from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial008c import app +@pytest.fixture( + name="mod", + params=[ + "tutorial008c", + "tutorial008c_an", + pytest.param("tutorial008c_an_py39", marks=needs_py39), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") - client = TestClient(app) - return client + return mod -def test_get_no_item(client: TestClient): +def test_get_no_item(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/items/foo") assert response.status_code == 404, response.text assert response.json() == {"detail": "Item not found, there's only a plumbus here"} -def test_get(client: TestClient): +def test_get(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/items/plumbus") assert response.status_code == 200, response.text assert response.json() == "plumbus" -def test_fastapi_error(client: TestClient): +def test_fastapi_error(mod: ModuleType): + client = TestClient(mod.app) with pytest.raises(FastAPIError) as exc_info: client.get("/items/portal-gun") assert "No response object was returned" in exc_info.value.args[0] -def test_internal_server_error(): - from docs_src.dependencies.tutorial008c import app - - client = TestClient(app, raise_server_exceptions=False) +def test_internal_server_error(mod: ModuleType): + client = TestClient(mod.app, raise_server_exceptions=False) response = client.get("/items/portal-gun") assert response.status_code == 500, response.text assert response.text == "Internal Server Error" diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008c_an.py b/tests/test_tutorial/test_dependencies/test_tutorial008c_an.py deleted file mode 100644 index 10fa1ab50..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial008c_an.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -from fastapi.exceptions import FastAPIError -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial008c_an import app - - client = TestClient(app) - return client - - -def test_get_no_item(client: TestClient): - response = client.get("/items/foo") - assert response.status_code == 404, response.text - assert response.json() == {"detail": "Item not found, there's only a plumbus here"} - - -def test_get(client: TestClient): - response = client.get("/items/plumbus") - assert response.status_code == 200, response.text - assert response.json() == "plumbus" - - -def test_fastapi_error(client: TestClient): - with pytest.raises(FastAPIError) as exc_info: - client.get("/items/portal-gun") - assert "No response object was returned" in exc_info.value.args[0] - - -def test_internal_server_error(): - from docs_src.dependencies.tutorial008c_an import app - - client = TestClient(app, raise_server_exceptions=False) - response = client.get("/items/portal-gun") - assert response.status_code == 500, response.text - assert response.text == "Internal Server Error" diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008c_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial008c_an_py39.py deleted file mode 100644 index 6c3acff50..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial008c_an_py39.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest -from fastapi.exceptions import FastAPIError -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial008c_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get_no_item(client: TestClient): - response = client.get("/items/foo") - assert response.status_code == 404, response.text - assert response.json() == {"detail": "Item not found, there's only a plumbus here"} - - -@needs_py39 -def test_get(client: TestClient): - response = client.get("/items/plumbus") - assert response.status_code == 200, response.text - assert response.json() == "plumbus" - - -@needs_py39 -def test_fastapi_error(client: TestClient): - with pytest.raises(FastAPIError) as exc_info: - client.get("/items/portal-gun") - assert "No response object was returned" in exc_info.value.args[0] - - -@needs_py39 -def test_internal_server_error(): - from docs_src.dependencies.tutorial008c_an_py39 import app - - client = TestClient(app, raise_server_exceptions=False) - response = client.get("/items/portal-gun") - assert response.status_code == 500, response.text - assert response.text == "Internal Server Error" diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008d.py b/tests/test_tutorial/test_dependencies/test_tutorial008d.py index 043496112..bc99bb383 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008d.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008d.py @@ -1,41 +1,51 @@ +import importlib +from types import ModuleType + import pytest from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial008d import app +@pytest.fixture( + name="mod", + params=[ + "tutorial008d", + "tutorial008d_an", + pytest.param("tutorial008d_an_py39", marks=needs_py39), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") - client = TestClient(app) - return client + return mod -def test_get_no_item(client: TestClient): +def test_get_no_item(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/items/foo") assert response.status_code == 404, response.text assert response.json() == {"detail": "Item not found, there's only a plumbus here"} -def test_get(client: TestClient): +def test_get(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/items/plumbus") assert response.status_code == 200, response.text assert response.json() == "plumbus" -def test_internal_error(client: TestClient): - from docs_src.dependencies.tutorial008d import InternalError - - with pytest.raises(InternalError) as exc_info: +def test_internal_error(mod: ModuleType): + client = TestClient(mod.app) + with pytest.raises(mod.InternalError) as exc_info: client.get("/items/portal-gun") assert ( exc_info.value.args[0] == "The portal gun is too dangerous to be owned by Rick" ) -def test_internal_server_error(): - from docs_src.dependencies.tutorial008d import app - - client = TestClient(app, raise_server_exceptions=False) +def test_internal_server_error(mod: ModuleType): + client = TestClient(mod.app, raise_server_exceptions=False) response = client.get("/items/portal-gun") assert response.status_code == 500, response.text assert response.text == "Internal Server Error" diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008d_an.py b/tests/test_tutorial/test_dependencies/test_tutorial008d_an.py deleted file mode 100644 index f29d8cdbe..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial008d_an.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial008d_an import app - - client = TestClient(app) - return client - - -def test_get_no_item(client: TestClient): - response = client.get("/items/foo") - assert response.status_code == 404, response.text - assert response.json() == {"detail": "Item not found, there's only a plumbus here"} - - -def test_get(client: TestClient): - response = client.get("/items/plumbus") - assert response.status_code == 200, response.text - assert response.json() == "plumbus" - - -def test_internal_error(client: TestClient): - from docs_src.dependencies.tutorial008d_an import InternalError - - with pytest.raises(InternalError) as exc_info: - client.get("/items/portal-gun") - assert ( - exc_info.value.args[0] == "The portal gun is too dangerous to be owned by Rick" - ) - - -def test_internal_server_error(): - from docs_src.dependencies.tutorial008d_an import app - - client = TestClient(app, raise_server_exceptions=False) - response = client.get("/items/portal-gun") - assert response.status_code == 500, response.text - assert response.text == "Internal Server Error" diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008d_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial008d_an_py39.py deleted file mode 100644 index 0a585f4ad..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial008d_an_py39.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial008d_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get_no_item(client: TestClient): - response = client.get("/items/foo") - assert response.status_code == 404, response.text - assert response.json() == {"detail": "Item not found, there's only a plumbus here"} - - -@needs_py39 -def test_get(client: TestClient): - response = client.get("/items/plumbus") - assert response.status_code == 200, response.text - assert response.json() == "plumbus" - - -@needs_py39 -def test_internal_error(client: TestClient): - from docs_src.dependencies.tutorial008d_an_py39 import InternalError - - with pytest.raises(InternalError) as exc_info: - client.get("/items/portal-gun") - assert ( - exc_info.value.args[0] == "The portal gun is too dangerous to be owned by Rick" - ) - - -@needs_py39 -def test_internal_server_error(): - from docs_src.dependencies.tutorial008d_an_py39 import app - - client = TestClient(app, raise_server_exceptions=False) - response = client.get("/items/portal-gun") - assert response.status_code == 500, response.text - assert response.text == "Internal Server Error" diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index 6b53c83bb..0af17e9bc 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -1,12 +1,28 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.dependencies.tutorial012 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial012", + "tutorial012_an", + pytest.param("tutorial012_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.dependencies.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_get_no_headers_items(): +def test_get_no_headers_items(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -45,7 +61,7 @@ def test_get_no_headers_items(): ) -def test_get_no_headers_users(): +def test_get_no_headers_users(client: TestClient): response = client.get("/users/") assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -84,19 +100,19 @@ def test_get_no_headers_users(): ) -def test_get_invalid_one_header_items(): +def test_get_invalid_one_header_items(client: TestClient): response = client.get("/items/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_get_invalid_one_users(): +def test_get_invalid_one_users(client: TestClient): response = client.get("/users/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_get_invalid_second_header_items(): +def test_get_invalid_second_header_items(client: TestClient): response = client.get( "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} ) @@ -104,7 +120,7 @@ def test_get_invalid_second_header_items(): assert response.json() == {"detail": "X-Key header invalid"} -def test_get_invalid_second_header_users(): +def test_get_invalid_second_header_users(client: TestClient): response = client.get( "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} ) @@ -112,7 +128,7 @@ def test_get_invalid_second_header_users(): assert response.json() == {"detail": "X-Key header invalid"} -def test_get_valid_headers_items(): +def test_get_valid_headers_items(client: TestClient): response = client.get( "/items/", headers={ @@ -124,7 +140,7 @@ def test_get_valid_headers_items(): assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] -def test_get_valid_headers_users(): +def test_get_valid_headers_users(client: TestClient): response = client.get( "/users/", headers={ @@ -136,7 +152,7 @@ def test_get_valid_headers_users(): assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py deleted file mode 100644 index 75adb69fc..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py +++ /dev/null @@ -1,250 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.dependencies.tutorial012_an import app - -client = TestClient(app) - - -def test_get_no_headers_items(): - response = client.get("/items/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_get_no_headers_users(): - response = client.get("/users/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_get_invalid_one_header_items(): - response = client.get("/items/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -def test_get_invalid_one_users(): - response = client.get("/users/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -def test_get_invalid_second_header_items(): - response = client.get( - "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Key header invalid"} - - -def test_get_invalid_second_header_users(): - response = client.get( - "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Key header invalid"} - - -def test_get_valid_headers_items(): - response = client.get( - "/items/", - headers={ - "X-Token": "fake-super-secret-token", - "X-Key": "fake-super-secret-key", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] - - -def test_get_valid_headers_users(): - response = client.get( - "/users/", - headers={ - "X-Token": "fake-super-secret-token", - "X-Key": "fake-super-secret-key", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py deleted file mode 100644 index e0a3d1ec2..000000000 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py +++ /dev/null @@ -1,266 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.dependencies.tutorial012_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get_no_headers_items(client: TestClient): - response = client.get("/items/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_get_no_headers_users(client: TestClient): - response = client.get("/users/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["header", "x-token"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["header", "x-key"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_get_invalid_one_header_items(client: TestClient): - response = client.get("/items/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -@needs_py39 -def test_get_invalid_one_users(client: TestClient): - response = client.get("/users/", headers={"X-Token": "invalid"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Token header invalid"} - - -@needs_py39 -def test_get_invalid_second_header_items(client: TestClient): - response = client.get( - "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Key header invalid"} - - -@needs_py39 -def test_get_invalid_second_header_users(client: TestClient): - response = client.get( - "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "X-Key header invalid"} - - -@needs_py39 -def test_get_valid_headers_items(client: TestClient): - response = client.get( - "/items/", - headers={ - "X-Token": "fake-super-secret-token", - "X-Key": "fake-super-secret-key", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] - - -@needs_py39 -def test_get_valid_headers_users(client: TestClient): - response = client.get( - "/users/", - headers={ - "X-Token": "fake-super-secret-token", - "X-Key": "fake-super-secret-key", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } From 409a850c6c21da8cd074bc3302b3e9244dc77e82 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:26:10 +0000 Subject: [PATCH 044/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2b46bb38e..c8cb58759 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for dependencies. PR [#13174](https://github.com/fastapi/fastapi/pull/13174) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_updates. PR [#13172](https://github.com/fastapi/fastapi/pull/13172) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_nested_models. PR [#13171](https://github.com/fastapi/fastapi/pull/13171) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_multiple_params. PR [#13170](https://github.com/fastapi/fastapi/pull/13170) by [@alejsdev](https://github.com/alejsdev). From 736712173ad75eebcd0e17d49ac30db0a9332b0f Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:26:50 +0000 Subject: [PATCH 045/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20co?= =?UTF-8?q?okie=5Fparams=20(#13176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_cookie_params/test_tutorial001.py | 29 ++++- .../test_cookie_params/test_tutorial001_an.py | 108 ----------------- .../test_tutorial001_an_py310.py | 114 ------------------ .../test_tutorial001_an_py39.py | 114 ------------------ .../test_tutorial001_py310.py | 114 ------------------ 5 files changed, 24 insertions(+), 455 deletions(-) delete mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py delete mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 7d0e669ab..90e8dfd37 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -1,8 +1,27 @@ +import importlib +from types import ModuleType + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.cookie_params.tutorial001 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.cookie_params.{request.param}") + + return mod @pytest.mark.parametrize( @@ -19,15 +38,15 @@ from docs_src.cookie_params.tutorial001 import app ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), ], ) -def test(path, cookies, expected_status, expected_response): - client = TestClient(app, cookies=cookies) +def test(path, cookies, expected_status, expected_response, mod: ModuleType): + client = TestClient(mod.app, cookies=cookies) response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): - client = TestClient(app) +def test_openapi_schema(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py deleted file mode 100644 index 2505876c8..000000000 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py +++ /dev/null @@ -1,108 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.cookie_params.tutorial001_an import app - - -@pytest.mark.parametrize( - "path,cookies,expected_status,expected_response", - [ - ("/items", None, 200, {"ads_id": None}), - ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), - ( - "/items", - {"ads_id": "ads_track", "session": "cookiesession"}, - 200, - {"ads_id": "ads_track"}, - ), - ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), - ], -) -def test(path, cookies, expected_status, expected_response): - client = TestClient(app, cookies=cookies) - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Ads Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Ads Id", "type": "string"} - ), - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py deleted file mode 100644 index 108f78b9c..000000000 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py +++ /dev/null @@ -1,114 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@needs_py310 -@pytest.mark.parametrize( - "path,cookies,expected_status,expected_response", - [ - ("/items", None, 200, {"ads_id": None}), - ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), - ( - "/items", - {"ads_id": "ads_track", "session": "cookiesession"}, - 200, - {"ads_id": "ads_track"}, - ), - ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), - ], -) -def test(path, cookies, expected_status, expected_response): - from docs_src.cookie_params.tutorial001_an_py310 import app - - client = TestClient(app, cookies=cookies) - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(): - from docs_src.cookie_params.tutorial001_an_py310 import app - - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Ads Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Ads Id", "type": "string"} - ), - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py deleted file mode 100644 index 8126a1052..000000000 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py +++ /dev/null @@ -1,114 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@needs_py39 -@pytest.mark.parametrize( - "path,cookies,expected_status,expected_response", - [ - ("/items", None, 200, {"ads_id": None}), - ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), - ( - "/items", - {"ads_id": "ads_track", "session": "cookiesession"}, - 200, - {"ads_id": "ads_track"}, - ), - ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), - ], -) -def test(path, cookies, expected_status, expected_response): - from docs_src.cookie_params.tutorial001_an_py39 import app - - client = TestClient(app, cookies=cookies) - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py39 -def test_openapi_schema(): - from docs_src.cookie_params.tutorial001_an_py39 import app - - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Ads Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Ads Id", "type": "string"} - ), - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py deleted file mode 100644 index 6711fa581..000000000 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ /dev/null @@ -1,114 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@needs_py310 -@pytest.mark.parametrize( - "path,cookies,expected_status,expected_response", - [ - ("/items", None, 200, {"ads_id": None}), - ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), - ( - "/items", - {"ads_id": "ads_track", "session": "cookiesession"}, - 200, - {"ads_id": "ads_track"}, - ), - ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), - ], -) -def test(path, cookies, expected_status, expected_response): - from docs_src.cookie_params.tutorial001_py310 import app - - client = TestClient(app, cookies=cookies) - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(): - from docs_src.cookie_params.tutorial001_py310 import app - - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Ads Id", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Ads Id", "type": "string"} - ), - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From cd521dff74da195170729610f592184f0f6664e3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:27:16 +0000 Subject: [PATCH 046/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c8cb58759..3230d7f81 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for cookie_params. PR [#13176](https://github.com/fastapi/fastapi/pull/13176) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for dependencies. PR [#13174](https://github.com/fastapi/fastapi/pull/13174) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_updates. PR [#13172](https://github.com/fastapi/fastapi/pull/13172) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_nested_models. PR [#13171](https://github.com/fastapi/fastapi/pull/13171) by [@alejsdev](https://github.com/alejsdev). From 8015f832d4f7788e5abf21bae4915e96683bea3c Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:28:09 +0000 Subject: [PATCH 047/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20ex?= =?UTF-8?q?tra=5Fdata=5Ftypes=20(#13177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_extra_data_types/test_tutorial001.py | 26 ++- .../test_tutorial001_an.py | 175 ----------------- .../test_tutorial001_an_py310.py | 184 ------------------ .../test_tutorial001_an_py39.py | 184 ------------------ .../test_tutorial001_py310.py | 184 ------------------ 5 files changed, 22 insertions(+), 731 deletions(-) delete mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py delete mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 5558671b9..b816c9cab 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -1,12 +1,30 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.extra_data_types.tutorial001 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.extra_data_types.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_extra_types(): +def test_extra_types(client: TestClient): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" data = { "start_datetime": "2018-12-22T14:00:00+00:00", @@ -27,7 +45,7 @@ def test_extra_types(): assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py deleted file mode 100644 index e309f8bd6..000000000 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py +++ /dev/null @@ -1,175 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.extra_data_types.tutorial001_an import app - -client = TestClient(app) - - -def test_extra_types(): - item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" - data = { - "start_datetime": "2018-12-22T14:00:00+00:00", - "end_datetime": "2018-12-24T15:00:00+00:00", - "repeat_at": "15:30:00", - "process_after": 300, - } - expected_response = data.copy() - expected_response.update( - { - "start_process": "2018-12-22T14:05:00+00:00", - "duration": 176_100, - "item_id": item_id, - } - ) - response = client.put(f"/items/{item_id}", json=data) - assert response.status_code == 200, response.text - assert response.json() == expected_response - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ) - } - }, - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": IsDict( - { - "title": "Repeat At", - "anyOf": [ - {"type": "string", "format": "time"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Repeat At", - "type": "string", - "format": "time", - } - ), - "process_after": IsDict( - { - "title": "Process After", - "type": "string", - "format": "duration", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Process After", - "type": "number", - "format": "time-delta", - } - ), - }, - "required": ["start_datetime", "end_datetime", "process_after"], - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py deleted file mode 100644 index ca110dc00..000000000 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py +++ /dev/null @@ -1,184 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.extra_data_types.tutorial001_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_extra_types(client: TestClient): - item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" - data = { - "start_datetime": "2018-12-22T14:00:00+00:00", - "end_datetime": "2018-12-24T15:00:00+00:00", - "repeat_at": "15:30:00", - "process_after": 300, - } - expected_response = data.copy() - expected_response.update( - { - "start_process": "2018-12-22T14:05:00+00:00", - "duration": 176_100, - "item_id": item_id, - } - ) - response = client.put(f"/items/{item_id}", json=data) - assert response.status_code == 200, response.text - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ) - } - }, - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": IsDict( - { - "title": "Repeat At", - "anyOf": [ - {"type": "string", "format": "time"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Repeat At", - "type": "string", - "format": "time", - } - ), - "process_after": IsDict( - { - "title": "Process After", - "type": "string", - "format": "duration", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Process After", - "type": "number", - "format": "time-delta", - } - ), - }, - "required": ["start_datetime", "end_datetime", "process_after"], - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py deleted file mode 100644 index 3386fb1fd..000000000 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py +++ /dev/null @@ -1,184 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.extra_data_types.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_extra_types(client: TestClient): - item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" - data = { - "start_datetime": "2018-12-22T14:00:00+00:00", - "end_datetime": "2018-12-24T15:00:00+00:00", - "repeat_at": "15:30:00", - "process_after": 300, - } - expected_response = data.copy() - expected_response.update( - { - "start_process": "2018-12-22T14:05:00+00:00", - "duration": 176_100, - "item_id": item_id, - } - ) - response = client.put(f"/items/{item_id}", json=data) - assert response.status_code == 200, response.text - assert response.json() == expected_response - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ) - } - }, - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": IsDict( - { - "title": "Repeat At", - "anyOf": [ - {"type": "string", "format": "time"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Repeat At", - "type": "string", - "format": "time", - } - ), - "process_after": IsDict( - { - "title": "Process After", - "type": "string", - "format": "duration", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Process After", - "type": "number", - "format": "time-delta", - } - ), - }, - "required": ["start_datetime", "end_datetime", "process_after"], - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py deleted file mode 100644 index 50c9aefdf..000000000 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py +++ /dev/null @@ -1,184 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.extra_data_types.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_extra_types(client: TestClient): - item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" - data = { - "start_datetime": "2018-12-22T14:00:00+00:00", - "end_datetime": "2018-12-24T15:00:00+00:00", - "repeat_at": "15:30:00", - "process_after": 300, - } - expected_response = data.copy() - expected_response.update( - { - "start_process": "2018-12-22T14:05:00+00:00", - "duration": 176_100, - "item_id": item_id, - } - ) - response = client.put(f"/items/{item_id}", json=data) - assert response.status_code == 200, response.text - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - ) - } - }, - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": IsDict( - { - "title": "Repeat At", - "anyOf": [ - {"type": "string", "format": "time"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Repeat At", - "type": "string", - "format": "time", - } - ), - "process_after": IsDict( - { - "title": "Process After", - "type": "string", - "format": "duration", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Process After", - "type": "number", - "format": "time-delta", - } - ), - }, - "required": ["start_datetime", "end_datetime", "process_after"], - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From ae93ec140a8d2eef947342d8f39dad68e3ac1189 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:28:31 +0000 Subject: [PATCH 048/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3230d7f81..c0506a09d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for extra_data_types. PR [#13177](https://github.com/fastapi/fastapi/pull/13177) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for cookie_params. PR [#13176](https://github.com/fastapi/fastapi/pull/13176) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for dependencies. PR [#13174](https://github.com/fastapi/fastapi/pull/13174) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for body_updates. PR [#13172](https://github.com/fastapi/fastapi/pull/13172) by [@alejsdev](https://github.com/alejsdev). From 3d017824bacba2d9df1ced81ff7f892ecca19d49 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:29:33 +0000 Subject: [PATCH 049/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20ex?= =?UTF-8?q?tra=5Fmodels=20(#13178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_extra_models/test_tutorial003.py | 25 ++- .../test_tutorial003_py310.py | 144 ------------------ .../test_extra_models/test_tutorial004.py | 23 ++- .../test_tutorial004_py39.py | 67 -------- .../test_extra_models/test_tutorial005.py | 23 ++- .../test_tutorial005_py39.py | 51 ------- 6 files changed, 58 insertions(+), 275 deletions(-) delete mode 100644 tests/test_tutorial/test_extra_models/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_extra_models/test_tutorial004_py39.py delete mode 100644 tests/test_tutorial/test_extra_models/test_tutorial005_py39.py diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 0ccb99948..73aa29903 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -1,12 +1,27 @@ +import importlib + +import pytest from dirty_equals import IsOneOf from fastapi.testclient import TestClient -from docs_src.extra_models.tutorial003 import app +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.extra_models.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_get_car(): +def test_get_car(client: TestClient): response = client.get("/items/item1") assert response.status_code == 200, response.text assert response.json() == { @@ -15,7 +30,7 @@ def test_get_car(): } -def test_get_plane(): +def test_get_plane(client: TestClient): response = client.get("/items/item2") assert response.status_code == 200, response.text assert response.json() == { @@ -25,7 +40,7 @@ def test_get_plane(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py deleted file mode 100644 index b2fe65fd9..000000000 --- a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py +++ /dev/null @@ -1,144 +0,0 @@ -import pytest -from dirty_equals import IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.extra_models.tutorial003_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_get_car(client: TestClient): - response = client.get("/items/item1") - assert response.status_code == 200, response.text - assert response.json() == { - "description": "All my friends drive a low rider", - "type": "car", - } - - -@needs_py310 -def test_get_plane(client: TestClient): - response = client.get("/items/item2") - assert response.status_code == 200, response.text - assert response.json() == { - "description": "Music is my aeroplane, it's my aeroplane", - "type": "plane", - "size": 5, - } - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": IsOneOf( - ["description", "type", "size"], - # TODO: remove when deprecating Pydantic v1 - ["description", "size"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "CarItem": { - "title": "CarItem", - "required": IsOneOf( - ["description", "type"], - # TODO: remove when deprecating Pydantic v1 - ["description"], - ), - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index 71f6a8c70..7628db30c 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -1,11 +1,26 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.extra_models.tutorial004 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial004", + pytest.param("tutorial004_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.extra_models.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_get_items(): +def test_get_items(client: TestClient): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [ @@ -14,7 +29,7 @@ def test_get_items(): ] -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py deleted file mode 100644 index 5475b92e1..000000000 --- a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py +++ /dev/null @@ -1,67 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.extra_models.tutorial004_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get_items(client: TestClient): - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - {"name": "Foo", "description": "There comes my hero"}, - {"name": "Red", "description": "It's my aeroplane"}, - ] - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "description"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - } - } - }, - } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index b0861c37f..553e44238 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -1,17 +1,32 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.extra_models.tutorial005 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial005", + pytest.param("tutorial005_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.extra_models.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_get_items(): +def test_get_items(client: TestClient): response = client.get("/keyword-weights/") assert response.status_code == 200, response.text assert response.json() == {"foo": 2.3, "bar": 3.4} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py deleted file mode 100644 index 7278e93c3..000000000 --- a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.extra_models.tutorial005_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_get_items(client: TestClient): - response = client.get("/keyword-weights/") - assert response.status_code == 200, response.text - assert response.json() == {"foo": 2.3, "bar": 3.4} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/keyword-weights/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Keyword Weights Keyword Weights Get", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - } - }, - "summary": "Read Keyword Weights", - "operationId": "read_keyword_weights_keyword_weights__get", - } - } - }, - } From d704f94cf0d6c95b33ca54ae2414e85aa377dd78 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:29:54 +0000 Subject: [PATCH 050/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c0506a09d..6f804b9a9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for extra_models. PR [#13178](https://github.com/fastapi/fastapi/pull/13178) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for extra_data_types. PR [#13177](https://github.com/fastapi/fastapi/pull/13177) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for cookie_params. PR [#13176](https://github.com/fastapi/fastapi/pull/13176) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for dependencies. PR [#13174](https://github.com/fastapi/fastapi/pull/13174) by [@alejsdev](https://github.com/alejsdev). From aa60185781da9709607ca0d136e7ac106a1a34bb Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:32:11 +0000 Subject: [PATCH 051/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20he?= =?UTF-8?q?ader=5Fparams=20(#13179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_header_params/test_tutorial001.py | 24 +++- .../test_header_params/test_tutorial001_an.py | 102 -------------- .../test_tutorial001_an_py310.py | 110 ---------------- .../test_tutorial001_py310.py | 110 ---------------- .../test_header_params/test_tutorial002.py | 25 +++- .../test_header_params/test_tutorial002_an.py | 113 ---------------- .../test_tutorial002_an_py310.py | 121 ----------------- .../test_tutorial002_an_py39.py | 124 ------------------ .../test_tutorial002_py310.py | 124 ------------------ .../test_header_params/test_tutorial003.py | 33 +++-- .../test_header_params/test_tutorial003_an.py | 110 ---------------- .../test_tutorial003_an_py310.py | 118 ----------------- .../test_tutorial003_an_py39.py | 118 ----------------- .../test_tutorial003_py310.py | 118 ----------------- 14 files changed, 64 insertions(+), 1286 deletions(-) delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_an.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_an.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py delete mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_py310.py diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 746fc0502..d6f7fe618 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -1,10 +1,26 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.header_params.tutorial001 import app +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.header_params.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -15,13 +31,13 @@ client = TestClient(app) ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), ], ) -def test(path, headers, expected_status, expected_response): +def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py deleted file mode 100644 index a715228aa..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.header_params.tutorial001_an import app - -client = TestClient(app) - - -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"User-Agent": "testclient"}), - ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), - ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), - ], -) -def test(path, headers, expected_status, expected_response): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User-Agent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User-Agent", "type": "string"} - ), - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py deleted file mode 100644 index caf85bc6c..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py +++ /dev/null @@ -1,110 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial001_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"User-Agent": "testclient"}), - ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), - ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User-Agent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User-Agent", "type": "string"} - ), - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py deleted file mode 100644 index 57e0a296a..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py +++ /dev/null @@ -1,110 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"User-Agent": "testclient"}), - ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), - ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "User-Agent", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "User-Agent", "type": "string"} - ), - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 78bac838c..7158f8651 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -1,10 +1,27 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.header_params.tutorial002 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + "tutorial002_an", + pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.header_params.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -26,13 +43,13 @@ client = TestClient(app) ), ], ) -def test(path, headers, expected_status, expected_response): +def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py deleted file mode 100644 index ffda8158f..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an.py +++ /dev/null @@ -1,113 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.header_params.tutorial002_an import app - -client = TestClient(app) - - -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"strange_header": None}), - ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), - ( - "/items", - {"strange_header": "FastAPI test"}, - 200, - {"strange_header": "FastAPI test"}, - ), - ( - "/items", - {"strange-header": "Not really underscore"}, - 200, - {"strange_header": None}, - ), - ], -) -def test(path, headers, expected_status, expected_response): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Strange Header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Strange Header", "type": "string"} - ), - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py deleted file mode 100644 index 6f332f3ba..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py +++ /dev/null @@ -1,121 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial002_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"strange_header": None}), - ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), - ( - "/items", - {"strange_header": "FastAPI test"}, - 200, - {"strange_header": "FastAPI test"}, - ), - ( - "/items", - {"strange-header": "Not really underscore"}, - 200, - {"strange_header": None}, - ), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Strange Header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Strange Header", "type": "string"} - ), - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py deleted file mode 100644 index 8202bc671..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py +++ /dev/null @@ -1,124 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial002_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"strange_header": None}), - ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), - ( - "/items", - {"strange_header": "FastAPI test"}, - 200, - {"strange_header": "FastAPI test"}, - ), - ( - "/items", - {"strange-header": "Not really underscore"}, - 200, - {"strange_header": None}, - ), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py39 -def test_openapi_schema(): - from docs_src.header_params.tutorial002_an_py39 import app - - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Strange Header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Strange Header", "type": "string"} - ), - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py deleted file mode 100644 index c113ed23e..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py +++ /dev/null @@ -1,124 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial002_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"strange_header": None}), - ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), - ( - "/items", - {"strange_header": "FastAPI test"}, - 200, - {"strange_header": "FastAPI test"}, - ), - ( - "/items", - {"strange-header": "Not really underscore"}, - 200, - {"strange_header": None}, - ), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(): - from docs_src.header_params.tutorial002_py310 import app - - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Strange Header", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Strange Header", "type": "string"} - ), - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index 6f7de8ed4..0b58227f6 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -1,10 +1,27 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.header_params.tutorial003 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + "tutorial003_an", + pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.header_params.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -12,21 +29,17 @@ client = TestClient(app) [ ("/items", None, 200, {"X-Token values": None}), ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), - ( - "/items", - [("x-token", "foo"), ("x-token", "bar")], - 200, - {"X-Token values": ["foo", "bar"]}, - ), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), ], ) -def test(path, headers, expected_status, expected_response): +def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py deleted file mode 100644 index 742ed41f4..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an.py +++ /dev/null @@ -1,110 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.header_params.tutorial003_an import app - -client = TestClient(app) - - -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"X-Token values": None}), - ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), - # TODO: fix this, is it a bug? - # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), - ], -) -def test(path, headers, expected_status, expected_response): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "x-token", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py deleted file mode 100644 index fdac4a416..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial003_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"X-Token values": None}), - ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), - # TODO: fix this, is it a bug? - # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "x-token", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py deleted file mode 100644 index c50543cc8..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial003_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"X-Token values": None}), - ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), - # TODO: fix this, is it a bug? - # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "x-token", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py deleted file mode 100644 index 3afb355e9..000000000 --- a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.header_params.tutorial003_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "path,headers,expected_status,expected_response", - [ - ("/items", None, 200, {"X-Token values": None}), - ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), - # TODO: fix this, is it a bug? - # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), - ], -) -def test(path, headers, expected_status, expected_response, client: TestClient): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": IsDict( - { - "title": "X-Token", - "anyOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - } - ), - "name": "x-token", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } From b123c5c489d7ade6039648b5eab2db090f69f224 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:32:34 +0000 Subject: [PATCH 052/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6f804b9a9..253f87923 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for header_params. PR [#13179](https://github.com/fastapi/fastapi/pull/13179) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for extra_models. PR [#13178](https://github.com/fastapi/fastapi/pull/13178) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for extra_data_types. PR [#13177](https://github.com/fastapi/fastapi/pull/13177) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for cookie_params. PR [#13176](https://github.com/fastapi/fastapi/pull/13176) by [@alejsdev](https://github.com/alejsdev). From 2e8db846b4b3d8deae94b757d99624f5e762bf1b Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:33:10 +0000 Subject: [PATCH 053/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20pa?= =?UTF-8?q?th=5Foperation=5Fconfigurations=20(#13180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_tutorial005.py | 28 ++- .../test_tutorial005_py310.py | 226 ------------------ .../test_tutorial005_py39.py | 226 ------------------ 3 files changed, 22 insertions(+), 458 deletions(-) delete mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py delete mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index d3792e701..0742f5d0e 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -1,13 +1,29 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.path_operation_configuration.tutorial005 import app +from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 + -from ...utils import needs_pydanticv1, needs_pydanticv2 +@pytest.fixture( + name="client", + params=[ + "tutorial005", + pytest.param("tutorial005_py39", marks=needs_py39), + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.path_operation_configuration.{request.param}" + ) -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_query_params_str_validations(): +def test_query_params_str_validations(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 42}) assert response.status_code == 200, response.text assert response.json() == { @@ -20,7 +36,7 @@ def test_query_params_str_validations(): @needs_pydanticv2 -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -123,7 +139,7 @@ def test_openapi_schema(): # TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 -def test_openapi_schema_pv1(): +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py deleted file mode 100644 index a68deb3df..000000000 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ /dev/null @@ -1,226 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.path_operation_configuration.tutorial005_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_query_params_str_validations(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "price": 42}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "price": 42, - "description": None, - "tax": None, - "tags": [], - } - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": {"title": "Price", "type": "number"}, - "tax": { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_py310 -@needs_pydanticv1 -def test_openapi_schema_pv1(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py deleted file mode 100644 index e17f2592d..000000000 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ /dev/null @@ -1,226 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.path_operation_configuration.tutorial005_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_query_params_str_validations(client: TestClient): - response = client.post("/items/", json={"name": "Foo", "price": 42}) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "price": 42, - "description": None, - "tax": None, - "tags": [], - } - - -@needs_py39 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": {"title": "Price", "type": "number"}, - "tax": { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_py39 -@needs_pydanticv1 -def test_openapi_schema_pv1(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From ec46247595ea7151d71b33ab62eca330700e7efc Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:33:35 +0000 Subject: [PATCH 054/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 253f87923..969ef2caa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for path_operation_configurations. PR [#13180](https://github.com/fastapi/fastapi/pull/13180) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for header_params. PR [#13179](https://github.com/fastapi/fastapi/pull/13179) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for extra_models. PR [#13178](https://github.com/fastapi/fastapi/pull/13178) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for extra_data_types. PR [#13177](https://github.com/fastapi/fastapi/pull/13177) by [@alejsdev](https://github.com/alejsdev). From 09ccfce228e51648dffe8d92d9ea3bdf66044824 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:34:48 +0000 Subject: [PATCH 055/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20pa?= =?UTF-8?q?th=5Fquery=5Fparams=20(#13181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_query_params/test_tutorial006.py | 18 +- .../test_tutorial006_py310.py | 180 ------------------ 2 files changed, 14 insertions(+), 184 deletions(-) delete mode 100644 tests/test_tutorial/test_query_params/test_tutorial006_py310.py diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index dbd63da16..a0b5ef494 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -1,13 +1,23 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params.tutorial006 import app +@pytest.fixture( + name="client", + params=[ + "tutorial006", + pytest.param("tutorial006_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_params.{request.param}") - c = TestClient(app) + c = TestClient(mod.app) return c diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py deleted file mode 100644 index 5055e3805..000000000 --- a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py +++ /dev/null @@ -1,180 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.query_params.tutorial006_py310 import app - - c = TestClient(app) - return c - - -@needs_py310 -def test_foo_needy_very(client: TestClient): - response = client.get("/items/foo?needy=very") - assert response.status_code == 200 - assert response.json() == { - "item_id": "foo", - "needy": "very", - "skip": 0, - "limit": None, - } - - -@needs_py310 -def test_foo_no_needy(client: TestClient): - response = client.get("/items/foo?skip=a&limit=b") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["query", "needy"], - "msg": "Field required", - "input": None, - }, - { - "type": "int_parsing", - "loc": ["query", "skip"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "a", - }, - { - "type": "int_parsing", - "loc": ["query", "limit"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "b", - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Limit", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Limit", "type": "integer"} - ), - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From 96808fd44fb5d8ffca3829bb8e045f783cbe15cb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:35:11 +0000 Subject: [PATCH 056/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 969ef2caa..33da093ec 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for path_query_params. PR [#13181](https://github.com/fastapi/fastapi/pull/13181) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for path_operation_configurations. PR [#13180](https://github.com/fastapi/fastapi/pull/13180) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for header_params. PR [#13179](https://github.com/fastapi/fastapi/pull/13179) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for extra_models. PR [#13178](https://github.com/fastapi/fastapi/pull/13178) by [@alejsdev](https://github.com/alejsdev). From c7d888a15f0500d06f0df618360e187fb965d3cb Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:42:25 +0000 Subject: [PATCH 057/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20re?= =?UTF-8?q?quest=5Fforms=20(#13184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_request_forms/test_tutorial001.py | 19 +- .../test_request_forms/test_tutorial001_an.py | 234 ----------------- .../test_tutorial001_an_py39.py | 242 ------------------ 3 files changed, 15 insertions(+), 480 deletions(-) delete mode 100644 tests/test_tutorial/test_request_forms/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index cbef9d30f..321f8022b 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -1,13 +1,24 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_forms.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_forms.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py deleted file mode 100644 index 88b8452bc..000000000 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py +++ /dev/null @@ -1,234 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_forms.tutorial001_an import app - - client = TestClient(app) - return client - - -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo"} - - -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login__post" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Body_login_login__post": { - "title": "Body_login_login__post", - "required": ["username", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py deleted file mode 100644 index 3229897c9..000000000 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py +++ /dev/null @@ -1,242 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_forms.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo"} - - -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login__post" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Body_login_login__post": { - "title": "Body_login_login__post", - "required": ["username", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From 9847cecf6f03721bca3f3b33528839cdd16e78cf Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:42:49 +0000 Subject: [PATCH 058/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 33da093ec..a9c19e858 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for request_forms. PR [#13184](https://github.com/fastapi/fastapi/pull/13184) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for path_query_params. PR [#13181](https://github.com/fastapi/fastapi/pull/13181) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for path_operation_configurations. PR [#13180](https://github.com/fastapi/fastapi/pull/13180) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for header_params. PR [#13179](https://github.com/fastapi/fastapi/pull/13179) by [@alejsdev](https://github.com/alejsdev). From d309c9e1403e645977afa7809d61d88e776140fa Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:43:21 +0000 Subject: [PATCH 059/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20re?= =?UTF-8?q?quest=5Fforms=5Fand=5Ffiles=20(#13185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_tutorial001.py | 19 +- .../test_tutorial001_an.py | 309 ----------------- .../test_tutorial001_an_py39.py | 317 ------------------ 3 files changed, 15 insertions(+), 630 deletions(-) delete mode 100644 tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index 1e1ad2a87..d12219245 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -1,14 +1,25 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="app") -def get_app(): - from docs_src.request_forms_and_files.tutorial001 import app +@pytest.fixture( + name="app", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_app(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_forms_and_files.{request.param}") - return app + return mod.app @pytest.fixture(name="client") diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py deleted file mode 100644 index 5daf4dbf4..000000000 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py +++ /dev/null @@ -1,309 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi import FastAPI -from fastapi.testclient import TestClient - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.request_forms_and_files.tutorial001_an import app - - return app - - -@pytest.fixture(name="client") -def get_client(app: FastAPI): - client = TestClient(app) - return client - - -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_form_no_file(client: TestClient): - response = client.post("/files/", data={"token": "foo"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_json(client: TestClient): - response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_file_no_token(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_files_and_token(tmp_path, app: FastAPI): - patha = tmp_path / "test.txt" - pathb = tmp_path / "testb.txt" - patha.write_text("") - pathb.write_text("") - - client = TestClient(app) - with patha.open("rb") as filea, pathb.open("rb") as fileb: - response = client.post( - "/files/", - data={"token": "foo"}, - files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "file_size": 14, - "token": "foo", - "fileb_content_type": "text/plain", - } - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file", "fileb", "token"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"}, - "fileb": { - "title": "Fileb", - "type": "string", - "format": "binary", - }, - "token": {"title": "Token", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py deleted file mode 100644 index 3f1204efa..000000000 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py +++ /dev/null @@ -1,317 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.request_forms_and_files.tutorial001_an_py39 import app - - return app - - -@pytest.fixture(name="client") -def get_client(app: FastAPI): - client = TestClient(app) - return client - - -@needs_py39 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_form_no_file(client: TestClient): - response = client.post("/files/", data={"token": "foo"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_file_no_token(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "fileb"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "token"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_files_and_token(tmp_path, app: FastAPI): - patha = tmp_path / "test.txt" - pathb = tmp_path / "testb.txt" - patha.write_text("") - pathb.write_text("") - - client = TestClient(app) - with patha.open("rb") as filea, pathb.open("rb") as fileb: - response = client.post( - "/files/", - data={"token": "foo"}, - files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "file_size": 14, - "token": "foo", - "fileb_content_type": "text/plain", - } - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file", "fileb", "token"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"}, - "fileb": { - "title": "Fileb", - "type": "string", - "format": "binary", - }, - "token": {"title": "Token", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From 0069963bbae58f7dcac15590e31205ab87e3cf12 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:43:44 +0000 Subject: [PATCH 060/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a9c19e858..2fa245cdc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for request_forms_and_files. PR [#13185](https://github.com/fastapi/fastapi/pull/13185) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_forms. PR [#13184](https://github.com/fastapi/fastapi/pull/13184) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for path_query_params. PR [#13181](https://github.com/fastapi/fastapi/pull/13181) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for path_operation_configurations. PR [#13180](https://github.com/fastapi/fastapi/pull/13180) by [@alejsdev](https://github.com/alejsdev). From 081901cc9900d03118ca752ac61cfe77d750539e Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:57:50 +0000 Subject: [PATCH 061/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20re?= =?UTF-8?q?quest=5Fmodel=20(#13195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_tutorial003_01.py | 23 ++- .../test_tutorial003_01_py310.py | 160 ----------------- .../test_tutorial003_05.py | 25 ++- .../test_tutorial003_05_py310.py | 103 ----------- .../test_response_model/test_tutorial004.py | 23 ++- .../test_tutorial004_py310.py | 147 ---------------- .../test_tutorial004_py39.py | 147 ---------------- .../test_response_model/test_tutorial005.py | 25 ++- .../test_tutorial005_py310.py | 166 ------------------ .../test_response_model/test_tutorial006.py | 25 ++- .../test_tutorial006_py310.py | 166 ------------------ 11 files changed, 98 insertions(+), 912 deletions(-) delete mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py delete mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py delete mode 100644 tests/test_tutorial/test_response_model/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_response_model/test_tutorial004_py39.py delete mode 100644 tests/test_tutorial/test_response_model/test_tutorial005_py310.py delete mode 100644 tests/test_tutorial/test_response_model/test_tutorial006_py310.py diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index 3a6a0b20d..3975856b6 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,12 +1,27 @@ +import importlib + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.response_model.tutorial003_01 import app +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003_01", + pytest.param("tutorial003_01_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_post_user(): +def test_post_user(client: TestClient): response = client.post( "/user/", json={ @@ -24,7 +39,7 @@ def test_post_user(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py deleted file mode 100644 index 6985b9de6..000000000 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py +++ /dev/null @@ -1,160 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.response_model.tutorial003_01_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_user(client: TestClient): - response = client.post( - "/user/", - json={ - "username": "foo", - "password": "fighter", - "email": "foo@example.com", - "full_name": "Grave Dohl", - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "foo", - "email": "foo@example.com", - "full_name": "Grave Dohl", - } - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/user/": { - "post": { - "summary": "Create User", - "operationId": "create_user_user__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserIn"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/BaseUser"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "BaseUser": { - "title": "BaseUser", - "required": IsOneOf( - ["username", "email", "full_name"], - # TODO: remove when deprecating Pydantic v1 - ["username", "email"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "UserIn": { - "title": "UserIn", - "required": ["username", "email", "password"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": { - "title": "Email", - "type": "string", - "format": "email", - }, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py index c7a39cc74..9500852e1 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -1,23 +1,38 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.response_model.tutorial003_05 import app +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003_05", + pytest.param("tutorial003_05_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_get_portal(): +def test_get_portal(client: TestClient): response = client.get("/portal") assert response.status_code == 200, response.text assert response.json() == {"message": "Here's your interdimensional portal."} -def test_get_redirect(): +def test_get_redirect(client: TestClient): response = client.get("/portal", params={"teleport": True}, follow_redirects=False) assert response.status_code == 307, response.text assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py deleted file mode 100644 index f80d62572..000000000 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py +++ /dev/null @@ -1,103 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.response_model.tutorial003_05_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_get_portal(client: TestClient): - response = client.get("/portal") - assert response.status_code == 200, response.text - assert response.json() == {"message": "Here's your interdimensional portal."} - - -@needs_py310 -def test_get_redirect(client: TestClient): - response = client.get("/portal", params={"teleport": True}, follow_redirects=False) - assert response.status_code == 307, response.text - assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/portal": { - "get": { - "summary": "Get Portal", - "operationId": "get_portal_portal_get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Teleport", - "type": "boolean", - "default": False, - }, - "name": "teleport", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index e9bde18dd..449a52b81 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,10 +1,25 @@ +import importlib + import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.response_model.tutorial004 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial004", + pytest.param("tutorial004_py39", marks=needs_py39), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client @pytest.mark.parametrize( @@ -27,13 +42,13 @@ client = TestClient(app) ), ], ) -def test_get(url, data): +def test_get(url, data, client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == data -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py deleted file mode 100644 index 6f8a3cbea..000000000 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ /dev/null @@ -1,147 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.response_model.tutorial004_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@pytest.mark.parametrize( - "url,data", - [ - ("/items/foo", {"name": "Foo", "price": 50.2}), - ( - "/items/bar", - {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, - ), - ( - "/items/baz", - { - "name": "Baz", - "description": None, - "price": 50.2, - "tax": 10.5, - "tags": [], - }, - ), - ], -) -def test_get(url, data, client: TestClient): - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == data - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax", "tags"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py deleted file mode 100644 index cfaa1eba2..000000000 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ /dev/null @@ -1,147 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.response_model.tutorial004_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -@pytest.mark.parametrize( - "url,data", - [ - ("/items/foo", {"name": "Foo", "price": 50.2}), - ( - "/items/bar", - {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, - ), - ( - "/items/baz", - { - "name": "Baz", - "description": None, - "price": 50.2, - "tax": 10.5, - "tags": [], - }, - ), - ], -) -def test_get(url, data, client: TestClient): - response = client.get(url) - assert response.status_code == 200, response.text - assert response.json() == data - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax", "tags"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index b20864c07..a633a3fdd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,18 +1,33 @@ +import importlib + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.response_model.tutorial005 import app +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_read_item_name(): +def test_read_item_name(client: TestClient): response = client.get("/items/bar/name") assert response.status_code == 200, response.text assert response.json() == {"name": "Bar", "description": "The Bar fighters"} -def test_read_item_public_data(): +def test_read_item_public_data(client: TestClient): response = client.get("/items/bar/public") assert response.status_code == 200, response.text assert response.json() == { @@ -22,7 +37,7 @@ def test_read_item_public_data(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py deleted file mode 100644 index de552c8f2..000000000 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ /dev/null @@ -1,166 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.response_model.tutorial005_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_read_item_name(client: TestClient): - response = client.get("/items/bar/name") - assert response.status_code == 200, response.text - assert response.json() == {"name": "Bar", "description": "The Bar fighters"} - - -@needs_py310 -def test_read_item_public_data(client: TestClient): - response = client.get("/items/bar/public") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Bar", - "description": "The Bar fighters", - "price": 62, - } - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 1e47e2ead..863522d1b 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,18 +1,33 @@ +import importlib + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.response_model.tutorial006 import app +from ...utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial006", + pytest.param("tutorial006_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.response_model.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_read_item_name(): +def test_read_item_name(client: TestClient): response = client.get("/items/bar/name") assert response.status_code == 200, response.text assert response.json() == {"name": "Bar", "description": "The Bar fighters"} -def test_read_item_public_data(): +def test_read_item_public_data(client: TestClient): response = client.get("/items/bar/public") assert response.status_code == 200, response.text assert response.json() == { @@ -22,7 +37,7 @@ def test_read_item_public_data(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py deleted file mode 100644 index 40058b12d..000000000 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ /dev/null @@ -1,166 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.response_model.tutorial006_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_read_item_name(client: TestClient): - response = client.get("/items/bar/name") - assert response.status_code == 200, response.text - assert response.json() == {"name": "Bar", "description": "The Bar fighters"} - - -@needs_py310 -def test_read_item_public_data(client: TestClient): - response = client.get("/items/bar/public") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Bar", - "description": "The Bar fighters", - "price": 62, - } - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}/name": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Name", - "operationId": "read_item_name_items__item_id__name_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/items/{item_id}/public": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Public Data", - "operationId": "read_item_public_data_items__item_id__public_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": IsOneOf( - ["name", "description", "price", "tax"], - # TODO: remove when deprecating Pydantic v1 - ["name", "price"], - ), - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From 2c83a11a7d6971c9efbb80129d6db7d70c9cadf4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 06:58:12 +0000 Subject: [PATCH 062/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2fa245cdc..933b7271e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for request_model. PR [#13195](https://github.com/fastapi/fastapi/pull/13195) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_forms_and_files. PR [#13185](https://github.com/fastapi/fastapi/pull/13185) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_forms. PR [#13184](https://github.com/fastapi/fastapi/pull/13184) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for path_query_params. PR [#13181](https://github.com/fastapi/fastapi/pull/13181) by [@alejsdev](https://github.com/alejsdev). From 3e12918325fcde4cd749ac4c7957fa95ecbc584b Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:30:50 +0000 Subject: [PATCH 063/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20sc?= =?UTF-8?q?hema=5Fextra=5Fexample=20(#13197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_tutorial001.py | 18 +- .../test_tutorial001_pv1.py | 18 +- .../test_tutorial001_pv1_py310.py | 129 ------------- .../test_tutorial001_py310.py | 135 ------------- .../test_tutorial004.py | 27 ++- .../test_tutorial004_an.py | 168 ----------------- .../test_tutorial004_an_py310.py | 177 ------------------ .../test_tutorial004_an_py39.py | 177 ------------------ .../test_tutorial004_py310.py | 177 ------------------ .../test_tutorial005.py | 21 ++- .../test_tutorial005_an.py | 166 ---------------- .../test_tutorial005_an_py310.py | 170 ----------------- .../test_tutorial005_an_py39.py | 170 ----------------- .../test_tutorial005_py310.py | 170 ----------------- 14 files changed, 65 insertions(+), 1658 deletions(-) delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1_py310.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py delete mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py index 98b187355..c21cbb4bc 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -1,14 +1,22 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +from ...utils import needs_py310, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py index 3520ef61d..b79f42e64 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py @@ -1,14 +1,22 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv1 +from ...utils import needs_py310, needs_pydanticv1 -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001_pv1 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001_pv1", + pytest.param("tutorial001_pv1_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1_py310.py deleted file mode 100644 index b2a4d15b1..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1_py310.py +++ /dev/null @@ -1,129 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001_pv1_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@needs_pydanticv1 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - # insert_assert(response.json()) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": {"type": "string", "title": "Description"}, - "price": {"type": "number", "title": "Price"}, - "tax": {"type": "number", "title": "Tax"}, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py deleted file mode 100644 index e63e33cda..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py +++ /dev/null @@ -1,135 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -@needs_pydanticv2 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - # insert_assert(response.json()) - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": True, - "schema": {"type": "integer", "title": "Item Id"}, - } - ], - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "price": {"type": "number", "title": "Price"}, - "tax": { - "anyOf": [{"type": "number"}, {"type": "null"}], - "title": "Tax", - }, - }, - "type": "object", - "required": ["name", "price"], - "title": "Item", - "examples": [ - { - "description": "A very nice Item", - "name": "Foo", - "price": 35.4, - "tax": 3.2, - } - ], - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index eac0d1e29..61aefd12a 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -1,13 +1,30 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.schema_extra_example.tutorial004 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + "tutorial004_an", + pytest.param("tutorial004_an_py39", marks=needs_py39), + pytest.param("tutorial004_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -# Test required and embedded body parameters with no bodies sent -def test_post_body_example(): +def test_post_body_example(client: TestClient): response = client.put( "/items/5", json={ @@ -20,7 +37,7 @@ def test_post_body_example(): assert response.status_code == 200 -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py deleted file mode 100644 index a9cecd098..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ /dev/null @@ -1,168 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.schema_extra_example.tutorial004_an import app - -client = TestClient(app) - - -# Test required and embedded body parameters with no bodies sent -def test_post_body_example(): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py deleted file mode 100644 index b6a735599..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial004_an_py310 import app - - client = TestClient(app) - return client - - -# Test required and embedded body parameters with no bodies sent -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py deleted file mode 100644 index 2493194a0..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial004_an_py39 import app - - client = TestClient(app) - return client - - -# Test required and embedded body parameters with no bodies sent -@needs_py39 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py deleted file mode 100644 index 15f54bd5a..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial004_py310 import app - - client = TestClient(app) - return client - - -# Test required and embedded body parameters with no bodies sent -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict( - { - "$ref": "#/components/schemas/Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } - ) - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py index 94a40ed5a..12859227b 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -1,13 +1,26 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39, needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005 import app +@pytest.fixture( + name="client", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + "tutorial005_an", + pytest.param("tutorial005_an_py39", marks=needs_py39), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py deleted file mode 100644 index da92f98f6..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py +++ /dev/null @@ -1,166 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_an import app - - client = TestClient(app) - return client - - -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py deleted file mode 100644 index 9109cb14e..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py deleted file mode 100644 index fd4ec0575..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py39 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py deleted file mode 100644 index 05df53422..000000000 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py +++ /dev/null @@ -1,170 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.schema_extra_example.tutorial005_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_example(client: TestClient): - response = client.put( - "/items/5", - json={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - ) - assert response.status_code == 200 - - -@needs_py310 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": IsDict({"$ref": "#/components/schemas/Item"}) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "allOf": [ - {"$ref": "#/components/schemas/Item"} - ], - "title": "Item", - } - ), - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } From 818eb1f365dd6daad7746499120b10a0a1e0d42d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 22:31:13 +0000 Subject: [PATCH 064/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 933b7271e..f1538d6f4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for schema_extra_example. PR [#13197](https://github.com/fastapi/fastapi/pull/13197) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_model. PR [#13195](https://github.com/fastapi/fastapi/pull/13195) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_forms_and_files. PR [#13185](https://github.com/fastapi/fastapi/pull/13185) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_forms. PR [#13184](https://github.com/fastapi/fastapi/pull/13184) by [@alejsdev](https://github.com/alejsdev). From 20079934334945cf1e527a37582742e1f8d6dbb0 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:35:40 +0000 Subject: [PATCH 065/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20se?= =?UTF-8?q?curity=20(#13200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_security/test_tutorial001.py | 28 +- .../test_security/test_tutorial001_an.py | 57 --- .../test_security/test_tutorial001_an_py39.py | 68 --- .../test_security/test_tutorial003.py | 40 +- .../test_security/test_tutorial003_an.py | 207 --------- .../test_tutorial003_an_py310.py | 223 --------- .../test_security/test_tutorial003_an_py39.py | 223 --------- .../test_security/test_tutorial003_py310.py | 223 --------- .../test_security/test_tutorial005.py | 108 +++-- .../test_security/test_tutorial005_an.py | 409 ---------------- .../test_tutorial005_an_py310.py | 437 ------------------ .../test_security/test_tutorial005_an_py39.py | 437 ------------------ .../test_security/test_tutorial005_py310.py | 437 ------------------ .../test_security/test_tutorial005_py39.py | 437 ------------------ .../test_security/test_tutorial006.py | 29 +- .../test_security/test_tutorial006_an.py | 65 --- .../test_security/test_tutorial006_an_py39.py | 77 --- 17 files changed, 146 insertions(+), 3359 deletions(-) delete mode 100644 tests/test_tutorial/test_security/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial003_an.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial003_an_py310.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial003_an_py39.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial005_an.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial005_an_py310.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial005_an_py39.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial005_py310.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial005_py39.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial006_an.py delete mode 100644 tests/test_tutorial/test_security/test_tutorial006_an_py39.py diff --git a/tests/test_tutorial/test_security/test_tutorial001.py b/tests/test_tutorial/test_security/test_tutorial001.py index 417bed8f7..f572d6e3e 100644 --- a/tests/test_tutorial/test_security/test_tutorial001.py +++ b/tests/test_tutorial/test_security/test_tutorial001.py @@ -1,31 +1,47 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.security.tutorial001 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_no_token(): +def test_no_token(client: TestClient): response = client.get("/items") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_token(): +def test_token(client: TestClient): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} -def test_incorrect_token(): +def test_incorrect_token(client: TestClient): response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py deleted file mode 100644 index 59460da7f..000000000 --- a/tests/test_tutorial/test_security/test_tutorial001_an.py +++ /dev/null @@ -1,57 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.security.tutorial001_an import app - -client = TestClient(app) - - -def test_no_token(): - response = client.get("/items") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_token(): - response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) - assert response.status_code == 200, response.text - assert response.json() == {"token": "testtoken"} - - -def test_incorrect_token(): - response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - } - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py deleted file mode 100644 index d8e712773..000000000 --- a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py +++ /dev/null @@ -1,68 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/items") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) - assert response.status_code == 200, response.text - assert response.json() == {"token": "testtoken"} - - -@needs_py39 -def test_incorrect_token(client: TestClient): - response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - } - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 18d4680f6..7a4c99401 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,18 +1,36 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.security.tutorial003 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + "tutorial003_an", + pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_login(): +def test_login(client: TestClient): response = client.post("/token", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 200, response.text assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} -def test_login_incorrect_password(): +def test_login_incorrect_password(client: TestClient): response = client.post( "/token", data={"username": "johndoe", "password": "incorrect"} ) @@ -20,20 +38,20 @@ def test_login_incorrect_password(): assert response.json() == {"detail": "Incorrect username or password"} -def test_login_incorrect_username(): +def test_login_incorrect_username(client: TestClient): response = client.post("/token", data={"username": "foo", "password": "secret"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Incorrect username or password"} -def test_no_token(): +def test_no_token(client: TestClient): response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_token(): +def test_token(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) assert response.status_code == 200, response.text assert response.json() == { @@ -45,14 +63,14 @@ def test_token(): } -def test_incorrect_token(): +def test_incorrect_token(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Invalid authentication credentials"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_incorrect_token_type(): +def test_incorrect_token_type(client: TestClient): response = client.get( "/users/me", headers={"Authorization": "Notexistent testtoken"} ) @@ -61,13 +79,13 @@ def test_incorrect_token_type(): assert response.headers["WWW-Authenticate"] == "Bearer" -def test_inactive_user(): +def test_inactive_user(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Inactive user"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py deleted file mode 100644 index a8f64d0c6..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_an.py +++ /dev/null @@ -1,207 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.security.tutorial003_an import app - -client = TestClient(app) - - -def test_login(): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -def test_login_incorrect_password(): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_login_incorrect_username(): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_no_token(): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_token(): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -def test_incorrect_token(): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_incorrect_token_type(): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_inactive_user(): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py deleted file mode 100644 index 7cbbcee2f..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py +++ /dev/null @@ -1,223 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial003_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -@needs_py310 -def test_incorrect_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_inactive_user(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py deleted file mode 100644 index 7b21fbcc9..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py +++ /dev/null @@ -1,223 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial003_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -@needs_py39 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -@needs_py39 -def test_incorrect_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_inactive_user(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial003_py310.py b/tests/test_tutorial/test_security/test_tutorial003_py310.py deleted file mode 100644 index 512504534..000000000 --- a/tests/test_tutorial/test_security/test_tutorial003_py310.py +++ /dev/null @@ -1,223 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial003_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "hashed_password": "fakehashedsecret", - "disabled": False, - } - - -@needs_py310 -def test_incorrect_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_inactive_user(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_token_post": { - "title": "Body_login_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 2e580dbb3..c7f791b03 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -1,18 +1,33 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.security.tutorial005 import ( - app, - create_access_token, - fake_users_db, - get_password_hash, - verify_password, +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + "tutorial005_an", + pytest.param("tutorial005_py39", marks=needs_py39), + pytest.param("tutorial005_an_py39", marks=needs_py39), + pytest.param("tutorial005_an_py310", marks=needs_py310), + ], ) +def get_mod(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -client = TestClient(app) + return mod -def get_access_token(username="johndoe", password="secret", scope=None): +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): data = {"username": username, "password": password} if scope: data["scope"] = scope @@ -22,7 +37,8 @@ def get_access_token(username="johndoe", password="secret", scope=None): return access_token -def test_login(): +def test_login(mod: ModuleType): + client = TestClient(mod.app) response = client.post("/token", data={"username": "johndoe", "password": "secret"}) assert response.status_code == 200, response.text content = response.json() @@ -30,7 +46,8 @@ def test_login(): assert content["token_type"] == "bearer" -def test_login_incorrect_password(): +def test_login_incorrect_password(mod: ModuleType): + client = TestClient(mod.app) response = client.post( "/token", data={"username": "johndoe", "password": "incorrect"} ) @@ -38,21 +55,24 @@ def test_login_incorrect_password(): assert response.json() == {"detail": "Incorrect username or password"} -def test_login_incorrect_username(): +def test_login_incorrect_username(mod: ModuleType): + client = TestClient(mod.app) response = client.post("/token", data={"username": "foo", "password": "secret"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "Incorrect username or password"} -def test_no_token(): +def test_no_token(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_token(): - access_token = get_access_token(scope="me") +def test_token(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(scope="me", client=client) response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) @@ -65,14 +85,16 @@ def test_token(): } -def test_incorrect_token(): +def test_incorrect_token(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Could not validate credentials"} assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_incorrect_token_type(): +def test_incorrect_token_type(mod: ModuleType): + client = TestClient(mod.app) response = client.get( "/users/me", headers={"Authorization": "Notexistent testtoken"} ) @@ -81,20 +103,24 @@ def test_incorrect_token_type(): assert response.headers["WWW-Authenticate"] == "Bearer" -def test_verify_password(): - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) +def test_verify_password(mod: ModuleType): + assert mod.verify_password( + "secret", mod.fake_users_db["johndoe"]["hashed_password"] + ) -def test_get_password_hash(): - assert get_password_hash("secretalice") +def test_get_password_hash(mod: ModuleType): + assert mod.get_password_hash("secretalice") -def test_create_access_token(): - access_token = create_access_token(data={"data": "foo"}) +def test_create_access_token(mod: ModuleType): + access_token = mod.create_access_token(data={"data": "foo"}) assert access_token -def test_token_no_sub(): +def test_token_no_sub(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( "/users/me", headers={ @@ -106,7 +132,9 @@ def test_token_no_sub(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_no_username(): +def test_token_no_username(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( "/users/me", headers={ @@ -118,8 +146,10 @@ def test_token_no_username(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_no_scope(): - access_token = get_access_token() +def test_token_no_scope(mod: ModuleType): + client = TestClient(mod.app) + + access_token = get_access_token(client=client) response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) @@ -128,7 +158,9 @@ def test_token_no_scope(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_nonexistent_user(): +def test_token_nonexistent_user(mod: ModuleType): + client = TestClient(mod.app) + response = client.get( "/users/me", headers={ @@ -140,9 +172,11 @@ def test_token_nonexistent_user(): assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' -def test_token_inactive_user(): +def test_token_inactive_user(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token( - username="alice", password="secretalice", scope="me" + username="alice", password="secretalice", scope="me", client=client ) response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} @@ -151,8 +185,9 @@ def test_token_inactive_user(): assert response.json() == {"detail": "Inactive user"} -def test_read_items(): - access_token = get_access_token(scope="me items") +def test_read_items(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(scope="me items", client=client) response = client.get( "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} ) @@ -160,8 +195,9 @@ def test_read_items(): assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] -def test_read_system_status(): - access_token = get_access_token() +def test_read_system_status(mod: ModuleType): + client = TestClient(mod.app) + access_token = get_access_token(client=client) response = client.get( "/status/", headers={"Authorization": f"Bearer {access_token}"} ) @@ -169,14 +205,16 @@ def test_read_system_status(): assert response.json() == {"status": "ok"} -def test_read_system_status_no_token(): +def test_read_system_status_no_token(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/status/") assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" -def test_openapi_schema(): +def test_openapi_schema(mod: ModuleType): + client = TestClient(mod.app) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py deleted file mode 100644 index 04c7d60bc..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ /dev/null @@ -1,409 +0,0 @@ -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from docs_src.security.tutorial005_an import ( - app, - create_access_token, - fake_users_db, - get_password_hash, - verify_password, -) - -client = TestClient(app) - - -def get_access_token(username="johndoe", password="secret", scope=None): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -def test_login(): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -def test_login_incorrect_password(): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_login_incorrect_username(): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -def test_no_token(): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_token(): - access_token = get_access_token(scope="me") - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -def test_incorrect_token(): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_incorrect_token_type(): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_verify_password(): - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -def test_get_password_hash(): - assert get_password_hash("secretalice") - - -def test_create_access_token(): - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -def test_token_no_sub(): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_no_username(): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_no_scope(): - access_token = get_access_token() - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_nonexistent_user(): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -def test_token_inactive_user(): - access_token = get_access_token( - username="alice", password="secretalice", scope="me" - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -def test_read_items(): - access_token = get_access_token(scope="me items") - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -def test_read_system_status(): - access_token = get_access_token() - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -def test_read_system_status_no_token(): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py deleted file mode 100644 index 9c7f83ed2..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_an_py310 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py310 -def test_incorrect_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_verify_password(): - from docs_src.security.tutorial005_an_py310 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py310 -def test_get_password_hash(): - from docs_src.security.tutorial005_an_py310 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py310 -def test_create_access_token(): - from docs_src.security.tutorial005_an_py310 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py310 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py310 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py310 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py deleted file mode 100644 index 04cc1b014..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_an_py39 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py39 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py39 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py39 -def test_incorrect_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_verify_password(): - from docs_src.security.tutorial005_an_py39 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py39 -def test_get_password_hash(): - from docs_src.security.tutorial005_an_py39 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py39 -def test_create_access_token(): - from docs_src.security.tutorial005_an_py39 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py39 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py39 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py39 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py39 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py deleted file mode 100644 index 98c60c1c2..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_py310 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py310 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py310 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py310 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py310 -def test_incorrect_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_verify_password(): - from docs_src.security.tutorial005_py310 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py310 -def test_get_password_hash(): - from docs_src.security.tutorial005_py310 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py310 -def test_create_access_token(): - from docs_src.security.tutorial005_py310 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py310 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py310 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py310 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py310 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py310 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py deleted file mode 100644 index cd2157d54..000000000 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ /dev/null @@ -1,437 +0,0 @@ -import pytest -from dirty_equals import IsDict, IsOneOf -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial005_py39 import app - - client = TestClient(app) - return client - - -def get_access_token( - *, username="johndoe", password="secret", scope=None, client: TestClient -): - data = {"username": username, "password": password} - if scope: - data["scope"] = scope - response = client.post("/token", data=data) - content = response.json() - access_token = content.get("access_token") - return access_token - - -@needs_py39 -def test_login(client: TestClient): - response = client.post("/token", data={"username": "johndoe", "password": "secret"}) - assert response.status_code == 200, response.text - content = response.json() - assert "access_token" in content - assert content["token_type"] == "bearer" - - -@needs_py39 -def test_login_incorrect_password(client: TestClient): - response = client.post( - "/token", data={"username": "johndoe", "password": "incorrect"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_login_incorrect_username(client: TestClient): - response = client.post("/token", data={"username": "foo", "password": "secret"}) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Incorrect username or password"} - - -@needs_py39 -def test_no_token(client: TestClient): - response = client.get("/users/me") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_token(client: TestClient): - access_token = get_access_token(scope="me", client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "johndoe", - "full_name": "John Doe", - "email": "johndoe@example.com", - "disabled": False, - } - - -@needs_py39 -def test_incorrect_token(client: TestClient): - response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_incorrect_token_type(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Notexistent testtoken"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_verify_password(): - from docs_src.security.tutorial005_py39 import fake_users_db, verify_password - - assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) - - -@needs_py39 -def test_get_password_hash(): - from docs_src.security.tutorial005_py39 import get_password_hash - - assert get_password_hash("secretalice") - - -@needs_py39 -def test_create_access_token(): - from docs_src.security.tutorial005_py39 import create_access_token - - access_token = create_access_token(data={"data": "foo"}) - assert access_token - - -@needs_py39 -def test_token_no_sub(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_username(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_no_scope(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not enough permissions"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_nonexistent_user(client: TestClient): - response = client.get( - "/users/me", - headers={ - "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" - }, - ) - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Could not validate credentials"} - assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' - - -@needs_py39 -def test_token_inactive_user(client: TestClient): - access_token = get_access_token( - username="alice", password="secretalice", scope="me", client=client - ) - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 400, response.text - assert response.json() == {"detail": "Inactive user"} - - -@needs_py39 -def test_read_items(client: TestClient): - access_token = get_access_token(scope="me items", client=client) - response = client.get( - "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] - - -@needs_py39 -def test_read_system_status(client: TestClient): - access_token = get_access_token(client=client) - response = client.get( - "/status/", headers={"Authorization": f"Bearer {access_token}"} - ) - assert response.status_code == 200, response.text - assert response.json() == {"status": "ok"} - - -@needs_py39 -def test_read_system_status_no_token(client: TestClient): - response = client.get("/status/") - assert response.status_code == 401, response.text - assert response.json() == {"detail": "Not authenticated"} - assert response.headers["WWW-Authenticate"] == "Bearer" - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/token": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Token"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login For Access Token", - "operationId": "login_for_access_token_token_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_for_access_token_token_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me__get", - "security": [{"OAuth2PasswordBearer": ["me"]}], - } - }, - "/users/me/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Own Items", - "operationId": "read_own_items_users_me_items__get", - "security": [{"OAuth2PasswordBearer": ["items", "me"]}], - } - }, - "/status/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read System Status", - "operationId": "read_system_status_status__get", - "security": [{"OAuth2PasswordBearer": []}], - } - }, - }, - "components": { - "schemas": { - "User": { - "title": "User", - "required": IsOneOf( - ["username", "email", "full_name", "disabled"], - # TODO: remove when deprecating Pydantic v1 - ["username"], - ), - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": IsDict( - { - "title": "Email", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Email", "type": "string"} - ), - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - "disabled": IsDict( - { - "title": "Disabled", - "anyOf": [{"type": "boolean"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Disabled", "type": "boolean"} - ), - }, - }, - "Token": { - "title": "Token", - "required": ["access_token", "token_type"], - "type": "object", - "properties": { - "access_token": {"title": "Access Token", "type": "string"}, - "token_type": {"title": "Token Type", "type": "string"}, - }, - }, - "Body_login_for_access_token_token_post": { - "title": "Body_login_for_access_token_token_post", - "required": ["username", "password"], - "type": "object", - "properties": { - "grant_type": IsDict( - { - "title": "Grant Type", - "anyOf": [ - {"pattern": "password", "type": "string"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Grant Type", - "pattern": "password", - "type": "string", - } - ), - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": IsDict( - { - "title": "Client Id", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Id", "type": "string"} - ), - "client_secret": IsDict( - { - "title": "Client Secret", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Client Secret", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "me": "Read information about the current user.", - "items": "Read items.", - }, - "tokenUrl": "token", - } - }, - } - }, - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index dc459b6fd..40b413806 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -1,26 +1,41 @@ +import importlib from base64 import b64encode +import pytest from fastapi.testclient import TestClient -from docs_src.security.tutorial006 import app +from ...utils import needs_py39 -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial006", + "tutorial006_an", + pytest.param("tutorial006_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.security.{request.param}") -def test_security_http_basic(): + client = TestClient(mod.app) + return client + + +def test_security_http_basic(client: TestClient): response = client.get("/users/me", auth=("john", "secret")) assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} -def test_security_http_basic_no_credentials(): +def test_security_http_basic_no_credentials(client: TestClient): response = client.get("/users/me") assert response.json() == {"detail": "Not authenticated"} assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" -def test_security_http_basic_invalid_credentials(): +def test_security_http_basic_invalid_credentials(client: TestClient): response = client.get( "/users/me", headers={"Authorization": "Basic notabase64token"} ) @@ -29,7 +44,7 @@ def test_security_http_basic_invalid_credentials(): assert response.json() == {"detail": "Invalid authentication credentials"} -def test_security_http_basic_non_basic_credentials(): +def test_security_http_basic_non_basic_credentials(client: TestClient): payload = b64encode(b"johnsecret").decode("ascii") auth_header = f"Basic {payload}" response = client.get("/users/me", headers={"Authorization": auth_header}) @@ -38,7 +53,7 @@ def test_security_http_basic_non_basic_credentials(): assert response.json() == {"detail": "Invalid authentication credentials"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py deleted file mode 100644 index 52ddd938f..000000000 --- a/tests/test_tutorial/test_security/test_tutorial006_an.py +++ /dev/null @@ -1,65 +0,0 @@ -from base64 import b64encode - -from fastapi.testclient import TestClient - -from docs_src.security.tutorial006_an import app - -client = TestClient(app) - - -def test_security_http_basic(): - response = client.get("/users/me", auth=("john", "secret")) - assert response.status_code == 200, response.text - assert response.json() == {"username": "john", "password": "secret"} - - -def test_security_http_basic_no_credentials(): - response = client.get("/users/me") - assert response.json() == {"detail": "Not authenticated"} - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - - -def test_security_http_basic_invalid_credentials(): - response = client.get( - "/users/me", headers={"Authorization": "Basic notabase64token"} - ) - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} - - -def test_security_http_basic_non_basic_credentials(): - payload = b64encode(b"johnsecret").decode("ascii") - auth_header = f"Basic {payload}" - response = client.get("/users/me", headers={"Authorization": auth_header}) - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, - } diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py deleted file mode 100644 index 52b22e573..000000000 --- a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py +++ /dev/null @@ -1,77 +0,0 @@ -from base64 import b64encode - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.security.tutorial006_an import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_security_http_basic(client: TestClient): - response = client.get("/users/me", auth=("john", "secret")) - assert response.status_code == 200, response.text - assert response.json() == {"username": "john", "password": "secret"} - - -@needs_py39 -def test_security_http_basic_no_credentials(client: TestClient): - response = client.get("/users/me") - assert response.json() == {"detail": "Not authenticated"} - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - - -@needs_py39 -def test_security_http_basic_invalid_credentials(client: TestClient): - response = client.get( - "/users/me", headers={"Authorization": "Basic notabase64token"} - ) - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} - - -@needs_py39 -def test_security_http_basic_non_basic_credentials(client: TestClient): - payload = b64encode(b"johnsecret").decode("ascii") - auth_header = f"Basic {payload}" - response = client.get("/users/me", headers={"Authorization": auth_header}) - assert response.status_code == 401, response.text - assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, - } From 6d51389f75d0e5fff17edc0e71bba604a744e440 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 22:36:07 +0000 Subject: [PATCH 066/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f1538d6f4..a604e5169 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for security. PR [#13200](https://github.com/fastapi/fastapi/pull/13200) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for schema_extra_example. PR [#13197](https://github.com/fastapi/fastapi/pull/13197) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_model. PR [#13195](https://github.com/fastapi/fastapi/pull/13195) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_forms_and_files. PR [#13185](https://github.com/fastapi/fastapi/pull/13185) by [@alejsdev](https://github.com/alejsdev). From 39698df8069b8cd705dd4cb2194bbf447d3eec49 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:36:49 +0000 Subject: [PATCH 067/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20se?= =?UTF-8?q?parate=5Fopenapi=5Fschemas=20(#13201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_tutorial001.py | 19 ++- .../test_tutorial001_py310.py | 136 ------------------ .../test_tutorial001_py39.py | 136 ------------------ .../test_tutorial002.py | 19 ++- .../test_tutorial002_py310.py | 136 ------------------ .../test_tutorial002_py39.py | 136 ------------------ 6 files changed, 28 insertions(+), 554 deletions(-) delete mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py index cdfae9f8c..059fb889b 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -1,14 +1,23 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +from ...utils import needs_py39, needs_py310, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module(f"docs_src.separate_openapi_schemas.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py deleted file mode 100644 index 3b22146f6..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial001_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py310 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py deleted file mode 100644 index 991abe811..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial001_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py39 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py39 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py index d2cf7945b..cc9afeab7 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -1,14 +1,23 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from ...utils import needs_pydanticv2 +from ...utils import needs_py39, needs_py310, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial002 import app +@pytest.fixture( + name="client", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest) -> TestClient: + mod = importlib.import_module(f"docs_src.separate_openapi_schemas.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py deleted file mode 100644 index 89c9ce977..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py310, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial002_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py310 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py310 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py deleted file mode 100644 index 6ac3d8f79..000000000 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client() -> TestClient: - from docs_src.separate_openapi_schemas.tutorial002_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_create_item(client: TestClient) -> None: - response = client.post("/items/", json={"name": "Foo"}) - assert response.status_code == 200, response.text - assert response.json() == {"name": "Foo", "description": None} - - -@needs_py39 -def test_read_items(client: TestClient) -> None: - response = client.get("/items/") - assert response.status_code == 200, response.text - assert response.json() == [ - { - "name": "Portal Gun", - "description": "Device to travel through the multi-rick-verse", - }, - {"name": "Plumbus", "description": None}, - ] - - -@needs_py39 -@needs_pydanticv2 -def test_openapi_schema(client: TestClient) -> None: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": {"$ref": "#/components/schemas/Item"}, - "type": "array", - "title": "Response Read Items Items Get", - } - } - }, - } - }, - }, - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "type": "array", - "title": "Detail", - } - }, - "type": "object", - "title": "HTTPValidationError", - }, - "Item": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name"], - "title": "Item", - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - "type": "array", - "title": "Location", - }, - "msg": {"type": "string", "title": "Message"}, - "type": {"type": "string", "title": "Error Type"}, - }, - "type": "object", - "required": ["loc", "msg", "type"], - "title": "ValidationError", - }, - } - }, - } From b5e40a6233d93e8805ba4c843bc82893d074d31a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 22:37:13 +0000 Subject: [PATCH 068/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a604e5169..169345509 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for separate_openapi_schemas. PR [#13201](https://github.com/fastapi/fastapi/pull/13201) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for security. PR [#13200](https://github.com/fastapi/fastapi/pull/13200) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for schema_extra_example. PR [#13197](https://github.com/fastapi/fastapi/pull/13197) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_model. PR [#13195](https://github.com/fastapi/fastapi/pull/13195) by [@alejsdev](https://github.com/alejsdev). From 182c28e57acace06f9c3e9f11fd5096be5bac781 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:39:18 +0000 Subject: [PATCH 069/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20re?= =?UTF-8?q?quest=5Fform=5Fmodels=20=20(#13183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem --- .../test_tutorial001.py | 19 +- .../test_tutorial001_an.py | 232 ----------------- .../test_tutorial001_an_py39.py | 240 ------------------ .../test_tutorial002.py | 19 +- .../test_tutorial002_an.py | 196 -------------- .../test_tutorial002_an_py39.py | 203 --------------- .../test_tutorial002_pv1.py | 26 +- .../test_tutorial002_pv1_an.py | 196 -------------- .../test_tutorial002_pv1_an_p39.py | 203 --------------- 9 files changed, 50 insertions(+), 1284 deletions(-) delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_an.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py index 46c130ee8..1ca3c96d3 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -1,13 +1,24 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001 import app +@pytest.fixture( + name="client", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py deleted file mode 100644 index 4e14d89c8..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py +++ /dev/null @@ -1,232 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001_an import app - - client = TestClient(app) - return client - - -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py deleted file mode 100644 index 2e6426aa7..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py +++ /dev/null @@ -1,240 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from tests.utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py index 76f480001..b3f6be63a 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -1,14 +1,23 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv2 +from ...utils import needs_py39, needs_pydanticv2 -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002 import app +@pytest.fixture( + name="client", + params=[ + "tutorial002", + "tutorial002_an", + pytest.param("tutorial002_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py deleted file mode 100644 index 179b2977d..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py +++ /dev/null @@ -1,196 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_an import app - - client = TestClient(app) - return client - - -@needs_pydanticv2 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -@needs_pydanticv2 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "extra_forbidden", - "loc": ["body", "extra"], - "msg": "Extra inputs are not permitted", - "input": "extra", - } - ] - } - - -@needs_pydanticv2 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - - -@needs_pydanticv2 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - - -@needs_pydanticv2 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py deleted file mode 100644 index 510ad9d7c..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py +++ /dev/null @@ -1,203 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_py39, needs_pydanticv2 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_an_py39 import app - - client = TestClient(app) - return client - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "extra_forbidden", - "loc": ["body", "extra"], - "msg": "Extra inputs are not permitted", - "input": "extra", - } - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - - -@needs_pydanticv2 -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py index 249b9379d..b503f23a5 100644 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py @@ -1,17 +1,27 @@ +import importlib + import pytest from fastapi.testclient import TestClient -from tests.utils import needs_pydanticv1 +from ...utils import needs_py39, needs_pydanticv1 -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_pv1 import app +@pytest.fixture( + name="client", + params=[ + "tutorial002_pv1", + "tutorial002_pv1_an", + pytest.param("tutorial002_pv1_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_form_models.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form(client: TestClient): response = client.post("/login/", data={"username": "Foo", "password": "secret"}) @@ -19,6 +29,7 @@ def test_post_body_form(client: TestClient): assert response.json() == {"username": "Foo", "password": "secret"} +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_extra_form(client: TestClient): response = client.post( @@ -36,6 +47,7 @@ def test_post_body_extra_form(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form_no_password(client: TestClient): response = client.post("/login/", data={"username": "Foo"}) @@ -51,6 +63,7 @@ def test_post_body_form_no_password(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form_no_username(client: TestClient): response = client.post("/login/", data={"password": "secret"}) @@ -66,6 +79,7 @@ def test_post_body_form_no_username(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_form_no_data(client: TestClient): response = client.post("/login/") @@ -86,6 +100,7 @@ def test_post_body_form_no_data(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) @@ -106,6 +121,7 @@ def test_post_body_json(client: TestClient): } +# TODO: remove when deprecating Pydantic v1 @needs_pydanticv1 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py deleted file mode 100644 index 44cb3c32b..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py +++ /dev/null @@ -1,196 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_pv1_an import app - - client = TestClient(app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py deleted file mode 100644 index 899549e40..000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py +++ /dev/null @@ -1,203 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from tests.utils import needs_py39, needs_pydanticv1 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial002_pv1_an_py39 import app - - client = TestClient(app) - return client - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_extra_form(client: TestClient): - response = client.post( - "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} - ) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.extra", - "loc": ["body", "extra"], - "msg": "extra fields not permitted", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - } - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "type": "value_error.missing", - "loc": ["body", "username"], - "msg": "field required", - }, - { - "type": "value_error.missing", - "loc": ["body", "password"], - "msg": "field required", - }, - ] - } - - -# TODO: remove when deprecating Pydantic v1 -@needs_pydanticv1 -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "additionalProperties": False, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From 9f3edbf537deb28ed56dd983b09156b365cfec87 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 22:39:42 +0000 Subject: [PATCH 070/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 169345509..162d8c752 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for request_form_models . PR [#13183](https://github.com/fastapi/fastapi/pull/13183) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for separate_openapi_schemas. PR [#13201](https://github.com/fastapi/fastapi/pull/13201) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for security. PR [#13200](https://github.com/fastapi/fastapi/pull/13200) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for schema_extra_example. PR [#13197](https://github.com/fastapi/fastapi/pull/13197) by [@alejsdev](https://github.com/alejsdev). From 280fe73c0398c912a38ffbf96e3897fc9cb78a5f Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:40:39 +0000 Subject: [PATCH 071/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20we?= =?UTF-8?q?bsockets=20(#13202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_websockets/test_tutorial002.py | 33 ++++-- .../test_websockets/test_tutorial002_an.py | 88 --------------- .../test_tutorial002_an_py310.py | 102 ------------------ .../test_tutorial002_an_py39.py | 102 ------------------ .../test_websockets/test_tutorial002_py310.py | 102 ------------------ 5 files changed, 26 insertions(+), 401 deletions(-) delete mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_an.py delete mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py delete mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py delete mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_py310.py diff --git a/tests/test_tutorial/test_websockets/test_tutorial002.py b/tests/test_tutorial/test_websockets/test_tutorial002.py index bb5ccbf8e..51aa5752a 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial002.py +++ b/tests/test_tutorial/test_websockets/test_tutorial002.py @@ -1,18 +1,37 @@ +import importlib + import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient from fastapi.websockets import WebSocketDisconnect -from docs_src.websockets.tutorial002 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="app", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + "tutorial002_an", + pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_app(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.websockets.{request.param}") + + return mod.app -def test_main(): +def test_main(app: FastAPI): client = TestClient(app) response = client.get("/") assert response.status_code == 200, response.text assert b"" in response.content -def test_websocket_with_cookie(): +def test_websocket_with_cookie(app: FastAPI): client = TestClient(app, cookies={"session": "fakesession"}) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws") as websocket: @@ -30,7 +49,7 @@ def test_websocket_with_cookie(): assert data == f"Message text was: {message}, for item ID: foo" -def test_websocket_with_header(): +def test_websocket_with_header(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: @@ -48,7 +67,7 @@ def test_websocket_with_header(): assert data == f"Message text was: {message}, for item ID: bar" -def test_websocket_with_header_and_query(): +def test_websocket_with_header_and_query(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: @@ -70,7 +89,7 @@ def test_websocket_with_header_and_query(): assert data == f"Message text was: {message}, for item ID: 2" -def test_websocket_no_credentials(): +def test_websocket_no_credentials(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws"): @@ -79,7 +98,7 @@ def test_websocket_no_credentials(): ) # pragma: no cover -def test_websocket_invalid_data(): +def test_websocket_invalid_data(app: FastAPI): client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an.py b/tests/test_tutorial/test_websockets/test_tutorial002_an.py deleted file mode 100644 index ec78d70d3..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_an.py +++ /dev/null @@ -1,88 +0,0 @@ -import pytest -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from docs_src.websockets.tutorial002_an import app - - -def test_main(): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -def test_websocket_with_cookie(): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -def test_websocket_with_header(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -def test_websocket_with_header_and_query(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -def test_websocket_no_credentials(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -def test_websocket_invalid_data(): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py deleted file mode 100644 index 23b4bcb78..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from ...utils import needs_py310 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial002_an_py310 import app - - return app - - -@needs_py310 -def test_main(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -@needs_py310 -def test_websocket_with_cookie(app: FastAPI): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -@needs_py310 -def test_websocket_with_header(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -@needs_py310 -def test_websocket_with_header_and_query(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -@needs_py310 -def test_websocket_no_credentials(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -@needs_py310 -def test_websocket_invalid_data(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py deleted file mode 100644 index 2d77f05b3..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from ...utils import needs_py39 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial002_an_py39 import app - - return app - - -@needs_py39 -def test_main(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -@needs_py39 -def test_websocket_with_cookie(app: FastAPI): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -@needs_py39 -def test_websocket_with_header(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -@needs_py39 -def test_websocket_with_header_and_query(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -@needs_py39 -def test_websocket_no_credentials(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -@needs_py39 -def test_websocket_invalid_data(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py deleted file mode 100644 index 03bc27bdf..000000000 --- a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocketDisconnect - -from ...utils import needs_py310 - - -@pytest.fixture(name="app") -def get_app(): - from docs_src.websockets.tutorial002_py310 import app - - return app - - -@needs_py310 -def test_main(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"" in response.content - - -@needs_py310 -def test_websocket_with_cookie(app: FastAPI): - client = TestClient(app, cookies={"session": "fakesession"}) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: fakesession" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: foo" - - -@needs_py310 -def test_websocket_with_header(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: bar" - - -@needs_py310 -def test_websocket_with_header_and_query(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: - message = "Message one" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - message = "Message two" - websocket.send_text(message) - data = websocket.receive_text() - assert data == "Session cookie or query token value is: some-token" - data = websocket.receive_text() - assert data == "Query parameter q is: 3" - data = websocket.receive_text() - assert data == f"Message text was: {message}, for item ID: 2" - - -@needs_py310 -def test_websocket_no_credentials(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover - - -@needs_py310 -def test_websocket_invalid_data(app: FastAPI): - client = TestClient(app) - with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): - pytest.fail( - "did not raise WebSocketDisconnect on __enter__" - ) # pragma: no cover From 6ba09082a0b8455a890a4877c8ab1e3f143be8d1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 19 Jan 2025 22:41:00 +0000 Subject: [PATCH 072/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 162d8c752..a242407ea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ✅ Simplify tests for websockets. PR [#13202](https://github.com/fastapi/fastapi/pull/13202) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for request_form_models . PR [#13183](https://github.com/fastapi/fastapi/pull/13183) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for separate_openapi_schemas. PR [#13201](https://github.com/fastapi/fastapi/pull/13201) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for security. PR [#13200](https://github.com/fastapi/fastapi/pull/13200) by [@alejsdev](https://github.com/alejsdev). From 8fa18e5e6ff5ae6bb620bf4a7bd0d1964f10f2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro?= Date: Wed, 22 Jan 2025 10:41:56 -0300 Subject: [PATCH 073/123] =?UTF-8?q?=F0=9F=8C=90=20Update=20Portuguese=20Tr?= =?UTF-8?q?anslation=20for=20`docs/pt/docs/tutorial/request-forms.md`=20(#?= =?UTF-8?q?13216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/request-forms.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/pt/docs/tutorial/request-forms.md b/docs/pt/docs/tutorial/request-forms.md index 756ceb581..572ddf003 100644 --- a/docs/pt/docs/tutorial/request-forms.md +++ b/docs/pt/docs/tutorial/request-forms.md @@ -6,7 +6,11 @@ Quando você precisar receber campos de formulário ao invés de JSON, você pod Para usar formulários, primeiro instale `python-multipart`. -Ex: `pip install python-multipart`. +Lembre-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e então instalar a dependência, por exemplo: + +```console +$ pip install python-multipart +``` /// From a215687c987ea882ef6c4f0788b6c5a8b71f75ff Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 13:42:19 +0000 Subject: [PATCH 074/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a242407ea..5f527d37b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Update Portuguese Translation for `docs/pt/docs/tutorial/request-forms.md`. PR [#13216](https://github.com/fastapi/fastapi/pull/13216) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). * 🌐 Update Portuguese translation for `docs/pt/docs/advanced/settings.md`. PR [#13209](https://github.com/fastapi/fastapi/pull/13209) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/oauth2-jwt.md`. PR [#13205](https://github.com/fastapi/fastapi/pull/13205) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Indonesian translation for `docs/id/docs/index.md`. PR [#13191](https://github.com/fastapi/fastapi/pull/13191) by [@gerry-sabar](https://github.com/gerry-sabar). From 548f67d465afff233fa1856b2d337d13595e3308 Mon Sep 17 00:00:00 2001 From: Daniel Kusy <36250676+DanielKusyDev@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:02:36 +0100 Subject: [PATCH 075/123] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20`jinja2`?= =?UTF-8?q?=20to=20>=3D3.1.5=20(#13194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index edfa81522..ae56ebb7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ standard = [ # For the test client "httpx >=0.23.0", # For templates - "jinja2 >=2.11.2", + "jinja2 >=3.1.5", # For forms and file uploads "python-multipart >=0.0.7", # To validate email fields @@ -79,7 +79,7 @@ all = [ # # For the test client "httpx >=0.23.0", # For templates - "jinja2 >=2.11.2", + "jinja2 >=3.1.5", # For forms and file uploads "python-multipart >=0.0.7", # For Starlette's SessionMiddleware, not commonly used with FastAPI From 1a38cc506e5732a8a0e2d4cfec3059f670a5ef90 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:03:00 +0000 Subject: [PATCH 076/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5f527d37b..d982a15f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -33,6 +33,10 @@ hide: * ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for additional_status_codes. PR [#13149](https://github.com/fastapi/fastapi/pull/13149) by [@tiangolo](https://github.com/tiangolo). +### Upgrades + +* ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). + ### Docs * ✏️ Update Strawberry integration docs. PR [#13155](https://github.com/fastapi/fastapi/pull/13155) by [@kinuax](https://github.com/kinuax). From 82c74789e8c2ba68f4c88b6e8c80928881578272 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 22 Jan 2025 19:21:40 +0100 Subject: [PATCH 077/123] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Starlette?= =?UTF-8?q?=20to=20allow=20up=20to=200.45.0:=20`>=3D0.40.0,<0.46.0`=20(#13?= =?UTF-8?q?117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ae56ebb7e..9510d36c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.42.0", + "starlette>=0.40.0,<0.46.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", ] From 91c05b9245269502ea3d28569b3cbe2fa556e465 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:22:02 +0000 Subject: [PATCH 078/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d982a15f2..1951d6559 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,7 @@ hide: ### Upgrades +* ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). * ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). ### Docs From 49e82ed2ac63c2aad957d01c857ee1fe597dc4ca Mon Sep 17 00:00:00 2001 From: Daniel Kusy <36250676+DanielKusyDev@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:23:13 +0100 Subject: [PATCH 079/123] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20`python-?= =?UTF-8?q?multipart`=20to=20>=3D0.0.18=20(#13219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9510d36c3..6fa0f080f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ standard = [ # For templates "jinja2 >=3.1.5", # For forms and file uploads - "python-multipart >=0.0.7", + "python-multipart >=0.0.18", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -81,7 +81,7 @@ all = [ # For templates "jinja2 >=3.1.5", # For forms and file uploads - "python-multipart >=0.0.7", + "python-multipart >=0.0.18", # For Starlette's SessionMiddleware, not commonly used with FastAPI "itsdangerous >=1.1.0", # For Starlette's schema generation, would not be used with FastAPI From e39143d56de41f8d72a3be8847030346cb1d8adc Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:24:12 +0000 Subject: [PATCH 080/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1951d6559..a806930f5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,7 @@ hide: ### Upgrades +* ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). * ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). * ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). From abd05a6d30e6e4c9b1496e0b94d362a9979c9f37 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Wed, 22 Jan 2025 13:24:58 -0500 Subject: [PATCH 081/123] =?UTF-8?q?=F0=9F=94=A7=20Add=20Pydantic=202=20tro?= =?UTF-8?q?ve=20classifier=20(#13199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6fa0f080f..381eb50bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 1", + "Framework :: Pydantic :: 2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", From 2b6f63df712f0ab98f071f64b3f8ac215ddf43be Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 22 Jan 2025 18:25:24 +0000 Subject: [PATCH 082/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a806930f5..13976b788 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -110,6 +110,7 @@ hide: ### Internal +* 🔧 Add Pydantic 2 trove classifier. PR [#13199](https://github.com/fastapi/fastapi/pull/13199) by [@johnthagen](https://github.com/johnthagen). * 👥 Update FastAPI People - Sponsors. PR [#13231](https://github.com/fastapi/fastapi/pull/13231) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor FastAPI People Sponsors to use 2 tokens. PR [#13228](https://github.com/fastapi/fastapi/pull/13228) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for FastAPI People - Sponsors. PR [#13225](https://github.com/fastapi/fastapi/pull/13225) by [@tiangolo](https://github.com/tiangolo). From 7183f0d683558390b973af8ad708e82567d4dc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 22 Jan 2025 22:48:57 +0000 Subject: [PATCH 083/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 13976b788..c7f01b82f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,12 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). +* ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). +* ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). + ### Refactors * ✅ Simplify tests for websockets. PR [#13202](https://github.com/fastapi/fastapi/pull/13202) by [@alejsdev](https://github.com/alejsdev). @@ -33,12 +39,6 @@ hide: * ✅ Simplify tests for background_tasks. PR [#13166](https://github.com/fastapi/fastapi/pull/13166) by [@alejsdev](https://github.com/alejsdev). * ✅ Simplify tests for additional_status_codes. PR [#13149](https://github.com/fastapi/fastapi/pull/13149) by [@tiangolo](https://github.com/tiangolo). -### Upgrades - -* ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). -* ⬆️ Bump Starlette to allow up to 0.45.0: `>=0.40.0,<0.46.0`. PR [#13117](https://github.com/fastapi/fastapi/pull/13117) by [@Kludex](https://github.com/Kludex). -* ⬆️ Upgrade `jinja2` to >=3.1.5. PR [#13194](https://github.com/fastapi/fastapi/pull/13194) by [@DanielKusyDev](https://github.com/DanielKusyDev). - ### Docs * ✏️ Update Strawberry integration docs. PR [#13155](https://github.com/fastapi/fastapi/pull/13155) by [@kinuax](https://github.com/kinuax). From fe513719ea98abade167d8a89e92f600d9d8f0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 22 Jan 2025 22:50:29 +0000 Subject: [PATCH 084/123] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?5.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c7f01b82f..c8a75756b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.115.7 + ### Upgrades * ⬆️ Upgrade `python-multipart` to >=0.0.18. PR [#13219](https://github.com/fastapi/fastapi/pull/13219) by [@DanielKusyDev](https://github.com/DanielKusyDev). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 823957822..c92279cfd 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.115.6" +__version__ = "0.115.7" from starlette import status as status From 4d60022c88e6ba92a89df4860ba2df3af7e8c926 Mon Sep 17 00:00:00 2001 From: alv2017 Date: Thu, 23 Jan 2025 11:46:41 +0200 Subject: [PATCH 085/123] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translat?= =?UTF-8?q?ion=20for=20`docs/ru/docs/tutorial/bigger-applications.md`=20(#?= =?UTF-8?q?13154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/bigger-applications.md | 556 +++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 docs/ru/docs/tutorial/bigger-applications.md diff --git a/docs/ru/docs/tutorial/bigger-applications.md b/docs/ru/docs/tutorial/bigger-applications.md new file mode 100644 index 000000000..7c3dc288f --- /dev/null +++ b/docs/ru/docs/tutorial/bigger-applications.md @@ -0,0 +1,556 @@ +# Большие приложения, в которых много файлов + +При построении приложения или веб-API нам редко удается поместить всё в один файл. + +**FastAPI** предоставляет удобный инструментарий, который позволяет нам структурировать приложение, сохраняя при этом всю необходимую гибкость. + +/// info | Примечание + +Если вы раньше использовали Flask, то это аналог шаблонов Flask (Flask's Blueprints). + +/// + +## Пример структуры приложения + +Давайте предположим, что наше приложение имеет следующую структуру: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   ├── dependencies.py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── users.py +│   └── internal +│   ├── __init__.py +│   └── admin.py +``` + +/// tip | Подсказка + +Обратите внимание, что в каждом каталоге и подкаталоге имеется файл `__init__.py` + +Это как раз то, что позволяет импортировать код из одного файла в другой. + +Например, в файле `app/main.py` может быть следующая строка: + +``` +from app.routers import items +``` + +/// + +* Всё помещается в каталоге `app`. В нём также находится пустой файл `app/__init__.py`. Таким образом, `app` является "Python-пакетом" (коллекцией модулей Python). +* Он содержит файл `app/main.py`. Данный файл является частью пакета (т.е. находится внутри каталога, содержащего файл `__init__.py`), и, соответственно, он является модулем пакета: `app.main`. +* Он также содержит файл `app/dependencies.py`, который также, как и `app/main.py`, является модулем: `app.dependencies`. +* Здесь также находится подкаталог `app/routers/`, содержащий `__init__.py`. Он является суб-пакетом: `app.routers`. +* Файл `app/routers/items.py` находится внутри пакета `app/routers/`. Таким образом, он является суб-модулем: `app.routers.items`. +* Точно также `app/routers/users.py` является ещё одним суб-модулем: `app.routers.users`. +* Подкаталог `app/internal/`, содержащий файл `__init__.py`, является ещё одним суб-пакетом: `app.internal`. +* А файл `app/internal/admin.py` является ещё одним суб-модулем: `app.internal.admin`. + + + +Та же самая файловая структура приложения, но с комментариями: + +``` +. +├── app # "app" пакет +│   ├── __init__.py # этот файл превращает "app" в "Python-пакет" +│   ├── main.py # модуль "main", напр.: import app.main +│   ├── dependencies.py # модуль "dependencies", напр.: import app.dependencies +│   └── routers # суб-пакет "routers" +│   │ ├── __init__.py # превращает "routers" в суб-пакет +│   │ ├── items.py # суб-модуль "items", напр.: import app.routers.items +│   │ └── users.py # суб-модуль "users", напр.: import app.routers.users +│   └── internal # суб-пакет "internal" +│   ├── __init__.py # превращает "internal" в суб-пакет +│   └── admin.py # суб-модуль "admin", напр.: import app.internal.admin +``` + +## `APIRouter` + +Давайте предположим, что для работы с пользователями используется отдельный файл (суб-модуль) `/app/routers/users.py`. + +Для лучшей организации приложения, вы хотите отделить операции пути, связанные с пользователями, от остального кода. + +Но так, чтобы эти операции по-прежнему оставались частью **FastAPI** приложения/веб-API (частью одного пакета) + +С помощью `APIRouter` вы можете создать *операции пути* (*эндпоинты*) для данного модуля. + + +### Импорт `APIRouter` + +Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`. + +```Python hl_lines="1 3" title="app/routers/users.py" +{!../../docs_src/bigger_applications/app/routers/users.py!} +``` + +### Создание *эндпоинтов* с помощью `APIRouter` + +В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`: + +```Python hl_lines="6 11 16" title="app/routers/users.py" +{!../../docs_src/bigger_applications/app/routers/users.py!} +``` + +Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`. + +`APIRouter` поддерживает все те же самые опции. + +`APIRouter` поддерживает все те же самые параметры, такие как `parameters`, `responses`, `dependencies`, `tags`, и т. д. + +/// tip | Подсказка + +В данном примере, в качестве названия переменной используется `router`, но вы можете использовать любое другое имя. + +/// + +Мы собираемся подключить данный `APIRouter` к нашему основному приложению на `FastAPI`, но сначала давайте проверим зависимости и создадим ещё один модуль с `APIRouter`. + +## Зависимости + +Нам понадобятся некоторые зависимости, которые мы будем использовать в разных местах нашего приложения. + +Мы поместим их в отдельный модуль `dependencies` (`app/dependencies.py`). + +Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка: + +//// tab | Python 3.9+ + +```Python hl_lines="3 6-8" title="app/dependencies.py" +{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 5-7" title="app/dependencies.py" +{!> ../../docs_src/bigger_applications/app_an/dependencies.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Подсказка + +Мы рекомендуем использовать версию `Annotated`, когда это возможно. + +/// + +```Python hl_lines="1 4-6" title="app/dependencies.py" +{!> ../../docs_src/bigger_applications/app/dependencies.py!} +``` + +//// + +/// tip | Подсказка + +Для простоты мы воспользовались неким воображаемым заголовоком. + +В реальных случаях для получения наилучших результатов используйте интегрированные утилиты обеспечения безопасности [Security utilities](security/index.md){.internal-link target=_blank}. + +/// + +## Ещё один модуль с `APIRouter` + +Давайте также предположим, что у вас есть *эндпоинты*, отвечающие за обработку "items", и они находятся в модуле `app/routers/items.py`. + +У вас определены следующие *операции пути* (*эндпоинты*): + +* `/items/` +* `/items/{item_id}` + +Тут всё точно также, как и в ситуации с `app/routers/users.py`. + +Но теперь мы хотим поступить немного умнее и слегка упростить код. + +Мы знаем, что все *эндпоинты* данного модуля имеют некоторые общие свойства: + +* Префикс пути: `/items`. +* Теги: (один единственный тег: `items`). +* Дополнительные ответы (responses) +* Зависимости: использование созданной нами зависимости `X-token` + +Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*, +мы добавим их в `APIRouter`. + +```Python hl_lines="5-10 16 21" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +Так как каждый *эндпоинт* начинается с символа `/`: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...то префикс не должен заканчиваться символом `/`. + +В нашем случае префиксом является `/items`. + +Мы также можем добавить в наш маршрутизатор (router) список `тегов` (`tags`) и дополнительных `ответов` (`responses`), которые являются общими для каждого *эндпоинта*. + +И ещё мы можем добавить в наш маршрутизатор список `зависимостей`, которые должны вызываться при каждом обращении к *эндпоинтам*. + +/// tip | Подсказка + +Обратите внимание, что также, как и в случае с зависимостями в декораторах *эндпоинтов* ([dependencies in *path operation decorators*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), никакого значения в *функцию эндпоинта* передано не будет. + +/// + +В результате мы получим следующие эндпоинты: + +* `/items/` +* `/items/{item_id}` + +...как мы и планировали. + +* Они будут помечены тегами из заданного списка, в нашем случае это `"items"`. + * Эти теги особенно полезны для системы автоматической интерактивной документации (с использованием OpenAPI). +* Каждый из них будет включать предопределенные ответы `responses`. +* Каждый *эндпоинт* будет иметь список зависимостей (`dependencies`), исполняемых перед вызовом *эндпоинта*. + * Если вы определили зависимости в самой операции пути, **то она также будет выполнена**. + * Сначала выполняются зависимости маршрутизатора, затем вызываются зависимости, определенные в декораторе *эндпоинта* ([`dependencies` in the decorator](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), и, наконец, обычные параметрические зависимости. + * Вы также можете добавить зависимости безопасности с областями видимости (`scopes`) [`Security` dependencies with `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. + +/// tip | Подсказка + +Например, с помощью зависимостей в `APIRouter` мы можем потребовать аутентификации для доступа ко всей группе *эндпоинтов*, не указывая зависимости для каждой отдельной функции *эндпоинта*. + +/// + +/// check | Заметка + +Параметры `prefix`, `tags`, `responses` и `dependencies` относятся к функционалу **FastAPI**, помогающему избежать дублирования кода. + +/// + +### Импорт зависимостей + +Наш код находится в модуле `app.routers.items` (файл `app/routers/items.py`). + +И нам нужно вызвать функцию зависимости из модуля `app.dependencies` (файл `app/dependencies.py`). + +Мы используем операцию относительного импорта `..` для импорта зависимости: + +```Python hl_lines="3" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +#### Как работает относительный импорт? + +/// tip | Подсказка + +Если вы прекрасно знаете, как работает импорт в Python, то переходите к следующему разделу. + +/// + +Одна точка `.`, как в данном примере: + +```Python +from .dependencies import get_token_header +``` +означает: + +* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` расположен в каталоге `app/routers/`)... +* ... найдите модуль `dependencies` (файл `app/routers/dependencies.py`)... +* ... и импортируйте из него функцию `get_token_header`. + +К сожалению, такого файла не существует, и наши зависимости находятся в файле `app/dependencies.py`. + +Вспомните, как выглядит файловая структура нашего приложения: + + + +--- + +Две точки `..`, как в данном примере: + +```Python +from ..dependencies import get_token_header +``` + +означают: + +* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... +* ... перейдите в родительский пакет (каталог `app/`)... +* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... +* ... и импортируйте из него функцию `get_token_header`. + +Это работает верно! 🎉 + +--- + +Аналогично, если бы мы использовали три точки `...`, как здесь: + +```Python +from ...dependencies import get_token_header +``` + +то это бы означало: + +* Начните с пакета, в котором находится данный модуль (файл `app/routers/items.py` находится в каталоге `app/routers/`)... +* ... перейдите в родительский пакет (каталог `app/`)... +* ... затем перейдите в родительский пакет текущего пакета (такого пакета не существует, `app` находится на самом верхнем уровне 😱)... +* ... найдите в нём модуль `dependencies` (файл `app/dependencies.py`)... +* ... и импортируйте из него функцию `get_token_header`. + +Это будет относиться к некоторому пакету, находящемуся на один уровень выше чем `app/` и содержащему свой собственный файл `__init__.py`. Но ничего такого у нас нет. Поэтому это приведет к ошибке в нашем примере. 🚨 + +Теперь вы знаете, как работает импорт в Python, и сможете использовать относительное импортирование в своих собственных приложениях любого уровня сложности. 🤓 + +### Добавление пользовательских тегов (`tags`), ответов (`responses`) и зависимостей (`dependencies`) + +Мы не будем добавлять префикс `/items` и список тегов `tags=["items"]` для каждого *эндпоинта*, т.к. мы уже их добавили с помощью `APIRouter`. + +Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*: + +```Python hl_lines="30-31" title="app/routers/items.py" +{!../../docs_src/bigger_applications/app/routers/items.py!} +``` + +/// tip | Подсказка + +Последний *эндпоинт* будет иметь следующую комбинацию тегов: `["items", "custom"]`. + +А также в его документации будут содержаться оба ответа: один для `404` и другой для `403`. + +/// + +## Модуль main в `FastAPI` + +Теперь давайте посмотрим на модуль `app/main.py`. + +Именно сюда вы импортируете и именно здесь вы используете класс `FastAPI`. + +Это основной файл вашего приложения, который объединяет всё в одно целое. + +И теперь, когда большая часть логики приложения разделена на отдельные модули, основной файл `app/main.py` будет достаточно простым. + +### Импорт `FastAPI` + +Вы импортируете и создаете класс `FastAPI` как обычно. + +Мы даже можем объявить глобальные зависимости [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора: + +```Python hl_lines="1 3 7" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +### Импорт `APIRouter` + +Теперь мы импортируем другие суб-модули, содержащие `APIRouter`: + +```Python hl_lines="4-5" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`. + +### Как работает импорт? + +Данная строка кода: + +```Python +from .routers import items, users +``` + +означает: + +* Начните с пакета, в котором содержится данный модуль (файл `app/main.py` содержится в каталоге `app/`)... +* ... найдите суб-пакет `routers` (каталог `app/routers/`)... +* ... и из него импортируйте суб-модули `items` (файл `app/routers/items.py`) и `users` (файл `app/routers/users.py`)... + +В модуле `items` содержится переменная `router` (`items.router`), та самая, которую мы создали в файле `app/routers/items.py`, она является объектом класса `APIRouter`. + +И затем мы сделаем то же самое для модуля `users`. + +Мы также могли бы импортировать и другим методом: + +```Python +from app.routers import items, users +``` + +/// info | Примечание + +Первая версия является примером относительного импорта: + +```Python +from .routers import items, users +``` + +Вторая версия является примером абсолютного импорта: + +```Python +from app.routers import items, users +``` + +Узнать больше о пакетах и модулях в Python вы можете из официальной документации Python о модулях + +/// + +### Избегайте конфликтов имен + +Вместо того чтобы импортировать только переменную `router`, мы импортируем непосредственно суб-модуль `items`. + +Мы делаем это потому, что у нас есть ещё одна переменная `router` в суб-модуле `users`. + +Если бы мы импортировали их одну за другой, как показано в примере: + +```Python +from .routers.items import router +from .routers.users import router +``` + +то переменная `router` из `users` переписал бы переменную `router` из `items`, и у нас не было бы возможности использовать их одновременно. + +Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули: + +```Python hl_lines="5" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` + +Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`: + +```Python hl_lines="10-11" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +/// info | Примечание + +`users.router` содержит `APIRouter` из файла `app/routers/users.py`. + +А `items.router` содержит `APIRouter` из файла `app/routers/items.py`. + +/// + +С помощью `app.include_router()` мы можем добавить каждый из маршрутизаторов (`APIRouter`) в основное приложение `FastAPI`. + +Он подключит все маршруты заданного маршрутизатора к нашему приложению. + +/// note | Технические детали + +Фактически, внутри он создаст все *операции пути* для каждой операции пути объявленной в `APIRouter`. + +И под капотом всё будет работать так, как будто бы мы имеем дело с одним файлом приложения. + +/// + +/// check | Заметка + +При подключении маршрутизаторов не стоит беспокоиться о производительности. + +Операция подключения займёт микросекунды и понадобится только при запуске приложения. + +Таким образом, это не повлияет на производительность. ⚡ + +/// + +### Подключение `APIRouter` с пользовательскими префиксом (`prefix`), тегами (`tags`), ответами (`responses`), и зависимостями (`dependencies`) + +Теперь давайте представим, что ваша организация передала вам файл `app/internal/admin.py`. + +Он содержит `APIRouter` с некоторыми *эндпоитами* администрирования, которые ваша организация использует для нескольких проектов. + +В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов, +то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`: + +```Python hl_lines="3" title="app/internal/admin.py" +{!../../docs_src/bigger_applications/app/internal/admin.py!} +``` + +Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`). + +Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`. + +```Python hl_lines="14-17" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации. + +В результате, в нашем приложении каждый *эндпоинт* модуля `admin` будет иметь: + +* Префикс `/admin`. +* Тег `admin`. +* Зависимость `get_token_header`. +* Ответ `418`. 🍵 + +Это будет иметь место исключительно для `APIRouter` в нашем приложении, и не затронет любой другой код, использующий его. + +Например, другие проекты, могут использовать тот же самый `APIRouter` с другими методами аутентификации. + +### Подключение отдельного *эндпоинта* + +Мы также можем добавить *эндпоинт* непосредственно в основное приложение `FastAPI`. + +Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷: + +```Python hl_lines="21-23" title="app/main.py" +{!../../docs_src/bigger_applications/app/main.py!} +``` + +и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`. + +/// info | Сложные технические детали + +**Примечание**: это сложная техническая деталь, которую, скорее всего, **вы можете пропустить**. + +--- + +Маршрутизаторы (`APIRouter`) не "монтируются" по-отдельности и не изолируются от остального приложения. + +Это происходит потому, что нужно включить их *эндпоинты* в OpenAPI схему и в интерфейс пользователя. + +В силу того, что мы не можем их изолировать и "примонтировать" независимо от остальных, *эндпоинты* клонируются (пересоздаются) и не подключаются напрямую. + +/// + +## Проверка автоматической документации API + +Теперь запустите приложение: + +
+ +```console +$ fastapi dev app/main.py + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Откройте документацию по адресу http://127.0.0.1:8000/docs. + +Вы увидите автоматическую API документацию. Она включает в себя маршруты из суб-модулей, используя верные маршруты, префиксы и теги: + + + +## Подключение существующего маршрута через новый префикс (`prefix`) + +Вы можете использовать `.include_router()` несколько раз с одним и тем же маршрутом, применив различные префиксы. + +Это может быть полезным, если нужно предоставить доступ к одному и тому же API через различные префиксы, например, `/api/v1` и `/api/latest`. + +Это продвинутый способ, который вам может и не пригодится. Мы приводим его на случай, если вдруг вам это понадобится. + +## Включение одного маршрутизатора (`APIRouter`) в другой + +Точно так же, как вы включаете `APIRouter` в приложение `FastAPI`, вы можете включить `APIRouter` в другой `APIRouter`: + +```Python +router.include_router(other_router) +``` + +Удостоверьтесь, что вы сделали это до того, как подключить маршрутизатор (`router`) к вашему `FastAPI` приложению, и *эндпоинты* маршрутизатора `other_router` были также подключены. From b6f6818d76f312fe30d798e52fff16927649db0a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 23 Jan 2025 09:47:05 +0000 Subject: [PATCH 086/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c8a75756b..bc725fafe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). + ## 0.115.7 ### Upgrades From 0e2d8d64a427905d673ecee9cfeaf9e74ed979e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:05:11 +0000 Subject: [PATCH 087/123] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-?= =?UTF-8?q?publish=20from=201.12.3=20to=201.12.4=20(#13251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.12.4. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.12.4) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 38df75928..bf88d59b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.3 + uses: pypa/gh-action-pypi-publish@v1.12.4 - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} From 6bbd315f3e7c08893591729cfb61a19eadb73eae Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 24 Jan 2025 17:05:37 +0000 Subject: [PATCH 088/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bc725fafe..b1020d6e4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). +### Internal + +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.115.7 ### Upgrades From 998a9139d3e34100874712f5a9261cdc67f8d4ed Mon Sep 17 00:00:00 2001 From: Rishat-F <66554797+Rishat-F@users.noreply.github.com> Date: Fri, 24 Jan 2025 22:44:31 +0300 Subject: [PATCH 089/123] =?UTF-8?q?=F0=9F=8C=90=20Update=20Russian=20trans?= =?UTF-8?q?lation=20for=20`docs/ru/docs/tutorial/dependencies/dependencies?= =?UTF-8?q?-in-path-operation-decorators.md`=20(#13252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dependencies/dependencies-in-path-operation-decorators.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index f9b9dec25..0e4eb95be 100644 --- a/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -18,7 +18,7 @@ Зависимости из dependencies выполнятся так же, как и обычные зависимости. Но их значения (если они были) не будут переданы в *функцию операции пути*. -/// подсказка +/// tip | Подсказка Некоторые редакторы кода определяют неиспользуемые параметры функций и подсвечивают их как ошибку. @@ -28,7 +28,7 @@ /// -/// дополнительная | информация +/// info | Примечание В этом примере мы используем выдуманные пользовательские заголовки `X-Key` и `X-Token`. From 1e44825ef2143e37e51edb86ae44d408d9dbb756 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 24 Jan 2025 19:44:54 +0000 Subject: [PATCH 090/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b1020d6e4..f25648b9a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Update Russian translation for `docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#13252](https://github.com/fastapi/fastapi/pull/13252) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). ### Internal From 24eb8eeeba23d93288ef40596d621f149e648e10 Mon Sep 17 00:00:00 2001 From: Rishat-F <66554797+Rishat-F@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:36:13 +0300 Subject: [PATCH 091/123] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translat?= =?UTF-8?q?ion=20for=20`docs/ru/docs/advanced/async-tests.md`=20(#13227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/advanced/async-tests.md | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/ru/docs/advanced/async-tests.md diff --git a/docs/ru/docs/advanced/async-tests.md b/docs/ru/docs/advanced/async-tests.md new file mode 100644 index 000000000..7849ad109 --- /dev/null +++ b/docs/ru/docs/advanced/async-tests.md @@ -0,0 +1,99 @@ +# Асинхронное тестирование + +Вы уже видели как тестировать **FastAPI** приложение, используя имеющийся класс `TestClient`. К этому моменту вы видели только как писать тесты в синхронном стиле без использования `async` функций. + +Возможность использования асинхронных функций в ваших тестах может быть полезнa, когда, например, вы асинхронно обращаетесь к вашей базе данных. Представьте, что вы хотите отправить запросы в ваше FastAPI приложение, а затем при помощи асинхронной библиотеки для работы с базой данных удостовериться, что ваш бекэнд корректно записал данные в базу данных. + +Давайте рассмотрим, как мы можем это реализовать. + +## pytest.mark.anyio + +Если мы хотим вызывать асинхронные функции в наших тестах, то наши тестовые функции должны быть асинхронными. AnyIO предоставляет для этого отличный плагин, который позволяет нам указывать, какие тестовые функции должны вызываться асинхронно. + +## HTTPX + +Даже если **FastAPI** приложение использует обычные функции `def` вместо `async def`, это все равно `async` приложение 'под капотом'. + +Чтобы работать с асинхронным FastAPI приложением в ваших обычных тестовых функциях `def`, используя стандартный pytest, `TestClient` внутри себя делает некоторую магию. Но эта магия перестает работать, когда мы используем его внутри асинхронных функций. Запуская наши тесты асинхронно, мы больше не можем использовать `TestClient` внутри наших тестовых функций. + +`TestClient` основан на HTTPX, и, к счастью, мы можем использовать его (`HTTPX`) напрямую для тестирования API. + +## Пример + +В качестве простого примера, давайте рассмотрим файловую структуру, схожую с описанной в [Большие приложения](../tutorial/bigger-applications.md){.internal-link target=_blank} и [Тестирование](../tutorial/testing.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Файл `main.py`: + +{* ../../docs_src/async_tests/main.py *} + +Файл `test_main.py` содержит тесты для `main.py`, теперь он может выглядеть так: + +{* ../../docs_src/async_tests/test_main.py *} + +## Запуск тестов + +Вы можете запустить свои тесты как обычно: + +
+ +```console +$ pytest + +---> 100% +``` + +
+ +## Подробнее + +Маркер `@pytest.mark.anyio` говорит pytest, что тестовая функция должна быть вызвана асинхронно: + +{* ../../docs_src/async_tests/test_main.py hl[7] *} + +/// tip | Подсказка + +Обратите внимание, что тестовая функция теперь `async def` вместо простого `def`, как это было при использовании `TestClient`. + +/// + +Затем мы можем создать `AsyncClient` со ссылкой на приложение и посылать асинхронные запросы, используя `await`. + +{* ../../docs_src/async_tests/test_main.py hl[9:12] *} + +Это эквивалентно следующему: + +```Python +response = client.get('/') +``` + +...которое мы использовали для отправки наших запросов с `TestClient`. + +/// tip | Подсказка + +Обратите внимание, что мы используем async/await с `AsyncClient` - запрос асинхронный. + +/// + +/// warning | Внимание + +Если ваше приложение полагается на lifespan события, то `AsyncClient` не запустит эти события. Чтобы обеспечить их срабатывание используйте `LifespanManager` из florimondmanca/asgi-lifespan. + +/// + +## Вызов других асинхронных функций + +Теперь тестовая функция стала асинхронной, поэтому внутри нее вы можете вызывать также и другие `async` функции, не связанные с отправлением запросов в ваше FastAPI приложение. Как если бы вы вызывали их в любом другом месте вашего кода. + +/// tip | Подсказка + +Если вы столкнулись с `RuntimeError: Task attached to a different loop` при вызове асинхронных функций в ваших тестах (например, при использовании MongoDB's MotorClient), то не забывайте инициализировать объекты, которым нужен цикл событий (event loop), только внутри асинхронных функций, например, в `'@app.on_event("startup")` callback. + +/// From 18127b790725a3a6ca378dd99eb2cfd9f2423ccd Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 27 Jan 2025 15:36:36 +0000 Subject: [PATCH 092/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f25648b9a..0dc660412 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/advanced/async-tests.md`. PR [#13227](https://github.com/fastapi/fastapi/pull/13227) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Update Russian translation for `docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#13252](https://github.com/fastapi/fastapi/pull/13252) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). From 8f359273b5663aecdc989c762fad3804c3a6df80 Mon Sep 17 00:00:00 2001 From: k94-ishi <32672580+k94-ishi@users.noreply.github.com> Date: Tue, 28 Jan 2025 00:39:04 +0900 Subject: [PATCH 093/123] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20transla?= =?UTF-8?q?tion=20for=20`docs/ja/docs/environment-variables.md`=20(#13226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ja/docs/environment-variables.md | 301 ++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 docs/ja/docs/environment-variables.md diff --git a/docs/ja/docs/environment-variables.md b/docs/ja/docs/environment-variables.md new file mode 100644 index 000000000..507af3a0c --- /dev/null +++ b/docs/ja/docs/environment-variables.md @@ -0,0 +1,301 @@ +# 環境変数 + +/// tip + +もし、「環境変数」とは何か、それをどう使うかを既に知っている場合は、このセクションをスキップして構いません。 + +/// + +環境変数(**env var**とも呼ばれる)はPythonコードの**外側**、つまり**OS**に存在する変数で、Pythonから読み取ることができます。(他のプログラムでも同様に読み取れます。) + +環境変数は、アプリケーションの**設定**の管理や、Pythonの**インストール**などに役立ちます。 + +## 環境変数の作成と使用 + +環境変数は**シェル(ターミナル)**内で**作成**して使用でき、それらにPythonは不要です。 + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// You could create an env var MY_NAME with +$ export MY_NAME="Wade Wilson" + +// Then you could use it with other programs, like +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ + +```console +// Create an env var MY_NAME +$ $Env:MY_NAME = "Wade Wilson" + +// Use it with other programs, like +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +## Pythonで環境変数を読み取る + +環境変数をPythonの**外側**、ターミナル(や他の方法)で作成し、**Python内で読み取る**こともできます。 + +例えば、以下のような`main.py`ファイルを用意します: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip + +`os.getenv()` の第2引数は、デフォルトで返される値を指定します。 + +この引数を省略するとデフォルト値として`None`が返されますが、ここではデフォルト値として`"World"`を指定しています。 + +/// + +次に、このPythonプログラムを呼び出します。 + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ export MY_NAME="Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ $Env:MY_NAME = "Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +環境変数はコードの外側で設定し、内側から読み取ることができるので、他のファイルと一緒に(`git`に)保存する必要がありません。そのため、環境変数をコンフィグレーションや**設定**に使用することが一般的です。 + +また、**特定のプログラムの呼び出し**のための環境変数を、そのプログラムのみ、その実行中に限定して利用できるよう作成できます。 + +そのためには、プログラム起動コマンドと同じコマンドライン上の、起動コマンド直前で環境変数を作成してください。 + +
+ +```console +// Create an env var MY_NAME in line for this program call +$ MY_NAME="Wade Wilson" python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python + +// The env var no longer exists afterwards +$ python main.py + +Hello World from Python +``` + +
+ +/// tip + +詳しくは The Twelve-Factor App: Config を参照してください。 + +/// + +## 型とバリデーション + +環境変数は**テキスト文字列**のみを扱うことができます。これは、環境変数がPython外部に存在し、他のプログラムやシステム全体(Linux、Windows、macOS間の互換性を含む)と連携する必要があるためです。 + +つまり、Pythonが環境変数から読み取る**あらゆる値**は **`str`型となり**、他の型への変換やバリデーションはコード内で行う必要があります。 + +環境変数を使用して**アプリケーション設定**を管理する方法については、[高度なユーザーガイド - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank}で詳しく学べます。 + +## `PATH`環境変数 + +**`PATH`**という**特別な**環境変数があります。この環境変数は、OS(Linux、macOS、Windows)が実行するプログラムを発見するために使用されます。 + +`PATH`変数は、複数のディレクトリのパスから成る長い文字列です。このパスはLinuxやMacOSの場合は`:`で、Windowsの場合は`;`で区切られています。 + +例えば、`PATH`環境変数は次のような文字列かもしれません: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +これは、OSはプログラムを見つけるために以下のディレクトリを探す、ということを意味します: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +ターミナル上で**コマンド**を入力すると、 OSはそのプログラムを見つけるために、`PATH`環境変数のリストに記載された**それぞれのディレクトリを探し**ます。 + +例えば、ターミナル上で`python`を入力すると、OSは`python`によって呼ばれるプログラムを見つけるために、そのリストの**先頭のディレクトリ**を最初に探します。 + +OSは、もしそのプログラムをそこで発見すれば**実行し**ますが、そうでなければリストの**他のディレクトリ**を探していきます。 + +### PythonのインストールとPATH環境変数の更新 + +Pythonのインストール時に`PATH`環境変数を更新したいか聞かれるかもしれません。 + +/// tab | Linux, macOS + +Pythonをインストールして、そのプログラムが`/opt/custompython/bin`というディレクトリに配置されたとします。 + +もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`/opt/custompython/bin`が追加されます。 + +`PATH`環境変数は以下のように更新されるでしょう: + +``` plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +このようにして、ターミナルで`python`と入力したときに、OSは`/opt/custompython/bin`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 + +/// + +/// tab | Windows + +Pythonをインストールして、そのプログラムが`C:\opt\custompython\bin`というディレクトリに配置されたとします。 + +もし、`PATH`環境変数を更新するように答えると、`PATH`環境変数に`C:\opt\custompython\bin`が追加されます。 + +`PATH`環境変数は以下のように更新されるでしょう: + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +このようにして、ターミナルで`python`と入力したときに、OSは`C:\opt\custompython\bin\python`(リストの末尾のディレクトリ)にあるPythonプログラムを見つけ、使用します。 + +/// + +つまり、ターミナルで以下のコマンドを入力すると: + +
+ +``` console +$ python +``` + +
+ +/// tab | Linux, macOS + +OSは`/opt/custompython/bin`にある`python`プログラムを**見つけ**て実行します。 + +これは、次のコマンドを入力した場合とほとんど同等です: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +/// + +/// tab | Windows + +OSは`C:\opt\custompython\bin\python`にある`python`プログラムを**見つけ**て実行します。 + +これは、次のコマンドを入力した場合とほとんど同等です: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +/// + +この情報は、[Virtual Environments](virtual-environments.md) について学ぶ際にも役立ちます。 + +## まとめ + +これで、**環境変数**とは何か、Pythonでどのように使用するかについて、基本的な理解が得られたはずです。 + +環境変数についての詳細は、Wikipedia: Environment Variable を参照してください。 + +環境変数の用途や適用方法が最初は直感的ではないかもしれませんが、開発中のさまざまなシナリオで繰り返し登場します。そのため、基本を知っておくことが重要です。 + +たとえば、この情報は次のセクションで扱う[Virtual Environments](virtual-environments.md)にも関連します。 From d2f5097dedcfb1eb695d21dbb19a5593cf1bd756 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 27 Jan 2025 15:39:30 +0000 Subject: [PATCH 094/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0dc660412..76333d313 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/environment-variables.md`. PR [#13226](https://github.com/fastapi/fastapi/pull/13226) by [@k94-ishi](https://github.com/k94-ishi). * 🌐 Add Russian translation for `docs/ru/docs/advanced/async-tests.md`. PR [#13227](https://github.com/fastapi/fastapi/pull/13227) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Update Russian translation for `docs/ru/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#13252](https://github.com/fastapi/fastapi/pull/13252) by [@Rishat-F](https://github.com/Rishat-F). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/bigger-applications.md`. PR [#13154](https://github.com/fastapi/fastapi/pull/13154) by [@alv2017](https://github.com/alv2017). From ff68d0894a7191f7dd107bd449e6bd40b3af1735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 20:34:56 +0000 Subject: [PATCH 095/123] =?UTF-8?q?=F0=9F=94=A8=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20Experts=20script,=20refactor=20and=20optimize=20data=20fetc?= =?UTF-8?q?hing=20to=20handle=20rate=20limits=20(#13267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/Dockerfile | 7 - .github/actions/people/action.yml | 10 - .github/actions/people/app/main.py | 682 ----------------------------- .github/workflows/people.yml | 37 +- docs/en/docs/fastapi-people.md | 33 ++ scripts/people.py | 401 +++++++++++++++++ 6 files changed, 462 insertions(+), 708 deletions(-) delete mode 100644 .github/actions/people/Dockerfile delete mode 100644 .github/actions/people/action.yml delete mode 100644 .github/actions/people/app/main.py create mode 100644 scripts/people.py diff --git a/.github/actions/people/Dockerfile b/.github/actions/people/Dockerfile deleted file mode 100644 index 1455106bd..000000000 --- a/.github/actions/people/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.9 - -RUN pip install httpx PyGithub "pydantic==2.0.2" pydantic-settings "pyyaml>=5.3.1,<6.0.0" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/people/action.yml b/.github/actions/people/action.yml deleted file mode 100644 index 71745b874..000000000 --- a/.github/actions/people/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Generate FastAPI People" -description: "Generate the data for the FastAPI People page" -author: "Sebastián Ramírez " -inputs: - token: - description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.FASTAPI_PEOPLE }}' - required: true -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py deleted file mode 100644 index b752d9d2b..000000000 --- a/.github/actions/people/app/main.py +++ /dev/null @@ -1,682 +0,0 @@ -import logging -import subprocess -import sys -from collections import Counter, defaultdict -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any, Container, DefaultDict, Dict, List, Set, Union - -import httpx -import yaml -from github import Github -from pydantic import BaseModel, SecretStr -from pydantic_settings import BaseSettings - -github_graphql_url = "https://api.github.com/graphql" -questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" - -discussions_query = """ -query Q($after: String, $category_id: ID) { - repository(name: "fastapi", owner: "fastapi") { - discussions(first: 100, after: $after, categoryId: $category_id) { - edges { - cursor - node { - number - author { - login - avatarUrl - url - } - title - createdAt - comments(first: 100) { - nodes { - createdAt - author { - login - avatarUrl - url - } - isAnswer - replies(first: 10) { - nodes { - createdAt - author { - login - avatarUrl - url - } - } - } - } - } - } - } - } - } -} -""" - - -prs_query = """ -query Q($after: String) { - repository(name: "fastapi", owner: "fastapi") { - pullRequests(first: 100, after: $after) { - edges { - cursor - node { - number - labels(first: 100) { - nodes { - name - } - } - author { - login - avatarUrl - url - } - title - createdAt - state - comments(first: 100) { - nodes { - createdAt - author { - login - avatarUrl - url - } - } - } - reviews(first:100) { - nodes { - author { - login - avatarUrl - url - } - state - } - } - } - } - } - } -} -""" - -sponsors_query = """ -query Q($after: String) { - user(login: "fastapi") { - sponsorshipsAsMaintainer(first: 100, after: $after) { - edges { - cursor - node { - sponsorEntity { - ... on Organization { - login - avatarUrl - url - } - ... on User { - login - avatarUrl - url - } - } - tier { - name - monthlyPriceInDollars - } - } - } - } - } -} -""" - - -class Author(BaseModel): - login: str - avatarUrl: str - url: str - - -# Discussions - - -class CommentsNode(BaseModel): - createdAt: datetime - author: Union[Author, None] = None - - -class Replies(BaseModel): - nodes: List[CommentsNode] - - -class DiscussionsCommentsNode(CommentsNode): - replies: Replies - - -class Comments(BaseModel): - nodes: List[CommentsNode] - - -class DiscussionsComments(BaseModel): - nodes: List[DiscussionsCommentsNode] - - -class DiscussionsNode(BaseModel): - number: int - author: Union[Author, None] = None - title: str - createdAt: datetime - comments: DiscussionsComments - - -class DiscussionsEdge(BaseModel): - cursor: str - node: DiscussionsNode - - -class Discussions(BaseModel): - edges: List[DiscussionsEdge] - - -class DiscussionsRepository(BaseModel): - discussions: Discussions - - -class DiscussionsResponseData(BaseModel): - repository: DiscussionsRepository - - -class DiscussionsResponse(BaseModel): - data: DiscussionsResponseData - - -# PRs - - -class LabelNode(BaseModel): - name: str - - -class Labels(BaseModel): - nodes: List[LabelNode] - - -class ReviewNode(BaseModel): - author: Union[Author, None] = None - state: str - - -class Reviews(BaseModel): - nodes: List[ReviewNode] - - -class PullRequestNode(BaseModel): - number: int - labels: Labels - author: Union[Author, None] = None - title: str - createdAt: datetime - state: str - comments: Comments - reviews: Reviews - - -class PullRequestEdge(BaseModel): - cursor: str - node: PullRequestNode - - -class PullRequests(BaseModel): - edges: List[PullRequestEdge] - - -class PRsRepository(BaseModel): - pullRequests: PullRequests - - -class PRsResponseData(BaseModel): - repository: PRsRepository - - -class PRsResponse(BaseModel): - data: PRsResponseData - - -# Sponsors - - -class SponsorEntity(BaseModel): - login: str - avatarUrl: str - url: str - - -class Tier(BaseModel): - name: str - monthlyPriceInDollars: float - - -class SponsorshipAsMaintainerNode(BaseModel): - sponsorEntity: SponsorEntity - tier: Tier - - -class SponsorshipAsMaintainerEdge(BaseModel): - cursor: str - node: SponsorshipAsMaintainerNode - - -class SponsorshipAsMaintainer(BaseModel): - edges: List[SponsorshipAsMaintainerEdge] - - -class SponsorsUser(BaseModel): - sponsorshipsAsMaintainer: SponsorshipAsMaintainer - - -class SponsorsResponseData(BaseModel): - user: SponsorsUser - - -class SponsorsResponse(BaseModel): - data: SponsorsResponseData - - -class Settings(BaseSettings): - input_token: SecretStr - github_repository: str - httpx_timeout: int = 30 - - -def get_graphql_response( - *, - settings: Settings, - query: str, - after: Union[str, None] = None, - category_id: Union[str, None] = None, -) -> Dict[str, Any]: - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - # category_id is only used by one query, but GraphQL allows unused variables, so - # keep it here for simplicity - variables = {"after": after, "category_id": category_id} - response = httpx.post( - github_graphql_url, - headers=headers, - timeout=settings.httpx_timeout, - json={"query": query, "variables": variables, "operationName": "Q"}, - ) - if response.status_code != 200: - logging.error( - f"Response was not 200, after: {after}, category_id: {category_id}" - ) - logging.error(response.text) - raise RuntimeError(response.text) - data = response.json() - if "errors" in data: - logging.error(f"Errors in response, after: {after}, category_id: {category_id}") - logging.error(data["errors"]) - logging.error(response.text) - raise RuntimeError(response.text) - return data - - -def get_graphql_question_discussion_edges( - *, - settings: Settings, - after: Union[str, None] = None, -): - data = get_graphql_response( - settings=settings, - query=discussions_query, - after=after, - category_id=questions_category_id, - ) - graphql_response = DiscussionsResponse.model_validate(data) - return graphql_response.data.repository.discussions.edges - - -def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None): - data = get_graphql_response(settings=settings, query=prs_query, after=after) - graphql_response = PRsResponse.model_validate(data) - return graphql_response.data.repository.pullRequests.edges - - -def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = None): - data = get_graphql_response(settings=settings, query=sponsors_query, after=after) - graphql_response = SponsorsResponse.model_validate(data) - return graphql_response.data.user.sponsorshipsAsMaintainer.edges - - -class DiscussionExpertsResults(BaseModel): - commenters: Counter - last_month_commenters: Counter - three_months_commenters: Counter - six_months_commenters: Counter - one_year_commenters: Counter - authors: Dict[str, Author] - - -def get_discussion_nodes(settings: Settings) -> List[DiscussionsNode]: - discussion_nodes: List[DiscussionsNode] = [] - discussion_edges = get_graphql_question_discussion_edges(settings=settings) - - while discussion_edges: - for discussion_edge in discussion_edges: - discussion_nodes.append(discussion_edge.node) - last_edge = discussion_edges[-1] - discussion_edges = get_graphql_question_discussion_edges( - settings=settings, after=last_edge.cursor - ) - return discussion_nodes - - -def get_discussions_experts( - discussion_nodes: List[DiscussionsNode], -) -> DiscussionExpertsResults: - commenters = Counter() - last_month_commenters = Counter() - three_months_commenters = Counter() - six_months_commenters = Counter() - one_year_commenters = Counter() - authors: Dict[str, Author] = {} - - now = datetime.now(tz=timezone.utc) - one_month_ago = now - timedelta(days=30) - three_months_ago = now - timedelta(days=90) - six_months_ago = now - timedelta(days=180) - one_year_ago = now - timedelta(days=365) - - for discussion in discussion_nodes: - discussion_author_name = None - if discussion.author: - authors[discussion.author.login] = discussion.author - discussion_author_name = discussion.author.login - discussion_commentors: dict[str, datetime] = {} - for comment in discussion.comments.nodes: - if comment.author: - authors[comment.author.login] = comment.author - if comment.author.login != discussion_author_name: - author_time = discussion_commentors.get( - comment.author.login, comment.createdAt - ) - discussion_commentors[comment.author.login] = max( - author_time, comment.createdAt - ) - for reply in comment.replies.nodes: - if reply.author: - authors[reply.author.login] = reply.author - if reply.author.login != discussion_author_name: - author_time = discussion_commentors.get( - reply.author.login, reply.createdAt - ) - discussion_commentors[reply.author.login] = max( - author_time, reply.createdAt - ) - for author_name, author_time in discussion_commentors.items(): - commenters[author_name] += 1 - if author_time > one_month_ago: - last_month_commenters[author_name] += 1 - if author_time > three_months_ago: - three_months_commenters[author_name] += 1 - if author_time > six_months_ago: - six_months_commenters[author_name] += 1 - if author_time > one_year_ago: - one_year_commenters[author_name] += 1 - discussion_experts_results = DiscussionExpertsResults( - authors=authors, - commenters=commenters, - last_month_commenters=last_month_commenters, - three_months_commenters=three_months_commenters, - six_months_commenters=six_months_commenters, - one_year_commenters=one_year_commenters, - ) - return discussion_experts_results - - -def get_pr_nodes(settings: Settings) -> List[PullRequestNode]: - pr_nodes: List[PullRequestNode] = [] - pr_edges = get_graphql_pr_edges(settings=settings) - - while pr_edges: - for edge in pr_edges: - pr_nodes.append(edge.node) - last_edge = pr_edges[-1] - pr_edges = get_graphql_pr_edges(settings=settings, after=last_edge.cursor) - return pr_nodes - - -class ContributorsResults(BaseModel): - contributors: Counter - commenters: Counter - reviewers: Counter - translation_reviewers: Counter - authors: Dict[str, Author] - - -def get_contributors(pr_nodes: List[PullRequestNode]) -> ContributorsResults: - contributors = Counter() - commenters = Counter() - reviewers = Counter() - translation_reviewers = Counter() - authors: Dict[str, Author] = {} - - for pr in pr_nodes: - author_name = None - if pr.author: - authors[pr.author.login] = pr.author - author_name = pr.author.login - pr_commentors: Set[str] = set() - pr_reviewers: Set[str] = set() - for comment in pr.comments.nodes: - if comment.author: - authors[comment.author.login] = comment.author - if comment.author.login == author_name: - continue - pr_commentors.add(comment.author.login) - for author_name in pr_commentors: - commenters[author_name] += 1 - for review in pr.reviews.nodes: - if review.author: - authors[review.author.login] = review.author - pr_reviewers.add(review.author.login) - for label in pr.labels.nodes: - if label.name == "lang-all": - translation_reviewers[review.author.login] += 1 - break - for reviewer in pr_reviewers: - reviewers[reviewer] += 1 - if pr.state == "MERGED" and pr.author: - contributors[pr.author.login] += 1 - return ContributorsResults( - contributors=contributors, - commenters=commenters, - reviewers=reviewers, - translation_reviewers=translation_reviewers, - authors=authors, - ) - - -def get_individual_sponsors(settings: Settings): - nodes: List[SponsorshipAsMaintainerNode] = [] - edges = get_graphql_sponsor_edges(settings=settings) - - while edges: - for edge in edges: - nodes.append(edge.node) - last_edge = edges[-1] - edges = get_graphql_sponsor_edges(settings=settings, after=last_edge.cursor) - - tiers: DefaultDict[float, Dict[str, SponsorEntity]] = defaultdict(dict) - for node in nodes: - tiers[node.tier.monthlyPriceInDollars][node.sponsorEntity.login] = ( - node.sponsorEntity - ) - return tiers - - -def get_top_users( - *, - counter: Counter, - authors: Dict[str, Author], - skip_users: Container[str], - min_count: int = 2, -): - users = [] - for commenter, count in counter.most_common(50): - if commenter in skip_users: - continue - if count >= min_count: - author = authors[commenter] - users.append( - { - "login": commenter, - "count": count, - "avatarUrl": author.avatarUrl, - "url": author.url, - } - ) - return users - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.model_dump_json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - discussion_nodes = get_discussion_nodes(settings=settings) - experts_results = get_discussions_experts(discussion_nodes=discussion_nodes) - pr_nodes = get_pr_nodes(settings=settings) - contributors_results = get_contributors(pr_nodes=pr_nodes) - authors = {**experts_results.authors, **contributors_results.authors} - maintainers_logins = {"tiangolo"} - bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"} - maintainers = [] - for login in maintainers_logins: - user = authors[login] - maintainers.append( - { - "login": login, - "answers": experts_results.commenters[login], - "prs": contributors_results.contributors[login], - "avatarUrl": user.avatarUrl, - "url": user.url, - } - ) - - skip_users = maintainers_logins | bot_names - experts = get_top_users( - counter=experts_results.commenters, - authors=authors, - skip_users=skip_users, - ) - last_month_experts = get_top_users( - counter=experts_results.last_month_commenters, - authors=authors, - skip_users=skip_users, - ) - three_months_experts = get_top_users( - counter=experts_results.three_months_commenters, - authors=authors, - skip_users=skip_users, - ) - six_months_experts = get_top_users( - counter=experts_results.six_months_commenters, - authors=authors, - skip_users=skip_users, - ) - one_year_experts = get_top_users( - counter=experts_results.one_year_commenters, - authors=authors, - skip_users=skip_users, - ) - top_contributors = get_top_users( - counter=contributors_results.contributors, - authors=authors, - skip_users=skip_users, - ) - top_reviewers = get_top_users( - counter=contributors_results.reviewers, - authors=authors, - skip_users=skip_users, - ) - top_translations_reviewers = get_top_users( - counter=contributors_results.translation_reviewers, - authors=authors, - skip_users=skip_users, - ) - - tiers = get_individual_sponsors(settings=settings) - keys = list(tiers.keys()) - keys.sort(reverse=True) - sponsors = [] - for key in keys: - sponsor_group = [] - for login, sponsor in tiers[key].items(): - sponsor_group.append( - {"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url} - ) - sponsors.append(sponsor_group) - - people = { - "maintainers": maintainers, - "experts": experts, - "last_month_experts": last_month_experts, - "three_months_experts": three_months_experts, - "six_months_experts": six_months_experts, - "one_year_experts": one_year_experts, - "top_contributors": top_contributors, - "top_reviewers": top_reviewers, - "top_translations_reviewers": top_translations_reviewers, - } - github_sponsors = { - "sponsors": sponsors, - } - # For local development - # people_path = Path("../../../../docs/en/data/people.yml") - people_path = Path("./docs/en/data/people.yml") - github_sponsors_path = Path("./docs/en/data/github_sponsors.yml") - people_old_content = people_path.read_text(encoding="utf-8") - github_sponsors_old_content = github_sponsors_path.read_text(encoding="utf-8") - new_people_content = yaml.dump( - people, sort_keys=False, width=200, allow_unicode=True - ) - new_github_sponsors_content = yaml.dump( - github_sponsors, sort_keys=False, width=200, allow_unicode=True - ) - if ( - people_old_content == new_people_content - and github_sponsors_old_content == new_github_sponsors_content - ): - logging.info("The FastAPI People data hasn't changed, finishing.") - sys.exit(0) - people_path.write_text(new_people_content, encoding="utf-8") - github_sponsors_path.write_text(new_github_sponsors_content, encoding="utf-8") - logging.info("Setting up GitHub Actions git user") - subprocess.run(["git", "config", "user.name", "github-actions"], check=True) - subprocess.run( - ["git", "config", "user.email", "github-actions@github.com"], check=True - ) - branch_name = "fastapi-people" - logging.info(f"Creating a new branch {branch_name}") - subprocess.run(["git", "checkout", "-b", branch_name], check=True) - logging.info("Adding updated file") - subprocess.run( - ["git", "add", str(people_path), str(github_sponsors_path)], check=True - ) - logging.info("Committing updated file") - message = "👥 Update FastAPI People" - result = subprocess.run(["git", "commit", "-m", message], check=True) - logging.info("Pushing branch") - subprocess.run(["git", "push", "origin", branch_name], check=True) - logging.info("Creating PR") - pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) - logging.info(f"Created PR: {pr.number}") - logging.info("Finished") diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index c60c63d1b..6ec3c1ad2 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -6,29 +6,48 @@ on: workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate) required: false - default: 'false' + default: "false" + +env: + UV_SYSTEM_PYTHON: 1 jobs: - fastapi-people: + job: if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - # Ref: https://github.com/actions/runner/issues/2033 - - name: Fix git safe.directory in container - run: mkdir -p /home/runner/work/_temp/_github_home && printf "[safe]\n\tdirectory = /github/workspace" > /home/runner/work/_temp/_github_home/.gitconfig + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - name: Install Dependencies + run: uv pip install -r requirements-github-actions.txt # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: ./.github/actions/people - with: - token: ${{ secrets.FASTAPI_PEOPLE }} + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} + - name: FastAPI People Experts + run: python ./scripts/people.py + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }} diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index ffc579b10..f2ca26013 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -47,9 +47,11 @@ This is the current list of team members. 😎 They have different levels of involvement and permissions, they can perform [repository management tasks](./management-tasks.md){.internal-link target=_blank} and together we [manage the FastAPI repository](./management.md){.internal-link target=_blank}.
+ {% for user in members["members"] %} + {% endfor %}
@@ -83,9 +85,15 @@ You can see the **FastAPI Experts** for: These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last month. 🤓
+ {% for user in people.last_month_experts[:10] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -95,9 +103,15 @@ These are the users that have been [helping others the most with questions in Gi These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last 3 months. 😎
+ {% for user in people.three_months_experts[:10] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -107,9 +121,15 @@ These are the users that have been [helping others the most with questions in Gi These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last 6 months. 🧐
+ {% for user in people.six_months_experts[:10] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -119,9 +139,15 @@ These are the users that have been [helping others the most with questions in Gi These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last year. 🧑‍🔬
+ {% for user in people.one_year_experts[:20] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -133,9 +159,15 @@ Here are the all time **FastAPI Experts**. 🤓🤯 These are the users that have [helped others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} through *all time*. 🧙
+ {% for user in people.experts[:50] %} +{% if user.login not in skip_users %} +
@{{ user.login }}
Questions replied: {{ user.count }}
+ +{% endif %} + {% endfor %}
@@ -149,6 +181,7 @@ These users have [created the most Pull Requests](help-fastapi.md#create-a-pull- They have contributed source code, documentation, etc. 📦
+ {% for user in (contributors.values() | list)[:50] %} {% if user.login not in skip_users %} diff --git a/scripts/people.py b/scripts/people.py new file mode 100644 index 000000000..f61fd31c9 --- /dev/null +++ b/scripts/people.py @@ -0,0 +1,401 @@ +import logging +import secrets +import subprocess +import time +from collections import Counter +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any, Container, Union + +import httpx +import yaml +from github import Github +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings + +github_graphql_url = "https://api.github.com/graphql" +questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" + +discussions_query = """ +query Q($after: String, $category_id: ID) { + repository(name: "fastapi", owner: "fastapi") { + discussions(first: 100, after: $after, categoryId: $category_id) { + edges { + cursor + node { + number + author { + login + avatarUrl + url + } + createdAt + comments(first: 50) { + totalCount + nodes { + createdAt + author { + login + avatarUrl + url + } + isAnswer + replies(first: 10) { + totalCount + nodes { + createdAt + author { + login + avatarUrl + url + } + } + } + } + } + } + } + } + } +} +""" + + +class Author(BaseModel): + login: str + avatarUrl: str | None = None + url: str | None = None + + +class CommentsNode(BaseModel): + createdAt: datetime + author: Union[Author, None] = None + + +class Replies(BaseModel): + totalCount: int + nodes: list[CommentsNode] + + +class DiscussionsCommentsNode(CommentsNode): + replies: Replies + + +class DiscussionsComments(BaseModel): + totalCount: int + nodes: list[DiscussionsCommentsNode] + + +class DiscussionsNode(BaseModel): + number: int + author: Union[Author, None] = None + title: str | None = None + createdAt: datetime + comments: DiscussionsComments + + +class DiscussionsEdge(BaseModel): + cursor: str + node: DiscussionsNode + + +class Discussions(BaseModel): + edges: list[DiscussionsEdge] + + +class DiscussionsRepository(BaseModel): + discussions: Discussions + + +class DiscussionsResponseData(BaseModel): + repository: DiscussionsRepository + + +class DiscussionsResponse(BaseModel): + data: DiscussionsResponseData + + +class Settings(BaseSettings): + github_token: SecretStr + github_repository: str + httpx_timeout: int = 30 + + +def get_graphql_response( + *, + settings: Settings, + query: str, + after: Union[str, None] = None, + category_id: Union[str, None] = None, +) -> dict[str, Any]: + headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} + variables = {"after": after, "category_id": category_id} + response = httpx.post( + github_graphql_url, + headers=headers, + timeout=settings.httpx_timeout, + json={"query": query, "variables": variables, "operationName": "Q"}, + ) + if response.status_code != 200: + logging.error( + f"Response was not 200, after: {after}, category_id: {category_id}" + ) + logging.error(response.text) + raise RuntimeError(response.text) + data = response.json() + if "errors" in data: + logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(data["errors"]) + logging.error(response.text) + raise RuntimeError(response.text) + return data + + +def get_graphql_question_discussion_edges( + *, + settings: Settings, + after: Union[str, None] = None, +) -> list[DiscussionsEdge]: + data = get_graphql_response( + settings=settings, + query=discussions_query, + after=after, + category_id=questions_category_id, + ) + graphql_response = DiscussionsResponse.model_validate(data) + return graphql_response.data.repository.discussions.edges + + +class DiscussionExpertsResults(BaseModel): + commenters: Counter[str] + last_month_commenters: Counter[str] + three_months_commenters: Counter[str] + six_months_commenters: Counter[str] + one_year_commenters: Counter[str] + authors: dict[str, Author] + + +def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]: + discussion_nodes: list[DiscussionsNode] = [] + discussion_edges = get_graphql_question_discussion_edges(settings=settings) + + while discussion_edges: + for discussion_edge in discussion_edges: + discussion_nodes.append(discussion_edge.node) + last_edge = discussion_edges[-1] + # Handle GitHub secondary rate limits, requests per minute + time.sleep(5) + discussion_edges = get_graphql_question_discussion_edges( + settings=settings, after=last_edge.cursor + ) + return discussion_nodes + + +def get_discussions_experts( + discussion_nodes: list[DiscussionsNode], +) -> DiscussionExpertsResults: + commenters = Counter[str]() + last_month_commenters = Counter[str]() + three_months_commenters = Counter[str]() + six_months_commenters = Counter[str]() + one_year_commenters = Counter[str]() + authors: dict[str, Author] = {} + + now = datetime.now(tz=timezone.utc) + one_month_ago = now - timedelta(days=30) + three_months_ago = now - timedelta(days=90) + six_months_ago = now - timedelta(days=180) + one_year_ago = now - timedelta(days=365) + + for discussion in discussion_nodes: + discussion_author_name = None + if discussion.author: + authors[discussion.author.login] = discussion.author + discussion_author_name = discussion.author.login + discussion_commentors: dict[str, datetime] = {} + for comment in discussion.comments.nodes: + if comment.author: + authors[comment.author.login] = comment.author + if comment.author.login != discussion_author_name: + author_time = discussion_commentors.get( + comment.author.login, comment.createdAt + ) + discussion_commentors[comment.author.login] = max( + author_time, comment.createdAt + ) + for reply in comment.replies.nodes: + if reply.author: + authors[reply.author.login] = reply.author + if reply.author.login != discussion_author_name: + author_time = discussion_commentors.get( + reply.author.login, reply.createdAt + ) + discussion_commentors[reply.author.login] = max( + author_time, reply.createdAt + ) + for author_name, author_time in discussion_commentors.items(): + commenters[author_name] += 1 + if author_time > one_month_ago: + last_month_commenters[author_name] += 1 + if author_time > three_months_ago: + three_months_commenters[author_name] += 1 + if author_time > six_months_ago: + six_months_commenters[author_name] += 1 + if author_time > one_year_ago: + one_year_commenters[author_name] += 1 + discussion_experts_results = DiscussionExpertsResults( + authors=authors, + commenters=commenters, + last_month_commenters=last_month_commenters, + three_months_commenters=three_months_commenters, + six_months_commenters=six_months_commenters, + one_year_commenters=one_year_commenters, + ) + return discussion_experts_results + + +def get_top_users( + *, + counter: Counter[str], + authors: dict[str, Author], + skip_users: Container[str], + min_count: int = 2, +) -> list[dict[str, Any]]: + users: list[dict[str, Any]] = [] + for commenter, count in counter.most_common(50): + if commenter in skip_users: + continue + if count >= min_count: + author = authors[commenter] + users.append( + { + "login": commenter, + "count": count, + "avatarUrl": author.avatarUrl, + "url": author.url, + } + ) + return users + + +def get_users_to_write( + *, + counter: Counter[str], + authors: dict[str, Author], + min_count: int = 2, +) -> list[dict[str, Any]]: + users: dict[str, Any] = {} + users_list: list[dict[str, Any]] = [] + for user, count in counter.most_common(60): + if count >= min_count: + author = authors[user] + user_data = { + "login": user, + "count": count, + "avatarUrl": author.avatarUrl, + "url": author.url, + } + users[user] = user_data + users_list.append(user_data) + return users_list + + +def update_content(*, content_path: Path, new_content: Any) -> bool: + old_content = content_path.read_text(encoding="utf-8") + + new_content = yaml.dump(new_content, sort_keys=False, width=200, allow_unicode=True) + if old_content == new_content: + logging.info(f"The content hasn't changed for {content_path}") + return False + content_path.write_text(new_content, encoding="utf-8") + logging.info(f"Updated {content_path}") + return True + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + settings = Settings() + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(settings.github_token.get_secret_value()) + repo = g.get_repo(settings.github_repository) + + discussion_nodes = get_discussion_nodes(settings=settings) + experts_results = get_discussions_experts(discussion_nodes=discussion_nodes) + + authors = experts_results.authors + maintainers_logins = {"tiangolo"} + maintainers = [] + for login in maintainers_logins: + user = authors[login] + maintainers.append( + { + "login": login, + "answers": experts_results.commenters[login], + "avatarUrl": user.avatarUrl, + "url": user.url, + } + ) + + experts = get_users_to_write( + counter=experts_results.commenters, + authors=authors, + ) + last_month_experts = get_users_to_write( + counter=experts_results.last_month_commenters, + authors=authors, + ) + three_months_experts = get_users_to_write( + counter=experts_results.three_months_commenters, + authors=authors, + ) + six_months_experts = get_users_to_write( + counter=experts_results.six_months_commenters, + authors=authors, + ) + one_year_experts = get_users_to_write( + counter=experts_results.one_year_commenters, + authors=authors, + ) + + people = { + "maintainers": maintainers, + "experts": experts, + "last_month_experts": last_month_experts, + "three_months_experts": three_months_experts, + "six_months_experts": six_months_experts, + "one_year_experts": one_year_experts, + } + + # For local development + # people_path = Path("../docs/en/data/people.yml") + people_path = Path("./docs/en/data/people.yml") + + updated = update_content(content_path=people_path, new_content=people) + + if not updated: + logging.info("The data hasn't changed, finishing.") + return + + logging.info("Setting up GitHub Actions git user") + subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run( + ["git", "config", "user.email", "github-actions@github.com"], check=True + ) + branch_name = f"fastapi-people-experts-{secrets.token_hex(4)}" + logging.info(f"Creating a new branch {branch_name}") + subprocess.run(["git", "checkout", "-b", branch_name], check=True) + logging.info("Adding updated file") + subprocess.run(["git", "add", str(people_path)], check=True) + logging.info("Committing updated file") + message = "👥 Update FastAPI People - Experts" + subprocess.run(["git", "commit", "-m", message], check=True) + logging.info("Pushing branch") + subprocess.run(["git", "push", "origin", branch_name], check=True) + logging.info("Creating PR") + pr = repo.create_pull(title=message, body=message, base="master", head=branch_name) + logging.info(f"Created PR: {pr.number}") + logging.info("Finished") + + +if __name__ == "__main__": + main() From e925c0ec8e4dd5ad0d2d7082de280678cead8d43 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 20:35:19 +0000 Subject: [PATCH 096/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 76333d313..3db7042fb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Internal +* 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.115.7 From 3da797aeb894d82be24d4a137046b65c2c1029c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 20:41:08 +0000 Subject: [PATCH 097/123] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peopl?= =?UTF-8?q?e=20-=20Experts=20(#13269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/people.yml | 1676 ++++++++++++++------------------------- 1 file changed, 592 insertions(+), 1084 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 02d1779e0..112567778 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,24 +1,35 @@ maintainers: - login: tiangolo - answers: 1885 - prs: 577 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 + answers: 1894 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo experts: +- login: tiangolo + count: 1894 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: github-actions + count: 770 + avatarUrl: https://avatars.githubusercontent.com/in/15368?v=4 + url: https://github.com/apps/github-actions - login: Kludex - count: 608 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + count: 644 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex -- login: dmontagu - count: 241 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu - login: jgould22 - count: 241 + count: 250 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 +- login: dmontagu + count: 240 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 + url: https://github.com/dmontagu +- login: YuriiMotov + count: 223 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 + url: https://github.com/YuriiMotov - login: Mause - count: 220 + count: 219 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause - login: ycd @@ -26,7 +37,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 url: https://github.com/ycd - login: JarroVGIT - count: 193 + count: 192 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 @@ -41,78 +52,82 @@ experts: count: 126 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 -- login: YuriiMotov - count: 104 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 - url: https://github.com/YuriiMotov +- login: JavierSanchezCastro + count: 85 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: raphaelauv count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: ArcLightSlavik - count: 71 - avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 - url: https://github.com/ArcLightSlavik - login: ghandic count: 71 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic -- login: JavierSanchezCastro - count: 64 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro +- login: ArcLightSlavik + count: 71 + avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 + url: https://github.com/ArcLightSlavik +- login: n8sty + count: 66 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: falkben count: 59 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben -- login: n8sty - count: 56 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: acidjunk count: 50 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk -- login: yinziyan1206 - count: 49 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 - login: sm-Fifteen count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes +- login: yinziyan1206 + count: 49 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 +- login: adriangb + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: adriangb +- login: insomnes count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 - url: https://github.com/adriangb -- login: frankie567 - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 - url: https://github.com/frankie567 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: odiseo0 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 url: https://github.com/odiseo0 +- login: frankie567 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 + url: https://github.com/frankie567 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: sinisaos + count: 39 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos - login: chbndrhnns - count: 38 + count: 37 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns - login: STeveShary count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary +- login: luzzodev + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 @@ -123,7 +138,7 @@ experts: url: https://github.com/panla - login: prostomarkeloff count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=6918e39a1224194ba636e897461a02a20126d7ad&v=4 url: https://github.com/prostomarkeloff - login: hasansezertasan count: 27 @@ -137,10 +152,6 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: acnebs - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 - url: https://github.com/acnebs - login: SirTelemak count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 @@ -149,6 +160,10 @@ experts: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 url: https://github.com/nymous +- login: acnebs + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 + url: https://github.com/acnebs - login: chrisK824 count: 22 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 @@ -157,30 +172,38 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 url: https://github.com/rafsaf +- login: ebottos94 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=8b91053b3abe4a9209375e3651e1c1ef192d884b&v=4 + url: https://github.com/ebottos94 - login: nsidnev count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev -- login: ebottos94 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 - login: chris-allnutt count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: retnikt - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 - url: https://github.com/retnikt +- login: estebanx64 + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 + url: https://github.com/estebanx64 - login: zoliknemet count: 18 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 url: https://github.com/zoliknemet -- login: nkhitrov +- login: retnikt + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 + url: https://github.com/retnikt +- login: sehraramiz count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 - url: https://github.com/nkhitrov + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz +- login: caeser1996 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 @@ -189,1170 +212,655 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar -- login: caeser1996 +- login: nkhitrov count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 - url: https://github.com/caeser1996 + avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=e19427d8dc296d6950e9c424adacc92d37496fe9&v=4 + url: https://github.com/nkhitrov +- login: jonatasoli + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 + url: https://github.com/jonatasoli - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: jorgerpo + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 + url: https://github.com/jorgerpo +- login: simondale00 + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/33907262?u=2721fb37014d50daf473267c808aa678ecaefe09&v=4 + url: https://github.com/simondale00 +- login: ghost + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 + url: https://github.com/ghost +- login: abhint + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 + url: https://github.com/abhint +- login: pythonweb2 + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 + url: https://github.com/pythonweb2 last_month_experts: +- login: Kludex + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex - login: YuriiMotov - count: 29 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov -- login: killjoy1221 +- login: sehraramiz count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: Kludex - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: JavierSanchezCastro - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: hasansezertasan - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: PhysicallyActive + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz +- login: luzzodev + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev +- login: yokwejuste + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 - url: https://github.com/PhysicallyActive -- login: n8sty - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: pedroconceicao - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32837064?u=5a0e6559bc391442629a28b6923790b54deb4464&v=4 - url: https://github.com/pedroconceicao + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 - login: PREPONDERANCE count: 2 avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 url: https://github.com/PREPONDERANCE -- login: aanchlia +- login: nbx3 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia -- login: 0sahil + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: XiaoXinYo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58521386?u=ac00b731c07c712d0baa57b8b70ac8422acf183c&v=4 - url: https://github.com/0sahil -- login: jgould22 + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: iloveitaly count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly three_months_experts: +- login: luzzodev + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: YuriiMotov - count: 101 + count: 33 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov -- login: JavierSanchezCastro - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro - login: Kludex - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex -- login: jgould22 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: killjoy1221 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: hasansezertasan - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: PhysicallyActive - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 - url: https://github.com/PhysicallyActive -- login: n8sty - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: sehraramiz - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?v=4 + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 url: https://github.com/sehraramiz -- login: acidjunk - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: estebanx64 - count: 3 + count: 7 avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 url: https://github.com/estebanx64 -- login: PREPONDERANCE - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 - url: https://github.com/PREPONDERANCE -- login: chrisK824 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: ryanisn - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/53449841?v=4 - url: https://github.com/ryanisn -- login: pythonweb2 +- login: yvallois + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 + url: https://github.com/yvallois +- login: yokwejuste + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste +- login: jgould22 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: omarcruzpantoja + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: viniciusCalcantara count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/15116058?u=4b64c643fad49225d854e1aaecd1ffc6f9071a1b&v=4 - url: https://github.com/omarcruzpantoja -- login: mskrip - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17459600?u=10019d5c38ae3374dd4a6743b0223e56a78d4855&v=4 - url: https://github.com/mskrip -- login: pedroconceicao - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32837064?u=5a0e6559bc391442629a28b6923790b54deb4464&v=4 - url: https://github.com/pedroconceicao -- login: Jackiexiao - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/18050469?u=a2003e21a7780477ba00bf87a9abef8af58e91d1&v=4 - url: https://github.com/Jackiexiao -- login: aanchlia - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia -- login: moreno-p + avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=3d7ffe5808843ee4372f9cc5a559ff1674cf1792&v=4 + url: https://github.com/viniciusCalcantara +- login: PREPONDERANCE count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/164261630?v=4 - url: https://github.com/moreno-p -- login: 0sahil + avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 + url: https://github.com/PREPONDERANCE +- login: nbx3 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58521386?u=ac00b731c07c712d0baa57b8b70ac8422acf183c&v=4 - url: https://github.com/0sahil -- login: patrick91 + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: XiaoXinYo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/667029?u=e35958a75ac1f99c81b4bc99e22db8cd665ae7f0&v=4 - url: https://github.com/patrick91 -- login: pprunty + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: JavierSanchezCastro count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58374462?u=5736576e586429abc97a803b8bcd4a6d828b8a2f&v=4 - url: https://github.com/pprunty -- login: angely-dev + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: iloveitaly count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4362224?v=4 - url: https://github.com/angely-dev -- login: mastizada + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly +- login: LincolnPuzey count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1975818?u=0751a06d7271c8bf17cb73b1b845644ab4d2c6dc&v=4 - url: https://github.com/mastizada -- login: sm-Fifteen + avatarUrl: https://avatars.githubusercontent.com/u/18750802?v=4 + url: https://github.com/LincolnPuzey +- login: Knighthawk-Leo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 - url: https://github.com/sm-Fifteen -- login: methane + avatarUrl: https://avatars.githubusercontent.com/u/72437494?u=27c68db94a3107b605e603cc136f4ba83f0106d5&v=4 + url: https://github.com/Knighthawk-Leo +- login: gelezo43 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/199592?v=4 - url: https://github.com/methane -- login: konstantinos1981 + avatarUrl: https://avatars.githubusercontent.com/u/40732698?u=611f39d3c1d2f4207a590937a78c1f10eed6232c&v=4 + url: https://github.com/gelezo43 +- login: dbfreem count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/39465388?v=4 - url: https://github.com/konstantinos1981 -- login: druidance + avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 + url: https://github.com/dbfreem +- login: AliYmn count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/160344534?v=4 - url: https://github.com/druidance -- login: fabianfalon + avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=98c1fca46c7e4dabe8c39d17b5e55d1511d41cf9&v=4 + url: https://github.com/AliYmn +- login: RichieB2B count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/3700760?u=95f69e31280b17ac22299cdcd345323b142fe0af&v=4 - url: https://github.com/fabianfalon -- login: VatsalJagani + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: Synrom count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/20964366?u=43552644be05c9107c029e26d5ab3be5a1920f45&v=4 - url: https://github.com/VatsalJagani -- login: khaledadrani + avatarUrl: https://avatars.githubusercontent.com/u/30272537?v=4 + url: https://github.com/Synrom +- login: iiotsrc count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/45245894?u=49ed5056426a149a5af29d385d8bd3847101d3a4&v=4 - url: https://github.com/khaledadrani -- login: ThirVondukr + avatarUrl: https://avatars.githubusercontent.com/u/131771119?u=bcaf2559ef6266af70b151b7fda31a1ee3dbecb3&v=4 + url: https://github.com/iiotsrc +- login: Kfir-G count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/50728601?u=167c0bd655e52817082e50979a86d2f98f95b1a3&v=4 - url: https://github.com/ThirVondukr + avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 + url: https://github.com/Kfir-G six_months_experts: - login: YuriiMotov - count: 104 + count: 72 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov - login: Kludex - count: 104 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + count: 39 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex +- login: sinisaos + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos +- login: luzzodev + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev - login: JavierSanchezCastro - count: 40 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro +- login: tiangolo + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: Kfir-G + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 + url: https://github.com/Kfir-G +- login: sehraramiz + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz +- login: estebanx64 + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 + url: https://github.com/estebanx64 +- login: ceb10n + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: yvallois + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 + url: https://github.com/yvallois +- login: n8sty + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty +- login: TomFaulkner + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/14956620?v=4 + url: https://github.com/TomFaulkner +- login: yokwejuste + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste - login: jgould22 - count: 40 + count: 4 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 -- login: hasansezertasan - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: n8sty - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: killjoy1221 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: aanchlia - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia -- login: estebanx64 - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 - url: https://github.com/estebanx64 -- login: PhysicallyActive - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 - url: https://github.com/PhysicallyActive -- login: dolfinus - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=a51b39001a2e5e7529b45826980becf786de2327&v=4 - url: https://github.com/dolfinus -- login: Ventura94 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/43103937?u=ccb837005aaf212a449c374618c4339089e2f733&v=4 - url: https://github.com/Ventura94 -- login: sehraramiz - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?v=4 - url: https://github.com/sehraramiz -- login: acidjunk - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk -- login: shashstormer - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/90090313?v=4 - url: https://github.com/shashstormer -- login: GodMoonGoodman - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/29688727?u=7b251da620d999644c37c1feeb292d033eed7ad6&v=4 - url: https://github.com/GodMoonGoodman -- login: flo-at - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/564288?v=4 - url: https://github.com/flo-at -- login: PREPONDERANCE +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 - url: https://github.com/PREPONDERANCE -- login: chrisK824 + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: viniciusCalcantara count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: angely-dev + avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=3d7ffe5808843ee4372f9cc5a559ff1674cf1792&v=4 + url: https://github.com/viniciusCalcantara +- login: pawelad count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/4362224?v=4 - url: https://github.com/angely-dev -- login: fmelihh + avatarUrl: https://avatars.githubusercontent.com/u/7062874?u=d27dc220545a8401ad21840590a97d474d7101e6&v=4 + url: https://github.com/pawelad +- login: dbfreem count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/99879453?u=671117dba9022db2237e3da7a39cbc2efc838db0&v=4 - url: https://github.com/fmelihh -- login: ryanisn + avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 + url: https://github.com/dbfreem +- login: Isuxiz count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/53449841?v=4 - url: https://github.com/ryanisn -- login: JoshYuJump + avatarUrl: https://avatars.githubusercontent.com/u/48672727?u=34d7b4ade252687d22a27cf53037b735b244bfc1&v=4 + url: https://github.com/Isuxiz +- login: bertomaniac count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/5901894?u=cdbca6296ac4cdcdf6945c112a1ce8d5342839ea&v=4 - url: https://github.com/JoshYuJump -- login: pythonweb2 + avatarUrl: https://avatars.githubusercontent.com/u/10235051?u=14484a96833228a7b29fee4a7916d411c242c4f6&v=4 + url: https://github.com/bertomaniac +- login: PhysicallyActive count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: omarcruzpantoja + avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 + url: https://github.com/PhysicallyActive +- login: Minibrams count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/15116058?u=4b64c643fad49225d854e1aaecd1ffc6f9071a1b&v=4 - url: https://github.com/omarcruzpantoja -- login: bogdan-coman-uv + avatarUrl: https://avatars.githubusercontent.com/u/8108085?u=b028dbc308fa8485e0e2e9402b3d03d8deb22bf9&v=4 + url: https://github.com/Minibrams +- login: AIdjis count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/92912507?v=4 - url: https://github.com/bogdan-coman-uv -- login: ahmedabdou14 + avatarUrl: https://avatars.githubusercontent.com/u/88404339?u=2a80d80b054e9228391e32fb9bb39571509dab6a&v=4 + url: https://github.com/AIdjis +- login: svlandeg count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=05365b155a1ff911532e8be316acfad2e0736f98&v=4 - url: https://github.com/ahmedabdou14 -- login: mskrip + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: PREPONDERANCE count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17459600?u=10019d5c38ae3374dd4a6743b0223e56a78d4855&v=4 - url: https://github.com/mskrip -- login: leonidktoto + avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 + url: https://github.com/PREPONDERANCE +- login: nbx3 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/159561986?v=4 - url: https://github.com/leonidktoto -- login: pedroconceicao + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: yanggeorge count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32837064?u=5a0e6559bc391442629a28b6923790b54deb4464&v=4 - url: https://github.com/pedroconceicao -- login: hwong557 + avatarUrl: https://avatars.githubusercontent.com/u/2434407?v=4 + url: https://github.com/yanggeorge +- login: XiaoXinYo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/460259?u=7d2f1b33ea5bda4d8e177ab3cb924a673d53087e&v=4 - url: https://github.com/hwong557 -- login: Jackiexiao + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: pythonweb2 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/18050469?u=a2003e21a7780477ba00bf87a9abef8af58e91d1&v=4 - url: https://github.com/Jackiexiao -- login: admo1 + avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 + url: https://github.com/pythonweb2 +- login: slafs count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/14835916?v=4 - url: https://github.com/admo1 -- login: binbjz + avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 + url: https://github.com/slafs +- login: AmirHmZz count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/8213913?u=22b68b7a0d5bf5e09c02084c0f5f53d7503114cd&v=4 - url: https://github.com/binbjz -- login: nameer + avatarUrl: https://avatars.githubusercontent.com/u/38752106?u=07f80e451bda00a9492bbc764e49d24ad3ada8cc&v=4 + url: https://github.com/AmirHmZz +- login: iloveitaly count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/3931725?u=6199fb065df098fc13ac0a5e649f89672b586732&v=4 - url: https://github.com/nameer -- login: moreno-p + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly +- login: LincolnPuzey count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/164261630?v=4 - url: https://github.com/moreno-p -- login: 0sahil + avatarUrl: https://avatars.githubusercontent.com/u/18750802?v=4 + url: https://github.com/LincolnPuzey +- login: alejsdev count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58521386?u=ac00b731c07c712d0baa57b8b70ac8422acf183c&v=4 - url: https://github.com/0sahil -- login: nymous + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=356f39ff3f0211c720b06d3dbb060e98884085e3&v=4 + url: https://github.com/alejsdev +- login: Knighthawk-Leo count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous -- login: patrick91 + avatarUrl: https://avatars.githubusercontent.com/u/72437494?u=27c68db94a3107b605e603cc136f4ba83f0106d5&v=4 + url: https://github.com/Knighthawk-Leo +- login: gelezo43 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/667029?u=e35958a75ac1f99c81b4bc99e22db8cd665ae7f0&v=4 - url: https://github.com/patrick91 -- login: pprunty + avatarUrl: https://avatars.githubusercontent.com/u/40732698?u=611f39d3c1d2f4207a590937a78c1f10eed6232c&v=4 + url: https://github.com/gelezo43 +- login: christiansicari count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/58374462?u=5736576e586429abc97a803b8bcd4a6d828b8a2f&v=4 - url: https://github.com/pprunty -- login: JonnyBootsNpants + avatarUrl: https://avatars.githubusercontent.com/u/29756552?v=4 + url: https://github.com/christiansicari +- login: 1001pepi count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/155071540?u=2d3a72b74a2c4c8eaacdb625c7ac850369579352&v=4 - url: https://github.com/JonnyBootsNpants -- login: richin13 + avatarUrl: https://avatars.githubusercontent.com/u/82064861?u=8c6ffdf2275d6970a07294752c545cd2702c57d3&v=4 + url: https://github.com/1001pepi +- login: AliYmn count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/8370058?u=8e37a4cdbc78983a5f4b4847f6d1879fb39c851c&v=4 - url: https://github.com/richin13 -- login: mastizada + avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=98c1fca46c7e4dabe8c39d17b5e55d1511d41cf9&v=4 + url: https://github.com/AliYmn +- login: RichieB2B count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1975818?u=0751a06d7271c8bf17cb73b1b845644ab4d2c6dc&v=4 - url: https://github.com/mastizada -- login: sm-Fifteen + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: Synrom count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 - url: https://github.com/sm-Fifteen -- login: amacfie + avatarUrl: https://avatars.githubusercontent.com/u/30272537?v=4 + url: https://github.com/Synrom +- login: ecly + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/8410422?v=4 + url: https://github.com/ecly +- login: iiotsrc + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/131771119?u=bcaf2559ef6266af70b151b7fda31a1ee3dbecb3&v=4 + url: https://github.com/iiotsrc +- login: simondale00 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/33907262?u=2721fb37014d50daf473267c808aa678ecaefe09&v=4 + url: https://github.com/simondale00 +- login: jd-solanki + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/47495003?u=6e225cb42c688d0cd70e65c6baedb9f5922b1178&v=4 + url: https://github.com/jd-solanki +- login: AumGupta + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/86357151?u=7d05aa606c0611a18f4db16cf26361ce10a6e195&v=4 + url: https://github.com/AumGupta +- login: DeoLeung + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/3764720?u=4c222ef513814de4c7fb3736d0a7adf11d953d43&v=4 + url: https://github.com/DeoLeung +- login: Reemyos + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/44867003?v=4 + url: https://github.com/Reemyos +- login: deight93 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/37678115?u=a608798b5bd0034183a9c430ebb42fb266db86ce&v=4 + url: https://github.com/deight93 +- login: Jkrox + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/83181939?u=d6a922d97129f7f3916d6a1c166bc011b3a72b7f&v=4 + url: https://github.com/Jkrox +- login: mmzeynalli + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/33568903?u=19efd0c0722730b83a70b7c86c36e5b7d83e07d2&v=4 + url: https://github.com/mmzeynalli +- login: ddahan + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1933516?u=1d200a620e8d6841df017e9f2bb7efb58b580f40&v=4 + url: https://github.com/ddahan +- login: jfeaver + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1091338?u=0bcba366447d8fadad63f6705a52d128da4c7ec2&v=4 + url: https://github.com/jfeaver +- login: Wurstnase count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/889657?u=d70187989940b085bcbfa3bedad8dbc5f3ab1fe7&v=4 - url: https://github.com/amacfie -- login: garg10may + avatarUrl: https://avatars.githubusercontent.com/u/8709415?u=f479af475a97aee9a1dab302cfc35d07e9ea245f&v=4 + url: https://github.com/Wurstnase +- login: tristan count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/8787120?u=7028d2b3a2a26534c1806eb76c7425a3fac9732f&v=4 - url: https://github.com/garg10may -- login: methane + avatarUrl: https://avatars.githubusercontent.com/u/1412?u=aab8aaa4cc0f1210ac45fc93873a5909d314c965&v=4 + url: https://github.com/tristan +- login: chandanch count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/199592?v=4 - url: https://github.com/methane -- login: konstantinos1981 + avatarUrl: https://avatars.githubusercontent.com/u/8663552?u=afc484bc0a952c83f1fb6a1583cda443f807cd66&v=4 + url: https://github.com/chandanch +- login: rvishruth count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/39465388?v=4 - url: https://github.com/konstantinos1981 -- login: druidance + avatarUrl: https://avatars.githubusercontent.com/u/79176273?v=4 + url: https://github.com/rvishruth +- login: mattmess1221 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/160344534?v=4 - url: https://github.com/druidance + avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 + url: https://github.com/mattmess1221 +- login: meower1 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/109747197?u=0a5cc2a6ae74e558f0afc2874da85132e5953d8b&v=4 + url: https://github.com/meower1 one_year_experts: -- login: Kludex - count: 207 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: jgould22 - count: 118 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: YuriiMotov - count: 104 + count: 223 avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 url: https://github.com/YuriiMotov +- login: Kludex + count: 83 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex - login: JavierSanchezCastro - count: 59 + count: 47 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro +- login: jgould22 + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: sinisaos + count: 39 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos +- login: luzzodev + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?v=4 + url: https://github.com/luzzodev +- login: tiangolo + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo - login: n8sty - count: 40 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: hasansezertasan - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: chrisK824 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: ahmedabdou14 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=05365b155a1ff911532e8be316acfad2e0736f98&v=4 - url: https://github.com/ahmedabdou14 -- login: arjwilliams - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/22227620?v=4 - url: https://github.com/arjwilliams -- login: killjoy1221 - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 - url: https://github.com/killjoy1221 -- login: WilliamStam - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/182800?v=4 - url: https://github.com/WilliamStam -- login: iudeen - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: nymous - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous -- login: aanchlia - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 - url: https://github.com/aanchlia - login: estebanx64 - count: 7 + count: 19 avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=45f015f95e1c0f06df602be4ab688d4b854cc8a8&v=4 url: https://github.com/estebanx64 -- login: pythonweb2 - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: romabozhanovgithub - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 - url: https://github.com/romabozhanovgithub +- login: sehraramiz + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 + url: https://github.com/sehraramiz - login: PhysicallyActive - count: 6 + count: 14 avatarUrl: https://avatars.githubusercontent.com/u/160476156?u=7a8e44f4a43d3bba636f795bb7d9476c9233b4d8&v=4 url: https://github.com/PhysicallyActive -- login: mikeedjones - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/4087139?u=cc4a242896ac2fcf88a53acfaf190d0fe0a1f0c9&v=4 - url: https://github.com/mikeedjones -- login: dolfinus - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=a51b39001a2e5e7529b45826980becf786de2327&v=4 - url: https://github.com/dolfinus -- login: ebottos94 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 -- login: Ventura94 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/43103937?u=ccb837005aaf212a449c374618c4339089e2f733&v=4 - url: https://github.com/Ventura94 -- login: White-Mask +- login: ceb10n + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: Kfir-G + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=0cd29db046a17f12f382d398141319fca7ff230a&v=4 + url: https://github.com/Kfir-G +- login: mattmess1221 + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=723662989f2027755e67d200137c13c53ae154ac&v=4 + url: https://github.com/mattmess1221 +- login: hasansezertasan + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 + url: https://github.com/hasansezertasan +- login: AIdjis + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/88404339?u=2a80d80b054e9228391e32fb9bb39571509dab6a&v=4 + url: https://github.com/AIdjis +- login: yvallois count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 - url: https://github.com/White-Mask -- login: sehraramiz + avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 + url: https://github.com/yvallois +- login: PREPONDERANCE count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?v=4 - url: https://github.com/sehraramiz + avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 + url: https://github.com/PREPONDERANCE +- login: pythonweb2 + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 + url: https://github.com/pythonweb2 - login: acidjunk count: 5 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk -- login: JoshYuJump +- login: gustavosett count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/5901894?u=cdbca6296ac4cdcdf6945c112a1ce8d5342839ea&v=4 - url: https://github.com/JoshYuJump -- login: alex-pobeditel-2004 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14791483?v=4 - url: https://github.com/alex-pobeditel-2004 -- login: shashstormer - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/90090313?v=4 - url: https://github.com/shashstormer -- login: wu-clan + avatarUrl: https://avatars.githubusercontent.com/u/99373133?u=1739ca547c3d200f1b72450520bce46a97aab184&v=4 + url: https://github.com/gustavosett +- login: binbjz count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/52145145?u=f8c9e5c8c259d248e1683fedf5027b4ee08a0967&v=4 - url: https://github.com/wu-clan -- login: abhint + avatarUrl: https://avatars.githubusercontent.com/u/8213913?u=22b68b7a0d5bf5e09c02084c0f5f53d7503114cd&v=4 + url: https://github.com/binbjz +- login: chyok count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 - url: https://github.com/abhint -- login: anthonycepeda + avatarUrl: https://avatars.githubusercontent.com/u/32629225?u=3b7c30e8a09426a1b9284f6e8a0ae53a525596bf&v=4 + url: https://github.com/chyok +- login: TomFaulkner count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 - url: https://github.com/anthonycepeda -- login: GodMoonGoodman + avatarUrl: https://avatars.githubusercontent.com/u/14956620?v=4 + url: https://github.com/TomFaulkner +- login: yokwejuste count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/29688727?u=7b251da620d999644c37c1feeb292d033eed7ad6&v=4 - url: https://github.com/GodMoonGoodman + avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=592c1e42aa0ee5cb94890e0b863e2acc78cc3bbc&v=4 + url: https://github.com/yokwejuste +- login: DeoLeung + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/3764720?u=4c222ef513814de4c7fb3736d0a7adf11d953d43&v=4 + url: https://github.com/DeoLeung - login: flo-at count: 4 avatarUrl: https://avatars.githubusercontent.com/u/564288?v=4 url: https://github.com/flo-at -- login: yinziyan1206 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: amacfie - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/889657?u=d70187989940b085bcbfa3bedad8dbc5f3ab1fe7&v=4 - url: https://github.com/amacfie -- login: commonism - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/164513?v=4 - url: https://github.com/commonism -- login: dmontagu +- login: GodMoonGoodman count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu -- login: sanzoghenzo + avatarUrl: https://avatars.githubusercontent.com/u/29688727?u=7b251da620d999644c37c1feeb292d033eed7ad6&v=4 + url: https://github.com/GodMoonGoodman +- login: bertomaniac count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/977953?v=4 - url: https://github.com/sanzoghenzo -- login: lucasgadams + avatarUrl: https://avatars.githubusercontent.com/u/10235051?u=14484a96833228a7b29fee4a7916d411c242c4f6&v=4 + url: https://github.com/bertomaniac +- login: alv2017 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/36425095?v=4 - url: https://github.com/lucasgadams -- login: NeilBotelho + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: msehnout count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/39030675?u=16fea2ff90a5c67b974744528a38832a6d1bb4f7&v=4 - url: https://github.com/NeilBotelho -- login: hhartzer + avatarUrl: https://avatars.githubusercontent.com/u/9369632?u=8c988f1b008a3f601385a3616f9327820f66e3a5&v=4 + url: https://github.com/msehnout +- login: viniciusCalcantara count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/100533792?v=4 - url: https://github.com/hhartzer -- login: binbjz + avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=3d7ffe5808843ee4372f9cc5a559ff1674cf1792&v=4 + url: https://github.com/viniciusCalcantara +- login: pawelad count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/8213913?u=22b68b7a0d5bf5e09c02084c0f5f53d7503114cd&v=4 - url: https://github.com/binbjz -- login: PREPONDERANCE + avatarUrl: https://avatars.githubusercontent.com/u/7062874?u=d27dc220545a8401ad21840590a97d474d7101e6&v=4 + url: https://github.com/pawelad +- login: ThirVondukr count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/112809059?u=30ab12dc9ddba2f94ab90e6ad4ad8bc5cfa7fccd&v=4 - url: https://github.com/PREPONDERANCE -- login: nameer + avatarUrl: https://avatars.githubusercontent.com/u/50728601?u=167c0bd655e52817082e50979a86d2f98f95b1a3&v=4 + url: https://github.com/ThirVondukr +- login: dbfreem + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 + url: https://github.com/dbfreem +- login: Isuxiz count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/3931725?u=6199fb065df098fc13ac0a5e649f89672b586732&v=4 - url: https://github.com/nameer + avatarUrl: https://avatars.githubusercontent.com/u/48672727?u=34d7b4ade252687d22a27cf53037b735b244bfc1&v=4 + url: https://github.com/Isuxiz - login: angely-dev count: 3 avatarUrl: https://avatars.githubusercontent.com/u/4362224?v=4 url: https://github.com/angely-dev -- login: fmelihh +- login: deight93 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/37678115?u=a608798b5bd0034183a9c430ebb42fb266db86ce&v=4 + url: https://github.com/deight93 +- login: mmzeynalli count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/99879453?u=671117dba9022db2237e3da7a39cbc2efc838db0&v=4 - url: https://github.com/fmelihh + avatarUrl: https://avatars.githubusercontent.com/u/33568903?u=19efd0c0722730b83a70b7c86c36e5b7d83e07d2&v=4 + url: https://github.com/mmzeynalli +- login: Minibrams + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/8108085?u=b028dbc308fa8485e0e2e9402b3d03d8deb22bf9&v=4 + url: https://github.com/Minibrams - login: ryanisn count: 3 avatarUrl: https://avatars.githubusercontent.com/u/53449841?v=4 url: https://github.com/ryanisn -- login: theobouwman +- login: svlandeg count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/16098190?u=dc70db88a7a99b764c9a89a6e471e0b7ca478a35&v=4 - url: https://github.com/theobouwman -- login: methane + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: alexandercronin count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/199592?v=4 - url: https://github.com/methane -top_contributors: -- login: nilslindemann - count: 130 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann -- login: jaystone776 - count: 49 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 -- login: waynerv - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: tokusumi - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 - url: https://github.com/tokusumi -- login: SwftAlpc - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 - url: https://github.com/SwftAlpc -- login: Kludex - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: hasansezertasan - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: dmontagu - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu -- login: Xewus - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus -- login: euri10 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 - url: https://github.com/euri10 -- login: mariacamilagl - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 - url: https://github.com/mariacamilagl -- login: AlertRED - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 - url: https://github.com/AlertRED -- login: Smlep - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep -- login: alejsdev - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=9ca449ad5161af12766ddd1a22988e9b14315f5c&v=4 - url: https://github.com/alejsdev -- login: hard-coders - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders -- login: KaniKim - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/19832624?u=40f8f7f3f36d5f2365ba2ad0b40693e60958ce70&v=4 - url: https://github.com/KaniKim -- login: xzmeng - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/40202897?v=4 - url: https://github.com/xzmeng -- login: Serrones - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 - url: https://github.com/Serrones -- login: rjNemo - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: pablocm83 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 - url: https://github.com/pablocm83 -- login: RunningIkkyu - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 - url: https://github.com/RunningIkkyu -- login: Alexandrhub - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub -- login: NinaHwang - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 - url: https://github.com/NinaHwang -- login: batlopes - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 - url: https://github.com/batlopes -- login: wshayes - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes -- login: samuelcolvin - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 - url: https://github.com/samuelcolvin -- login: Attsun1031 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 - url: https://github.com/Attsun1031 -- login: ComicShrimp - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=d2fbf412e7730183ce91686ca48d4147e1b7dc74&v=4 - url: https://github.com/ComicShrimp -- login: rostik1410 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/11443899?u=e26a635c2ba220467b308a326a579b8ccf4a8701&v=4 - url: https://github.com/rostik1410 -- login: tamtam-fitness - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 - url: https://github.com/tamtam-fitness -- login: jekirl - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 - url: https://github.com/jekirl -- login: jfunez - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4 - url: https://github.com/jfunez -- login: ycd - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 - url: https://github.com/ycd -- login: komtaki - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 - url: https://github.com/komtaki -- login: hitrust - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 - url: https://github.com/hitrust -- login: JulianMaurin - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/63545168?u=b7d15ac865268cbefc2d739e2f23d9aeeac1a622&v=4 - url: https://github.com/JulianMaurin -- login: lsglucas - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas -- login: BilalAlpaslan - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan -- login: adriangb - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 - url: https://github.com/adriangb -- login: iudeen - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: axel584 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 - url: https://github.com/axel584 -- login: ivan-abc - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 - url: https://github.com/ivan-abc -- login: divums + avatarUrl: https://avatars.githubusercontent.com/u/8014288?u=69580504c51a0cdd756fc47b23bb7f404bd694e7&v=4 + url: https://github.com/alexandercronin +- login: aanchlia count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/1397556?v=4 - url: https://github.com/divums -- login: prostomarkeloff + avatarUrl: https://avatars.githubusercontent.com/u/2835374?u=3c3ed29aa8b09ccaf8d66def0ce82bc2f7e5aab6&v=4 + url: https://github.com/aanchlia +- login: chrisK824 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 - url: https://github.com/prostomarkeloff -- login: nsidnev + avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 + url: https://github.com/chrisK824 +- login: omarcruzpantoja count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 - url: https://github.com/nsidnev -- login: pawamoy + avatarUrl: https://avatars.githubusercontent.com/u/15116058?u=4b64c643fad49225d854e1aaecd1ffc6f9071a1b&v=4 + url: https://github.com/omarcruzpantoja +- login: ahmedabdou14 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 - url: https://github.com/pawamoy -top_reviewers: -- login: Kludex - count: 158 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: BilalAlpaslan - count: 86 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan -- login: yezz123 - count: 85 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 - url: https://github.com/yezz123 -- login: iudeen - count: 55 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: tokusumi - count: 51 - avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 - url: https://github.com/tokusumi -- login: Xewus - count: 50 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus -- login: hasansezertasan - count: 50 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: waynerv - count: 47 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: Laineyzhang55 - count: 47 - avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 - url: https://github.com/Laineyzhang55 -- login: ycd - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 - url: https://github.com/ycd -- login: cikay - count: 41 - avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 - url: https://github.com/cikay -- login: alejsdev - count: 38 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=9ca449ad5161af12766ddd1a22988e9b14315f5c&v=4 - url: https://github.com/alejsdev -- login: JarroVGIT - count: 34 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT -- login: AdrianDeAnda - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 - url: https://github.com/AdrianDeAnda -- login: ArcLightSlavik - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 - url: https://github.com/ArcLightSlavik -- login: cassiobotaro - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 - url: https://github.com/cassiobotaro -- login: lsglucas - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas -- login: komtaki - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 - url: https://github.com/komtaki -- login: YuriiMotov - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=e83a39697a2d33ab2ec9bfbced794ee48bc29cec&v=4 - url: https://github.com/YuriiMotov -- login: Ryandaydev - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=48f68868db8886fce31a1d802c1003914c6cd7c6&v=4 - url: https://github.com/Ryandaydev -- login: LorhanSohaky - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky -- login: dmontagu - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu -- login: nilslindemann - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann -- login: hard-coders - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders -- login: rjNemo - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: odiseo0 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 - url: https://github.com/odiseo0 -- login: 0417taehyun - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 - url: https://github.com/0417taehyun -- login: JavierSanchezCastro - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: Smlep - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep -- login: zy7y - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 - url: https://github.com/zy7y -- login: junah201 - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/75025529?u=2451c256e888fa2a06bcfc0646d09b87ddb6a945&v=4 - url: https://github.com/junah201 -- login: peidrao - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 - url: https://github.com/peidrao -- login: yanever - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 - url: https://github.com/yanever -- login: SwftAlpc - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 - url: https://github.com/SwftAlpc -- login: axel584 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 - url: https://github.com/axel584 -- login: codespearhead - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/72931357?u=0fce6b82219b604d58adb614a761556425579cb5&v=4 - url: https://github.com/codespearhead -- login: Alexandrhub - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub -- login: DevDae - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 - url: https://github.com/DevDae -- login: Aruelius - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/25380989?u=574f8cfcda3ea77a3f81884f6b26a97068e36a9d&v=4 - url: https://github.com/Aruelius -- login: OzgunCaglarArslan - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/86166426?v=4 - url: https://github.com/OzgunCaglarArslan -- login: pedabraham - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 - url: https://github.com/pedabraham -- login: delhi09 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 - url: https://github.com/delhi09 -- login: wdh99 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 - url: https://github.com/wdh99 -- login: sh0nk - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 - url: https://github.com/sh0nk -- login: r0b2g1t - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 - url: https://github.com/r0b2g1t -- login: RunningIkkyu - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 - url: https://github.com/RunningIkkyu -- login: ivan-abc - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 - url: https://github.com/ivan-abc -- login: AlertRED - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 - url: https://github.com/AlertRED -- login: solomein-sv - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 - url: https://github.com/solomein-sv -top_translations_reviewers: -- login: s111d - count: 146 - avatarUrl: https://avatars.githubusercontent.com/u/4954856?v=4 - url: https://github.com/s111d -- login: Xewus - count: 128 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus -- login: tokusumi - count: 104 - avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 - url: https://github.com/tokusumi -- login: hasansezertasan - count: 91 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan -- login: AlertRED - count: 70 - avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 - url: https://github.com/AlertRED -- login: Alexandrhub - count: 68 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub -- login: waynerv - count: 63 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: hard-coders - count: 53 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders -- login: Laineyzhang55 - count: 48 - avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 - url: https://github.com/Laineyzhang55 -- login: Kludex - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: komtaki - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 - url: https://github.com/komtaki -- login: alperiox - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/34214152?u=2c5acad3461d4dbc2d48371ba86cac56ae9b25cc&v=4 - url: https://github.com/alperiox -- login: Winand - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/53390?u=bb0e71a2fc3910a8e0ee66da67c33de40ea695f8&v=4 - url: https://github.com/Winand -- login: solomein-sv - count: 38 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 - url: https://github.com/solomein-sv -- login: lsglucas - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas -- login: SwftAlpc - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 - url: https://github.com/SwftAlpc -- login: nilslindemann - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann -- login: rjNemo - count: 34 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: akarev0 - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/53393089?u=6e528bb4789d56af887ce6fe237bea4010885406&v=4 - url: https://github.com/akarev0 -- login: romashevchenko - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/132477732?v=4 - url: https://github.com/romashevchenko -- login: wdh99 - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 - url: https://github.com/wdh99 -- login: LorhanSohaky - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky -- login: cassiobotaro - count: 29 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=a08022b191ddbd0a6159b2981d9d878b6d5bb71f&v=4 - url: https://github.com/cassiobotaro -- login: pedabraham - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 - url: https://github.com/pedabraham -- login: Smlep - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep -- login: dedkot01 - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/26196675?u=e2966887124e67932853df4f10f86cb526edc7b0&v=4 - url: https://github.com/dedkot01 -- login: hsuanchi - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/24913710?u=0b094ae292292fee093818e37ceb645c114d2bff&v=4 - url: https://github.com/hsuanchi -- login: dpinezich - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/3204540?u=a2e1465e3ee10d537614d513589607eddefde09f&v=4 - url: https://github.com/dpinezich -- login: maoyibo - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 - url: https://github.com/maoyibo -- login: 0417taehyun - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 - url: https://github.com/0417taehyun -- login: BilalAlpaslan - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan -- login: zy7y - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 - url: https://github.com/zy7y -- login: mycaule - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/6161385?u=e3cec75bd6d938a0d73fae0dc5534d1ab2ed1b0e&v=4 - url: https://github.com/mycaule -- login: sh0nk - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 - url: https://github.com/sh0nk -- login: axel584 - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 - url: https://github.com/axel584 -- login: AGolicyn - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/86262613?u=3c21606ab8d210a061a1673decff1e7d5592b380&v=4 - url: https://github.com/AGolicyn -- login: OzgunCaglarArslan - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/86166426?v=4 - url: https://github.com/OzgunCaglarArslan -- login: Attsun1031 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 - url: https://github.com/Attsun1031 -- login: ycd - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=29682e4b6ac7d5293742ccf818188394b9a82972&v=4 - url: https://github.com/ycd -- login: delhi09 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 - url: https://github.com/delhi09 -- login: rogerbrinkmann - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/5690226?v=4 - url: https://github.com/rogerbrinkmann -- login: DevDae - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 - url: https://github.com/DevDae -- login: sattosan - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/20574756?u=b0d8474d2938189c6954423ae8d81d91013f80a8&v=4 - url: https://github.com/sattosan -- login: ComicShrimp - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=d2fbf412e7730183ce91686ca48d4147e1b7dc74&v=4 - url: https://github.com/ComicShrimp -- login: junah201 - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/75025529?u=2451c256e888fa2a06bcfc0646d09b87ddb6a945&v=4 - url: https://github.com/junah201 -- login: simatheone - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/78508673?u=1b9658d9ee0bde33f56130dd52275493ddd38690&v=4 - url: https://github.com/simatheone -- login: ivan-abc - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 - url: https://github.com/ivan-abc -- login: JavierSanchezCastro - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: bezaca - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 - url: https://github.com/bezaca + avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=d87b866e7c1db970d6f8e8031643818349b046d5&v=4 + url: https://github.com/ahmedabdou14 +- login: nbx3 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/34649527?u=943812f69e0d40adbd3fa1c9b8ef50dd971a2a45&v=4 + url: https://github.com/nbx3 +- login: yanggeorge + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/2434407?v=4 + url: https://github.com/yanggeorge +- login: XiaoXinYo + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=b3b7cb758997f283c271a581833e407229dab82c&v=4 + url: https://github.com/XiaoXinYo +- login: anantgupta129 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/66518357?u=6e25dcd84638f17d2c6df5dc26f07fd7c6dc118e&v=4 + url: https://github.com/anantgupta129 +- login: slafs + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 + url: https://github.com/slafs +- login: CarlosOliveira-23 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/102637302?u=cf350a4db956f30cbb2c27d3be0d15c282e32b14&v=4 + url: https://github.com/CarlosOliveira-23 +- login: monchin + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/18521800?v=4 + url: https://github.com/monchin +- login: AmirHmZz + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/38752106?u=07f80e451bda00a9492bbc764e49d24ad3ada8cc&v=4 + url: https://github.com/AmirHmZz +- login: Leon0824 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4 + url: https://github.com/Leon0824 +- login: iloveitaly + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 + url: https://github.com/iloveitaly +- login: msukmanowsky + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/362755?u=782e6bf5b9f0356c3f74b4d894fda9f179252086&v=4 + url: https://github.com/msukmanowsky +- login: shurshilov + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/11828278?u=6bcadc5ce4f2f56a514331c9f68eb987d4afe29a&v=4 + url: https://github.com/shurshilov +- login: LincolnPuzey + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/18750802?v=4 + url: https://github.com/LincolnPuzey From 8525b879edfd85c1739e513139aab7120b780044 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 20:41:36 +0000 Subject: [PATCH 098/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3db7042fb..20f800d85 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Add Japanese translation for `docs/ja/docs/environment-variables.md`. PR [#13226](https://github.com/fastapi/fastapi/pull/13226) by [@k94-ishi](https://github.com/k94-ishi). From 326fec16b9e3d61c584182b212436f88d81d0acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 21:47:33 +0000 Subject: [PATCH 099/123] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20and=20m?= =?UTF-8?q?ove=20`scripts/notify=5Ftranslations.py`,=20no=20need=20for=20a?= =?UTF-8?q?=20custom=20GitHub=20Action=20(#13270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../actions/notify-translations/Dockerfile | 7 --- .../actions/notify-translations/action.yml | 10 --- .github/workflows/notify-translations.yml | 20 +++--- .../main.py => scripts/notify_translations.py | 63 +++++++++++-------- 4 files changed, 51 insertions(+), 49 deletions(-) delete mode 100644 .github/actions/notify-translations/Dockerfile delete mode 100644 .github/actions/notify-translations/action.yml rename .github/actions/notify-translations/app/main.py => scripts/notify_translations.py (89%) diff --git a/.github/actions/notify-translations/Dockerfile b/.github/actions/notify-translations/Dockerfile deleted file mode 100644 index b68b4bb1a..000000000 --- a/.github/actions/notify-translations/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.9 - -RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/notify-translations/action.yml b/.github/actions/notify-translations/action.yml deleted file mode 100644 index c3579977c..000000000 --- a/.github/actions/notify-translations/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Notify Translations" -description: "Notify in the issue for a translation when there's a new PR available" -author: "Sebastián Ramírez " -inputs: - token: - description: 'Token, to read the GitHub API. Can be passed in using {{ secrets.GITHUB_TOKEN }}' - required: true -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 187322bca..c96992689 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -15,15 +15,14 @@ on: required: false default: 'false' -permissions: - discussions: write - env: UV_SYSTEM_PYTHON: 1 jobs: - notify-translations: + job: runs-on: ubuntu-latest + permissions: + discussions: write steps: - name: Dump GitHub context env: @@ -42,12 +41,19 @@ jobs: cache-dependency-glob: | requirements**.txt pyproject.toml + - name: Install Dependencies + run: uv pip install -r requirements-github-actions.txt # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: ./.github/actions/notify-translations - with: - token: ${{ secrets.GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Notify Translations + run: python ./scripts/notify_translations.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUMBER: ${{ github.event.inputs.number || null }} + DEBUG: ${{ github.event.inputs.debug_enabled || 'false' }} diff --git a/.github/actions/notify-translations/app/main.py b/scripts/notify_translations.py similarity index 89% rename from .github/actions/notify-translations/app/main.py rename to scripts/notify_translations.py index 716232d49..7a43019a6 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/scripts/notify_translations.py @@ -7,12 +7,13 @@ from typing import Any, Dict, List, Union, cast import httpx from github import Github -from pydantic import BaseModel, BaseSettings, SecretStr +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings awaiting_label = "awaiting-review" lang_all_label = "lang-all" approved_label = "approved-1" -translations_path = Path(__file__).parent / "translations.yml" + github_graphql_url = "https://api.github.com/graphql" questions_translations_category_id = "DIC_kwDOCZduT84CT5P9" @@ -176,19 +177,20 @@ class AllDiscussionsResponse(BaseModel): class Settings(BaseSettings): github_repository: str - input_token: SecretStr + github_token: SecretStr github_event_path: Path github_event_name: Union[str, None] = None httpx_timeout: int = 30 - input_debug: Union[bool, None] = False + debug: Union[bool, None] = False + number: int | None = None class PartialGitHubEventIssue(BaseModel): - number: int + number: int | None = None class PartialGitHubEvent(BaseModel): - pull_request: PartialGitHubEventIssue + pull_request: PartialGitHubEventIssue | None = None def get_graphql_response( @@ -202,9 +204,7 @@ def get_graphql_response( comment_id: Union[str, None] = None, body: Union[str, None] = None, ) -> Dict[str, Any]: - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - # some fields are only used by one query, but GraphQL allows unused variables, so - # keep them here for simplicity + headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} variables = { "after": after, "category_id": category_id, @@ -228,37 +228,40 @@ def get_graphql_response( data = response.json() if "errors" in data: logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(data["errors"]) logging.error(response.text) raise RuntimeError(response.text) return cast(Dict[str, Any], data) -def get_graphql_translation_discussions(*, settings: Settings): +def get_graphql_translation_discussions( + *, settings: Settings +) -> List[AllDiscussionsDiscussionNode]: data = get_graphql_response( settings=settings, query=all_discussions_query, category_id=questions_translations_category_id, ) - graphql_response = AllDiscussionsResponse.parse_obj(data) + graphql_response = AllDiscussionsResponse.model_validate(data) return graphql_response.data.repository.discussions.nodes def get_graphql_translation_discussion_comments_edges( *, settings: Settings, discussion_number: int, after: Union[str, None] = None -): +) -> List[CommentsEdge]: data = get_graphql_response( settings=settings, query=translation_discussion_query, discussion_number=discussion_number, after=after, ) - graphql_response = CommentsResponse.parse_obj(data) + graphql_response = CommentsResponse.model_validate(data) return graphql_response.data.repository.discussion.comments.edges def get_graphql_translation_discussion_comments( *, settings: Settings, discussion_number: int -): +) -> list[Comment]: comment_nodes: List[Comment] = [] discussion_edges = get_graphql_translation_discussion_comments_edges( settings=settings, discussion_number=discussion_number @@ -276,43 +279,49 @@ def get_graphql_translation_discussion_comments( return comment_nodes -def create_comment(*, settings: Settings, discussion_id: str, body: str): +def create_comment(*, settings: Settings, discussion_id: str, body: str) -> Comment: data = get_graphql_response( settings=settings, query=add_comment_mutation, discussion_id=discussion_id, body=body, ) - response = AddCommentResponse.parse_obj(data) + response = AddCommentResponse.model_validate(data) return response.data.addDiscussionComment.comment -def update_comment(*, settings: Settings, comment_id: str, body: str): +def update_comment(*, settings: Settings, comment_id: str, body: str) -> Comment: data = get_graphql_response( settings=settings, query=update_comment_mutation, comment_id=comment_id, body=body, ) - response = UpdateCommentResponse.parse_obj(data) + response = UpdateCommentResponse.model_validate(data) return response.data.updateDiscussionComment.comment -if __name__ == "__main__": +def main() -> None: settings = Settings() - if settings.input_debug: + if settings.debug: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) - logging.debug(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) + logging.debug(f"Using config: {settings.model_dump_json()}") + g = Github(settings.github_token.get_secret_value()) repo = g.get_repo(settings.github_repository) if not settings.github_event_path.is_file(): raise RuntimeError( f"No github event file available at: {settings.github_event_path}" ) contents = settings.github_event_path.read_text() - github_event = PartialGitHubEvent.parse_raw(contents) + github_event = PartialGitHubEvent.model_validate_json(contents) + logging.info(f"Using GitHub event: {github_event}") + number = ( + github_event.pull_request and github_event.pull_request.number + ) or settings.number + if number is None: + raise RuntimeError("No PR number available") # Avoid race conditions with multiple labels sleep_time = random.random() * 10 # random number between 0 and 10 seconds @@ -323,8 +332,8 @@ if __name__ == "__main__": time.sleep(sleep_time) # Get PR - logging.debug(f"Processing PR: #{github_event.pull_request.number}") - pr = repo.get_pull(github_event.pull_request.number) + logging.debug(f"Processing PR: #{number}") + pr = repo.get_pull(number) label_strs = {label.name for label in pr.get_labels()} langs = [] for label in label_strs: @@ -415,3 +424,7 @@ if __name__ == "__main__": f"There doesn't seem to be anything to be done about PR #{pr.number}" ) logging.info("Finished") + + +if __name__ == "__main__": + main() From a058d8ecbc41b329bad2f5ca0575c50b7bc171b2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 21:47:55 +0000 Subject: [PATCH 100/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 20f800d85..2abee78dc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). From 0a2b24653b2b0e2a4753a1e239013b26fa1516b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 22:30:15 +0000 Subject: [PATCH 101/123] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix=20`notify=5Ftr?= =?UTF-8?q?anslations.py`=20empty=20env=20var=20handling=20for=20PR=20labe?= =?UTF-8?q?l=20events=20vs=20workflow=5Fdispatch=20(#13272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/notify_translations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/notify_translations.py b/scripts/notify_translations.py index 7a43019a6..c300624db 100644 --- a/scripts/notify_translations.py +++ b/scripts/notify_translations.py @@ -176,6 +176,8 @@ class AllDiscussionsResponse(BaseModel): class Settings(BaseSettings): + model_config = {"env_ignore_empty": True} + github_repository: str github_token: SecretStr github_event_path: Path From 92b745461cb280aea3c5905c5e241502207064fa Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 22:30:38 +0000 Subject: [PATCH 102/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2abee78dc..e65fe9848 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#13251](https://github.com/fastapi/fastapi/pull/13251) by [@dependabot[bot]](https://github.com/apps/dependabot). From e747f1938a3853c6af695f6447fd8f8749e1ba9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 22:36:15 +0000 Subject: [PATCH 103/123] =?UTF-8?q?=F0=9F=94=A7=20Update=20Sponsors=20badg?= =?UTF-8?q?es=20(#13271)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors_badge.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 3e885a2f7..d507a500f 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -29,7 +29,12 @@ logins: - andrew-propelauth - svix - zuplo-oss + - zuplo - Kong - speakeasy-api - jess-render - blockbee-io + - liblaber + - render-sponsorships + - renderinc + - stainless-api From 93e9fed2e84554197a0b480104a9e3c5b8897545 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 22:36:38 +0000 Subject: [PATCH 104/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e65fe9848..c0cb88e21 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* 🔧 Update Sponsors badges. PR [#13271](https://github.com/fastapi/fastapi/pull/13271) by [@tiangolo](https://github.com/tiangolo). * ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People Experts script, refactor and optimize data fetching to handle rate limits. PR [#13267](https://github.com/fastapi/fastapi/pull/13267) by [@tiangolo](https://github.com/tiangolo). From 8c6f10b64a333cb1395da5cb714fd1aa28e7b5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Jan 2025 23:35:19 +0000 Subject: [PATCH 105/123] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20AnyIO=20?= =?UTF-8?q?max=20version=20for=20tests,=20new=20range:=20`>=3D3.2.1,<5.0.0?= =?UTF-8?q?`=20(#13273)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++++ requirements-tests.txt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0daf7472..793e789e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,6 +81,10 @@ jobs: - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" + # TODO: Remove this once Python 3.8 is no longer supported + - name: Install older AnyIO in Python 3.8 + if: matrix.python-version == '3.8' + run: uv pip install "anyio[trio]<4.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh diff --git a/requirements-tests.txt b/requirements-tests.txt index 5be052307..91e7fb7aa 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -6,7 +6,7 @@ mypy ==1.8.0 dirty-equals ==0.8.0 sqlmodel==0.0.22 flask >=1.1.2,<4.0.0 -anyio[trio] >=3.2.1,<4.0.0 +anyio[trio] >=3.2.1,<5.0.0 PyJWT==2.8.0 pyyaml >=5.3.1,<7.0.0 passlib[bcrypt] >=1.7.2,<2.0.0 From eab0653a346196bff6928710410890a300aee4ae Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Jan 2025 23:35:44 +0000 Subject: [PATCH 106/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c0cb88e21..ad8b85d0e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ⬆️ Upgrade AnyIO max version for tests, new range: `>=3.2.1,<5.0.0`. PR [#13273](https://github.com/fastapi/fastapi/pull/13273) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Sponsors badges. PR [#13271](https://github.com/fastapi/fastapi/pull/13271) by [@tiangolo](https://github.com/tiangolo). * ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action. PR [#13270](https://github.com/fastapi/fastapi/pull/13270) by [@tiangolo](https://github.com/tiangolo). From bd106fc750fb0d4b114b69e1f1f9ae5dabaada44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 29 Jan 2025 18:02:27 +0000 Subject: [PATCH 107/123] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Add=20support=20fo?= =?UTF-8?q?r=20Python=203.13=20(#13274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 1 + pyproject.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 793e789e2..5e8092641 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,7 @@ jobs: strategy: matrix: python-version: + - "3.13" - "3.12" - "3.11" - "3.10" diff --git a/pyproject.toml b/pyproject.toml index 381eb50bf..51d63fd44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] @@ -162,6 +163,8 @@ filterwarnings = [ # Ref: https://github.com/python-trio/trio/pull/3054 # Remove once there's a new version of Trio 'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio', + # Ignore flaky coverage / pytest warning about SQLite connection, only applies to Python 3.13 and Pydantic v1 + 'ignore:Exception ignored in. Date: Wed, 29 Jan 2025 18:02:50 +0000 Subject: [PATCH 108/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ad8b85d0e..e21d2521f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Internal +* ⬆️ Add support for Python 3.13. PR [#13274](https://github.com/fastapi/fastapi/pull/13274) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade AnyIO max version for tests, new range: `>=3.2.1,<5.0.0`. PR [#13273](https://github.com/fastapi/fastapi/pull/13273) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Sponsors badges. PR [#13271](https://github.com/fastapi/fastapi/pull/13271) by [@tiangolo](https://github.com/tiangolo). * ♻️ Fix `notify_translations.py` empty env var handling for PR label events vs workflow_dispatch. PR [#13272](https://github.com/fastapi/fastapi/pull/13272) by [@tiangolo](https://github.com/tiangolo). From c5b5af7c532e66d3ac25bd36630e2cd89f24d049 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:04:34 +0000 Subject: [PATCH 109/123] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20re?= =?UTF-8?q?quest=5Ffiles=20(#13182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_tutorial003.py | 21 +- .../test_tutorial003_an.py | 273 ----------------- .../test_tutorial003_an_py310.py | 279 ------------------ .../test_tutorial003_an_py39.py | 279 ------------------ .../test_tutorial003_py310.py | 279 ------------------ .../test_request_files/test_tutorial001.py | 42 +-- .../test_request_files/test_tutorial001_02.py | 35 ++- .../test_tutorial001_02_an.py | 208 ------------- .../test_tutorial001_02_an_py310.py | 220 -------------- .../test_tutorial001_02_an_py39.py | 220 -------------- .../test_tutorial001_02_py310.py | 220 -------------- .../test_request_files/test_tutorial001_03.py | 28 +- .../test_tutorial001_03_an.py | 159 ---------- .../test_tutorial001_03_an_py39.py | 167 ----------- .../test_request_files/test_tutorial001_an.py | 218 -------------- .../test_tutorial001_an_py39.py | 228 -------------- .../test_request_files/test_tutorial002.py | 39 ++- .../test_request_files/test_tutorial002_an.py | 249 ---------------- .../test_tutorial002_an_py39.py | 268 ----------------- .../test_tutorial002_py39.py | 279 ------------------ .../test_request_files/test_tutorial003.py | 35 ++- .../test_request_files/test_tutorial003_an.py | 194 ------------ .../test_tutorial003_an_py39.py | 222 -------------- .../test_tutorial003_py39.py | 222 -------------- 24 files changed, 146 insertions(+), 4238 deletions(-) delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py delete mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_an.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py delete mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_py39.py diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index c26f8b89b..d18ceae48 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,13 +1,26 @@ +import importlib + import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_py39, needs_py310 + -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003 import app +@pytest.fixture( + name="client", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + "tutorial003_an", + pytest.param("tutorial003_an_py39", marks=needs_py39), + pytest.param("tutorial003_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}") - client = TestClient(app) + client = TestClient(mod.app) return client diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py deleted file mode 100644 index 62c7e2fad..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py +++ /dev/null @@ -1,273 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_an import app - - client = TestClient(app) - return client - - -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py deleted file mode 100644 index f46430fb5..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py +++ /dev/null @@ -1,279 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -@needs_py310 -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py deleted file mode 100644 index 29071cddc..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py +++ /dev/null @@ -1,279 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -@needs_py39 -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py deleted file mode 100644 index 133afe9b5..000000000 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ /dev/null @@ -1,279 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.body_multiple_params.tutorial003_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_body_valid(client: TestClient): - response = client.put( - "/items/5", - json={ - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - ) - assert response.status_code == 200 - assert response.json() == { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - } - - -@needs_py310 -def test_post_body_no_data(client: TestClient): - response = client.put("/items/5", json=None) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_post_body_empty_list(client: TestClient): - response = client.put("/items/5", json=[]) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "item"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "user"], - "msg": "Field required", - "input": None, - }, - { - "type": "missing", - "loc": ["body", "importance"], - "msg": "Field required", - "input": None, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": IsDict( - { - "title": "Full Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Full Name", "type": "string"} - ), - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index f5817593b..b06919961 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,23 +1,28 @@ +import importlib + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.request_files.tutorial001 import app +from ...utils import needs_py39 -client = TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial001", + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} + client = TestClient(mod.app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -45,7 +50,7 @@ def test_post_form_no_body(): ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -73,41 +78,38 @@ def test_post_body_json(): ) -def test_post_file(tmp_path): +def test_post_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": 14} -def test_post_large_file(tmp_path): +def test_post_large_file(tmp_path, client: TestClient): default_pydantic_max_size = 2**16 path = tmp_path / "test.txt" path.write_bytes(b"x" * (default_pydantic_max_size + 1)) - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": default_pydantic_max_size + 1} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 42f75442a..9075a1756 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -1,46 +1,63 @@ +import importlib +from pathlib import Path + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.request_files.tutorial001_02 import app +from ...utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_02", + pytest.param("tutorial001_02_py310", marks=needs_py310), + "tutorial001_02_an", + pytest.param("tutorial001_02_an_py39", marks=needs_py39), + pytest.param("tutorial001_02_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 200, response.text assert response.json() == {"message": "No file sent"} -def test_post_uploadfile_no_body(): +def test_post_uploadfile_no_body(client: TestClient): response = client.post("/uploadfile/") assert response.status_code == 200, response.text assert response.json() == {"message": "No upload file sent"} -def test_post_file(tmp_path): +def test_post_file(tmp_path: Path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": 14} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path: Path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py deleted file mode 100644 index f63eb339c..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py +++ /dev/null @@ -1,208 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.request_files.tutorial001_02_an import app - -client = TestClient(app) - - -def test_post_form_no_body(): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -def test_post_uploadfile_no_body(): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -def test_post_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py deleted file mode 100644 index 94b6ac67e..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py +++ /dev/null @@ -1,220 +0,0 @@ -from pathlib import Path - -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_02_an_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -@needs_py310 -def test_post_uploadfile_no_body(client: TestClient): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -@needs_py310 -def test_post_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py310 -def test_post_upload_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py deleted file mode 100644 index fcb39f8f1..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py +++ /dev/null @@ -1,220 +0,0 @@ -from pathlib import Path - -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_02_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -@needs_py39 -def test_post_uploadfile_no_body(client: TestClient): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -@needs_py39 -def test_post_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py39 -def test_post_upload_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py deleted file mode 100644 index a700752a3..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ /dev/null @@ -1,220 +0,0 @@ -from pathlib import Path - -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py310 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_02_py310 import app - - client = TestClient(app) - return client - - -@needs_py310 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No file sent"} - - -@needs_py310 -def test_post_uploadfile_no_body(client: TestClient): - response = client.post("/uploadfile/") - assert response.status_code == 200, response.text - assert response.json() == {"message": "No upload file sent"} - - -@needs_py310 -def test_post_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py310 -def test_post_upload_file(tmp_path: Path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": IsDict( - { - "allOf": [ - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ], - "title": "Body", - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - ) - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": IsDict( - { - "title": "File", - "anyOf": [ - {"type": "string", "format": "binary"}, - {"type": "null"}, - ], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "File", "type": "string", "format": "binary"} - ) - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index f02170814..9fbe2166c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -1,33 +1,47 @@ +import importlib + +import pytest from fastapi.testclient import TestClient -from docs_src.request_files.tutorial001_03 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_03", + "tutorial001_03_an", + pytest.param("tutorial001_03_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -client = TestClient(app) + client = TestClient(mod.app) + return client -def test_post_file(tmp_path): +def test_post_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"file_size": 14} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" path.write_bytes(b"") - client = TestClient(app) with path.open("rb") as file: response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py deleted file mode 100644 index acfb749ce..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py +++ /dev/null @@ -1,159 +0,0 @@ -from fastapi.testclient import TestClient - -from docs_src.request_files.tutorial001_03_an import app - -client = TestClient(app) - - -def test_post_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py deleted file mode 100644 index 36e5faac1..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py +++ /dev/null @@ -1,167 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_03_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py39 -def test_post_upload_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py deleted file mode 100644 index 1c78e3679..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an.py +++ /dev/null @@ -1,218 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from docs_src.request_files.tutorial001_an import app - -client = TestClient(app) - - -def test_post_form_no_body(): - response = client.post("/files/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_json(): - response = client.post("/files/", json={"file": "Foo"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -def test_post_large_file(tmp_path): - default_pydantic_max_size = 2**16 - path = tmp_path / "test.txt" - path.write_bytes(b"x" * (default_pydantic_max_size + 1)) - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": default_pydantic_max_size + 1} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py deleted file mode 100644 index 843fcec28..000000000 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py +++ /dev/null @@ -1,228 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from ...utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_files.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_form_no_body(client: TestClient): - response = client.post("/files/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/files/", json={"file": "Foo"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "file"], - "msg": "Field required", - "input": None, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": 14} - - -@needs_py39 -def test_post_large_file(tmp_path, client: TestClient): - default_pydantic_max_size = 2**16 - path = tmp_path / "test.txt" - path.write_bytes(b"x" * (default_pydantic_max_size + 1)) - - with path.open("rb") as file: - response = client.post("/files/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"file_size": default_pydantic_max_size + 1} - - -@needs_py39 -def test_post_upload_file(tmp_path, client: TestClient): - path = tmp_path / "test.txt" - path.write_bytes(b"") - - with path.open("rb") as file: - response = client.post("/uploadfile/", files={"file": file}) - assert response.status_code == 200, response.text - assert response.json() == {"filename": "test.txt"} - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index db1552e5c..446a87657 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,12 +1,35 @@ +import importlib + +import pytest from dirty_equals import IsDict +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.request_files.tutorial002 import app +from ...utils import needs_py39 + + +@pytest.fixture( + name="app", + params=[ + "tutorial002", + "tutorial002_an", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_an_py39", marks=needs_py39), + ], +) +def get_app(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.request_files.{request.param}") -client = TestClient(app) + return mod.app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -34,7 +57,7 @@ def test_post_form_no_body(): ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text assert response.json() == IsDict( @@ -62,7 +85,7 @@ def test_post_body_json(): ) -def test_post_files(tmp_path): +def test_post_files(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") path2 = tmp_path / "test2.txt" @@ -81,7 +104,7 @@ def test_post_files(tmp_path): assert response.json() == {"file_sizes": [14, 15]} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") path2 = tmp_path / "test2.txt" @@ -100,14 +123,14 @@ def test_post_upload_file(tmp_path): assert response.json() == {"filenames": ["test.txt", "test2.txt"]} -def test_get_root(): +def test_get_root(app: FastAPI): client = TestClient(app) response = client.get("/") assert response.status_code == 200, response.text assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -def test_get_root(): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") path2 = tmp_path / "test2.txt" @@ -24,7 +47,7 @@ def test_post_files(tmp_path): assert response.json() == {"file_sizes": [14, 15]} -def test_post_upload_file(tmp_path): +def test_post_upload_file(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") path2 = tmp_path / "test2.txt" @@ -43,14 +66,14 @@ def test_post_upload_file(tmp_path): assert response.json() == {"filenames": ["test.txt", "test2.txt"]} -def test_get_root(): +def test_get_root(app: FastAPI): client = TestClient(app) response = client.get("/") assert response.status_code == 200, response.text assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -def test_post_upload_file(tmp_path): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -def test_get_root(): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/files/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"file_sizes": [14, 15]} - - -@needs_py39 -def test_post_upload_file(tmp_path, app: FastAPI): - path = tmp_path / "test.txt" - path.write_bytes(b"") - path2 = tmp_path / "test2.txt" - path2.write_bytes(b"") - - client = TestClient(app) - with path.open("rb") as file, path2.open("rb") as file2: - response = client.post( - "/uploadfiles/", - files=( - ("files", ("test.txt", file)), - ("files", ("test2.txt", file2)), - ), - ) - assert response.status_code == 200, response.text - assert response.json() == {"filenames": ["test.txt", "test2.txt"]} - - -@needs_py39 -def test_get_root(app: FastAPI): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200, response.text - assert b" Date: Thu, 30 Jan 2025 12:04:59 +0000 Subject: [PATCH 110/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e21d2521f..2f9da1465 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). + ### Docs * 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). From d5ecbaceae3a4e19191bf3771a1dbfd33aa1e8f5 Mon Sep 17 00:00:00 2001 From: Rahul Pai <50425728+skarfie123@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:17:09 +0000 Subject: [PATCH 111/123] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`OAuth2PasswordReq?= =?UTF-8?q?uestForm`=20and=20`OAuth2PasswordRequestFormStrict`=20fixed=20`?= =?UTF-8?q?grant=5Ftype`=20"password"=20RegEx=20(#9783)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg Co-authored-by: Sebastián Ramírez --- fastapi/security/oauth2.py | 4 +-- tests/test_security_oauth2.py | 25 +++++++++++++------ tests/test_security_oauth2_optional.py | 25 +++++++++++++------ ...st_security_oauth2_optional_description.py | 25 +++++++++++++------ .../test_security/test_tutorial003.py | 4 +-- .../test_security/test_tutorial005.py | 4 +-- 6 files changed, 57 insertions(+), 30 deletions(-) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 6adc55bfe..5ffad5986 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -63,7 +63,7 @@ class OAuth2PasswordRequestForm: *, grant_type: Annotated[ Union[str, None], - Form(pattern="password"), + Form(pattern="^password$"), Doc( """ The OAuth2 spec says it is required and MUST be the fixed string @@ -217,7 +217,7 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): self, grant_type: Annotated[ str, - Form(pattern="password"), + Form(pattern="^password$"), Doc( """ The OAuth2 spec says it is required and MUST be the fixed string diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 7d914d034..2b7e3457a 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,3 +1,4 @@ +import pytest from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -137,10 +138,18 @@ def test_strict_login_no_grant_type(): ) -def test_strict_login_incorrect_grant_type(): +@pytest.mark.parametrize( + argnames=["grant_type"], + argvalues=[ + pytest.param("incorrect", id="incorrect value"), + pytest.param("passwordblah", id="password with suffix"), + pytest.param("blahpassword", id="password with prefix"), + ], +) +def test_strict_login_incorrect_grant_type(grant_type: str): response = client.post( "/login", - data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 assert response.json() == IsDict( @@ -149,9 +158,9 @@ def test_strict_login_incorrect_grant_type(): { "type": "string_pattern_mismatch", "loc": ["body", "grant_type"], - "msg": "String should match pattern 'password'", - "input": "incorrect", - "ctx": {"pattern": "password"}, + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, } ] } @@ -161,9 +170,9 @@ def test_strict_login_incorrect_grant_type(): "detail": [ { "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', + "msg": 'string does not match regex "^password$"', "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "ctx": {"pattern": "^password$"}, } ] } @@ -248,7 +257,7 @@ def test_openapi_schema(): "properties": { "grant_type": { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", }, "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 0da3b911e..046ac5763 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,5 +1,6 @@ from typing import Optional +import pytest from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -141,10 +142,18 @@ def test_strict_login_no_grant_type(): ) -def test_strict_login_incorrect_grant_type(): +@pytest.mark.parametrize( + argnames=["grant_type"], + argvalues=[ + pytest.param("incorrect", id="incorrect value"), + pytest.param("passwordblah", id="password with suffix"), + pytest.param("blahpassword", id="password with prefix"), + ], +) +def test_strict_login_incorrect_grant_type(grant_type: str): response = client.post( "/login", - data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 assert response.json() == IsDict( @@ -153,9 +162,9 @@ def test_strict_login_incorrect_grant_type(): { "type": "string_pattern_mismatch", "loc": ["body", "grant_type"], - "msg": "String should match pattern 'password'", - "input": "incorrect", - "ctx": {"pattern": "password"}, + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, } ] } @@ -165,9 +174,9 @@ def test_strict_login_incorrect_grant_type(): "detail": [ { "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', + "msg": 'string does not match regex "^password$"', "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "ctx": {"pattern": "^password$"}, } ] } @@ -252,7 +261,7 @@ def test_openapi_schema(): "properties": { "grant_type": { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", }, "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 85a9f9b39..629cddca2 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,5 +1,6 @@ from typing import Optional +import pytest from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -142,10 +143,18 @@ def test_strict_login_no_grant_type(): ) -def test_strict_login_incorrect_grant_type(): +@pytest.mark.parametrize( + argnames=["grant_type"], + argvalues=[ + pytest.param("incorrect", id="incorrect value"), + pytest.param("passwordblah", id="password with suffix"), + pytest.param("blahpassword", id="password with prefix"), + ], +) +def test_strict_login_incorrect_grant_type(grant_type: str): response = client.post( "/login", - data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, ) assert response.status_code == 422 assert response.json() == IsDict( @@ -154,9 +163,9 @@ def test_strict_login_incorrect_grant_type(): { "type": "string_pattern_mismatch", "loc": ["body", "grant_type"], - "msg": "String should match pattern 'password'", - "input": "incorrect", - "ctx": {"pattern": "password"}, + "msg": "String should match pattern '^password$'", + "input": grant_type, + "ctx": {"pattern": "^password$"}, } ] } @@ -166,9 +175,9 @@ def test_strict_login_incorrect_grant_type(): "detail": [ { "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', + "msg": 'string does not match regex "^password$"', "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "ctx": {"pattern": "^password$"}, } ] } @@ -253,7 +262,7 @@ def test_openapi_schema(): "properties": { "grant_type": { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", }, "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 7a4c99401..37fc2618f 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -149,7 +149,7 @@ def test_openapi_schema(client: TestClient): { "title": "Grant Type", "anyOf": [ - {"pattern": "password", "type": "string"}, + {"pattern": "^password$", "type": "string"}, {"type": "null"}, ], } @@ -158,7 +158,7 @@ def test_openapi_schema(client: TestClient): # TODO: remove when deprecating Pydantic v1 { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", } ), diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index c7f791b03..88c3d7815 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -363,7 +363,7 @@ def test_openapi_schema(mod: ModuleType): { "title": "Grant Type", "anyOf": [ - {"pattern": "password", "type": "string"}, + {"pattern": "^password$", "type": "string"}, {"type": "null"}, ], } @@ -372,7 +372,7 @@ def test_openapi_schema(mod: ModuleType): # TODO: remove when deprecating Pydantic v1 { "title": "Grant Type", - "pattern": "password", + "pattern": "^password$", "type": "string", } ), From 30b270be9ac9cf931b0efaac549ba0ad8112f547 Mon Sep 17 00:00:00 2001 From: Shahriyar Rzayev Date: Thu, 30 Jan 2025 13:17:20 +0100 Subject: [PATCH 112/123] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20duplicated?= =?UTF-8?q?=20code=20portion=20to=20a=20static=20method=20in=20the=20`APIK?= =?UTF-8?q?eyBase`=20super=20class=20(#3142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg --- fastapi/security/api_key.py | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index d68bdb037..70c2dca8a 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -9,7 +9,15 @@ from typing_extensions import Annotated, Doc class APIKeyBase(SecurityBase): - pass + @staticmethod + def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: + if not api_key: + if auto_error: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + return None + return api_key class APIKeyQuery(APIKeyBase): @@ -101,14 +109,7 @@ class APIKeyQuery(APIKeyBase): async def __call__(self, request: Request) -> Optional[str]: api_key = request.query_params.get(self.model.name) - if not api_key: - if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - else: - return None - return api_key + return self.check_api_key(api_key, self.auto_error) class APIKeyHeader(APIKeyBase): @@ -196,14 +197,7 @@ class APIKeyHeader(APIKeyBase): async def __call__(self, request: Request) -> Optional[str]: api_key = request.headers.get(self.model.name) - if not api_key: - if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - else: - return None - return api_key + return self.check_api_key(api_key, self.auto_error) class APIKeyCookie(APIKeyBase): @@ -291,11 +285,4 @@ class APIKeyCookie(APIKeyBase): async def __call__(self, request: Request) -> Optional[str]: api_key = request.cookies.get(self.model.name) - if not api_key: - if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - else: - return None - return api_key + return self.check_api_key(api_key, self.auto_error) From 041b2e1c4643c9837d2e7f8589351492cf76497a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:17:34 +0000 Subject: [PATCH 113/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2f9da1465..a07b2023c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix `OAuth2PasswordRequestForm` and `OAuth2PasswordRequestFormStrict` fixed `grant_type` "password" RegEx. PR [#9783](https://github.com/fastapi/fastapi/pull/9783) by [@skarfie123](https://github.com/skarfie123). + ### Refactors * ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). From 0541693bc7611da858f71d896a3b9780751c04f8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:17:52 +0000 Subject: [PATCH 114/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a07b2023c..b1c34800c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Refactors +* ♻️ Move duplicated code portion to a static method in the `APIKeyBase` super class. PR [#3142](https://github.com/fastapi/fastapi/pull/3142) by [@ShahriyarR](https://github.com/ShahriyarR). * ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). ### Docs From 9667ce87a908eecc2be2a215adcb55c7e1b38040 Mon Sep 17 00:00:00 2001 From: Ysabel <5388340+togogh@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:19:10 +0800 Subject: [PATCH 115/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20Request=20Body'?= =?UTF-8?q?s=20`tutorial002`=20to=20deal=20with=20`tax=3D0`=20case=20(#132?= =?UTF-8?q?30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: svlandeg --- docs_src/body/tutorial002.py | 2 +- docs_src/body/tutorial002_py310.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs_src/body/tutorial002.py b/docs_src/body/tutorial002.py index 7f5183908..5cd86216b 100644 --- a/docs_src/body/tutorial002.py +++ b/docs_src/body/tutorial002.py @@ -17,7 +17,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): item_dict = item.dict() - if item.tax: + if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) return item_dict diff --git a/docs_src/body/tutorial002_py310.py b/docs_src/body/tutorial002_py310.py index 8928b72b8..454c45c88 100644 --- a/docs_src/body/tutorial002_py310.py +++ b/docs_src/body/tutorial002_py310.py @@ -15,7 +15,7 @@ app = FastAPI() @app.post("/items/") async def create_item(item: Item): item_dict = item.dict() - if item.tax: + if item.tax is not None: price_with_tax = item.price + item.tax item_dict.update({"price_with_tax": price_with_tax}) return item_dict From d97647fd572169cf0434919464de5406057e32f4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:19:41 +0000 Subject: [PATCH 116/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b1c34800c..76955df17 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Docs +* 📝 Update Request Body's `tutorial002` to deal with `tax=0` case. PR [#13230](https://github.com/fastapi/fastapi/pull/13230) by [@togogh](https://github.com/togogh). * 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). ### Translations From 3d02a920ab7c4b2d26bab67b10e35fc90a923ce1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:20:24 +0000 Subject: [PATCH 118/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 76955df17..e343337a7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Refactors +* ✅ Simplify tests for body_multiple_params . PR [#13237](https://github.com/fastapi/fastapi/pull/13237) by [@alejsdev](https://github.com/alejsdev). * ♻️ Move duplicated code portion to a static method in the `APIKeyBase` super class. PR [#3142](https://github.com/fastapi/fastapi/pull/3142) by [@ShahriyarR](https://github.com/ShahriyarR). * ✅ Simplify tests for request_files. PR [#13182](https://github.com/fastapi/fastapi/pull/13182) by [@alejsdev](https://github.com/alejsdev). From 83ab6ac95797395b5664626b66d1c3f1f5b0e8dc Mon Sep 17 00:00:00 2001 From: timothy <53824764+timothy-jeong@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:21:44 +0900 Subject: [PATCH 119/123] =?UTF-8?q?=F0=9F=93=9D=20Change=20the=20word=20"u?= =?UTF-8?q?nwrap"=20to=20"unpack"=20in=20`docs/en/docs/tutorial/extra-mode?= =?UTF-8?q?ls.md`=20(#13061)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: timothy <53824764+jts8257@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- docs/en/docs/tutorial/extra-models.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 5fac3f69e..ed1590ece 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -70,9 +70,9 @@ we would get a Python `dict` with: } ``` -#### Unwrapping a `dict` +#### Unpacking a `dict` -If we take a `dict` like `user_dict` and pass it to a function (or class) with `**user_dict`, Python will "unwrap" it. It will pass the keys and values of the `user_dict` directly as key-value arguments. +If we take a `dict` like `user_dict` and pass it to a function (or class) with `**user_dict`, Python will "unpack" it. It will pass the keys and values of the `user_dict` directly as key-value arguments. So, continuing with the `user_dict` from above, writing: @@ -117,11 +117,11 @@ would be equivalent to: UserInDB(**user_in.dict()) ``` -...because `user_in.dict()` is a `dict`, and then we make Python "unwrap" it by passing it to `UserInDB` prefixed with `**`. +...because `user_in.dict()` is a `dict`, and then we make Python "unpack" it by passing it to `UserInDB` prefixed with `**`. So, we get a Pydantic model from the data in another Pydantic model. -#### Unwrapping a `dict` and extra keywords +#### Unpacking a `dict` and extra keywords And then adding the extra keyword argument `hashed_password=hashed_password`, like in: From 55f8a446c7c02ac6bb26e7adcdeb5ade2408a0ba Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Jan 2025 12:23:00 +0000 Subject: [PATCH 120/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e343337a7..e4e8a339a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Docs +* 📝 Change the word "unwrap" to "unpack" in `docs/en/docs/tutorial/extra-models.md`. PR [#13061](https://github.com/fastapi/fastapi/pull/13061) by [@timothy-jeong](https://github.com/timothy-jeong). * 📝 Update Request Body's `tutorial002` to deal with `tax=0` case. PR [#13230](https://github.com/fastapi/fastapi/pull/13230) by [@togogh](https://github.com/togogh). * 👥 Update FastAPI People - Experts. PR [#13269](https://github.com/fastapi/fastapi/pull/13269) by [@tiangolo](https://github.com/tiangolo). From 7128971f1d61e2e1e6f220a5f66baa925b635278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 30 Jan 2025 13:58:14 +0000 Subject: [PATCH 121/123] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.11?= =?UTF-8?q?5.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e4e8a339a..9dd9cc65c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.115.8 + ### Fixes * 🐛 Fix `OAuth2PasswordRequestForm` and `OAuth2PasswordRequestFormStrict` fixed `grant_type` "password" RegEx. PR [#9783](https://github.com/fastapi/fastapi/pull/9783) by [@skarfie123](https://github.com/skarfie123). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index c92279cfd..e3e0200ae 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.115.7" +__version__ = "0.115.8" from starlette import status as status From df8f281674a1aec51d98ab74b6d8073bcc298ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 31 Jan 2025 17:01:48 +0000 Subject: [PATCH 122/123] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20add?= =?UTF-8?q?=20Permit=20(#13288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/en/data/sponsors.yml | 3 +++ docs/en/docs/img/sponsors/permit.png | Bin 0 -> 39234 bytes 3 files changed, 4 insertions(+) create mode 100644 docs/en/docs/img/sponsors/permit.png diff --git a/README.md b/README.md index 6492ad745..f6da22b21 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 4231452e4..f9bf33ae9 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -48,6 +48,9 @@ silver: - url: https://www.stainlessapi.com/?utm_source=fastapi&utm_medium=referral title: Stainless | Generate best-in-class SDKs img: https://fastapi.tiangolo.com/img/sponsors/stainless.png + - url: https://www.permit.io/blog/implement-authorization-in-fastapi?utm_source=github&utm_medium=referral&utm_campaign=fastapi + title: Fine-Grained Authorization for FastAPI + img: https://fastapi.tiangolo.com/img/sponsors/permit.png bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. diff --git a/docs/en/docs/img/sponsors/permit.png b/docs/en/docs/img/sponsors/permit.png new file mode 100644 index 0000000000000000000000000000000000000000..4f07f22e27326c34108ca177f0a974182bb44914 GIT binary patch literal 39234 zcmV(^K-IsAP);cq>unf=)J2*6%Z9sQJldp>NqwW9YIGdgN_x)f(=m^QRxDr(yI_4 zp#=yeA-&&T&N+L1>uGE6b900E&inqp1vmHHeRf%Ut*88-vYzGqz28jX0iSy3;5@t! z{{Z~C;2nJ85xxVr@J%rsuZ6GWq;SYS@Gl>lbER-jRnMW8=(VZK{!YDo9S*sIoKHvO z4Cw;@?dY?k4mw{5pK4BfLg$9h1^UC${k(k0u7$ma^wS}+&*9(HI!DLCcZJa18vW(% z`G_y(kaaB%gncjXPs7B0rLnRhq%RA4W-T0dGQPR{%UNH;i zBaxwpjt`xnpF>~xeEQ&1dTRI`hc*FUTH_mP_h}p*-e%bM$+t{q&hdNk zxhcP=BQlZkqamQNPr`S@j^{?kAfYSye0dKg)9jt~{pmpoy+@#}@W3kfi~HikJ(EC7 zG=_Yxyg%Px?kDfSH`edHjTw)m%m>p;!O8pv@=mMJck5KG}?_5w~BtA;zcaa%Q z$Bl_Cg)kcZU87?}_<9_2jEI$Ql^XGQy&(KUFRaWPnau7*MI7Sl{r2rzkM&Y7e5+X7o zL3c>?fh1(o`2Nn-v@=ESixLVDZ%;Q-rgqsAluos;%oyQbsmM!XaP6ZAE9Timgh0)n z5w1yyp;ICdo-d!$%qKLj;Jz_Yfd!0Dv|}|L7O#Ym$k+(2Shv$C0OE9}&c2kvNclV< zPtbBLM4Ay%ns7;(S@{7augoigo0(V#j^>?}4Sl8k^Hm)aI*sW`Xp3Y%Ju@kV&#~`5 z{=*Yzq+>moquKjc5|(+c3=wdtbe{wx3+)2<`S2!Kbb^^f6O&2u_Fjc@_&+J~98$k@ zPL0$f(JS2wv(Yfy;}xUnNErGZKR=C38kxYSD(2|+32S68&y)dT;wvbD84sp1 z8QzZDGi#m+TXhcdJ#$(AWzvw-mZq|tB7_2KUY|*KBmVHTqUXW(u>a!Q=x8W?Q9l%^ zpz5PQjD4loL*VCNvSQnvl7$%?Qn(m+Y?2?*}4b5daTv!g@HRydS zdM~|iU}(>M)3I{K{Pah7N0;*01!d(j-KI)iKYKnm(Qy}IllQJ<&*3pGdF{Ir3(;Lc zjvJ5g@l=FH5CB5=Uf={F0q&EFP(bc4@PzM?l)We`qSiy%~*@%lU;`tC~M zU8s+u+~wZRZo*O%qO8JCncvZ+;kv-QK7%LvuFebJhjB;(;$fu{m`(bt^J)sQM#EB2 zhyc~qUWB1{j8WpfVI-Quw3*}1Bd2L$YA&a{5++`O4syTjoWy0CJrZYBqTYszq!wus z(!M$9>>!d?E=jy;(!3h?QT{pY3-sl^GMn?NjYJx=Cgpn@e9XGV&n>~g55E^Z&om-b zJF+08hZppIUKA>i9SOdI4zv@5pc=p%l3}=7GwM;T28HNOG^LD?j-RRJgq4*-IWL+4 zkvBYR6q;{>)U`kqwS=LwFQoLoQ1?LmI03Ie)MMFln~#^KP{BoVZq7d~?q`Mvh+yhW zgvdGE4>Nl*CKR87cWQ>YIIRK&jV^R3TohO}ulm3Afu$w%e`PToqZ1X<#|fVP{FDF4_a2o zpP5XNf;_aUP`Q%R{!ZqcwSS|WOBy*H4kJ$j$cxEE%$m}5dOZnfMhl6eFoqcXP$F!V zrZ7mUcqf5r7{{GU2;Lj^G|tWtW1QMV?LIk>IgpGE(#Xb{26s896O3DtX;M4cN3&E; zNF$^bo6@O$ohTi-90Qr%Uw}I9gy_r$WR~;JG5=#x6#B{QMtSHmCsxo9s@W@E#ZCsj#2ytyDmqUIvMSW)i>WF&fvr5((MLiI zuZ&aVft1m;H~01oo#%o#@d$eJq;eSxWzPtY zy>KeT_`!)c#lm;B_hK8q!gDTW1$|jlk6c0TUo!-e8$Sb26(9;Z(p`Iq6?xP;1W-hDn@Z49{>XfjPb8a(4FcF_HYjjy^h#ylHcV&UPg++K|hjzj|PE zef8N}d#B^&1$xI@$3Za2Xczg&G|q%#@Y05l!}1+evBgJ5LW~3%kI6fVBh8ojnDZ)d z1_N{8M(;(4ff0$A70TeA=QLHJ3ANW)^9o}?Y*u|Yn?O$t9H6ihyFO07 zuNzCRzEis|F?~|9@6}d{w!1gj=kC56 zsfuJDwY5mmju{TKkC1D(J`b#WJoID8oA;URaiI*ScttOb+T48j63(d_6+4%Ae6j6S z(0&-N5v_0tDUaTr+4`PCGPRrnWvCn8VMvWRc;t)*ejOLps7wxfrOEaor%NR#FOV^+ z`5Us%>YIfj!7%dJ#{3CB!jtGp^-7VV^UUJl`&JyyYq*#8l?bjTg3YOrW+JTs5mi)m z5DgC#a}Vh_vL`qZn_O*t;Wi(^u8Uq?C2bXjF)Ej&raF z$BjY5f?46aqN{G*hRTYU!JcM&V9HF+*SI<-zu2tgcqLDF7G7cvPBv;b3G6^uzD$%) zl|G^eoMcK6jeH+_7vA0e>4eJ`*)Q8H6* zaAItebEs7Bv0z)JXfw$N9Wx3McO$^w)uwh}-?M>PhXdPR41*ZH+~;(-mI)byM_;)9 zZfJMdFI-a{1h%XIGxCAw5^o%`oxDTYQHr?Jnp0^2rgEl>mUwP7GOqBfcT91 z=u6R>rjatu7w=Kyu(mVJ#SsVXuacZgBm4+-)_}$%mav89*Q`Tn?o2cuxgV0&2Jt}a zd$5uzcy=0av_c@bbhjp+V}q(LipZ#VJ&&KS)5ziu$el|*2Sy7GHkaAh@T)sh#fnhC z(A%>ITm^g{H7Nv9JZWA^d6)=Fom(gUD_s2U6EAWmBQmrN4PfiUAbKL>#0!;sbJH`i zEj~Lic@uEcr-8ry0$6b?Gcd}{z|NH_wHceed~T-sOs1+B0}Mw; zt*FAm=bmeH&oOAH$r!n=>0fn@Fvp0(F+%WadupuI&f)I(3SRHE~_atE=$;qg>~lDR_&|45RYtLQ@A)hiurok zi&k}r&22WE@`a2I^`jUemVtYHv3Lg`ohcG?Eg;uJ#U^yhN%h^^n+g)U@iw23>*>P%eM1_n&oxvNqDe}Z1#|Z(Dv?fNX zM7XOo6xm+2gnE<{4U4FBW_Xy!@KPon>ieOh*w7U$*d$-1Ibx)K6m+U%8j{ScAJXgP zePl4bXf75kY4*<150QW61X10)a-x}BCOCC?^g49}WgJ9ul^mv3dyk+W_L}38=0;S1 z`aAf*xXeEfFN1$!H45)}GaB}vhuyW>)*d&sbQKx9;y7gG4|bSI79G^ql`g4p&$Ogj zbcSiX&n&tScH|f@nPk-@&o&YLDDAQtL!cvg#C?dfGw4m(cIVKZ7v)dvmYR?@?5YI*8eTX9Ke?`(6F0kwzukwiMuXwL8I75m8f# zymUdBRARWox)INcqbx%?3<(vXc9+Z=rD}ekdsr* zmBl$Dfz*V+lq<|I7vF(bG4FB%k{MMpY@AS$m@bWiGTx0TGch6&LU+n!K0*zz^Wb8v zH+n-MQb*DsT@s~BW;4quxlH#VS`d7sFk>>q<@A<&0>iGUBN<}m%JtWwc6e*u@pNFj zOkw*pG|V{;&97gKq2({5^3o;&9ma4xrjTfW%&W@$+|44|((b7`Fy^PP3juAUL&T&? zcAn$6GRAyU`J7S+ux4Jolp{7gC`E6F!kHAWkAxsM1dIq-R~0$g08Wg48*9#9a*_d* zgMv^vnK@jPXo-1AgA!~W*}7G;?sJ9`Zi5WKP`CMBMN^M#_XRDy$5Ft7li;5Ct%%Ds zB8n(yjH+SiVAr(F>~CRo)&4^6!qA$d8f6zU=tlSDQ{+B`LY$cviQYI-$6PdRBlAX~ z7$Yr6BAh@0s7q7Lr~(nk*Qr(rLu-r>5z#0|FmrPN-zgCayfe*Mh+Hh}=XGS9k!7ZR zVR0HChgke0%3rkP!zoy?LH3z5mcwAFX3WJQcY<=9 z=J2QDKviKaaT%@2Mz&^g1S=VhZqNs~+UO$+)$h+tGa5~gYMlZ?nA@jX?m8!JQ& zN?z$;OlF}DW-_ryaIi|KZ z&6KUgWyQym=-nhHDz|bXq$yP7cM&jlLF`AylrAwQ%`vi~j5L~W=J$5-7KXCOmM$Ce zNd$IReY7K)IBPmw)Qy_XZ9%>NG)BSDEAPJ#UCnmyXst545593X%&RT!-;RODeghwV zH!L_Ah4JkeUb@;>xAidpr5t%kyhbGSeZ7Q0f{o4^+FneuFp)F`sWg;&4ksxJWYdg4 zny_++5$S(H`%sA_Z%&XmpLK-EP^Sa#m%{)lRY>~`@tK$OlNV@3E2kTOgoxXg|uEK6aN*(bYcWRH?w_S0XY07Q=v$y-s7sS|zT6l6#TQprSG{ zNu+D+=~r+=J#BUhAw@pALA|%r^U3?j3eCy8Q6EA?fzlNjw9TJ~(w_5B`@`)>@A?}? zw(68Y4Da2Af21YNW6^TNNBH;fv+FVZ%ti@`Qg>3Pf>n~6eIbD0mNE(*%}81Tr#6>d zaY9SDwiNGIS+yhFXB*pBAw1-4#gEK`z&B?LN4y#PqFue|We?4x$;}^MU>Qb|<`NNI z7{6lnNn^?>_qF~Xknnc8zKg^six_prZm0TQDuLNB)FN9Zx z<-E(8>APrGoA)d`OqYD>caj&Kdp*@$u+WIh+o$m|{-xnnQJRu{C$?5j*+UUoNyoLQ z1Xf9rlNFlBp@J<{L=@>oZn* zg}eevkWlK)Tv7E6F`1QY+z4${rxlo8rCH$a(eusu2S(d9%QD@>VC>C0Q0I z7=)Qv;hI8I7ooMip)#lt8WiL}*gX4A4~UnS?iOuf89AO1HP6%&EJDpm0-I`(+^`xI zyQuN=8Q=P^x`y;r_{E!f{S)Tu690^_Fa)IM+vKkm20updmZ7 zn!^xw9wly&ceSRk#H)-F$+I>*-~|Q6R8}+hv!~>2d=j9ZLQ~_E-V`2`lPaHv%Xh9($VqUUy%Pqh;zXTrqA@JOv@xN+& zo&X&Fe&DL}>O)Z!QlgX>bNsNc!$B7TWZr2u8z8HJYEcL|7c*Q`gXH}4nZqb*czJh$ z+Oh~<#sVf`)gG3xj_D&_(wOREb{8iJ7M9QjBghv{u%4Lb+Me#@%^O8 z0Ce7l!oeUM+_0EIlCO|COqqlUx$nJq2pKh@`u*R;kWs!><^;R9wTl038-Dp=RJXl= z`=(uh=Lg1$b{+V~?y({;_fgY=7HJH`IUKDXAKm(17C`uFUa{!EJ`WbvTmuU@7a5U+ z8igOL4r<;pdP-z!dOOFk13*dl*Pz>(!$u1+?2Z|E;Wm$IybvUo~LY#BUN*sQv` zAWGLotqe(EuE?f&d8rZ8Q!7kp3`1Xame1|X z*PnP<5oH-+X3o$#G*7`)DKrGBS4zo?&05!zxOn_iu7=`yHB~N~ugJHjwXc-YkuQT& zr}+goa-OW&#l%dcdC9X(jWp$OpJ>m~+O`+LdC}I0L1!HSv`yUgT)IckCi$U8i6QI4 z4+)L6OS8aW3~~8b5=IOO==&*)6e8@ltK@?{&#|rNG{;-+Dyt-YEHz{~)#`bcKqg?} zBcGTPI~z7Od5XxKex{qe0ZfHh9$EaIAd}dKHW7w?B$iF@Rq+u`u)vxVOqm&nW9QDD zd`0KH;|Q(5z5E~xC0B{$BAp5YB$>Aw&O(ge0!?i$+5hxltF~d)s#Tc1Brx${ud6+J zbKrtr)&N}YCRHS2W(i_AQgY&I2YrC;~ zhnDQb?%E5holeVr0T8V+wJifo&SH@w)V^Kq`4U6p%;(3d7tQ$_BdN`nJFicEjus7GMUMV@TAA zwl7%dM{j0=Ayw>{>d6Y}y=h#w=^z9zXx(y}R52rCMu=8K^i1nv%?@KJj2K@PVNvdI z#JpGh=zI9dH@|{yJ9gm1pZ<4z_*0()xv*i+vK{X_Q$*B0K0y9&qR@3n`ML-26< z5X?(oz~Xc{GcO(cKW$_eljd%0A$XJ`3vlcLYN0j|tw?edFE(d{H`gM~vIza4ktrPP zy)4wwM3|ZiZqnTT&Tn|w;|fYuc@HifBJ4dFd*CrGKsCx?rqI#I*e}88a(y-o|4k0B z1K*KpRbt*JualpVZMavMxbD8;9_QZ_G|T-#3qPnDRbfJOSSg$xI}baj9TPRu$ToZS z?Cg(?PYE)pb+LAdD`f!e5~E>TI5MdE>ot&{<&En(n%qH`GoxtiG^h9(8!4=H7U2&_ zC@%cAQ)#h*X;JM^bY4w8Ak4*a*gHVCazRcZKDhCrXE3`ljN$lsBE+oq22cK@SBZQo zh`_dlDb}gSe+Xau{3mhs<(I`v-g5qj@wqR4C1}H6$$>;OmuiVHFQ*_mttaN9Cz+9n z$*m$KN;sDfL1nRM2aUON@wiGQ^;^2STxr2VOh5QAwts8iyOMubXY^xu&mQcq{Y&}V z`2VYYBuvGkVjp(iwh~R{M$d)zEb}bD<8%m7BeU2Js9H>kOZ?e&!bCzxq!C9;+#tpq$$SV*yDhlrXCD?|@LV}Wkotii z;Z-++=meul^RIr;97_TzIsA}tD2W+s+5x!xwj1$x zG4;TA!ao_}%-vh$1bcTGL-7kDx3;&SKKoQ)t%Q4=rFUwEV9u+Z&zeFe801mr{nwC+ z&oM6IxVz}wFjErQ{iux_GX_1~BCu*6~7 z674kR_aNYQ&g?(%y3XQy5$seZh9i_L8sbsXqj$&lKsHCB@zdvX`sCo+t5LToVnl250~l~kc%|0tdeDqsZ1q#{kGAn{ z17;*EFdpkMGkFg8)!u+-(*yatt^L6go!US)FgRsEfwvJ0dg-G~RtrdYR&TAtaA)tx{jdb$zKXNiX{@Yi4(MMMIpl7g(;}?v{4(2uU z2>5)*iXE8R(SQY$n|FUx%9%du0!hcL4ykPp3AL~@1>#X}Y{x4#TKGcn@b*9a8u#6P z6Fzp)f8wogdn^3XU%~ZvDgI5w(@y3>u`DpRGU$BYW{KMs6bpON$Y`WBmBBEn4FzpH zByoIl8@5gef^KclqTzGX1jLc-UMJF~b%J9JNyqvE+)W2vEIkAj#VzC~U@htpV1Ib` z==}1Vo^&n<#tSa`8bfG89UuGj=Y@f*gXr(+;&-iU)Tb>^LuzP2DphJ;D0;WzUGM)W zRy?^JFFd^rGZ!ole}4pi%PKgt-jxfp1?ao~d+c-?t&R2D+2-9{ zBj)`~`F+g5#CZ1x23(;GING@<6n?`@yD)h)T+_qHsBQIAu_oO!{!NWy!b!OpG-F8w zy~rrdQBET~=HDqJUT^L!ae5`$!(Bvh>>JRqqQ>Z~21@EAk;BwAJxG$Ukx2`KYeIUy zbGkgyRvhi7U3%MUtk~FxKYnySOzmtC2%TqIa$-$c0&G}@^*e{~@&9>_&!h8q+l+y! zSiriaS{KW)_G4!gukb)UOx1SK5y!;OPdxEBDt+DjnF#zlU;H#qIsJ6B1rM%&;SRWt zU_VoSck9F~ciT{Ou$dWlZ+*d7cWgwuekJ^- z7vlmGM58_Pt~jZhq9G11`Q^4CBOGijm)zL)W|RsAzrCjiFWqswf9d}F+yQSr2d&3` z8iOlt!SIWB?ehF-SQq{qoq-nwCVWrn;=qvW`R}6&>%-5D{JGz^p&#MLx8kDVtN0i( z>;cK0%&be(WASpjATaF$Y{e8le`JfE=+34f@PZ(hLe9)ZX@ycT#SsiWn))oOPJ{c< z%s5!A#OXrnY>|lJ7$Nhb-b<7Doq4WuDMdksRGgHLCGRKMRC(|7ZdzMoA^a5iH@C%S z?s^j0#5XO|^W^6ho8?uv2Mw~pYd*BTzd_V=^h*gk=G(Wv`XBh^cfP`(iHWbh>Q{Kz z`R{`Z&MmU6r+DE5>_*!REMM1)OV59O zy!WU6(ojJb39>A(7wxX$>%;9ues#AKgRUH zu-LlVyM@O(H)d)Wm%GE5eE|>McQ>D7eLMB+b8*Dc$08jw8R>J62HL4eC|G4}MD0Yd z<}hVnm&=Of)yO}Xpedl5B)D7y6Qg@z(De=s`X{dWty^&7N$6a377FdtG4$Lmb=r7Z zx-QFJgJ@A=+}8(9N?LSxO<;8o`d!AD#JI-HucnzNy3iWvc*~GSmKu`M zuDuvSGgAibd&9j?1tPqq?wketEW+_Y3%3O={Kt>(i&xc13scn&4Xv2ergXfzFxY5~ zH0Rc*D9*yR?=?TWjG31j5iM)Fz`W$qz4Qy8VyK;O1VxGAe)3|Wly4Huqw0-k#K-d)DV`gYQr&&7pbz7!q%9_wn4+-|ORWD6LOa~J|K zg`^@X#j@onjNa5#3|hDu4dpT#8_I5HcORbo{qOv=Lk@K_4mbd9GY`S=nujAJ=fgtO zE-k;I4(3|BsOb&(;=ql&9496w$UK^TJktK189r0{`tXlMgR3XHX>1DyS9Jv|qMUh{ z0f}G`4^?%kWsTy#;kzM;Q(6T^Z_qbRKq{qvkTW|{S-vGUFs}u5mZ~=retI2H=gN&@ zUycT!2@+HMmVRqvSw>#z`LD+vAN~ODxb5b;OO8MBR2+YK@N7PJJKB0i1nVg*6=Ip_ z9dVpA|H6|(ry2qa4_YdatmCCjvDzs3*9|-bpd~4wSS*CGD-f)98ycya7A=& zQI~M4h%m$1d?YS@Daq67NEsfH0YlkWXI&=Dx|{Z;|N1^Iy|@E6T=N_L`}DWG4WGJL zAnb0FKNbbpefxii2iuxH+mTHpiMYX>>en;?+ z5*J7-Tbnd(CNBE+j~NmY@p7y+Fc&c=Y1GRuy#!zT{!ey!4t-A$b>7|qU9@+it)p|7 zbDG;aaMnM42ycJqd3bsCYWyPH`+?w4lk36EOZOm6d*Z352N)k<{?%6m=E#=Q(er

=WMDDAr#Fp;TDW7fKOeyNR&^B(cOS7csl z6pA1wZfkGnzI^JUui)=Ca(a=S{HHHv$L!hU=>@?OF5V_yTTZ1MU5$M9Bkzr$*R5HN z#~!>pYDv<>Z+`o`*q$@kOUn7@*Zqm@UTS3Hee^T`&fl9YOK_2(;>bqV{OEVLs&X;JF`TpgBLDocgOXrec zVUP1DjRa5res*m3_?E!9;hv9bT$cbHufKf-6%5n~kM>D3f`$!_uAS@Qd$wc0v(Cby zM;(i|zV^^KhBQtzuC66SA_g~Q1xj-H3JzIVEG~@qJ}yyacMUl!6A37nD2uGQA%vL< zXe^a|*HFLf?HRxef4Uvpo__(0-*_g*hd{;}mCd9ntqjyR8&AM(rMuekkxipma9R=2d07tWU|XnwO1RHhr3%qF5fjU;|KKYtHIjgxP0IsZeLF=;IPvJhLQazIPoy30M@dhU4vdEe>Tn@;yU z@%w|Jpkz!Ef87i7)hx zl_y4)lxE?PxMB>l{yS(N%I`>HIi7$_!;3eSOFbaXx!|OrX7$~OTSTn8O9aWcr$4? z6cgFaAl3PnwY<9(fIZ#yS#9*rK4@Qy4*7u)_8RPwch_bDu47y)Vz8GeQp0{Z+BHX( zi^z}uQ75HkC1ptwVoR68z!Z|QD+P_$SSVvhkC<0815e$jeRw%Q=zKeS_)&(<px1Z}DHG9`sUQSf7e1S^T9(@mWNDSm#EC9I-}E#sLU3F)^0)7iu+CH01*Wcd zVCCBf{`E8X_2oYdHt^^0j`QEoTK9+F`cF)qJ|nY(ch{EYN~j$4ZYE;x1ANRkX2(FP z{kCO>BiawH$<{2-m^q6Xfi)M!U7x$nW6u-onaumTjU;y02qxD?oXCGy2LG-Y$t}>` zE#t@yA={I_D$(@!8_#QnR=qggB38JfZc*}xMmg5`#$(6IB47D5oVUIHJWg-s3hK(1 zY0h`0zkPnigGD*L&-1)oo)r=l$6HDs@?ag4!Q9f|hAUNH>FIIfn==!SG%zt*-V?nx z853dit)+c%dBah7@6bI_3y<_bUsd@8$B1{wwE0H1H~o}4TQHRF2>zymya7eiY}-^g zduc9f>#LbC(-9G6$&#CSPxeRB`43Q<;5BPxNu`W?zZxN#BTNkyl7dC8jz|xt^|@5Q zofwLW&yffD(JN&Q?bK-TxEA+|b@)&l@ znKmmD8vMV~q(|!Ur9~eT_TdF8Zn$gLq!Dd-%}VW|`|iTt@A&}yn&8E)1|i-$j(^sy zcru{p+W1e#es_hqfS;yQ$>WAvJX@M4+Z{leH%Ao z=Jff}U%TjQrAfemwfK67{O8EFAS5 zpL6r?{v1(4vApO%)<(6DDZ7gja{4xNR@*&y-xYw=O7@!1o;V?#@XRikEMBrNip}lO zbARdmJ)Pk{F)x`DU7hkyR~zu}cyR>SHQ&jMaS0SnrS^|}7B*0%)>t@U}~|8>!)1EVfx?fCHv|D8iq zWb2bt&%EcmYm_DSor^!spEs>rgZIDrB%Je}4+nFkgL9`z_~jEv*h4=UwDXP)vVNN3 zA{?~#6+=s_$iXd*=#Mw>uIMbuMtV+GWLa=ieY&xNO6n-#q|z@&XxW{*POpdC6&YrY;Yo zDiL#!SJ>$#X@#8}ws2Qg7C5U=F-l`O74>!YLcC%oc!N6|6MXlVzvZ|lHPW(4C;$1ab<9gi{uEY(|?^+so;tepqg1QyIe{NsUhqnq(n|PZxuu!Hb}6CQQ0Ax zSTjbLssw{SHFP6;j`N;rYjpho2J=!I-{dgd)6d|5s|?3JO8;FZri3k1!4YK1*<${r z!Rvm)Iu5qosEvWT`V!qv(PaS5A?H-Fs)m3S4%Y$2q`k4?uN6FU)sU>!J^9N5b9uC` zl!f~}n_m4}+hTuXUb+`)_i%4|#)`j$K-rXijB!N@StZJ7Bf7dQoLyRN$Y7e!@RzRh zqRrKwx*mJ)wLsD&M|hU=?Z^{OlVtw@yD1T3)D}VUE9l_bAS_(4`*Vij;+*__eO}i# zE%lwPjriEbU$0~0d^`N8dJ}MxMPF(>SBThCf&#A|30EpqvI7Ua={8sEF#oicr-_O01z8MHpykN} zs)2X2qL5Y}86z81rq6Wz|FjWYpAmG~k<)iJS@uPmDo&I8 zFCWCR8!I^IoU*LwQTx^9ttui2TenQdL#T@gaTkai;-AhIKp1vT#52l&^=|Qa{I_oJ zMQ7rVe|t8>g>R7Lj&8GgXJW}=$KdE=j-~LH$5XdDH-2vLMDJDiEOy-Ncowz|jSF$> z{A#EeOD3n9xg9p{rqA>oF%KNYzbRuZe-7KYL*018+3&=Ghaau|oF(4+t{`Am-HErp z^{v5{KbEtl$WG1o;hgtefX&->1{=5w`-khv+4u6A)pgGxh>Z0s0uw(2N-MhX%irK7 zhPT~xeTelgXV^^1Fa*shaig=f3I6#9;MYAV6yu#bK@&y1Va8(R5lEhWEL{Rbf=y%_ z5H1|RCqha!Tlrjt&RiC^A`Tl@t@z=d?qCPMvbUHRmVPK{W6g+A)9#uv>eTWOCU-V6 zOs9}lURYEIhw=Ph9z*Yn^?qXKc!Kpje@Rh%mAsKS_gqUQnSwOBPAs=YOFF4HT1x6T zso+T=`(i>Hh*L$5Z(1=JSsak*g3JuAMTeGrL6GO*_hQBUO}OJbV*IyFOmO-oP5hf1 zIkxhSbw|->++g=hWd(vl{FnhlMOCGvB zPO$8Npq}wGYBAZeWRK%yIUjjdc?sZBkIgSQMEgMplG4?NvUL!gpn9iToaW z9Ouw(eeRMalUrA6pTy?F?8AfCc%BF|YMa72TY~>#?ukcXcg1bq5AGCaPzzDr zfga7{9UAl;DoeEPQSmX*$|`$5iaXQV^ta~iPULKPo?nGbZaF3aX!i||m+rwt# z6XL*rRR>ac;hHR5rmJN?_+F&cl#S6~td zbh@**4=slth2crF@u|E1L6bupF(g57VNl0^&ukY4j-)U+6a;%u0FK)NiEqU~S8uRb zwg+ zJub46I(gcSwCqjN*UA@_FnvbC6 zcSKri`5!T;)vHsqV|1>hAa?f;ptNWS#w=Wf59e~CM{6S)b*6sal~KC`qi$hl9VABG zx;6anqcjpv4k`i(6pN;qZ$q(+_Ler^-rVY%%1v~P(Y{5>vOzbOlaK{%_)Sb%8<4S~ zS`fjr9=!fQ=HwHEBC{f6##^f>$>ltHW|!rVDv!4k2oZc|h#!VnFxkPTg{_emo_%Nm zOHMCm8u{no-@_(eIY|pMMapJJfz$ly z3{K}cCrfI`-apoD)^ai&j*@zzu1>2XK1&O>c}bY4WsH><7C@d!^&T!R$p$O|hX^*2 zxV)>zG&_;Tyu}GT;(j_n%qx!f9Pzg)0~LC4e_$!#pAC!Ai)uPenu2OfVcME)PpW?t zbga7QLgl|}44WkXBhjd2nrGHTOU%e?d8Z14#x9j>`I(86rR@wm6BSh8IC5X`bMz!F zZstj+af#ULUho*G{C!3pAST_s8G}1}(7&@gXw$9X@2&8|6vvUe!G%JBlkF)FyI3ls z7(C6zv>2GQh;p&)`FPN{#Kh%7IWSEoVeZI?Q1j*oZT;^jt?wP2)7k z%Yidg;_O~DNOynJ@3II!Cco$;lQd4%E=e=~;Igo@4rjP~!{_?L?_D9w)I|l1+atl$ z#RXODB~og?cC2Nx`aMW`e=j{ozFRz&1$N1bWWd)p6mInOS_c?mFH=p`V6n zxw?iFwc{bs3|_6S=w#m{#wl7=(J3I|&AgKs0ggl}jOnbDHEVoEScw#UteFPWYf7Tw z2Kp=5x}yt?do9BBLk`EtHqg~2jM~*5qHJ4z_ohv5pr<#$+hMlFiwPH9<_xN0p~zZQ zK6CChuY?MW$xKX)TPVwK0SO6jx0s-y5~ZME$54yDmyQ>Co0&>TFmlm47bi#RX|Ztj z947E0BvTH3cp#pty<~0knz7spDggb?bh^DJKMpL-VcCl$}mocNaXGCq4L`LUUa|$~%1mFbT z^ZrhiYKk4bz1Xs&8rl|L)uCZd`j8aIz(b|LoDC&ksujW`0uW|K zr8UPTrJCmj+`MrXpZwo>-V9iWXbAH)qF(G7?D2;bzmgFKTt}txs z9yv`3)M?@RQn7i)MTVTsM2RzpyeiJ!Pk-kje%)93J{qpI+t%OI>9|;)ahzSzu(I+; z57~S}5>+ z=yPOLXbqrx_#1_oi;z|~|5Jt)B`Drjd8Z8S=N)k{BF+m(Vg5{*Mai5STXM;(7dqc` z_HEu>ELa%fv#K92&N8v@=n0J-QZt&IQb8jdOeT2!O6~MZnwfctiH&jn+NE58JyKh} zDFA0cn7{3!O_yq_N@n`ScYvY=c^#+ABy*A~W7-jnlGkcp_VKCk*8+`Q9em!kUwPge zAsp+O-n+Wz)ty|Act`7By&xKSjo0_{aXXy+bClLH#E@p0K3v_C2BjJW4SeQF(R&E- z<)I+Ielc=D!e2wPy~ zo$xE~fa{#;8}@yRBl~!?Hf|4xX?r^?yEzTsy;QU=EmYXK)^p4-jf5#5w8Toi)5n-7 zA2UA z>pMB^Qm@H*iE~Vj+%Y*ZFS2}&#AFD>$aF3u5&i777cOp6ZtvpEY2=YF?r-llLeNCd zIe(7Zh!C9=Qe{@X2j9>0u7s%Mb|o}ItqCoQ7acZ0am;)iPTh2!Y{!at!_4GE6l!70 z3KUP3Vt8q@+)BK~iK>6cMd3K4l^Ca(7rai$>xOWdgU@S_OI}|#sE*X-?0uuk1@ETd z#lDSm;u@r!@st)_wo#aeO|ZAj5&oj!w9o7r^ic1$ zSBY<(3jTNQ)JqQv23ve;5^P)PCo%8o7bh4uTb>>rSOeYCU)rmQ0@@{;PLo33m;_$` z)9-zb+`KEvdqmG_)Rc(jg)tRAs@!#UZ&awD~T=xF%Far_AhP7?>GR5Kg>lR|a8%d{4vq_tz&7upsa@37=Fhx>n);|+8 zM=edYlr2nDbrKq(#T*rFz^3T+F_ZDVxyjfdX2TNlwfvC(P5_LY~lN>9{TCHkk z>l)kU-Z}S$t7GnXkAhxa^>x%T;!4wcUiW5_odpS@)iQ`yhcN7_h}iRjn+QvvQuR?; zM^hoEyg6w;6@+7O*xN==(OH;h<2Z_@;+1ids2O-nHRKqzi}iqDIW4PefNUWnN=%`u zu}-#dpSxVv&_Pv%vJ9ci5t@o}r>ar3mCq;r$M>aY(RNw9xIVT1*1erwq3B3-Dr+mJ zoecj{t*eU6k3EMyQf9Y0!OHyHRHr-P6-K-~keqm?{30DhxSiL?v5WF}U7^GoU8&Zw z)Qzu3sBH}&P>fKD&+_1TzYA~X$d$)Y;MHI^->hA=?hO~!IO*!d&lKvImm1l>+7VAtMwa*DD-vz2bD1FoE(B14jv%7C%I3>U3a5$^?k{|N249k> zaHy%qRLz}9Hdzm}~=x6ciWxgv_i6th!@8`8T=A}jnADp7d z$^B7xNoP2zJrPVqp5Q|6IYfX#B5_6)-Lm4ca-=${-o`@2m`uHCN+GxW1AP=mQrgt0 z&%=n;L27W(vnhW(5Sfb!0X`B|6o%QRXWr+%51H7@%NFUgCA&+}Gmk8VEdJJk77CJ` z{&Lcn&A#qw6gHxBJmn7i z+;fCcoNpv85jsKn%7eac=R~$OELXO^8l^Bx&XuGL%QI^F)TA=%h|xpW7@MO(7@a2> zBGgE;1xX58+%%|TR08r^%EknC*pFliW!U)a4Bjfo>5G_l&8QGjf?tai5?!}-`5Ljo#a%w;X+jsBLyZJkJ_PKNIrK)QCVM;$46F)hg#QBGuDp|z{LJUA1g2HZ4NwG?8? zlgIOs+6LP2O*fr3B217=mh#5#8NvAcG~|XIIhNFUbN=3MroI{-5<+aWpVyGDW7v2* z>xj?3bGZk_*H66UaT1)FxR0b=j@&hnV!pIDZ;1KwMFq8=*;ATx2gOcD$)9N-B}VQC zB?l1fYUuPBx3_v9d=^S+Xvf-W2d=&L4><4Kx1sgv-y)$(A4a5Fwgr}62)$OESrIW= zzs#A$&!Ck^Q}zaECqcSL*E;x}F9#y%=BzLa2F7(_HPk6n#4ae4$ATPN`@bj718?L;D~<9)+Bz|e-CQC2p&lwA z=yO}GGSpPmtg&N7Ig}f(tF3tL@bm*@12sBr)q`P<|0WJO1qt%fBDcH(P#H`5olj;6 zzna@HxcFq;cH1pD^2nnxVe3lx4KKn#hhkBfMA20{O3;w%AHnc+Q)kmQ4?(*QptgN0 zss|l`KmF-89C5^v7`tf`(sgTf-kse`$eSF!l*gT##Bv-G^TX2bycJfMJOS1DQ}NK! zzhU<5IhZz7Lhbo=KEAJP@xhK+jWr|Z`q0tLXS!wY1u8{5lsngJ9N5JR^&8SCyxYBM zU;aMvQ5EJ|)F?ufA~h4$;58M58=nf3`V_~8K|BA^;(6L8lk8qU1opT>?Cl&V7nR~L zia^h_jlop@%^ETg%#?-hkxdQ{J@k-Cc0T!p6HdS{fB9Yf;SayZUVH6@V<%39+wr30 zRx@k{7sNNs=rj*lJoG-Na-!f6=ACvRdd4lnbvOMHD^}c&wzf7bT(B3;IO%NoC$D#I zcuVl0#uT31q(m|3@~>7KI|o1d>Seg&&c9;M1q-pHJjRu2C6w39|B_9%goSufMc#48 z+srUn5y7Up+?AjS%7J+gJM=)Ddg|%;?D=Q0kJ6b7h4*%MFT$3OZ3HgDd9^G`X=`?YJF(?GaVv+{%EJ&kx*1+@^^ zjAFr_=?PkKP}*k>F8JvC!#y|S{P(;sgi05okZK;iDhbcaSCdR&(^td;g+#sQ0rH2= z*M>MlEhC#ME#_wrw5TPP)x@NlW*F_Ws>GVu4vD6Drjb#n;sr@lwgbnA>9mS5zF6`w zA<^}+qcyP3V5;~X;3A=^)Qh6e<(W5oev}rH&Xd7Bc2HqB5b8q@J%aCj?>m7}_eOiW zXq-b2Jrp1L$Vd72>t1(4n2oPGulI_MzO`sYzDpzMr6YjtUzZtrGb#h*g| zyWra=I5+z^*?5jx?`B7)3uyb%rb!-wDe3v89DHF{49i;|ld*h5yW2ESz?Z-L#W?J9 zULUk*f6ySak8>D1U#=&AMHnZy=nTZU4ejSY|0zyB=@cA!*r$w(c$P}zX2Bo#L8*hi z<)xtc?-Aykv9EIz=J3X8yq0*!df=sS@9B#JYMafsErj_gG`8ZzB@Jk8X~Vwz?H7h1 zK#9q-vqw{B!+F)G9zb!=gW%f7h(_t%A=P3a2x#TK_u|ScufURh_r>@iU=N?O5MQ|H zOIWgGU-UlrtZN=SE?mDLTrX>;(~X-@eg1hQf%%K`=LywS0v9(m@pIEHTTof{e4r!Q z(xSX*9=7&&G4me(n%Cj<)8B~hXJ15V!E}^nO{N_s;Jf=#S-&0C7dQEaL*_ddj-e_W zdL?bm7<_bHo5GXl=C;yI%>&^jPAFhtk{95^;!l=kuN2im+qPLjNVGw%o?^* z#^)wg_$tX5pTDi;rGVMe%Dg5|^G>Ne6c*T(u`(oIAy(C(fi;InxaJ2x_#q}w7#C=# zpTE8O57*<0D}I4P4mlL-)~&;Z7k&n3opl!e{O8;8?6c3{jc3_rez`Q^I`7g2m64`m;$3Oik zuD|{|-0;U+aPGP9!bvBcfP)V{B%HSn3xZbt^rtVxuYY}I__+u@-92H#UmFC#I)0_g zFaJ62y!Iyi`Zrhdxz9cKEWUB+f8w~~UxPb?R_^WX#c{{I2A}xYN71_OKJ-pMGCc3+ zxcR0Vd0dY>?s&ZSy&u4o*5_RB*hTos&whcMZ@v+2t!+5%v^VH9=EhfN!jGMc$CfVT zK1`W11^;vPRXG2=ccJpq1Fq0H7a#lNXE15<6kPbBcY_M1UU$PSxaOLx@!jwK0N?xW zm-#OD-g`IBd15Jk``c?pmV~~3^{ZbF$M3=9$&>L-+#?)4g4@b6X*w&1dhzlz5m zUy3P{r{biOPsVw{Wbnl@KJlqfg=bAhYv?m+-Lu~G7F_m&@8QEAzJTxhgUi0JW~B)3 zFMa6}%sK27MpwTI^GoflH@z9lmOYM1lP2Sx?>rAb{NZJM?iVlq0$zXONjUuQBly_+ z-+uuP+It~(3=QL3U;heLu3X6^_PW=d7)+RR&=d&xCs+Og4?cK*IQDwnaQ(GtX=%l4 z!{@i2cq)b;cuY*7N#pRHuUyQ}e)ypW!aV*6?|IMrgEDylSN+?iSoPA2*kg~mc*7gc zz%hp(<_cq4@&5Pyb2v5!ef@n{v}iF-+vg}qiI{|UYozT~c+p8sCy630>Ob)M%R#&gbIRLVY_hOb?eq*5L57w?- z8%dYGC!3fUmwxE;_rL!=USajsOTWRNY4R_;@N;9MMoDY5If?%`#nVFZ``XDTB2nHKL-Y1ACBL3=N)+a+uw=fj(rVoyzzQmbEUU^F3UB^>1+OF~{N5z`Qr#d{ez`3=yVG3^NYI|NPHy zu;-q82EuzEdi#2@blH;_Dv#kp_Zv2Bz{UWrYXKn)RjcR?eWYX5jt+2}ge)=XNhh5e z1?clHNX;-}+&*RIzSbGoRGfs5|H}mo)i1cE-c+$x@@%1M$32h4YD>2lc{&XvT_}|~dym@njP<|6`zx`H* z$(zD;^!>g|q-ewuM;^sm7ktl6=Gc6QjccdcSM*U_L{zIaAivtAs%&DBLMQakv1ItnU&&cO zV5W=43G2(M@G~DTKp3nb499ylRMle-dk`pZY@`q9iH8ccu zc6bf}h)P#AueO@mzD#`T^rF5>$rc~6_qpf39mgMk3@*9kKbWybF{tsbcfA`Y9=s6$ zaq-1$8~^pMvdzYIftg8D)6c?%d*P74pro~*ec>h4N-d}sS{!L!YSX65uJELJ7cbsR zfA+?SC%#U{#^IoY55{kP^(&n7mN(*VWX@~8FOTYFtW@f~pbR)DR@QF`; z8prHC6`%jN&$6ApV#Tw}_{6+>2IzO-fd|Fkj|O2teIsT*@PLEx>5qR5Z#ZgS{QJLO zq@(7u`J!yBvD2|}XFr|`knxn0Ps1_C92@%6$~B`K<{u58A2tv85xY^Ge9Ec(j`Qa2 ziL*~X34`6+_&D9;Q~&x29JOc`&Lbucg!)2Y>RWHQnfw0bFMkzB?L8Zx{P@Q)ckVp= z{`c3|0AO;^oPYS;@A0m)-&{9q9l=IB`^+=Y+9ulhonih+TN4A5w!QQl-^Nk%X9e{2 z6J}iN$KkkT&i-FMx^&-mjXZ^l2p>z!z!y`gs< zI*cX|hj(uDzPXdno7{|M}J$( zNA?CbuCLoslg5&cPnTXl%i{CtvWh4&KiZ1Jga_RL2ax?WMJT!`+Wze8(8adDFxx)m(^-x7UGG zlBo&PdEWRf74Ks7l6kRXIbT&h<~|0eMUzSrw$h5Zw+i}aw*8^OXs4@fG8KxEj=*@z&SL28>VP3NV&3kh*ZLQ4{5Cc(zZ>fItZVcx zZOx5nC>AU*%We9!X{>pF`O9DN&zWx&=3KaNA=2Ogs}1aQg}$xf*ul8&y6f<(UtPgW zO6UCTZ+~NEGze|2sT}-<-J64Vc>y!mV*#S>b@s*RNt^IU2pi=+g`>5WmJ<6ft&!7= z1i_T>`$s>zoPSPx!y9nG0SEGTQ>IP{%ye92>HorTy6)ZCS_zX8V&iuQZA*i3 z`A>hsjx2(=)W^r4d>Vyv6QBG-(9lDL7PReoc=&!}J^w0Ebk17jTE=I^0BMbFduus9 zklMVt^LbLqW?t}?Pomc~F$3fj5==ZAsqC{Pkf9u@CSpbLddGv z*ck3PFF@QE&|UFZ@Wyw$RWGk)4Viyg(=wCyJ+N*RNOPMeo*qI~EunvZ{_~&0@ky9C zaZ=9x7iguiF{iK8f(u_YX;H&Gd69j)Wy>b4-?kZx&NGuC0su=4BWAPhEYg@I1eaHiiGXltF+q(N zI&)lk*lH#hA+KgP>2kjqp7EMN7KH%I6*t1;t$+E%#D+S|T=rdpy0yd9m&q+~4UJ{* z+QfU9hl4J6p31Ly@WSBCy5NEj2FJzmtfilQ_Spcp&%v;+zi04+U2oUf6j5yx=-uL56U%WWj@o!^B&t5Vc zQXk^CPdxJi+p`1_Pd)WC{)RO1o6b5jIHay-NPYI%=iocv`7iwQd*92q*{Jrf^?LvP z55O$}8bAKTW8s=V#C}Rbkw*>~1&7rs_}%Y+i?4m{68!#m*D&)^`^;xPm%pXBT3id0E5Y%Vftk%LsSXV?%#Kd1*S|hM>Z9V=l1)qw zs_U=27EeC8EI{*344vQmz7Mi?9~FGWZh9$CY)M8szB|{f4RdlPJG98&C%gEimsT?K zo^#II>+ZwL4w6KKvqmRr$)uEV@{n>xFv%{}dV>F_aPhlOyjUMfRT*3cT%n6nI5g&B zAvo2BvS`-1m`m+sT|{Ia+56PVk{Td7rzkFG3(UPFZA5hJ>loHqN3zjlX3u}08%PZ) z1^c(!Iu+&dvswEddgx(j*tFC)HzcSvhj767xtJd8AF?|d$_>2X_+f_~iq^sPK4}f# zw~pt~(}F!_Vr;pJcl`5*SPKyYfB3^632^NVSpV0{{aC8xkQ6Z3x83#b+>8%={0o>l zXCB6npTHXULm&Du-g3qp@YMC+#l(e&2FF^7ZQvQ>no(W-NDzEwcltYIzn(qj?12w_ z;6ou~bTE6J&A~D)AOH#@H8eDa`;Nnb;d{63d9+O5JJ>%>>?D~swgn64&c`rMgQF=O`Jp!r(Em>rIDgL99bcfbJ$VCypv z;Fy!o;=gCkoW;ycZGNzc_n0;T)vk?bnz9$hhENTCSK0Ie&i&^P@(z{5!^7-n_}72^ z6b4s49-?_;gH~a1IeiJ546i3`^Ui&(H@Pi-0mZeW)+?<&_M&$S!TWAV%+r!a9 zAKgW^nHyJdx7AH%v5F$}cYiaDl^NL^%n21HTIyn~$Z22UxWq?;(}`kqTI$sf#ah$$ zgWN3?g6IB4{z0(9*ZH$DW<@Sn%SS;L&fhqYV0i2T`F^Me1K zlfK=zxpDIk4#D|G*R}rnU>lERdD*{hoo^X8jm?zd-W^no-!-;|*k*_Wmp3iZa!~5Nynb>HDH-+*A|;)a~EC4ufl+z@)Ykwo>`iDT_e;;^FAt9>U1d_tLg! zF}2zIjs1xm9>0M75`L)Lw|B23sP8rpg#J!j#Dr7n-HG0{Px{vB`?#iY(=gb(4W&{u zPe-+Hr>m@Aj=`h}W9A+tz-xHW7s`zSshT$E)w_~0wwRFOV@g98fZ>W~r&^&b^ z2yWMg2hhFd8IA=vPur7Y|gAC^UD3^M-t2ZdV5)=*-G6wmtm_T4u~g z>y%kcCA|Tb7kt_XoxtHwj6w#eJm`7*_jDZS?M>McK3KF{3zA(7I*vg@C6e9YXgV@_#Vg_2Ce zjxhOR8ly@aVR`4Zv;m^soM(K!TmN7M+q(KFLkP_cWsGfaavTlJ~`8!KGo` zlS-J=@VOR*Xq7U1$_e`p!t;GOO!nBeAV`L**wNQdMJIfFQ-f;=+P$l9(D(KaQ4R?A zd+eCTV3U>o@Nms->+S>fk4&Ocz+Saf>KKmeZ1pV-CD%7t@msrknOGVdieW5TJ#E$5 z8#LpNu6|z&_E=MRpQdo%zQG|3gy&m1H@b$#Z(K)97~dh+J21=w(j|z7d>BjD5XNqM zPrs)yiM&U15Prb+u6{Q#R6{9@O-D;}7@J1l(>LUL2Bg|P9cwI?5#GTMg?FKOWYCs+ zL?=ynSYxT=Tbmkr?tA(NnZO%LWnZlcjO+-GqK5F?uHGPAYZdmbBvr1*MO7u;xWHUg zPphkM07JtS&H!$2Zbo}R8(i!Z}01M1A$?wBIWqD4wS>32B(wX z-qqu1jL7aEN5>n=zI(9Wb$1U0q!C=7#S%JOg5V1C*wfXIo`C@_lsC4m6HP&|^7`>a z(l)(DfP7>UCqAKy%kt-_CKm55kSg%DOehT&OKg{+u*Znf9}J@tP4vhcIOKq;8FR5_ zSIakgv9l6AKC0WOT>%5i%AtuQ%t=+goDCO+j`WZAV09)}e7#`Mpz)JCEn(5B;U9TQ~)^nmZ>^VOFyaQ*N~;##DJ_iZ1kauzj!%-SCn#Dj{PY zd-IwU)ZDU)5?XJNoE!{T67@r9MM)cHa{RGss9EkcvUkomMpIr(@;Xc5loCa;43E4@ z;3(syJj(Jps;eI_mzysOY0yg-5=o3?dPtOLWXPNrbJqn&dSflBhm2Ze?=;8z2T9&f zq9mxqui;PrJnWFqV|G^4i+*e3Qe2#0)bFY~9@IBkv1a#{F)ZmlE9?}e!uOIxQAv0a zz6?Xc+oH%g$_i)hbwT=<)*!qn8df~f`4YNTm9i!iUliw&to(rPJIdB5)u|;fZ&eon zd5;WE3p8diPFp@2vC$I>96Oe`bWrAvwr(Axxcd2Z5!eGcCkEM9;oA_7)itelYRS!P}+2Z*gg&sB!vccR~1fGy{& z(;lt2#o*a=aF6idl|yPUEsCx~vMc=5(Qa~)B%CUyG<9N&PD<`X(5n+&L!2r^=F)l1(xpXg z>k_Lt;c$8NC!juiUf|cd-1X{N76aXm>)&~ z&SiQK$TKfufd?A1TpB1pn>KXTs;Y6Jc~vwnPfj({&_26R{_#0J;BrCnSum)AsYvsAA0vvPBWY2)TJ#vulW^7iHx_6p^q7Mn#St%X^5V4r%ukOkaLuo zYhH<1nX$&jFyvXhaTi1z=Ha%m!)e?|KS_aGlZ#wKOxO(cV)#0>g##YI5#LSdAmP- zN=439yb2~|QX2AVxLHYolD!CXcmm`*&J3A61pjrG=>K`UTbW1_N~voJo(U93qs|`}s^{ z7*2|lU{AEABu?weC^rxh@?>jC2CwgE`o@G6G19Oh79WQvYPtOTyNstZ*Fl!}!dLUc z6z@c&DnW3bd$xX?Pft~tP+>GeqU7^v(Eil2cj7xI_2giywtSi!BQMM^g%M@v0p0jb z=%(asvdq58@(Y|rLL;HejrlE7E&pt`m=L`5X4i@qyQGWdf1C*mKnm4yDIQxyZ-T-3 zY%7QiNa(%2p)0#536}yr)Jg~GZwmhmhMU!Na0J;%oYb}$)n`)HA}bFsf`P}ap0s6$ z%c7ENOrjFqmcWmE;Yc$=1F1Ti%lbnTT48+PT~;q88v(l=SB1>@0kZZED9%^v6OK!@-{poxx|;WBon0ypL>}RljxAGAb0iioV$MMf740MRed~BkXtw{UAt# zg$k9A;gx63N(g0w*)rwD*7gcq5yx504NknNX>NMei-t*cUbRpkS28KWM~cWXziClt z^XhaD>PS*vG?x>!kufcFId(?ytT!@2t=C0-uUziqxltWtjup8^7?uadnN}pkW)(LL z1?d6+-BBREoj${h8C_qg!!Qv)iv(^OC6Jnek@n<4scL=ivuDsX1+SH+;p5EtsL;G3 zikw|)lPOPfhCaO6ED`F(+hmi)jxC=pK7he4njrq`B_Ykj-CS>ecLW&)Qe^^3D7g zT4D$}(GBDu&jQVhuY_o&5D%*Y$O&t@2|>fgaHf=C&G^cOl!yPlk~j2Hr?eVJ#_*b$kpY-Q$lIx=(Zvcfl!%YY zL4`OYUWrh|l*=o26*KgXWS1A!UX{h`vc$@6VeDgwjqJdZK7=$+O}y;B!6PpwqSi72 zm37V`FL$7Wp222s<7jZZj)3iw=wv|D?u=@hpu1*zjI;r~ofo|bK0i;9mgIHevkD~o z7?-SBSPhr`(dm(R1NDjutdPm^u@*E~B+r^^yO!%8}8Ajtrz7%LIrz zKZT;=HFBPzEg$84B6I2*z1lNAIzmk`>ab)n0LS($RHkQ&Eid0(HjBp8NgRwUj00sK z`Be1cT?>vKPut@X!xW9QwUC`S8eC3M`MRl0GP}&>=+|t)oE_sb8W3@YoK-B2GKdXF zb$z)wx)VE+y}eLdPqjzZ8~I$-hY=wS$$ZkbDbW*yNVzWe8P>U6-&8#1<($}0PO@jr z(PVwGH_E|(r%Nc6T8jDP6ne+h#VQq1OF55BV%x|$7)okrk72zy)mmmJH!fby87kri zE|NZ^-xPDRmU~ieEX_Rz6AmsD%6U({F}*NR)K^gwxQxz(0eFj4E+Y(CQD%he(z0PHt*O`3xu{b@@@qC$0A2#KKG;VZt$6HW$9ZYPxYqqeP+CY<4}A!w|$D! zZ6!s(esxh392QS#AVh|rGsys_z!(}nUY@fH%`1BuH7hYXPrN+ykEIVIH| z8-$eg5!*F7pjbm9YBKePWj%B@yi}-f-?spVz6YN6rh?0yGl>=+H{}bju}SF88rmu< z$dcUq*T>F%#+3QtsTX?4s7`r(7s#(u>{JCg$<`AG8qE6|`_IVFt0PVsKD%X zNpAj#@p-$WvKJ8)>o0rtgD6t~dB)79N}1Le#v^;SxM@?D-OH{~=5{i#T-lO-vo@o4gej!Es(=DTB(MLmBK{%#krP z=BOotGjj17bL^M^OxVZK_9?D!i|4X0(+&V;>mIIDE~a-g+{UNjH$EBQN2A(JL#92X zKh;P?4!%xZN;r&$iM*dRQ(y8b?2a!>&>U5>Je@E-*LQY8#A%+;q9tHw>ts5cR2Z%1 zcO!cy@OF{YC~Q*>Ck(la9I5H@JE1M< zvkJ!uXLU#fNaQ`(UU#OD6Eb2*95P;rM3PN4{o4?3Md2gbbcxY{ms;GmrELnUaH@pk zbUnO}fKR(4kEau4`z6G9vj(_1po8dWfyx*!UDpkyTpVNRw~1CAAg|s_r*>3)EK4Dc z-1vrexJ75<*GnJ4%(0Vk^uEXOExHE!am2-+L1$A_I6fXn&tHJkmK=zg^N!;>QQUgB zPz@%BZBOUDu(8iB=j`WS-b)4if_g>uOGXmI%&Y-6d+~z%oXsg7SxtmM^dvh2{!XB& zQ_pY|Dv`OPj7P?3abjJ)Q3gXj#5eC)J+u-}5f%&Q&%wm#*xTyNw6S_9*iDIL`c8$Zt!KsIi& z)d6Rv`TlN)jnCTFG&1F629#m_2giC6g$hKYK&#rTVOBy?n1e!*vB=eS5koJbSQg@F_x9u=6 z;#{$W!py0GCX38JwU;*{9T<-I(Z(SOQJHjD1&=xt#ZjKJMF|zx+7DY%D7A$l)bj0TeW6*OO3ZpE!nKaC52e>G+{q*%6d5dU`a={PDd?5yzj*A|ws zrhhAbaL;pCy=^N$=jP>)qazH$;%PJSZ>OA&nN-Vp#cc*>Vf4o?N$=IWrr?qOCc?8l zflYVP7!1obWT&_!Q{a(r3i_@E*j&o@`Bhd!Ic7BmG&6zE^|C*cFrb`ddE)T7bW1rW zV~8lNRbV(>vf0R`W-(!&qw0w1i$Tk-_%Rg}z{r*^zZ8Z2zk_tyHB?$iHJ(nc>;dc| zngXIpVSVE_pmStV3qyV_0V$UW(jw#_mrd9QJEtFx?>w~}-@f^WfpLd+>C^1C4*dKz zr{K652RJOhK8#4Se%xJ1`+8Y|LNU%06mYiG{K zFP~i=1XCkEviJa09=#{qHIvNArXtFV55jk!c>+7PZs+?_>nt~5c6$eooi_{n?z_kh zKlvom%{$@7clxI$b-3$ad@cyOnK)wlB&5%;R?9{9nu@z4X8r^%ODj`SWH~ER(AmsN z?yY(9R8U)PFiU1(BVmym7es&~fM*uHgqOpTf!CU4i>Qa}nC6?(dTBwLt%7 zSS1lDNC5Mq3Zs}Qp=+9b%|xD3GI`U7gnu}BL{cgpD9x^tF(wQy@vF&Bg^rwF6zZrT zm$J!EbIK(3a4_dEwob6UTg!WIvs*YsRW6kk2LlA+nMSzsJmTtEL==H{SGn zkmlW8Th-Hr{@47153GFCJA%xuxnf#?F^_P& zanK}rHw`);{OO5A52f(lb zd%=v+MXEcM*t|$Un2U*pmfH;v51?VoOF`?Nfe+sG0B$~PEIzl-1b^A`$9X*OUAG=H zr#E4F*C5_8YYe`=?__i(ErBUp@WXM9_`pN!aM>NV0gFw@pB8s0qo$z2UoFcD6^Fl7gpvVFo2tUDqwe8KjS#`YJFwE^1(BcAEJa z8iU7s;fXl@>Yp=%zM6LNBliaF)qs!f`FiJ{x!$ywq!#=9#$Lc3XS|Iy_sB-tZ1~yj zXlftt+{6j$u^wjM3VOFea}zinmORmp%J7bz8Z>j6)(-}=FhH=Ko5=fH_dHg*t<6ZP zoVV!wK);L>o!8Wg;`BZ6i#u=Q{u9dh&J&N|8+$Ewwe9z^IYcy;Rx)qzaZ_i6caTN< zWKNKP`{Lj3!!K4W=kfZ>31^~zS^YfLxNdC9T9=+pbVj=nMzfTuDwi5pfZ4!uex@0i zv?=9L)*{8)w0a|ZZ-t=BC=5|tDjs3ZmrY5zCB9?PNp55NEL{1!8!;;l(7}(z2Nz7m z_g{P-UE!r?28c~}x@U7A{`i{dEM1QH<74PbOL+OLB{+IQ3qEk`4fxGHw_;~n#L@E> z1e0BL-S4Z45hKVXpHD< zDh_A3M%iM++2GsiAr@v4?kI%QUp#y{2!}lUE;f);&$-F_1Y7Az_!ob~P`M5!6XTAV zRyXOq(?Dl9*3v28H8h9UeK8Po_$iKqoB9S+R@@T0n;?b?6Yq3eOcxzH^ieae#Gxtk z%8yw6K1x}psjag z6e!6iU;inz*{vPu{mT_x{X|A8VFlAyF8c+ie88;^L(Q5xyg+A5Blep$5wmN3NT2y@ z5bnKV8HTa!nz|TwZrX&M!@(wRYhnd}Dui(KgjRpghOKValsUNPg%wfQ+@0ba=XJcQmmp7w(=LY}w@89h@ zIwxWE_HCFI82UrUoq%^7_)d8tbAk;9r^nhygClHdEIlh*e2fSf)4(JyhRo!hVGb1` ze6v?`+X$EtVB>?9omJCph-@RYXkRMC*F_P0E!pSO=?pd;ev?=626pSb_8@$aa;X3nLpXLJY5%v{a4Db1dXxNHAd&DBb6 zZfzLiJ+9pK*$mP5KLhFgR|0z+geTf(288glUF>qw?)wE|Pzqtj%-`?b3l`zO=j|Ej zq{iT+G;0CA`1>oF`QN>8F;=r6x)0x3_6R<^$2{Pr6|`VG@ItYUiD@T_dDEsM3Eb8d zgh;uk2m6hi#0kL*2Of0dK{-1yKLtMXG=MiCcSQ38#l(3Z5I__nU?m* z5wV&B%B(QwhpoaQ(?OTuacLfM$OPgY4o__>iU;Ti&uz!O-4osNjjQp&<4(bn>C>@h z{YxBfSQg+jd7^!7z%A<@3LfedOU8Gy23_4(30m4Sa}gtBumMLm_qj!58~G&{cg%F; z82jLLSMqa6C@f$1a-9=w2JMWn;x=*T>v~$3#Un(gz;l9z`)inr#Ep{k!mabDcX6Gl*a9u|K66)agf_5KY`^i7SX)v*{^_rOWbJ7;}eX=i2+0(u8cfVogH8{R=cnD;3?l*2C`XAl^+o_9S@wbkAZF~%!TkYGyTM@ab zd$_xIZ0BbXV{cyhEIzj6V0?S|BmDD)cYO#$t6m7hC^l8T5iK+|meEiwqp76@jU8j^ zzE6Gq`^rv=ockP|SB41L(Ogw+q9!ft4E|Ex((Z-Rs%Knuf^|#-?epU;rO&}3b5cjJs~|WJHlW_9Omn4x%O}D` z2GBp*siHHJsB@|iW4BIXw;$P6+vXXTn6-TZMs1{pYp;%heapU%ijLE+@DjjLgQ#fz zS0F8GtYaBry@m0*WUf&>wq+uRibiXseTivq2uym+lo`y-SFCul?i^x!^1b?*g&*>&)$UrstvaA5PUl@;F-7n0oMn!?BGe$ z9qj={&)IMML>x740-m|~3M7>wfB4wRu5bG~-{7+Hsh#CUoEz@F@~)euKtXu-%{w+= zT4$RpPMd>YKE9Nnf6>85Vdveq_{mMp?zky4SrFZ|c_R+%40O76lN-59Q+do-6!tw3 z#m-KYo125l@i1#&5)elRO}y^;2Z4Y-V{@4YQ}PPbLoYrhMQy5RDq^N(I^Q5v(TP2x zl&J;WH4rT!cg})IHI!G-@6vd^RDsZh2wKQIylsPf{^lQ0$j#4f9p@^+Q@3UAfkDeA zSTmU5%I$5qcl){!4{pYPuj~u3q{ZL&+PO?PHw7o(JzK)D#Z;!$GxIJ9Qr7i%`>qgz z`s(xD=vwv=-tqRs0@N-9t=o*RFP@B>whm*pAM3upZzGo9y$Ux5t$YsEg$j0Z;+$#R zY!`b6A6F51D|Mlx33IDsM3RM9XB5Pv8@;p|dNG0AF^xUR=$MFf)6^9a0}=KGhSH0>=Ie&IIB)->;?IPElpR}e#7a+*YK8)KHTm_h_sE&NB0nUU?- zslu?-KC<{ghR@%7^3hTIMb{V8vhH>$9(|v~^rTryGv?c@5Q-s%+*GWgW7Yu}bMPq~ zsv#Xe%kCR&-Lx={D{>N}JRStiCCirLW2c^n@|Ks~j%OZ*TlbV}YH6;!cN(rMZCQmv zxe$DsMRWv*mTPRmnjj#^{w1f>j1WHB=qqku?J9h1--DQe$uV{NQLja9+r~PiMoz2q z@BK4>CPDJ}wgIsiPUlT5*yx?EJ^3tbS-zah2<03Z1VSP-RVeslg+!RV}iSP z<96=;toByk>vwhkFeitsZkvxK{ZIRwo_^k)vZgb5v|Bk@r0RRg_Z0J3c)uAIWcqh% z)LI|gMNUnt3KpA;$Hy|Qat>d3WKaB*c8T zJaGp@XAWNlX#MTwe+wFXPh51^X_&cV4F;dSukM;cxMuR$4qs}dkcvt?dDB2V7gO*T8qKUmZUaY^M;|zl1n(|I^Q)@9C#~=bn4I zk$sn3+r&k4OxM{<@KzYtTM}>ht|*>o)wl&M$pZabC(NLA-m#;qN}OI*=Ul#4l~oy)`X^pB8Dqz2rG%I?j9O)J$L-bzi#4Ocjl6roO#uK@6}lPiO=Ag@Pezi zZ^f*@+*h5r5a;~l`}n|PTb!}dl5rg{uQhq2XBGz8-k>nlX)`C_!rT55l-+DcTALX9 z$iHmD(eoFg`@UbIJ$O{9fV)>)&zx>%hFM0EqU%iK#sOAuwaCeO`$|7V*sKXXEM(>o@^} zG%5X%a5!ns{=0UJ3z#Uv41X4;&BJ$Y{2hNqLV$ioYrhO8!iR#>Yxt!X>fZ78@Eocs zO>Lm78-1aV>u$Lb4c)zdV#k=^h^p7X%AQVLdZG}_%&YSch`)d|h%B*(5^SmtDySo> z7G15#C{<#`(Nk%M$zjJK1|Y8TRSiyi36z=|d{bk}-ZfreLXNEf+4{Gx!sab6a>ffO zz^8tH2@V}M9mlkd3;hWpr!{@J@%dnFXJaHO2 z+B>ncd0b%NRahK|= zfoVP_jA_M|mmbEmPu`CsrUxZ({7LxXsu%ej@*bah?Ca`SLOd7Z#NhGhoQP$8{j6cx z`y3otBO5uzz8&s$T@YFyJ>^Zn^Uu|N-xpx?GuQn#DA58+!6V($*o284?Y?E~Sn^C` zG!d6TqCINUxtxA5lTRmC5Th+1Q*3OC)pBs3OFZYS294hmymM5B8kN;pl}+(3}pTZ?M~k zYn&#YWC;sa0#zM4hB&e;$IK-V%?BEiRBaIgABD0{T3cLo?U%zl2iP}t5BxuG6#Lz= zE@=C`cl|DD3ezwho0t8!AJfw0$b?DE5$SZ5nOX^6lIvv-OZuJp21s~oMGcup=UPH& zXZwo#e9(aI&g+GNzqfqpt4in~W_bIwY1p~?uddkQV!}_ohLKhXvDNSs1?b>#zaxi} zHByrkCQs#F_Mlz>lo!4p(?V!&+~!s236YLXPu+(L4tQON;oi&rbft1!s6c;r7s_av zcOa6$WEek_@BN(z?&j|=I_OB8oK{0ba;H6+dpkXt48*)7RPNfc5&H-8uC5OO0nD8- zIfSc1*ex(D$-$&l;7C9%O-FfWAwt_a$7>M`GtH1UU+J%!tpIiU6Ym+dxTqm7DT&Pe zY1Fu)-fiWNn$8@WkrgaLAT_lKwNL`D3`uC8>H^yxO$}wP)12zUz)Cg6Q1Bjy-|RUJ ztc{_esvq3B$@Ohn$2ZyX%sq}Zcte2FMk7F0@UGwjd%|}bXDkjYIwA?7WXhF!}lF8J+Wu0sG%@~ak zM{Q)=5%YC#7A;B;d(R{B54ACYc-KB9;iOSuIla=@v10vat6>DXZR z4ptJRSt+JVDV6kj=g>~fj?bpI~qqPj}CPghPi|Ciq)l{}`nb!@z*~{o+t>(J=OkQ)F#U8eJ zioiyZ)Vz?MomgIPi^7YFhI{1HmCiPL6>txAuYSmc{u+#W0hI+PR-HqQuQ<9=# zd|NO(6&5Wzhxdk+p%QWwnE$kA>~)(a<;_Y4H4x+BQBP za8;1vJ7p)s7%rN;=kD!TV$N=b$s?JRcGsJG$mlk8a9+8A2Ql^F(~+!xC`=J0c@Ij0 zfSn_Y2_nr1kct^DoMNhTuG$(>!#Z3uzO5N+@BIzlcl`P248%^}*jHj=^5XvdHK(HC zg$I#tdD1m?wj&A=DUrl(o7rvcpdUN#904lJ&P6DGw8Z~Lu|Mh zy#4I)4W4X@)mkH4ne65ZLach@?N^`_LPj5c?YWNn{fiY@o-_4TnAeAx??EW-*o<+( zLDkyY$#$pLm3m!U*81}fKHgD3==|2;joupWMg8b(P^3T_lv5&wuh?WOJ4$Y2mJ5i2 z-?@W|x+l80#nva(v>q^o=(xNta>U1zcIP)m9c5WVwk|dEJ{P>@#v(tUk*ew6l5|r3 za#_t~OO{9^*P3%z^BREqoa8tJYK53KgoMD@MrPlNXJ;5e>spnV+4E-8!NW~?*Yv%w zrV{4jTD(C*Gz2I!COFbMTbodBEM&Rf1G-|s@e9J5{dw=A~1J7*R#EP0oAtPj6;2=fx7QrPJ4d7MXS$+$Nm zX*-M|HM(Dhf9$t5XHxv2<})udMhNJF9_KAcRT?Xu&|kC*!s^Z2F=qdhF*Iv`CSr0H zQ3!}uQFwau6H;ngq(Y3OTv zb3;6j{GXe5bYp1jLX6+%XtYk4&O$>%PK~@LF=^J#OK(E+l!fS=y*QjREee+D<#KQ& zt;f2DZpOqy1;s)EYr3nLdCVKpIel)t7q+bDy_sY`=`a;b@A6|R+e2_kT}GrC13KB> z(~rVFhoLq20;mzhE(TOUJ0()jG{a=;-d1i?aT zrTekR(Av`%=r-YX#lh+$%`G~XQLKZlFqL?HI4`~P8{SDp4XKVNu4}42YXs|U=LS*x zE}Q7wob!q#58cVoJeZjyBr-rjFV#JiT+Wl54}|9mIO+lDz?tf!I9cjQ>(Z7O1e@r0 zt>%NRRN*aC=;6g+bI`URZH*1STq=?&;0z_G=UoJo_t7KmayvBmL;+lsTfWXw#|zEF z^}Pc_=qXRc)MJ9@am^R-s+%z^rA8978pF0}T$u8kU&(7tjK1+XS?KUeEiiC#`j=6? z=X)q5lGVw@{f5mL*ws156URmAQJ-X_%K+)T!J!J)Z|_F0YsC1yjz;^GdCXYUn4tm! z+WFFQ^sj!*k83SC+ES%hNHP?_A*3*uLHMuV+U>S=^CAM?#%1P&PBfN_bqI*&nYPE- z8VG%Ouu6MdalEuTxWgC2F?%6qiztYi}{iH6J=x~L%nv04Vj z@><@min2l0$~bdD${Jc^L`Kf!AgMIfE=GZvscR+H%mi6@i{fB!CpEQl1s>PgTX9hX zSD3JjGp89ztRa-?C4bT(Q)04!L-JlLJJtk&(j?lB;;*}F#H7Zs{puJYkMj80z??&P zLBgwU%^h-|-Ppmgh5q<-3uV*MTp2r8HA>zJ)$OzZ*gpgXV^=qToQqpQKpP<_Y=mnaB3xG zXcc4$t5v7q=LhEZlg6~Vu^r8{cT`|}@xHl2fX)e}qZe3%5q(YSOxdSG`6vYbieo3S z5HFBJM{{+?x3{1@Xxrg%9_@cs2y<61mDsNV-O{8S>^fdH>txOe+S2ichN7Pln6A>n zRT#-h%S#xmH5ohVOI*1UGgcMNguhyX?LKG9B(kT&`v+fjIT7Fg5R?zq3+XOj-3XrY=Q^gIp}r>S2jZ;k;^dEepexW9ft<60((6*$ReC zJJBZLm!;)%3(J=hoEa&rvC3MEoWK<6ZNQnSh;Y+e^(u-PPwH%j5XJ6aoqR%`T? zT1^%@RZ}MDQlUWMPN(a{eY9&G3w+Bk;>Xcr3Pr5~M_toOnYIE){*~>WLadiIWOrJq zP^co2KAQiPHdK?f5Kgv6sY;rJ=mr&9;gX=`eM3pLBWHv;VcvuT!!FenVu@C$8RZ3T za-T~Ujg_IH__O^Dud0ZPJm|%>gv&`%UxVN`Ei0?|YMKy~FDIyBPg%-KE5j0b6g?}8 z=|)Ddx<&QOn>Ts&Eo9VknOh|4%9_ZLs<)N~zNS~`%9H4Eq)m%;BQXg_DK?fnJ3|K| zA)?$y9RRS$WPfXKa=1$~%uEQ1pit?i>z8BZRen)J=BV=eOobaHZXs)qm9BzE-H{gV zTl);!j(8`M3Gaj-UW?kspYW<6Zp3^9b$4gWk`^V;bbpup9_o{H_(Si?RnMb(x>WTbwn@ zEo!t^NzI5MuImOr%r?MRLL2pmar=qJ@48)W*+X*6nI(*c$Hcqvi z;vrhC_(<|n!c40k#=Kr1hul`%AOrAu)SH`RBXxRtt~MXBLqrj}qD8ixFwko-nw(%{ zTc+7)@=R0W#gMCm+HtTgZcAOQ)@MychIHbn%G~0ypjie9kz2mtA)eQwbim~*XYfUd zzEL$NMhLM5i5)^)ZriG*cxd^(7#gl(+P=qO?hzkDb?sL(NS$Xw^DvWkHHK{-BMfN_ z`)abbCd|&D_$xFLD1`}=FyzYN^{Q%)IOAFeO9qWEi%1U~IpA$)RioJ4r|LE>N)0mA z&aub?mv^m3K?hS3vXU$3FM%?O$xC`nMF1Y+$}V;|WQxTKCCE8B6HNUv3F`N{3#h;o z6I0a=TeqFW{HM%hu*@DhP}66S-CrH4(3~=CDoaTdtNZL^u|dk=7JDzdKo?)>A|ZQy z8B@NyGXx@A6Gc|!?Nw?zaMZgJ@prB|A>)CHcFtY{I`~N@O_h`D%PB#Ju@)JxzC4XV zZAu0xiu4h^rYMd|yq456T2(DsLj8K$NQrQfBr;oBEerManJ^-OQFSiY56RPJ2%W}M z!ps;@s7;eqNM#2)BV{^QwY7S%Vcf2{)LK;_3G%K&k#vG&wK z%n3{#M(Z?`DZQV#FQq404b?LYoxaI6 zmCC^utkUM)t}t@NNv$HpHF^Fkdzgv2*(O z(w7z5*%MrxUK7mo8b_Q}gfK$5h)XbWp$GB$^Gzx_G@@(C5#5YVcEzZ84Q!bz_o&mB z&NlTXFEX8}RUX|4L1?nj#pP|JWY6|PJv3ttSoI+agP#;4`B`0WTuOgJ#q#S z^T*`PRhnkTZ#F|vQsi)vLoVaR!eI(oe7sN|l{r|9isJ8)%RWj)Q?g}4S6_AD~$}8;T|SJ+K8Gr5~G9F zIO+16YehSs-y}d4j#H Date: Fri, 31 Jan 2025 17:02:11 +0000 Subject: [PATCH 123/123] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9dd9cc65c..12d6487aa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔧 Update sponsors, add Permit. PR [#13288](https://github.com/fastapi/fastapi/pull/13288) by [@tiangolo](https://github.com/tiangolo). + ## 0.115.8 ### Fixes