diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59754525d7424..6fb4947ca7375 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] pydantic-version: ["pydantic-v1", "pydantic-v2"] fail-fast: false steps: diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004.py index 64099abe9cffe..44ed2ace9c00f 100644 --- a/docs_src/security/tutorial004.py +++ b/docs_src/security/tutorial004.py @@ -1,8 +1,9 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Union from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel @@ -78,9 +79,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py index ca350343d2082..622c11ea391a1 100644 --- a/docs_src/security/tutorial004_an.py +++ b/docs_src/security/tutorial004_an.py @@ -1,8 +1,9 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Union from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel @@ -79,9 +80,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py index 8bf5f3b7185cd..a903914ff6bae 100644 --- a/docs_src/security/tutorial004_an_py310.py +++ b/docs_src/security/tutorial004_an_py310.py @@ -1,8 +1,9 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel @@ -78,9 +79,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py index a634e23de9843..68cb169748255 100644 --- a/docs_src/security/tutorial004_an_py39.py +++ b/docs_src/security/tutorial004_an_py39.py @@ -1,8 +1,9 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Annotated, Union from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel @@ -78,9 +79,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py index 797d56d0431ab..4b52ffdf589e3 100644 --- a/docs_src/security/tutorial004_py310.py +++ b/docs_src/security/tutorial004_py310.py @@ -1,7 +1,8 @@ -from datetime import datetime, timedelta +from datetime import timedelta from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel @@ -77,9 +78,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index bd0a33581c21c..fdfdf5caae455 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -7,6 +7,7 @@ OAuth2PasswordRequestForm, SecurityScopes, ) +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, ValidationError @@ -93,9 +94,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py index ec4fa1a07e2cd..d6bbbe56a1f3d 100644 --- a/docs_src/security/tutorial005_an.py +++ b/docs_src/security/tutorial005_an.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -7,6 +7,7 @@ OAuth2PasswordRequestForm, SecurityScopes, ) +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, ValidationError @@ -94,9 +95,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py index 45f3fc0bd6de1..185e2e09c2e3f 100644 --- a/docs_src/security/tutorial005_an_py310.py +++ b/docs_src/security/tutorial005_an_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -7,6 +7,7 @@ OAuth2PasswordRequestForm, SecurityScopes, ) +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, ValidationError @@ -93,9 +94,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py index ecb5ed5160d86..a29394088ab32 100644 --- a/docs_src/security/tutorial005_an_py39.py +++ b/docs_src/security/tutorial005_an_py39.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Annotated, List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -7,6 +7,7 @@ OAuth2PasswordRequestForm, SecurityScopes, ) +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, ValidationError @@ -93,9 +94,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py index ba756ef4f4d67..33120e632bf82 100644 --- a/docs_src/security/tutorial005_py310.py +++ b/docs_src/security/tutorial005_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from fastapi import Depends, FastAPI, HTTPException, Security, status from fastapi.security import ( @@ -6,6 +6,7 @@ OAuth2PasswordRequestForm, SecurityScopes, ) +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, ValidationError @@ -92,9 +93,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index 9e4dbcffba38d..ab24c7ee94b31 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -7,6 +7,7 @@ OAuth2PasswordRequestForm, SecurityScopes, ) +from fastapi.utils import utcnow from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, ValidationError @@ -93,9 +94,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/fastapi/_compat.py b/fastapi/_compat.py index fc605d0ec68e3..397c446dc1495 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -1,6 +1,7 @@ from collections import deque from copy import copy from dataclasses import dataclass, is_dataclass +from datetime import datetime from enum import Enum from typing import ( Any, @@ -627,3 +628,15 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) for sub_annotation in get_args(annotation) ) + + +# in Python 3.12, datetime.datetime.utcnow() was deprecated +# in favor of datetime.datetime.now(datetime.UTC) +try: + from datetime import UTC +except ImportError: + utcnow = datetime.utcnow +else: + + def utcnow() -> datetime: + return datetime.now(UTC) diff --git a/fastapi/utils.py b/fastapi/utils.py index f8463dda24675..02611414b768e 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -24,6 +24,7 @@ UndefinedType, Validator, lenient_issubclass, + utcnow, # noqa: F401 ) from fastapi.datastructures import DefaultPlaceholder, DefaultType from pydantic import BaseModel, create_model diff --git a/pyproject.toml b/pyproject.toml index e67486ae31bf8..19999fa8934cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,8 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] @@ -107,6 +109,11 @@ filterwarnings = [ "ignore::trio.TrioDeprecationWarning", # TODO remove pytest-cov 'ignore::pytest.PytestDeprecationWarning:pytest_cov', + # TODO: remove after upgrading SQLAlchemy to a version that includes the following changes + # https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2 + 'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy', + # TODO: remove after upgrading python-jose to a version that explicitly supports Python 3.12 + 'ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:jose', ] [tool.coverage.run] diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 541f84bca1ca7..2222be9783c08 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -57,7 +57,7 @@ async def unrelated(foo: Annotated[str, object()]): { "ctx": {"min_length": 1}, "loc": ["query", "foo"], - "msg": "String should have at least 1 characters", + "msg": "String should have at least 1 character", "type": "string_too_short", "input": "", "url": match_pydantic_error_url("string_too_short"),