Compare commits

...

6 Commits

Author SHA1 Message Date
github-actions[bot]
62dbcc4ed8 👥 Update FastAPI People - Experts 2026-02-23 12:20:45 +00:00
Motov Yurii
a3c8c37272 🔨 Fix FastAPI People workflow (#14951) 2026-02-23 12:44:47 +01:00
github-actions[bot]
2826124378 📝 Update release notes
[skip ci]
2026-02-22 18:22:03 +00:00
Sebastián Ramírez
4da264f0f3 👷 Do not run codspeed with coverage as it's not tracked (#14966) 2026-02-22 19:21:38 +01:00
github-actions[bot]
c5559a66dd 📝 Update release notes
[skip ci]
2026-02-22 18:14:11 +00:00
Sebastián Ramírez
1cea8f659c 👷 Do not include benchmark tests in coverage to speed up coverage processing (#14965) 2026-02-22 19:13:49 +01:00
5 changed files with 583 additions and 484 deletions

View File

@@ -68,10 +68,8 @@ jobs:
python-version: "3.13"
coverage: coverage
uv-resolution: highest
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
- os: ubuntu-latest
python-version: "3.13"
coverage: coverage
uv-resolution: highest
codspeed: codspeed
- os: ubuntu-latest
@@ -109,20 +107,10 @@ jobs:
run: uv pip install "git+https://github.com/Kludex/starlette@main"
- run: mkdir coverage
- name: Test
if: matrix.codspeed != 'codspeed'
run: uv run --no-sync bash scripts/test.sh
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
- name: CodSpeed benchmarks
if: matrix.codspeed == 'codspeed'
uses: CodSpeedHQ/action@v4
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
with:
mode: simulation
run: uv run --no-sync coverage run -m pytest tests/ --codspeed
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
- name: Store coverage files
if: matrix.coverage == 'coverage'
@@ -132,6 +120,39 @@ jobs:
path: coverage
include-hidden-files: true
benchmark:
needs:
- changes
if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
env:
UV_PYTHON: "3.13"
UV_RESOLUTION: highest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Install Dependencies
run: uv sync --no-dev --group tests --extra all
- name: CodSpeed benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: uv run --no-sync pytest tests/benchmarks --codspeed
coverage-combine:
needs:
- test
@@ -176,6 +197,7 @@ jobs:
if: always()
needs:
- coverage-combine
- benchmark
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context

View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,11 @@ hide:
## Latest Changes
### Internal
* 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo).
* 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo).
## 0.131.0
### Breaking Changes

View File

@@ -242,6 +242,7 @@ relative_files = true
context = '${CONTEXT}'
dynamic_context = "test_function"
omit = [
"tests/benchmarks/*",
"docs_src/response_model/tutorial003_04_py39.py",
"docs_src/response_model/tutorial003_04_py310.py",
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?

View File

@@ -5,6 +5,7 @@ import time
from collections import Counter
from collections.abc import Container
from datetime import datetime, timedelta, timezone
from math import ceil
from pathlib import Path
from typing import Any
@@ -15,12 +16,63 @@ from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings
github_graphql_url = "https://api.github.com/graphql"
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0"
questions_category_id = "DIC_kwDOCZduT84B6E2a"
POINTS_PER_MINUTE_LIMIT = 84 # 5000 points per hour
class RateLimiter:
def __init__(self) -> None:
self.last_query_cost: int = 1
self.remaining_points: int = 5000
self.reset_at: datetime = datetime.fromtimestamp(0, timezone.utc)
self.last_request_start_time: datetime = datetime.fromtimestamp(0, timezone.utc)
self.speed_multiplier: float = 1.0
def __enter__(self) -> "RateLimiter":
now = datetime.now(tz=timezone.utc)
# Handle primary rate limits
primary_limit_wait_time = 0.0
if self.remaining_points <= self.last_query_cost:
primary_limit_wait_time = (self.reset_at - now).total_seconds() + 2
logging.warning(
f"Approaching GitHub API rate limit, remaining points: {self.remaining_points}, "
f"reset time in {primary_limit_wait_time} seconds"
)
# Handle secondary rate limits
secondary_limit_wait_time = 0.0
points_per_minute = POINTS_PER_MINUTE_LIMIT * self.speed_multiplier
interval = 60 / (points_per_minute / self.last_query_cost)
time_since_last_request = (now - self.last_request_start_time).total_seconds()
if time_since_last_request < interval:
secondary_limit_wait_time = interval - time_since_last_request
final_wait_time = ceil(max(primary_limit_wait_time, secondary_limit_wait_time))
logging.info(f"Sleeping for {final_wait_time} seconds to respect rate limit")
time.sleep(max(final_wait_time, 1))
self.last_request_start_time = datetime.now(tz=timezone.utc)
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
pass
def update_request_info(self, cost: int, remaining: int, reset_at: str) -> None:
self.last_query_cost = cost
self.remaining_points = remaining
self.reset_at = datetime.fromisoformat(reset_at.replace("Z", "+00:00"))
rate_limiter = RateLimiter()
discussions_query = """
query Q($after: String, $category_id: ID) {
repository(name: "fastapi", owner: "fastapi") {
discussions(first: 100, after: $after, categoryId: $category_id) {
discussions(first: 30, after: $after, categoryId: $category_id) {
edges {
cursor
node {
@@ -58,6 +110,11 @@ query Q($after: String, $category_id: ID) {
}
}
}
rateLimit {
cost
remaining
resetAt
}
}
"""
@@ -120,7 +177,7 @@ class Settings(BaseSettings):
github_token: SecretStr
github_repository: str
httpx_timeout: int = 30
sleep_interval: int = 5
speed_multiplier: float = 1.0
def get_graphql_response(
@@ -158,11 +215,18 @@ def get_graphql_question_discussion_edges(
settings: Settings,
after: str | None = None,
) -> list[DiscussionsEdge]:
data = get_graphql_response(
settings=settings,
query=discussions_query,
after=after,
category_id=questions_category_id,
with rate_limiter:
data = get_graphql_response(
settings=settings,
query=discussions_query,
after=after,
category_id=questions_category_id,
)
rate_limiter.update_request_info(
cost=data["data"]["rateLimit"]["cost"],
remaining=data["data"]["rateLimit"]["remaining"],
reset_at=data["data"]["rateLimit"]["resetAt"],
)
graphql_response = DiscussionsResponse.model_validate(data)
return graphql_response.data.repository.discussions.edges
@@ -185,8 +249,6 @@ def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]:
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(settings.sleep_interval)
discussion_edges = get_graphql_question_discussion_edges(
settings=settings, after=last_edge.cursor
)
@@ -318,6 +380,7 @@ def main() -> None:
logging.basicConfig(level=logging.INFO)
settings = Settings()
logging.info(f"Using config: {settings.model_dump_json()}")
rate_limiter.speed_multiplier = settings.speed_multiplier
g = Github(settings.github_token.get_secret_value())
repo = g.get_repo(settings.github_repository)