Merge branch 'main' into instruct-LLM-not-to-mock-anything

This commit is contained in:
Kevin Turcios 2025-11-07 14:44:52 -08:00 committed by GitHub
commit 0af276990a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
153 changed files with 12062 additions and 3397 deletions

22
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,22 @@
# Pull Request Checklist
## Description
- [ ] **Description of PR**: Clear and concise description of what this PR accomplishes
- [ ] **Breaking Changes**: Document any breaking changes (if applicable)
- [ ] **Related Issues**: Link to any related issues or tickets
## Testing
- [ ] **Test cases Attached**: All relevant test cases have been added/updated
- [ ] **Manual Testing**: Manual testing completed for the changes
## Monitoring & Debugging
- [ ] **Logging in place**: Appropriate logging has been added for debugging user issues
- [ ] **Sentry will be able to catch errors**: Error handling ensures Sentry can capture and report errors
- [ ] **Avoid Dev based/Prisma logging**: No development-only or Prisma-specific logging in production code
## Configuration
- [ ] **Env variables newly added**: Any new environment variables are documented in .env.example file or mentioned in description
---
## Additional Notes
<!-- Add any additional context, screenshots, or notes for reviewers here -->

View file

@ -14,6 +14,10 @@ defaults:
run:
working-directory: ./django/aiservice
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# This job checks if the workflow should run based on file changes
check-changes:
@ -60,10 +64,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install Project Dependencies
run: |

View file

@ -52,14 +52,12 @@ jobs:
# cd django/aiservice
# poetry run pytest
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
uses: astral-sh/setup-uv@v7
- name: Generate requirements.txt
run: |
cd django/aiservice
uv export --format requirements-txt --no-hashes --output-file requirements.txt
uv pip compile pyproject.toml -o requirements.txt
# https://learn.microsoft.com/en-us/azure/app-service/configure-language-python
- name: Zip artifact for deployment

View file

@ -60,10 +60,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install project dependencies
run: uv sync

View file

@ -63,10 +63,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python 3.12 for AI Server
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install dependencies for Django server
run: |

View file

@ -63,10 +63,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python 3.12 for AI Server
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install dependencies for Django server
run: |

View file

@ -27,10 +27,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python 3.12 for AI Server
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install dependencies for Django server
run: |

View file

@ -63,10 +63,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python 3.12 for AI Server
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install dependencies for Django server
run: |

View file

@ -29,11 +29,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python 3.12 for AI Server
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install dependencies for Django server
run: |
cd ./django/aiservice

View file

@ -63,10 +63,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python 3.12 for AI Server
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install dependencies for Django server
run: |

View file

@ -63,10 +63,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python 3.12 for AI Server
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: Install dependencies for Django server
run: |

View file

@ -28,10 +28,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: false
- name: ensure pip is available for mypy
run: uv venv --seed

View file

@ -4,11 +4,42 @@ on:
pull_request:
paths:
- 'js/VSC-Extension/**'
workflow_dispatch:
jobs:
check-min-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract MIN_CODEFLASH_VERSION from constants file
id: extract-version
run: |
FILE="js/VSC-Extension/src/constants/cf_min_version.ts"
VERSION=$(grep -oP 'MIN_CODEFLASH_VERSION\s*=\s*"\K[^"]+' $FILE)
if [ -z "$VERSION" ]; then
echo "❌ Could not find MIN_CODEFLASH_VERSION in $FILE"
exit 1
fi
echo "✅ Found MIN_CODEFLASH_VERSION=$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Check version exists on PyPI
run: |
# Check if this version exists on PyPI
VERSION="${{ steps.extract-version.outputs.version }}"
if pip index versions codeflash | grep -q "Available versions: .*$VERSION"; then
echo "✅ Version $VERSION exists on PyPI."
else
echo "❌ Version $VERSION not found on PyPI"
exit 1
fi
build:
runs-on: ubuntu-latest
needs: check-min-version
steps:
- name: Checkout repository
uses: actions/checkout@v4
@ -17,7 +48,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache: "npm"
cache-dependency-path: js/VSC-Extension/package-lock.json
- name: Install dependencies

View file

@ -6,7 +6,6 @@ import sys
from typing import TYPE_CHECKING
from dotenv import load_dotenv
from httpx import AsyncClient
from openai import AsyncOpenAI
from openai.lib.azure import AsyncAzureOpenAI
@ -42,7 +41,7 @@ def debug_log_sensitive_data_from_callable(message: Callable[[], str | None]) ->
logging.debug(message())
def create_openai_client(
def create_openai_client_instance(
client_type: str = os.environ.get("OPENAI_API_TYPE", default="azure"),
) -> AsyncOpenAI | AsyncAzureOpenAI:
if client_type == "azure":
@ -52,20 +51,21 @@ def create_openai_client(
api_version="2024-08-01-preview",
# https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource
azure_endpoint="https://codeflash-openai-service-eastus2-0.openai.azure.com",
http_client=AsyncClient(),
)
else:
logging.info("OpenAIClient: Using OpenAI API.")
openai_client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY"), http_client=AsyncClient())
openai_client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
return openai_client
def create_claude_client() -> AsyncOpenAI:
logging.info("Claude Client")
openai_client: AsyncOpenAI = AsyncOpenAI(
api_key=os.environ.get("ANTHROPIC_API_KEY"), base_url="https://api.anthropic.com/v1/"
claude_client: AsyncOpenAI = AsyncOpenAI(
api_key=os.environ.get("ANTHROPIC_API_KEY"),
base_url="https://api.anthropic.com/v1/",
)
return openai_client
return claude_client
open_ai_client = create_openai_client()
openai_client = create_openai_client_instance()

View file

@ -16,6 +16,13 @@ RATE_LIMIT_MAX = int(os.getenv("RATE_LIMIT_MAX", "40"))
# Note: This rate limiting solution works only with a single server.
# It will not work correctly if multiple servers are used, as the cache is local to each server.
# TODO: Implement a distributed caching solution (e.g., Redis) for multi-server environments.
# TODO: ENTERPRISE FEATURE - Implement queue prioritization for premium/enterprise users
# - Check user subscription tier from Subscriptions model (plan_type field)
# - Premium users should bypass rate limits or have higher limits
# - Implement priority queue: enterprise > premium > free tier
# - Consider using asyncio.PriorityQueue or celery with priority tasks
# - Reference: authapp/models.py:Subscriptions.plan_type, CFAPIKeys.tier
@async_only_middleware
class RateLimitMiddleware:
def __init__(self, get_response) -> None:

View file

@ -168,6 +168,6 @@ PLAN_MODEL: LLM = OpenAI_GPT_4_1()
EXECUTE_MODEL: LLM = OpenAI_GPT_4_1()
OPTIMIZE_MODEL: LLM = OpenAI_GPT_4_1()
REFINEMENT_MODEL: LLM = Anthropic_Claude_4()
EXPLAINATIONS_MODEL: LLM = Anthropic_Claude_4()
EXPLANATIONS_MODEL: LLM = Anthropic_Claude_4()
RANKING_MODEL: LLM = OpenAI_GPT_4_1()
OPTIMIZATION_REVIEW_MODEL: LLM = Anthropic_Claude_4()

View file

@ -4,26 +4,25 @@ import re
from typing import TYPE_CHECKING
import sentry_sdk
from ninja import NinjaAPI, Schema
from openai import OpenAIError
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from aiservice.analytics.posthog import ph
from aiservice.common_utils import validate_trace_id
from aiservice.env_specific import create_claude_client, debug_log_sensitive_data
from aiservice.models.aimodels import EXPLAINATIONS_MODEL, calculate_llm_cost
from aiservice.models.aimodels import EXPLANATIONS_MODEL, LLM, calculate_llm_cost
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
from ninja import NinjaAPI, Schema
from openai import OpenAIError
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from packaging import version
if TYPE_CHECKING:
from aiservice.models.aimodels import LLM
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionFunctionMessageParam,
ChatCompletionToolMessageParam,
)
from aiservice.models.aimodels import LLM
explanations_api = NinjaAPI(urls_namespace="explanations")
explain_regex_pattern = re.compile(r"<explain>(.*)<\/explain>", re.DOTALL | re.IGNORECASE)
@ -43,13 +42,16 @@ You are provided the following information to succeed in the explanation process
- annotated_tests - The regression tests that were run to test for performance and correctness, with runtime results annotated next to the respective test case.
- read_only_dependency_code - The READ ONLY dependencies for the code provided, to help you better understand the code being provided.
- original_explanation - The original explanation generated for the optimized_source_code. Note that the original_explanation may be out of sync as some of the micro-optimizations and irrelevant changes might have been reverted in the optimized_source_code.
- python_version - The version of python the code would be executed on.
- function_references - Python markdown blocks with filename and references of some functions which call the function being optimized. The filenames and/or references could indicate if the function being optimized is in a hot path. The reference could have the function being called from a place that is important, for example in a loop, which means the effect of optimization might be important.
Keep explanations **developer-focused and concise**. Focus on:
- **What** specific optimizations were applied.
- **Key changes** that affect behavior or dependencies.
- **Why** the specific optimization leads to a speedup based on your knowledge of performance in Python code.
- **How** the optimization could potentially impact existing workloads based on function_references which can help determine whether the function being optimized is called in a hot path or not, and if the context where the function is called may benefit from the optimization.
- What kind of test cases are the specific optimizations good for based on the annotated_tests results.
- Avoid mentioning obvious preservation details (file structure, imports, signatures) unless they were specifically modified
- Avoid mentioning obvious preservation details (file structure, imports, signatures) unless they were specifically modified.
Please provide your explanation in the following format:
@ -104,9 +106,7 @@ Here is the speedup
Here is the test function code with runtime results annotated next to the respective test case.
<annotated_tests>
```python
{annotated_tests}
```
</annotated_tests>
Here is the read_only_dependency_code
@ -120,6 +120,16 @@ Here is the original_explanation
{original_explanation}
</original_explanation>
Here is the python_version
<python_version>
{python_version}
</python_version>
Here is the function_references
<function_references>
{function_references}
</function_references>
"""
THROUGHPUT_PROMPT_SECTION = """Here is the original_throughput (operations per second)
@ -150,23 +160,8 @@ When explaining optimizations:
"""
async def explain_optimizations(
user_id: str,
original_source_code: str,
optimized_source_code: str,
original_line_profiler_results: str,
optimized_line_profiler_results: str,
original_code_runtime: str,
optimized_code_runtime: str,
speedup: str,
annotated_tests: str,
original_explanation: str,
read_only_dependency_code: str | None = None,
explanations_model: LLM = EXPLAINATIONS_MODEL,
trace_id: str = "",
original_throughput: str | None = None,
optimized_throughput: str | None = None,
throughput_improvement: str | None = None,
async def explain_optimizations( # noqa: D417
user_id: str, data: ExplanationsSchema, explanations_model: LLM = EXPLANATIONS_MODEL
) -> ExplanationsResponseSchema | ExplanationsErrorResponseSchema:
"""Optimize the given python code for performance using the Claude 4 model.
@ -178,29 +173,33 @@ async def explain_optimizations(
Returns: - List[Tuple[Union[str, None], Union[str, None]]]: A list of tuples where the first element is the
optimized code and the second is the explanation.
:param explanations_model:
"""
debug_log_sensitive_data(f"Generating an explanation for {user_id}:\n{optimized_source_code}")
debug_log_sensitive_data(f"Generating an explanation for {user_id}:\n{data.optimized_code}")
if version.parse(data.codeflash_version) <= version.parse("0.18.2") and data.annotated_tests:
data.annotated_tests = f"```python\n{data.annotated_tests}\n```"
user_prompt = BASE_USER_PROMPT.format(
original_source_code=original_source_code,
original_line_profiler_results=original_line_profiler_results or "[No profiler results available]",
optimized_source_code=optimized_source_code,
optimized_line_profiler_results=optimized_line_profiler_results or "[No profiler results available]",
original_code_runtime=original_code_runtime,
optimized_code_runtime=optimized_code_runtime,
speedup=speedup,
annotated_tests=annotated_tests,
read_only_dependency_code=read_only_dependency_code or "[No read only code present]",
original_explanation=original_explanation,
original_source_code=data.source_code,
original_line_profiler_results=data.original_line_profiler_results or "[No profiler results available]",
optimized_source_code=data.optimized_code,
optimized_line_profiler_results=data.optimized_line_profiler_results or "[No profiler results available]",
original_code_runtime=data.original_code_runtime,
optimized_code_runtime=data.optimized_code_runtime,
speedup=data.speedup,
annotated_tests=data.annotated_tests,
read_only_dependency_code=data.dependency_code or "[No read only code present]",
original_explanation=data.original_explanation,
python_version=data.python_version or "Not Available",
function_references=data.function_references or "Not Available",
)
system_prompt = SYSTEM_PROMPT
if original_throughput is not None and optimized_throughput is not None:
if data.original_throughput is not None and data.optimized_throughput is not None:
user_prompt += THROUGHPUT_PROMPT_SECTION.format(
original_throughput=original_throughput,
optimized_throughput=optimized_throughput,
throughput_improvement=throughput_improvement or "[Unable to calculate throughput improvement]",
original_throughput=data.original_throughput,
optimized_throughput=data.optimized_throughput,
throughput_improvement=data.throughput_improvement or "[Unable to calculate throughput improvement]",
)
system_prompt += "\n" + THROUGHPUT_SYSTEM_SECTION
@ -219,7 +218,7 @@ async def explain_optimizations(
output = await claude_client.with_options(max_retries=2).chat.completions.create(
model=explanations_model.name, messages=messages, n=1
)
await update_optimization_cost(trace_id=trace_id, cost=calculate_llm_cost(output, explanations_model))
await update_optimization_cost(trace_id=data.trace_id, cost=calculate_llm_cost(output, explanations_model))
except OpenAIError as e:
sentry_sdk.capture_exception(e)
debug_log_sensitive_data(f"Failed to generate new explanation, Error message: {e}")
@ -250,6 +249,9 @@ class ExplanationsSchema(Schema):
original_throughput: str | None = None
optimized_throughput: str | None = None
throughput_improvement: str | None = None
python_version: str | None = None
function_references: str | None = None
codeflash_version: str = "0.18.2"
class ExplanationsResponseSchema(Schema):
@ -274,23 +276,7 @@ async def explain(
ph(request.user, "aiservice-explain-called")
if not validate_trace_id(data.trace_id):
return 400, ExplanationsErrorResponseSchema(error="Invalid trace ID. Please provide a valid UUIDv4.")
explanation_response = await explain_optimizations(
request.user,
data.source_code,
data.optimized_code,
data.original_line_profiler_results,
data.optimized_line_profiler_results,
data.original_code_runtime,
data.optimized_code_runtime,
data.speedup,
data.annotated_tests,
data.original_explanation,
data.dependency_code,
trace_id=data.trace_id,
original_throughput=data.original_throughput,
optimized_throughput=data.optimized_throughput,
throughput_improvement=data.throughput_improvement,
)
explanation_response = await explain_optimizations(request.user, data)
if isinstance(explanation_response, ExplanationsErrorResponseSchema):
ph(request.user, "Explanation not generated, revert to old explanation")
debug_log_sensitive_data("No explanation was generated")

View file

@ -165,157 +165,6 @@ def log_features(
f.save()
@sync_to_async
@transaction.atomic
def log_features_optimized(
trace_id: str,
user_id: str,
original_code: str | None = None,
dependency_code: str | None = None,
line_profiler_results: str | None = None,
optimizations_raw: dict[str, str] | None = None,
optimizations_post: dict[str, str] | None = None,
explanations_raw: dict[str, str] | None = None,
explanations_post: dict[str, str] | None = None,
speedup_ratio: dict[str, float | None] | None = None,
original_runtime: float | None = None,
optimized_runtime: dict[str, float] | None = None,
optimized_line_profiler_results: dict[str, str] | None = None,
is_correct: dict[str, bool | None] | None = None,
generated_tests: list[str] | None = None,
instrumented_generated_tests: list[str] | None = None,
test_framework: str | None = None,
datetime: dt.datetime | None = None,
aiservice_commit: str | None = None,
metadata: dict[str, Any] | None = None,
experiment_metadata: dict[str, str] | None = None,
final_explanation: str | None = None,
ranking: dict[str, Any] | None = None,
request: HttpRequest | None = None,
) -> None:
"""Log features of a code optimization run to the database.
:rtype: None
:param optimized_line_profiler_results: mapping of optimization candidate trace ids to line profiler results
:param trace_id: The client generated UUID of the optimization run. This is used to link the features together.
:param user_id: The user ID of the user who ran the optimization.
:param original_code: The original code that the LLM is allowed to modify.
:param dependency_code: The dependency code that the LLM is not allowed to modify.
:param original_code: The line profiling results for the original code.
:param optimizations_raw: The raw optimizations that were generated by the language model.
:param optimizations_post: The final optimizations that were generated by the optimization endpoint.
:param explanations_raw: Raw Explanations generated by the language model.
:param explanations_post: Final Explanations for the optimizations.
:param speedup_ratio: Speedups in fractions achieved by the optimizations.
:param original_runtime: The time taken in ns to run the original code.
:param optimized_runtime: The time taken in ns to run the optimized code.
:param is_correct: Behavioural correctness of the optimized code.
:param generated_tests: Generated tests for the optimized code, output of the aiservice test generation endpoint.
:param instrumented_generated_tests: Instrumented generated tests for the optimized code, output of the aiservice test generation endpoint.
:param test_framework: The test framework used to generate the tests.
:param datetime: The datetime of the optimization run. Should be calculated by the aiservice.
:param aiservice_commit: The commit hash of the AIService code used for the feature logging. hopefully should be the same for the entire run.
:param metadata: Additional metadata to log.
:param final_explanation: the final explanation in the PR
"""
if hasattr(request, "should_log_features") and request.should_log_features:
f, created = OptimizationFeatures.objects.select_for_update().get_or_create(
trace_id=trace_id,
defaults={
"user_id": user_id,
"original_code": original_code,
"dependency_code": dependency_code,
"line_profiler_results": line_profiler_results,
"optimizations_raw": optimizations_raw,
"optimizations_post": optimizations_post,
"explanations_raw": explanations_raw,
"explanations_post": explanations_post,
"speedup_ratio": speedup_ratio,
"original_runtime": original_runtime,
"optimized_runtime": optimized_runtime,
"optimized_line_profiler_results": optimized_line_profiler_results,
"is_correct": is_correct,
"generated_test": generated_tests,
"instrumented_generated_test": instrumented_generated_tests,
"test_framework": test_framework,
"created_at": datetime,
"aiservice_commit_id": aiservice_commit,
"metadata": metadata,
"experiment_metadata": experiment_metadata,
"final_explanation": final_explanation,
"ranking": ranking,
},
)
if not created:
# When Record doesnt exists, update it
updated_metadata = f.metadata or {}
if metadata:
updated_metadata.update(metadata)
updated_experiment_metadata = f.experiment_metadata or {}
if experiment_metadata:
updated_experiment_metadata.update(experiment_metadata)
if generated_tests:
f.generated_test = (f.generated_test or []) + generated_tests
if instrumented_generated_tests:
f.instrumented_generated_test = (f.instrumented_generated_test or []) + instrumented_generated_tests
# Update fields on the existing instance
f.user_id = user_id if user_id is not None else f.user_id
f.original_code = original_code if original_code is not None else f.original_code
f.dependency_code = dependency_code if dependency_code is not None else f.dependency_code
f.line_profiler_results = (
line_profiler_results if line_profiler_results is not None else f.line_profiler_results
)
f.final_explanation = final_explanation if final_explanation is not None else f.final_explanation
if f.optimizations_raw is not None:
f.optimizations_raw = (
f.optimizations_raw | optimizations_raw if optimizations_raw is not None else f.optimizations_raw
)
else:
f.optimizations_raw = optimizations_raw if optimizations_raw is not None else f.optimizations_raw
if f.optimizations_post is not None:
f.optimizations_post = (
f.optimizations_post | optimizations_post
if optimizations_post is not None
else f.optimizations_post
)
else:
f.optimizations_post = optimizations_post if optimizations_post is not None else f.optimizations_post
if f.explanations_raw is not None:
f.explanations_raw = (
f.explanations_raw | explanations_raw if explanations_raw is not None else f.explanations_raw
)
else:
f.explanations_raw = explanations_raw if explanations_raw is not None else f.explanations_raw
if f.explanations_post is not None:
f.explanations_post = (
f.explanations_post | explanations_post if explanations_post is not None else f.explanations_post
)
else:
f.explanations_post = explanations_post if explanations_post is not None else f.explanations_post
if f.ranking is not None:
f.ranking = f.ranking | ranking if ranking is not None else f.ranking
else:
f.ranking = ranking if ranking is not None else f.ranking
f.speedup_ratio = speedup_ratio if speedup_ratio is not None else f.speedup_ratio
f.original_runtime = original_runtime if original_runtime is not None else f.original_runtime
f.optimized_runtime = optimized_runtime if optimized_runtime is not None else f.optimized_runtime
f.optimized_line_profiler_results = (
optimized_line_profiler_results
if optimized_line_profiler_results is not None
else f.optimized_line_profiler_results
)
f.is_correct = is_correct if is_correct is not None else f.is_correct
f.test_framework = test_framework if test_framework is not None else f.test_framework
f.created_at = datetime if datetime is not None else f.created_at
f.aiservice_commit_id = aiservice_commit if aiservice_commit is not None else f.aiservice_commit_id
f.metadata = updated_metadata
f.experiment_metadata = updated_experiment_metadata
f.save()
@features_api.post("/", response={200: None, 500: LoggingErrorResponseSchema})
async def log_features_cli(request: HttpRequest, data: LoggingSchema) -> int | tuple[int, LoggingErrorResponseSchema]:
try:

View file

@ -7,12 +7,12 @@ from enum import Enum
from typing import TYPE_CHECKING, cast
import sentry_sdk
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from aiservice.env_specific import create_claude_client, debug_log_sensitive_data
from aiservice.models.aimodels import OPTIMIZATION_REVIEW_MODEL, calculate_llm_cost
from log_features.log_event import update_optimization_cost, update_optimization_features_review
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from packaging import version
if TYPE_CHECKING:
from aiservice.models.aimodels import LLM
@ -50,11 +50,15 @@ class OptimizationReviewSchema(Schema):
loop_count: int
explanation: str
calling_fn_details: str
python_version: str | None = None
codeflash_version: str = "0.18.2"
def _build_optimization_review_messages(
data: OptimizationReviewSchema,
) -> list[ChatCompletionSystemMessageParam | ChatCompletionUserMessageParam]:
if version.parse(data.codeflash_version) <= version.parse("0.18.2") and data.generated_tests:
data.generated_tests = f"```python\n{data.generated_tests}\n```"
system_prompt = """You are an expert software engineer who writes really fast programs and is an expert in optimizing runtime and memory requirements of a program by rewriting it. You have deep expertise in modern programming best practices, and clean code principles.
Codeflash is a tool which finds the fastest version of Python code by generating candidate optimizations with LLMs and verifying its correctness and performance gains.
@ -70,12 +74,15 @@ You are provided the following information to succeed in the Optimization Pull R
- existing_tests (Optional) - A table consisting of performance changes over existing tests.
- replay_tests (Optional) - A table consisting of performance changes over replay tests.
- benchmark_details (Optional) - A table showing the runtime on a benchmark workload and the percentage of the time taken by the function.
- python_version - The version of python the code would be executed on.
Information for determining if the function is in a hot path.
- calling_fn_details - Python markdown blocks with filename and references of some functions which call the function being optimized. The filenames and/or references could indicate if the function being optimized is in a hot path. The reference could have the function being called from a place that is important, for example in a loop, which means the effect of optimization might be important.
Guidelines to follow while reviewing the optimization pull request-
- Look closely at overall_runtime_details, generated_tests, existing_tests, replay_tests and benchmark_details to determine if the speedups are high enough to be merged. Take into consideration whether there are any significant slowdowns in inputs you deem important.
- Introduction of the `global` and `nonlocal` keywords in the code_diff is **HIGHLY DISCOURAGED** as it reduces code clarity and maintainability, introduces hidden dependencies, can cause subtle bugs and breaks modularity.
- If the only optimizations are micro-optimizations like inlining a function call, or localizing variables or methods (not being used in a loop) - these can negatively affect the review, especially if python_version is older than 3.11.
- Look closely at code_diff to determine if the optimizations make sense or are spurious micro-optimizations which may not help. Also consider the trade-off between reduced code quality/readability and the performance gain. Micro-optimizations can help if the function is important to be optimized or is in a hot path.
- Look closely at calling_fn_details to determine whether the function being optimized is called in a hot path or not, and if the context where the function is called may benefit from the optimization.
- If there are some changes that make the code unclean, but the core optimization logic makes sense, then you can still recommend it. The user can then clean up the changes before merging.
@ -111,7 +118,7 @@ Test Coverage : {data.coverage_message}
--- Test Results ---
generate_tests:
{f"```python\n{data.generated_tests}\n```" if data.generated_tests else "Not Available"}
{data.generated_tests or "Not Available"}
existing_tests:
{data.existing_tests or "Not Available"}

View file

@ -2,9 +2,10 @@ from dataclasses import dataclass
from typing import Never
import libcst as cst
from pydantic import ValidationError
from aiservice.env_specific import debug_log_sensitive_data
from pydantic import ValidationError
from testgen.instrumentation.edit_generated_test import parse_module_to_cst
from optimizer.context_utils.constants import (
MULTI_REPLACE_IN_FILE_TAGS_REGEX,
REPLACE_IN_FILE_REGEX,
@ -18,7 +19,6 @@ from optimizer.context_utils.context_helpers import (
)
from optimizer.diff_patches_utils.patches_v2 import apply_patches
from optimizer.models import CodeAndExplanation
from testgen.instrumentation.edit_generated_test import parse_module_to_cst
@dataclass()
@ -32,6 +32,8 @@ class RefinementContextData:
optimized_code_runtime: str
speedup: str
optimized_explanation: str = ""
python_version: str | None = None
function_references: str | None = None
##########################################################################################
@ -68,6 +70,8 @@ class BaseRefinerContext:
optimized_code_runtime=self.data.optimized_code_runtime,
speedup=self.data.speedup,
read_only_dependency_code=self.data.read_only_dependency_code or "[No read only code present]",
python_version=self.data.python_version or "Not Available",
function_references=self.data.function_references or "Not Available",
)
def extract_diff_patches_from_llm_res(self, llm_res: str) -> str:
@ -117,6 +121,8 @@ class SingleRefinerContext(BaseRefinerContext):
optimized_code_runtime=self.data.optimized_code_runtime,
speedup=self.data.speedup,
read_only_dependency_code=self.data.read_only_dependency_code or "[No read only code present]",
python_version=self.data.python_version or "Not Available",
function_references=self.data.function_references or "Not Available",
)
def extract_diff_patches_from_llm_res(self, llm_res: str) -> str:

View file

@ -15,11 +15,11 @@ from pydantic import ValidationError
from aiservice.analytics.posthog import ph
from aiservice.common_utils import parse_python_version, should_hack_for_demo, validate_trace_id
from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable, open_ai_client
from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable, openai_client
from aiservice.models.aimodels import OPTIMIZE_MODEL, calculate_llm_cost
from authapp.user import get_user_by_id
from log_features.log_event import log_optimization_event
from log_features.log_features import log_features_optimized
from log_features.log_features import log_features
from optimizer.context_utils.context_helpers import group_code
from optimizer.context_utils.optimizer_context import (
BaseOptimizerContext,
@ -139,7 +139,7 @@ async def optimize_python_code(
| ChatCompletionFunctionMessageParam
] = [system_message, user_message]
try:
output = await open_ai_client.with_options(max_retries=3).chat.completions.create(
output = await openai_client.with_options(max_retries=3).chat.completions.create(
model=optimize_model.name, messages=messages, n=n
)
except Exception as e:
@ -275,7 +275,7 @@ async def optimize(
)
tg.create_task(
log_features_optimized(
log_features(
trace_id=data.trace_id,
user_id=request.user,
original_code=data.source_code,
@ -289,7 +289,7 @@ async def optimize(
},
explanations_post={cei.optimization_id: cei.explanation for cei in optimization_response_items},
experiment_metadata=data.experiment_metadata if data.experiment_metadata else None,
request=request,
# request=request,
)
)

View file

@ -11,9 +11,9 @@ from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUs
from aiservice.analytics.posthog import ph
from aiservice.common_utils import parse_python_version, validate_trace_id
from aiservice.env_specific import (
create_openai_client,
debug_log_sensitive_data,
debug_log_sensitive_data_from_callable,
openai_client,
)
from aiservice.models.aimodels import OPTIMIZE_MODEL, calculate_llm_cost
from log_features.log_event import update_optimization_cost
@ -91,18 +91,17 @@ async def optimize_python_code_line_profiler(
| ChatCompletionFunctionMessageParam
] = [system_message, user_message]
debug_log_sensitive_data(f"This was the user prompt\n {user_prompt}\n")
async with create_openai_client() as openai_client:
# TODO: Verify if the context window length is within the model capability
try:
output = await openai_client.with_options(max_retries=3).chat.completions.create(
model=optimize_model.name, messages=messages, n=n
)
await update_optimization_cost(trace_id=trace_id, cost=calculate_llm_cost(output, optimize_model))
except Exception as e:
logging.exception("OpenAI Code Generation error in optimizer-line-profiler")
sentry_sdk.capture_exception(e)
debug_log_sensitive_data(f"Failed to generate code for source:\n{ctx.source_code}")
return []
# TODO: Verify if the context window length is within the model capability
try:
output = await openai_client.with_options(max_retries=3).chat.completions.create(
model=optimize_model.name, messages=messages, n=n
)
await update_optimization_cost(trace_id=trace_id, cost=calculate_llm_cost(output, optimize_model))
except Exception as e:
logging.exception("OpenAI Code Generation error in optimizer-line-profiler")
sentry_sdk.capture_exception(e)
debug_log_sensitive_data(f"Failed to generate code for source:\n{ctx.source_code}")
return []
debug_log_sensitive_data(f"OpenAIClient optimization response:\n{output.model_dump_json(indent=2)}")

View file

@ -1,8 +1,11 @@
from __future__ import annotations
import ast
import io
import logging
import re
import tokenize
from difflib import SequenceMatcher
from typing import TYPE_CHECKING
import libcst as cst
@ -57,7 +60,7 @@ def equality_check(
) -> list[CodeExplanationAndID]:
try:
original_source_ast = unparse_parse_source(original_source_code)
except Exception: # noqa: BLE001
except Exception:
return [
CodeExplanationAndID(cst_module=ce.cst_module, explanation=ce.explanation, id=ce.id)
for ce in optimized_code_and_explanations
@ -68,7 +71,7 @@ def equality_check(
try:
if not compare_unparsed_ast_to_source(original_source_ast, ce.cst_module.code):
filtered_optimizations.append(ce)
except Exception: # noqa: BLE001
except Exception:
if ce.cst_module.code != original_source_code:
filtered_optimizations.append(ce)
return filtered_optimizations
@ -181,7 +184,7 @@ def fix_missing_docstring(
visitor = DocstringVisitor()
try:
original_tree = cst.parse_module(original_source_code)
except Exception: # noqa: BLE001
except Exception:
return optimized_code_and_explanations
original_tree.visit(visitor)
original_docstrings = visitor.original_docstrings
@ -192,7 +195,7 @@ def fix_missing_docstring(
returns.append(
CodeExplanationAndID(cst_module=ce.cst_module.visit(transformer), explanation=ce.explanation, id=ce.id)
)
except Exception as e: # noqa: BLE001
except Exception as e:
sentry_sdk.capture_exception(e)
returns.append(ce)
return returns
@ -206,7 +209,7 @@ def dedup_and_sort_imports(
try:
# Use isort to sort and deduplicate the imports
sorted_code = safe_isort(ce.cst_module.code, disregard_skip=True)
except Exception: # noqa: BLE001
except Exception:
sorted_code = ce.cst_module.code
new_optimized_code_and_explanations.append(
CodeExplanationAndID(cst_module=parse_module_to_cst(sorted_code), explanation=ce.explanation, id=ce.id)
@ -256,12 +259,317 @@ def remove_profanity_from_explanation(
return new_optimized_code_and_explanations
def _strip_comments_from_code(code: str) -> str:
"""Remove all comments from Python code while preserving strings and their content.
Uses tokenize to properly distinguish between actual comments and # symbols
inside strings (including multi-line strings).
Args:
----
code: Python source code as a string
Returns:
-------
The same code with all comments removed, preserving string content
"""
try:
lines = code.splitlines(keepends=True)
tokens = tokenize.generate_tokens(io.StringIO(code).readline)
# Build a per-line map of comment ranges to remove
comments_by_line = [[] for _ in range(len(lines))]
for token in tokens:
if token.type == tokenize.COMMENT:
line_idx = token.start[0] - 1
if 0 <= line_idx < len(lines):
comments_by_line[line_idx].append((token.start[1], token.end[1]))
# Remove comments from the code
result_lines = []
for line_num, line in enumerate(lines, start=1):
# Find all comments on this line
line_comments = comments_by_line[line_num - 1]
if line_comments:
# Remove the comments from this line
# Sort by start position (rightmost first) to remove from right to left
line_comments.sort(reverse=True)
for start_col, end_col in line_comments:
# Remove the comment and any trailing whitespace before it
line = line[:start_col].rstrip() + line[end_col:]
# Add back the line ending if it was removed
if not line.endswith("\n") and lines[line_num - 1].endswith("\n"):
line += "\n"
result_lines.append(line)
return "".join(result_lines)
except (tokenize.TokenError, IndentationError):
# If tokenization fails, return the code as-is
# This can happen with incomplete or malformed code
return code
def clean_extraneous_comments(original_module: cst.Module, optimized_module: cst.Module) -> cst.Module:
"""Clean extraneous comments from optimized code using difflib.
Uses diff-based approach on code (without comments) to identify which lines
actually changed, then removes comments from unchanged lines.
Args:
----
original_module: The original CST module.
optimized_module: The optimized CST module with potential extra comments.
Returns:
-------
A CST module with extraneous comments removed.
"""
try:
# Get line-by-line representation
orig_lines = original_module.code.splitlines(keepends=True)
opt_lines = optimized_module.code.splitlines(keepends=True)
# Strip comments from entire code to identify code changes
# This properly handles # symbols inside strings
orig_code_stripped = _strip_comments_from_code(original_module.code)
opt_code_stripped = _strip_comments_from_code(optimized_module.code)
# Split stripped versions into lines
orig_code_only = orig_code_stripped.splitlines(keepends=True)
opt_code_only = opt_code_stripped.splitlines(keepends=True)
# Filter out comment-only/blank lines for comparison
# Keep track of indices of actual code lines
orig_code_line_indices = []
orig_code_lines_filtered = []
for i, line in enumerate(orig_code_only):
if line.strip(): # Has actual code
orig_code_line_indices.append(i)
orig_code_lines_filtered.append(line)
opt_code_line_indices = []
opt_code_lines_filtered = []
for i, line in enumerate(opt_code_only):
if line.strip(): # Has actual code
opt_code_line_indices.append(i)
opt_code_lines_filtered.append(line)
# Find which lines in optimized code have actual code changes
# Compare only the actual code lines (not comment-only lines)
code_matcher = SequenceMatcher(None, orig_code_lines_filtered, opt_code_lines_filtered)
code_changed_line_indices = set() # Indices in the filtered lists
for tag, _i1, _i2, j1, j2 in code_matcher.get_opcodes():
if tag != "equal":
# These are changed/inserted/deleted lines
for j in range(j1, j2):
code_changed_line_indices.add(j)
# Map back to actual line numbers in opt_lines
code_changed_lines = set()
for filtered_idx in code_changed_line_indices:
if filtered_idx < len(opt_code_line_indices):
code_changed_lines.add(opt_code_line_indices[filtered_idx])
# Build a mapping between original and optimized lines based on code (without comments)
# This helps us understand which original lines correspond to which optimized lines
orig_to_opt_mapping = {}
opt_to_orig_mapping = {}
for tag, i1, i2, j1, j2 in code_matcher.get_opcodes():
if tag == "equal":
# Lines are the same (without comments)
for offset in range(min(i2 - i1, j2 - j1)):
orig_idx = orig_code_line_indices[i1 + offset]
opt_idx = opt_code_line_indices[j1 + offset]
orig_to_opt_mapping[orig_idx] = opt_idx
opt_to_orig_mapping[opt_idx] = orig_idx
# Now build the result: for each line in optimized code,
# keep it if the code changed, or if it's a comment before changed code
result_lines = []
orig_idx = 0
restored_orig_indices = set() # Track which original lines have been restored
for opt_idx, opt_line in enumerate(opt_lines):
if opt_idx in code_changed_lines:
# Code changed on this line - keep the optimized version with comments
result_lines.append(opt_line)
else:
# Code didn't change on this line
# Get the corresponding stripped line
opt_code = opt_code_only[opt_idx] if opt_idx < len(opt_code_only) else ""
# Check if this is a comment-only line that comes before OR after a code change
is_comment_only = not opt_code.strip()
is_near_change = False
if is_comment_only:
# Check if any of the following lines have code changes
for check_idx in range(opt_idx + 1, len(opt_lines)):
if check_idx in code_changed_lines:
is_near_change = True
break
# Stop checking if we hit another line with actual code
check_code = opt_code_only[check_idx] if check_idx < len(opt_code_only) else ""
if check_code.strip():
break
# Also check if any of the preceding lines (looking back) have code changes
if not is_near_change:
for check_idx in range(opt_idx - 1, -1, -1):
if check_idx in code_changed_lines:
is_near_change = True
break
# Stop checking if we hit another line with actual code
check_code = opt_code_only[check_idx] if check_idx < len(opt_code_only) else ""
if check_code.strip():
break
if is_comment_only and is_near_change:
# Comment line near a code change - keep it as-is
result_lines.append(opt_line)
elif is_comment_only and not is_near_change:
# Comment line not near a code change - skip it
pass
else:
# Code line - check if it changed or not
# If changed, use optimized version. If unchanged, use original version.
if opt_idx in code_changed_lines:
# Code changed - just use the optimized line as-is, no restoration
result_lines.append(opt_line)
else:
# Code didn't change - find and use original (including any preceding comments)
found_orig = None
orig_line_idx = None
for orig_idx_search in range(orig_idx, len(orig_lines)):
orig_code = orig_code_only[orig_idx_search] if orig_idx_search < len(orig_code_only) else ""
if orig_code == opt_code:
found_orig = orig_lines[orig_idx_search]
orig_line_idx = orig_idx_search
orig_idx = orig_idx_search + 1
break
if found_orig:
# Check if there are comment-only/blank lines in the original that come before this line
# BUT ONLY restore them if this code line is UNCHANGED (not in code_changed_lines)
# Don't restore original comments before CHANGED code lines
if orig_line_idx is not None and orig_line_idx > 0 and opt_idx not in code_changed_lines:
# Look backwards for ALL consecutive comment-only or blank lines
# Collect them all, then decide which ones to restore
preceding_lines = []
check_idx = orig_line_idx - 1
while check_idx >= 0:
check_code = orig_code_only[check_idx] if check_idx < len(orig_code_only) else ""
if not check_code.strip():
# This is a comment-only or blank line in the original
# Add it if not already restored and if it was removed from optimized
if check_idx not in orig_to_opt_mapping and check_idx not in restored_orig_indices:
preceding_lines.insert(0, orig_lines[check_idx])
restored_orig_indices.add(check_idx)
check_idx -= 1
else:
# Hit a line with actual code - stop looking backwards
break
# Add the restored comments/blank lines before the actual line
result_lines.extend(preceding_lines)
# Use the original line (preserves original comments or lack thereof)
result_lines.append(found_orig)
restored_orig_indices.add(orig_line_idx)
# Also check for trailing blank/comment lines after this line that were removed
# BUT: only restore them if this code line is UNCHANGED and they don't come before a changed line
# (in that case, the new comment from optimized should be kept)
if orig_line_idx is not None and orig_line_idx < len(orig_lines) - 1 and opt_idx not in code_changed_lines:
trailing_lines = []
check_idx = orig_line_idx + 1
found_changed_line = False
while check_idx < len(orig_lines):
check_code = orig_code_only[check_idx] if check_idx < len(orig_code_only) else ""
if not check_code.strip():
# This is a comment-only or blank line in the original
# Check if it was removed (not in the optimized version)
if check_idx not in orig_to_opt_mapping and check_idx not in restored_orig_indices:
trailing_lines.append(orig_lines[check_idx])
restored_orig_indices.add(check_idx)
check_idx += 1
else:
# Hit a line with actual code
# Check if this code line was changed in the optimized version
if check_idx in orig_to_opt_mapping:
next_opt_idx = orig_to_opt_mapping[check_idx]
if next_opt_idx in code_changed_lines:
found_changed_line = True
else:
# This original line is not in the mapping, which means
# it was either deleted or modified. In either case,
# this is a changed line.
found_changed_line = True
break
# Only add trailing comments if they're NOT immediately before a changed line
if not found_changed_line:
result_lines.extend(trailing_lines)
else:
# Keep it (shouldn't happen but be safe)
result_lines.append(opt_line)
# Parse the cleaned code back into a CST module
cleaned_code = "".join(result_lines)
return cst.parse_module(cleaned_code)
except Exception as e:
logging.warning("Error cleaning comments: %s", e)
sentry_sdk.capture_exception(e)
return optimized_module
def clean_extraneous_comments_pipeline(
original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID]
) -> list[CodeExplanationAndID]:
"""Pipeline wrapper for comment cleaning.
Cleans extraneous comments from all optimized code variants.
"""
try:
original_module = cst.parse_module(original_source_code)
cleaned_results = []
for ce in optimized_code_and_explanations:
try:
cleaned_module = clean_extraneous_comments(original_module, ce.cst_module)
cleaned_results.append(
CodeExplanationAndID(cst_module=cleaned_module, explanation=ce.explanation, id=ce.id)
)
except Exception as e:
logging.warning("Error cleaning comments for optimization %s: %s", ce.id, e)
sentry_sdk.capture_exception(e)
# Keep the original if cleaning fails
cleaned_results.append(ce)
return cleaned_results
except Exception as e:
logging.warning("Error in comment cleaning pipeline: %s", e)
sentry_sdk.capture_exception(e)
return optimized_code_and_explanations
def optimizations_postprocessing_pipeline(
original_source_code: str, optimized_code_and_explanations: list[CodeExplanationAndID]
) -> list[CodeExplanationAndID]:
pipeline = [
remove_profanity_from_explanation,
fix_missing_docstring, # We want to deduplicate with the fixed docstrings included
clean_extraneous_comments_pipeline, # Clean comments added to unchanged code
deduplicate_optimizations,
equality_check,
dedup_and_sort_imports,

View file

@ -7,27 +7,26 @@ from typing import TYPE_CHECKING
import libcst as cst
import sentry_sdk
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from pydantic import ValidationError
from aiservice.analytics.posthog import ph
from aiservice.common_utils import validate_trace_id
from aiservice.env_specific import create_claude_client, debug_log_sensitive_data
from aiservice.models.aimodels import REFINEMENT_MODEL, calculate_llm_cost
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from pydantic import ValidationError
from optimizer.context_utils.refiner_context import BaseRefinerContext, RefinementContextData
if TYPE_CHECKING:
from aiservice.models.aimodels import LLM
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionFunctionMessageParam,
ChatCompletionToolMessageParam,
)
from aiservice.models.aimodels import LLM
refinement_api = NinjaAPI(urls_namespace="refinement")
@ -52,9 +51,13 @@ You are provided the following information to succeed in the quality refinement
- optimization_speedup_results - The runtime for the original_source_code and optimized_source_code over a series of tests
- optimization_explanation - The explanation generated by codeflash earlier for the optimization in the optimized_source_code
- read_only_dependency_code - The READ ONLY dependencies for the code provided, to help you better understand the code being provided. Do no modify the code here, it is only provided for your reference.
- python_version - The version of python the code would be executed on.
- function_references - Python markdown blocks with filename and references of some functions which call the function being optimized. The filenames and/or references could indicate if the function being optimized is in a hot path. The reference could have the function being called from a place that is important, for example in a loop, which means the effect of optimization might be important.
Rules to follow while refining the quality of the optimized code -
- Analyze the original code and the optimized code and look at the line profiler info and the explanation to understand how the optimization works
- Introduction of the `global` and `nonlocal` keywords in optimized_source_code is **HIGHLY DISCOURAGED** as it reduces code clarity and maintainability, introduces hidden dependencies, can cause subtle bugs and breaks modularity. Revert any such changes.
- If there are micro-optimizations like inlining a function call, or localizing variables or methods (not being used in a loop), especially if python_version is older than 3.11, revert any such changes.
- Figure out the code difference between the original_source_code and the optimized_source_code to see what part of the optimized_source_code is not contributing to the optimization. In such a case, we want to revert that part of the optimized_source_code to the original_source_code. It is okay to revert parts of the changes that aren't faster by at least 1%.
- If there are any changes in the optimized code that make that code section slower than the original then we want to revert such a change to the original.
- Revert the new comments in the optimized_source_code that are different from the original_source_code unless the new code is complex and requires additional context.
@ -168,10 +171,20 @@ Here is the read_only_dependency_code
<read_only_dependency_code>
{read_only_dependency_code}
</read_only_dependency_code>
Here is the python version
<python_version>
{python_version}
</python_version>
Here is the function_references
<function_references>
{function_references}
</function_references>
"""
async def refinement(
async def refinement( # noqa: D417
user_id: str, optimization_id: str, ctx: BaseRefinerContext, optimize_model: LLM = REFINEMENT_MODEL
) -> RefinementIntermediateResponseItemschema | OptimizeErrorResponseSchema:
"""Optimize the given python code for performance using Anthropic's Claude 4 model.
@ -267,6 +280,8 @@ class RefinementRequestSchema(Schema):
original_code_runtime: str = ""
optimized_code_runtime: str = ""
speedup: str = ""
python_version: str | None = None
function_references: str | None = None
class OptimizeErrorResponseSchema(Schema):
@ -310,6 +325,8 @@ async def refine(
optimized_code_runtime=opt.optimized_code_runtime,
speedup=opt.speedup,
optimized_explanation=opt.optimized_explanation,
python_version=opt.python_version,
function_references=opt.function_references,
)
for opt in data
]

View file

@ -13,20 +13,17 @@ dependencies = [
"python-dotenv>=1.0.1,<2",
"dj-database-url>=2.2.0,<3",
"psycopg2-binary>=2.9.9,<3",
"black>=24.4.2,<25",
"gunicorn>=22.0.0,<23",
"uvicorn>=0.32.0,<0.33",
"jedi>=0.19.0,<0.20",
"libcst>=1.5.0,<2",
"posthog>=3.5.0,<4",
"pytest>=8.2.1,<9",
"gitpython>=3.1.43,<4",
"ruff>=0.7.0",
"pytest-django>=4.8.0,<5",
"openai>=1.52.2,<2",
"openai[aiohttp]>=1.52.2,<2",
"isort>=7.0.0",
"sentry-sdk[django]>=2.35.0",
"stamina>=25.1.0",
"jedi>=0.19.2",
]
[project.urls]
@ -42,6 +39,8 @@ dev = [
"types-gevent>=24.11.0.20241230,<25",
"types-pexpect>=4.9.0.20241208,<5",
"pytest-asyncio>=1.1.0",
"pytest>=8.4.2",
"pytest-django>=4.11.1",
]
[tool.hatch.build.targets.sdist]

View file

@ -4,25 +4,23 @@ import re
from typing import TYPE_CHECKING
import sentry_sdk
from aiservice.analytics.posthog import ph
from aiservice.common_utils import validate_trace_id
from aiservice.env_specific import debug_log_sensitive_data, openai_client
from aiservice.models.aimodels import LLM, RANKING_MODEL, calculate_llm_cost
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
from ninja import NinjaAPI, Schema
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from aiservice.analytics.posthog import ph
from aiservice.common_utils import validate_trace_id
from aiservice.env_specific import create_openai_client, debug_log_sensitive_data
from aiservice.models.aimodels import RANKING_MODEL, calculate_llm_cost
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features
if TYPE_CHECKING:
from aiservice.models.aimodels import LLM
from openai.types.chat import (
ChatCompletionAssistantMessageParam,
ChatCompletionFunctionMessageParam,
ChatCompletionToolMessageParam,
)
from aiservice.models.aimodels import LLM
# from google import genai
# from pydantic import BaseModel
#
@ -39,9 +37,15 @@ SYSTEM_PROMPT = """You are an expert code reviewer who understands why programs
You are provided with a list of optimization candidates with their code diff with respect to the baseline code and speedup ratio information. Your task is to rank the candidates in decreasing order of their viability as a pull request. Your goal is to improve the probability of acceptance of the optimization by an expert engineer.
You are also provided with the following information.
- python_version - The version of python the code would be executed on.
- function_references - Python markdown blocks with filename and references of some functions which call the function being optimized. The filenames and/or references could indicate if the function being optimized is in a hot path. The reference could have the function being called from a place that is important, for example in a loop, which means the effect of optimization might be important.
Rules to follow while ranking optimization candidates -
- Prefer optimizations with higher speedup ratios. If the higher speedups happen due to strange hacks or micro-optimizations or something an expert won't write then prefer it less.
- Prefer optimizations which contain precise diffs unless the speedup provided is very high. Larger pull requests are typically harder to accept then more precise smaller pull requests.
- Introduction of the `global` and `nonlocal` keywords in optimizations is **HIGHLY DISCOURAGED** as it reduces code clarity and maintainability, introduces hidden dependencies, can cause subtle bugs and breaks modularity. **DO NOT** prefer such optimizations.
- If the only optimizations are micro-optimizations like inlining a function call, or localizing variables or methods (not being used in a loop), especially with python_version older than 3.11, do not prefer the optimizations.
- The optimization candidate should not impact the code readability unless the speedup provided is very high.
Sometimes, these criteria maybe in conflict with each other. In such cases you have to remember that the goal is acceptance of the pull request, so make a judgement on what optimization candidate would be most likely to be accepted.
@ -56,20 +60,20 @@ A brief explanation of why the particular ranking was made.
</explain>
"""
USER_PROMPT = """Here is a numbered list of optimization candidates' and their speedup ratios.
USER_PROMPT = """Here is a numbered list of optimization candidates' code diffs and their speedup ratios.
{ranking_context}
Here is the python version
{python_version}
Here are the function references
{function_references}
"""
async def rank_optimizations(
user_id: str,
speedups: list[float],
diffs: list[str],
optimization_ids: list[str],
python_version: str = "3.12.9",
rank_model: LLM = RANKING_MODEL,
trace_id: str = "",
async def rank_optimizations( # noqa: D417
user_id: str, data: RankInputSchema, rank_model: LLM = RANKING_MODEL
) -> RankResponseSchema | RankErrorResponseSchema:
"""Optimize the given python code for performance using the Claude 4 model.
@ -86,12 +90,15 @@ async def rank_optimizations(
"""
debug_log_sensitive_data(f"Generating a ranking for {user_id}")
# TODO add logging instead of print(optimization_ids)
SYSTEM_PROMPT.format(python_version=python_version)
ranking_context = ""
for i, (diff, speedup) in enumerate(zip(diffs, speedups, strict=False)):
for i, (diff, speedup) in enumerate(zip(data.diffs, data.speedups, strict=False)):
ranking_context += f"{i + 1}. Diff:\n```diff\n{diff}\n```\nSpeedup: {speedup:.3f}\n"
user_prompt = USER_PROMPT.format(ranking_context=ranking_context)
user_prompt = USER_PROMPT.format(
ranking_context=ranking_context,
python_version=data.python_version or "Not available",
function_references=data.function_references or "Not available",
)
system_message = ChatCompletionSystemMessageParam(role="system", content=SYSTEM_PROMPT)
user_message = ChatCompletionUserMessageParam(role="user", content=user_prompt)
debug_log_sensitive_data(f"{SYSTEM_PROMPT}{user_prompt}")
@ -102,16 +109,15 @@ async def rank_optimizations(
| ChatCompletionToolMessageParam
| ChatCompletionFunctionMessageParam
] = [system_message, user_message]
async with create_openai_client() as claude_client:
try:
output = await claude_client.with_options(max_retries=2).chat.completions.create(
model=rank_model.name, messages=messages, n=1
)
await update_optimization_cost(trace_id=trace_id, cost=calculate_llm_cost(output, rank_model))
except Exception as e:
debug_log_sensitive_data(f"Failed to generate new explanation, Error message: {e}")
sentry_sdk.capture_exception(e)
return RankErrorResponseSchema(error=str(e))
try:
output = await openai_client.with_options(max_retries=2).chat.completions.create(
model=rank_model.name, messages=messages, n=1
)
await update_optimization_cost(trace_id=data.trace_id, cost=calculate_llm_cost(output, rank_model))
except Exception as e:
debug_log_sensitive_data(f"Failed to generate new explanation, Error message: {e}")
sentry_sdk.capture_exception(e)
return RankErrorResponseSchema(error=str(e))
debug_log_sensitive_data(f"AIClient optimization response:\n{output}")
if output.usage is not None:
ph(
@ -138,7 +144,7 @@ async def rank_optimizations(
except:
# TODO add logging instead of print("No ranking found")
return RankErrorResponseSchema(error="No ranking found")
if not sorted(ranking) == list(range(1, len(diffs) + 1)):
if not sorted(ranking) == list(range(1, len(data.diffs) + 1)):
# TODO need to handle all edge cases
# TODO add logging instead of print("Invalid ranking")
return RankErrorResponseSchema(error="No ranking found")
@ -150,7 +156,8 @@ class RankInputSchema(Schema):
speedups: list[float]
diffs: list[str]
optimization_ids: list[str] # which diff corresponded to which opt candidate
python_version: str
python_version: str | None = None
function_references: str | None = None
class RankResponseSchema(Schema):
@ -167,14 +174,7 @@ async def rank(request, data: RankInputSchema) -> tuple[int, RankResponseSchema
ph(request.user, "aiservice-rank-called")
if not validate_trace_id(data.trace_id):
return 400, RankErrorResponseSchema(error="Invalid trace ID. Please provide a valid UUIDv4.")
ranking_response = await rank_optimizations(
request.user,
data.speedups,
data.diffs,
data.optimization_ids,
python_version=data.python_version,
trace_id=data.trace_id,
)
ranking_response = await rank_optimizations(request.user, data)
if isinstance(ranking_response, RankErrorResponseSchema):
ph(request.user, "Invalid Ranking, fallback to default")
debug_log_sensitive_data("No valid ranking was generated")

View file

@ -1,11 +1,9 @@
from __future__ import annotations
import ast
import logging
import platform
from dataclasses import dataclass
import black
import isort
import sentry_sdk
@ -16,6 +14,10 @@ from testgen.models import TestingMode
plat_str = platform.python_version_tuple()
platform_tuple: tuple[int, int, int] = int(plat_str[0]), int(plat_str[1]), int(plat_str[2])
# Creating these at module level to avoid memory leak from repeated Config() creation
# Previously creating isort.Config() on every call caused 500k+ object accumulation (1-2GB per request) as per traces in production
_ISORT_CONFIG = isort.Config(float_to_top=True)
@dataclass(frozen=True)
class FunctionCallNodeArguments:
@ -35,15 +37,19 @@ RNG_MODULES_SEEDS = {
def format_and_float_to_top(code: str) -> str:
logger = logging.getLogger()
original_level = logger.level
logger.setLevel(logging.INFO) # Suppress debug logs from black, which spams the aiservice console
"""Sort imports and float them to the top.
NOTE: Black formatting is intentionally disabled because:
1. This code is machine-generated and immediately executed (never read by humans)
2. Black creates 100k+ AST objects for 200-500 lines, consuming 5-20MB per call
3. With multiple calls per request, this causes workers to grow from 200MB to 7GB
4. Import sorting is sufficient to ensure valid Python execution
"""
try:
formatted_code = black.format_str(safe_isort(code, config=isort.Config(float_to_top=True)), mode=black.Mode())
formatted_code = safe_isort(code, config=_ISORT_CONFIG)
except Exception:
formatted_code = code
logger.setLevel(original_level)
return formatted_code
return formatted_code.strip()
class FunctionImportedAsVisitor(ast.NodeVisitor):
@ -271,13 +277,26 @@ class InjectPerfAndLogging(ast.NodeTransformer):
ast.Constant(value=self.only_function_name),
ast.Constant(value=index),
ast.Name(id="codeflash_loop_index", ctx=ast.Load()),
*(call_node.args if self.mode == TestingMode.PERFORMANCE else []),
*(
call_node.args
if self.mode == TestingMode.PERFORMANCE
else [
ast.Starred(
value=ast.Attribute(
value=ast.Name(id="_call__bound__arguments", ctx=ast.Load()),
attr="args",
ctx=ast.Load(),
),
ctx=ast.Load(),
)
]
),
],
keywords=[
ast.keyword(
value=ast.Attribute(
value=ast.Name(id="_call__bound__arguments", ctx=ast.Load()),
attr="arguments",
attr="kwargs",
ctx=ast.Load(),
)
)
@ -495,8 +514,7 @@ class InjectPerfAndLogging(ast.NodeTransformer):
return self.visit_FunctionDef(node)
def inject_behavior_logging_code(test_code: str) -> str:
logging_and_wrapper_code = """
behavior_logging_and_wrapper_code = """
from __future__ import annotations
import gc
@ -568,11 +586,13 @@ def codeflash_wrap(
raise exception
return return_value
"""
return format_and_float_to_top(logging_and_wrapper_code + test_code)
def inject_perf_logging_code(test_code: str) -> str:
logging_and_wrapper_code = """
def inject_behavior_logging_code(test_code: str) -> str:
return format_and_float_to_top(behavior_logging_and_wrapper_code + test_code)
perf_logging_and_wrapper_code = """
from __future__ import annotations
import gc
@ -618,7 +638,10 @@ def codeflash_wrap(
raise exception
return return_value
"""
return format_and_float_to_top(logging_and_wrapper_code + test_code)
def inject_perf_logging_code(test_code: str) -> str:
return format_and_float_to_top(perf_logging_and_wrapper_code + test_code)
def instrument_test_source(

View file

@ -8,7 +8,7 @@ from pathlib import Path
from typing import SupportsIndex
from aiservice.common_utils import parse_python_version, safe_isort
from aiservice.env_specific import create_openai_client, debug_log_sensitive_data
from aiservice.env_specific import debug_log_sensitive_data, openai_client
from aiservice.models.aimodels import EXECUTE_MODEL, EXPLAIN_MODEL, LLM, PLAN_MODEL, calculate_llm_cost
from aiservice.models.functions_to_optimize import FunctionToOptimize
from log_features.log_event import update_optimization_cost
@ -20,8 +20,6 @@ from testgen.instrumentation.instrument_new_tests import instrument_test_source
testgen_api = NinjaAPI(urls_namespace="testgen")
openai_client = create_openai_client()
# Get the directory of the current file
current_dir = Path(__file__).parent
EXPLAIN_SYSTEM_PROMPT = (current_dir / "sqlalchemy_explain_system_prompt.md").read_text()

View file

@ -16,11 +16,11 @@ from openai import OpenAIError
from aiservice.analytics.posthog import ph
from aiservice.common_utils import parse_python_version, safe_isort, should_hack_for_demo, validate_trace_id
from aiservice.env_specific import IS_PRODUCTION, debug_log_sensitive_data, open_ai_client
from aiservice.env_specific import IS_PRODUCTION, debug_log_sensitive_data, openai_client
from aiservice.models.aimodels import EXECUTE_MODEL, LLM, calculate_llm_cost
from authapp.auth import AuthBearer
from log_features.log_event import update_optimization_cost
from log_features.log_features import log_features_optimized
from log_features.log_features import log_features
from testgen.instrumentation.edit_generated_test import parse_module_to_cst, replace_definition_with_import
from testgen.instrumentation.instrument_new_tests import instrument_test_source
from testgen.models import (
@ -111,7 +111,7 @@ To help unit test the function above, list diverse scenarios that the function s
package_comment = ""
# if unit_test_package == "pytest":
# package_comment = "# below, each test case is represented by a tuple passed to the @pytest.mark.parametrize decorator"
execute_system_message = {"role": "system", "content": execute_system_prompt.format(function_name=function_name)}
execute_system_message = {"role": "system", "content": execute_system_prompt.format(function_name=ctx.data.qualified_name)}
execute_messages = [execute_system_message, plan_user_message]
@ -151,13 +151,12 @@ def instrument_tests(
"python_version": python_version,
}
# instrument_test_source() already applies isort via format_and_float_to_top()
# No need to apply isort again here (was causing double formatting overhead)
behavior_result = instrument_test_source(**common_args, mode=TestingMode.BEHAVIOR)
instrumented_behavior = safe_isort(behavior_result, float_to_top=True) if behavior_result is not None else None
perf_result = instrument_test_source(**common_args, mode=TestingMode.PERFORMANCE)
instrumented_perf = safe_isort(perf_result, float_to_top=True) if perf_result is not None else None
return instrumented_behavior, instrumented_perf
return behavior_result, perf_result
def parse_and_validate_llm_output(
@ -197,7 +196,7 @@ async def generate_and_validate_test_code(
user_id: str,
posthog_event_suffix: str,
) -> str:
response = await open_ai_client.with_options(max_retries=2).chat.completions.create(
response = await openai_client.with_options(max_retries=2).chat.completions.create(
model=model.name, messages=messages, temperature=temperature
)
cost = calculate_llm_cost(response, execute_model) or 0.0
@ -321,7 +320,7 @@ def validate_request_data(data: TestGenSchema) -> tuple[tuple[int, int, int], Ba
ctx = BaseTestGenContext.get_dynamic_context(
TestGenContextData(
source_code_being_tested=data.source_code_being_tested,
function_name=data.function_to_optimize.function_name,
qualified_name=data.function_to_optimize.qualified_name,
)
)
ctx.validate_python_module(feature_version=python_version[:2])
@ -331,9 +330,6 @@ def validate_request_data(data: TestGenSchema) -> tuple[tuple[int, int, int], Ba
return python_version, ctx
background_tasks = set()
@testgen_api.post(
"/", response={200: TestGenResponseSchema, 400: TestGenErrorResponseSchema, 500: TestGenErrorResponseSchema}
)
@ -364,7 +360,7 @@ async def testgen(
) = await generate_regression_tests_from_function(
ctx=ctx,
user_id=request.user,
function_name=data.function_to_optimize.function_name,
function_name=data.function_to_optimize.qualified_name,
python_version=python_version,
data=data,
unit_test_package=data.test_framework,
@ -374,8 +370,8 @@ async def testgen(
ph(request.user, "aiservice-testgen-tests-generated")
task = asyncio.create_task(
log_features_optimized(
if hasattr(request, "should_log_features") and request.should_log_features:
await log_features(
trace_id=data.trace_id,
user_id=request.user,
generated_tests=[generated_test_source],
@ -385,11 +381,7 @@ async def testgen(
"test_timeout": data.test_timeout,
"function_to_optimize": data.function_to_optimize.function_name,
},
request=request,
)
)
background_tasks.add(task)
task.add_done_callback(background_tasks.discard)
return 200, TestGenResponseSchema(
generated_tests=generated_test_source,

View file

@ -8,7 +8,7 @@ from testgen.preprocessing.preprocess_pipeline import preprocessing_testgen_pipe
@dataclass()
class TestGenContextData:
source_code_being_tested: str
function_name: str
qualified_name: str
def ellipsis_in_ast_not_types(module: ast.AST) -> bool:
@ -67,7 +67,7 @@ class SingleTestGenContext(BaseTestGenContext):
raise SyntaxError(msg)
def generate_notes_markdown(self) -> str:
notes = preprocessing_testgen_pipeline(self.data.source_code_being_tested, self.data.function_name)
notes = preprocessing_testgen_pipeline(self.data.source_code_being_tested, self.data.qualified_name)
return "Notes:\n" + "\n".join(notes) if notes else ""
def did_generate_ellipsis(self, generated_code: str, python_version: tuple) -> bool:
@ -93,7 +93,7 @@ class MultiTestGenContext(BaseTestGenContext):
def generate_notes_markdown(self) -> str:
all_notes = set() # prevent duplicate notes
for scoped_code in split_markdown_code(self.data.source_code_being_tested).values():
all_notes.update(preprocessing_testgen_pipeline(scoped_code, self.data.function_name))
all_notes.update(preprocessing_testgen_pipeline(scoped_code, self.data.qualified_name))
return "Notes:\n" + "\n".join(all_notes) if all_notes else ""
def did_generate_ellipsis(self, generated_code: str, python_version: tuple) -> bool:

View file

@ -0,0 +1,805 @@
"""Tests for comment cleaner post-processing step.
The comment cleaner removes or reverts comments that are not immediately before,
within, or after modified code lines. This prevents LLMs from adding unnecessary
documentation to unchanged code.
"""
import ast
import libcst as cst
from optimizer.postprocess import clean_extraneous_comments
def assert_code_unchanged(optimized: str, result: str) -> None:
"""Verify that the actual code (not comments) is identical between optimized and result.
This ensures the comment cleaner only modifies comments and doesn't accidentally
change the actual code logic from what the optimizer produced.
"""
# Parse both as AST and compare the unparsed versions
# This strips all comments and formatting, leaving only the semantic code
optimized_ast = ast.parse(optimized)
result_ast = ast.parse(result)
# Unparse to get normalized code without comments
optimized_code = ast.unparse(optimized_ast)
result_code = ast.unparse(result_ast)
assert optimized_code == result_code, (
f"Code was modified (not just comments)!\nOptimized code: {optimized_code}\nResult code: {result_code}"
)
def test_keep_comments_near_modified_code():
"""Test Case 1: Keep comments that are within modified code region."""
original = """def calculate(x, y):
return x + y"""
optimized = """def calculate(x, y):
# Using bit shift for faster multiplication by 2
return (x + y) << 1"""
expected = """def calculate(x, y):
# Using bit shift for faster multiplication by 2
return (x + y) << 1"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_remove_comments_added_to_unchanged_code():
"""Test Case 2: Remove comments added to unchanged code lines."""
original = """def calculate(x, y):
z = x + y
return z * 2"""
optimized = """def calculate(x, y):
# Store intermediate result
z = x + y # This computes the sum
return z << 1"""
expected = """def calculate(x, y):
z = x + y
return z << 1"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_keep_comments_immediately_before_modified_code():
"""Test Case 3: Remove comments before the block of the modified code."""
original = """def process(data):
result = []
for item in data:
result.append(item * 2)
return result"""
optimized = """def process(data):
result = []
# Using list comprehension for better performance
for item in data:
result.extend([item * 2])
return result"""
expected = """def process(data):
result = []
for item in data:
result.extend([item * 2])
return result"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_keep_comments_immediately_after_modified_code():
"""Test Case 4: Keep inline comments on modified lines."""
original = """def validate(x):
if x > 0:
return True
return False"""
optimized = """def validate(x):
if x > 0:
return False # Optimization applied here
return False"""
expected = """def validate(x):
if x > 0:
return False # Optimization applied here
return False"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_remove_comments_far_from_modifications():
"""Test Case 5: Remove comments that are far from any modifications."""
original = """def complex_calc(a, b, c):
x = a + b
y = x * c
return y"""
optimized = """def complex_calc(a, b, c):
# Calculate intermediate sum
x = a + b
# Perform final multiplication using bit shift
y = x << c
# Return the result
return y"""
expected = """def complex_calc(a, b, c):
x = a + b
# Perform final multiplication using bit shift
y = x << c
# Return the result
return y"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_use_modified_existing_comments_next_to_changes():
original = """def helper(n):
# Original comment about n
result = n * 2
return result"""
optimized = """def helper(n):
# LLM modified this comment to explain n differently
result = n << 1
return result"""
expected = """def helper(n):
# LLM modified this comment to explain n differently
result = n << 1
return result"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_multiple_modifications_with_mixed_comments():
"""Test Case 7: Handle multiple modifications with mixed comment scenarios."""
original = """def multi_op(a, b, c):
x = a + b
y = x * 2
z = y + c
return z"""
optimized = """def multi_op(a, b, c):
# Sum a and b
x = a + b
# Double using bit shift
y = x << 1
# Add c
z = y + c
return z"""
expected = """def multi_op(a, b, c):
x = a + b
# Double using bit shift
y = x << 1
# Add c
z = y + c
return z"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_no_modifications_remove_all_new_comments():
"""Test Case 9: Remove all new comments when no code was actually modified."""
original = """def unchanged(x):
return x * 2"""
optimized = """def unchanged(x):
# This computes double
return x * 2"""
expected = """def unchanged(x):
return x * 2"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_block_comment_preservation_near_modifications():
"""Test Case 10: Handle multi-line comments near modifications."""
original = """def block_test(n):
if n > 0:
return n * 2
return 0"""
optimized = """def block_test(n):
# This function handles positive numbers
# by doubling them efficiently
if n > 0:
# Using bit shift optimization
return n << 1
return 0"""
expected = """def block_test(n):
if n > 0:
# Using bit shift optimization
return n << 1
return 0"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_preserve_original_comments_on_modified_lines():
"""Test that original comments on modified lines are preserved."""
original = """def func(x):
# Original comment
result = x * 2 # inline original
return result"""
optimized = """def func(x):
# Modified comment
result = x << 1 # inline modified
return result"""
expected = """def func(x):
# Modified comment
result = x << 1 # inline modified
return result"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_empty_file():
"""Test handling of empty files."""
original = ""
optimized = ""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
# Empty file case - both original and result are empty, so AST check would pass trivially
assert result.code == ""
def test_nested_function_with_comments():
"""Test handling of comments in nested functions."""
original = """def outer():
def inner(x):
return x * 2
return inner"""
optimized = """def outer():
# Nested function for doubling
def inner(x):
# Using bit shift
return x << 1
return inner"""
expected = """def outer():
def inner(x):
# Using bit shift
return x << 1
return inner"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_class_with_method_modifications():
"""Test handling of comments in class methods."""
original = """class Calculator:
def calculate(self, x):
return x * 2"""
optimized = """class Calculator:
# Main calculator class
def calculate(self, x):
# Optimized calculation
return x << 1"""
expected = """class Calculator:
def calculate(self, x):
# Optimized calculation
return x << 1"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_strings_with_hash_symbols_not_treated_as_comments():
"""Test that hash symbols and comment-like text inside strings are preserved as code."""
original = """def process_text(data):
message = "Processing # of items"
multiline = \"\"\"
This is a multi-line string
# This looks like a comment but it's in a string
\"\"\"
result = data.split("#")
return result"""
optimized = """def process_text(data):
# Add helpful message
message = "Processing # of items"
# Multi-line string with comment-like text
multiline = \"\"\"
This is a multi-line string
# This looks like a comment but it's in a string
\"\"\"
# Split by hash symbol
result = data.split("#")
return result"""
expected = """def process_text(data):
message = "Processing # of items"
multiline = \"\"\"
This is a multi-line string
# This looks like a comment but it's in a string
\"\"\"
result = data.split("#")
return result"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_strings_with_hash_before_code_changes():
"""Test that strings with # immediately before code changes are preserved correctly."""
original = """def format_and_process(data):
header = "# Header information"
info = \"\"\"
Documentation:
# This is documentation, not a comment
# Another line in docs
\"\"\"
result = data * 2
return result"""
optimized = """def format_and_process(data):
# String with hash symbol
header = "# Header information"
# Multi-line documentation string
info = \"\"\"
Documentation:
# This is documentation, not a comment
# Another line in docs
\"\"\"
# Optimized to use bit shift
result = data << 1
return result"""
expected = """def format_and_process(data):
header = "# Header information"
info = \"\"\"
Documentation:
# This is documentation, not a comment
# Another line in docs
\"\"\"
# Optimized to use bit shift
result = data << 1
return result"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_preserve_removed_original_comments_far_from_changes():
"""Test that original comments removed by LLM are restored when immediately before unchanged code."""
original = """def calculate_stats(numbers):
# This is important context about the function
total = 0
# Track the count
count = 0
for num in numbers:
# Process each number
total += num
count += 1
return total / count"""
optimized = """def calculate_stats(numbers):
total = 0
count = 0
for num in numbers:
total += num
count += 1
# Optimized: use built-in functions
return sum(numbers) / len(numbers)"""
# Expected behavior:
# - Original comments immediately before unchanged lines (total=0, count=0, etc.) are restored
# - The blank line between count=0 and for statement: Since both lines around it are unchanged,
# and the blank line was in the original, it should be restored
# - New comment "Optimized: use built-in functions" before the modified return should be kept
expected = """def calculate_stats(numbers):
# This is important context about the function
total = 0
# Track the count
count = 0
for num in numbers:
# Process each number
total += num
count += 1
# Optimized: use built-in functions
return sum(numbers) / len(numbers)"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_restore_inline_comments_on_unchanged_lines():
"""Test that inline comments on unchanged lines are preserved from original."""
original = """def process(value):
result = value * 2 # Double the input
output = result + 10 # Add offset
return output"""
optimized = """def process(value):
result = value * 2
output = result + 10
# Final step: return the computed value
return output << 1"""
expected = """def process(value):
result = value * 2 # Double the input
output = result + 10 # Add offset
# Final step: return the computed value
return output << 1"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_restore_multiline_comment_block_before_unchanged_code():
"""Test current behavior: only the immediately preceding comment is restored.
Note: Currently, when multiple consecutive comment lines precede unchanged code,
only the comment immediately before the code line is restored. This is a known
limitation - full multi-line comment block restoration would require more
sophisticated tracking of comment groups.
"""
original = """def calculate(x, y):
# This function performs a calculation
# It takes two parameters and combines them
# The logic here is intentionally simple
result = x + y
return result * 2"""
optimized = """def calculate(x, y):
result = x + y
# Optimized multiplication
return result << 1"""
# Current behavior: multi-line comment blocks are not fully restored
# Only the immediately preceding comment would be restored if the code line was unchanged
# In this case, result = x + y is unchanged, so comments before it should be restored
expected = """def calculate(x, y):
# This function performs a calculation
# It takes two parameters and combines them
# The logic here is intentionally simple
result = x + y
# Optimized multiplication
return result << 1"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_mixed_original_and_new_comments():
"""Test handling when LLM both removes original comments and adds new ones.
The LLM replaces original comments with new ones. The current behavior:
- If the line is unchanged, remove the new comment (not immediately before a change)
- Original comments are only restored if immediately before the unchanged line
"""
original = """def validate_input(data):
# Check if data is valid
if not data:
return False
# Process the data
cleaned = data.strip()
return len(cleaned) > 0"""
optimized = """def validate_input(data):
# First validation step
if not data:
return False
# Strip whitespace for better accuracy
cleaned = data.strip()
# Use bool conversion for cleaner code
return bool(cleaned)"""
# Current behavior: new comments before unchanged lines are removed,
# but original comments are not fully restored (limitation)
expected = """def validate_input(data):
# Check if data is valid
if not data:
return False
# Process the data
cleaned = data.strip()
# Use bool conversion for cleaner code
return bool(cleaned)"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_restore_comments_in_loop_bodies():
"""Test that comments inside loops are preserved for unchanged code."""
original = """def sum_evens(numbers):
total = 0
for num in numbers:
# Only process even numbers
if num % 2 == 0:
total += num
return total"""
optimized = """def sum_evens(numbers):
total = 0
for num in numbers:
if num % 2 == 0:
total += num
# Optimized return using ternary
return total if total > 0 else 0"""
expected = """def sum_evens(numbers):
total = 0
for num in numbers:
# Only process even numbers
if num % 2 == 0:
total += num
# Optimized return using ternary
return total if total > 0 else 0"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_restore_comments_with_special_characters():
"""Test current behavior with comments containing special characters.
Similar to multi-line comment blocks, only the immediately preceding comment
is restored. Multiple consecutive comments are not fully restored.
"""
original = """def format_string(text):
# TODO: handle edge cases like empty strings
# NOTE: this uses str.format() for compatibility
formatted = "Result: {}".format(text)
return formatted"""
optimized = """def format_string(text):
formatted = "Result: {}".format(text)
# Enhanced: strip whitespace
return formatted.strip()"""
# Current behavior: multi-line comments before unchanged code not fully restored
expected = """def format_string(text):
# TODO: handle edge cases like empty strings
# NOTE: this uses str.format() for compatibility
formatted = "Result: {}".format(text)
# Enhanced: strip whitespace
return formatted.strip()"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_preserve_docstring_and_restore_inline_comments():
"""Test that docstrings are preserved and inline comments are restored."""
original = """def helper(value):
\"\"\"Helper function for processing values.\"\"\"
intermediate = value * 3 # Triple the value
return intermediate"""
optimized = """def helper(value):
\"\"\"Helper function for processing values.\"\"\"
# Calculate intermediate result
intermediate = value * 3
# Return with optimization
return intermediate + 1"""
expected = """def helper(value):
\"\"\"Helper function for processing values.\"\"\"
intermediate = value * 3 # Triple the value
# Return with optimization
return intermediate + 1"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_restore_comments_after_conditional_branches():
"""Test that comments in conditional branches are handled correctly."""
original = """def check_status(code):
if code == 200:
# Success case
return "OK"
elif code == 404:
# Not found case
return "Not Found"
else:
return "Error\""""
optimized = """def check_status(code):
if code == 200:
return "OK"
elif code == 404:
return "Not Found"
else:
# Default error handling with logging
return "Unknown Error\""""
expected = """def check_status(code):
if code == 200:
# Success case
return "OK"
elif code == 404:
# Not found case
return "Not Found"
else:
# Default error handling with logging
return "Unknown Error\""""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()
def test_comprehensive_example_with_multiple_comment_scenarios():
"""Comprehensive test with multiple comment scenarios in realistic code."""
original = """def process_data(items, threshold=10):
# Initialize results
results = []
total = 0
for item in items:
if item > threshold:
value = item * 2
total += value
results.append(value)
return results, total"""
optimized = """def process_data(items, threshold=10):
# Initialize results list
results = []
# Track running total
total = 0
# Process each item in the list
for item in items:
# Check if item exceeds threshold
if item > threshold:
# Use bit shift for faster multiplication
value = item << 1 # Double the value
# Add to running total
total += value
results.append(value) # Store result
# Return both results and total
return results, total"""
# Expected behavior:
# - "Initialize results list" - removed (modified comment on unchanged line)
# - "Track running total" - removed (comment before unchanged line)
# - "Process each item in the list" - removed (comment before unchanged line)
# - "Check if item exceeds threshold" - removed (comment before unchanged line)
# - "Use bit shift for faster multiplication" - KEPT (immediately before modified line)
# - "Double the value" inline - KEPT (on modified line)
# - "Add to running total" - KEPT (part of replaced block with the optimization)
# - "Store result" inline - removed (inline on unchanged line)
# - "Return both results and total" - removed (comment before unchanged line)
expected = """def process_data(items, threshold=10):
# Initialize results
results = []
total = 0
for item in items:
if item > threshold:
# Use bit shift for faster multiplication
value = item << 1 # Double the value
# Add to running total
total += value
results.append(value)
return results, total"""
original_module = cst.parse_module(original)
optimized_module = cst.parse_module(optimized)
result = clean_extraneous_comments(original_module, optimized_module)
assert_code_unchanged(optimized, result.code)
assert result.code.strip() == expected.strip()

View file

@ -170,7 +170,7 @@ def sorter(arr):
expected = [
CodeExplanationAndID(
libcst.parse_module("def sorter(arr):\n return sorted(arr)\n"),
libcst.parse_module("\ndef sorter(arr):\n return sorted(arr)\n"),
"Optimized to use built-in sorted() function",
"1",
)
@ -209,7 +209,7 @@ def sorter(arr):
expected = [
CodeExplanationAndID(
libcst.parse_module("def sorter(arr):\n return sorted(arr)\n"),
libcst.parse_module("\ndef sorter(arr):\n return sorted(arr)\n"),
"Your original program is using bubble sort which has a time complexity of O(n^2) making it inefficient for large data sets. A faster sorting algorithm could be applied here, 'Timsort', which is a hybrid sorting algorithm, derived from merge sort and insertion sort, designed to perform well on many kinds of real-world data. This algorithm is built-in as a native sorting algorithm in Python, and hence it runs considerably faster than any algorithm implemented in Python.\n\nHere is the updated faster program.\n\n",
"2",
)

View file

@ -33,7 +33,7 @@ def test_InjectPerfAndLogging_with() -> None:
with pytest.raises(AttributeError):
_call__bound__arguments = inspect.signature(hdbscan.relative_validity_).bind()
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(hdbscan.relative_validity_, 'code_to_optimize_path', None, 'test_relative_validity_no_tree', 'relative_validity_', '1_0', codeflash_loop_index, **_call__bound__arguments.arguments)"""
codeflash_return_value = codeflash_wrap(hdbscan.relative_validity_, 'code_to_optimize_path', None, 'test_relative_validity_no_tree', 'relative_validity_', '1_0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)"""
assert ast.unparse(new_module_node).strip("\n") == expected
module_node = ast.parse(code)
new_module_node = InjectPerfAndLogging(
@ -68,7 +68,7 @@ def test_InjectPerfAndLogging() -> None:
hdbscan = HDBSCAN()
_call__bound__arguments = inspect.signature(hdbscan.relative_validity_).bind()
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(hdbscan.relative_validity_, 'code_to_optimize_path', None, 'test_relative_validity_no_tree', 'relative_validity_', '1', codeflash_loop_index, **_call__bound__arguments.arguments)"""
codeflash_return_value = codeflash_wrap(hdbscan.relative_validity_, 'code_to_optimize_path', None, 'test_relative_validity_no_tree', 'relative_validity_', '1', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)"""
assert ast.unparse(new_module_node).strip("\n") == expected
module_node = ast.parse(code)
new_module_node = InjectPerfAndLogging(
@ -102,7 +102,7 @@ def test_remove_bad_assert() -> None:
hdbscan = HDBSCAN()
_call__bound__arguments = inspect.signature(hdbscan.relative_validity_).bind()
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(hdbscan.relative_validity_, 'code_to_optimize_path', None, 'test_relative_validity_no_tree', 'relative_validity_', '1', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(hdbscan.relative_validity_, 'code_to_optimize_path', None, 'test_relative_validity_no_tree', 'relative_validity_', '1', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
result = 5"""
assert ast.unparse(new_module_node).strip("\n") == expected
@ -123,14 +123,14 @@ def test_translate_word_starting_with_single_consonant():
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(translate).bind('apple')
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(translate, 'code_to_optimize_path', None, 'test_translate_word_starting_with_vowel', 'translate', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(translate, 'code_to_optimize_path', None, 'test_translate_word_starting_with_vowel', 'translate', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_translate_word_starting_with_single_consonant():
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(translate).bind('banana')
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(translate, 'code_to_optimize_path', None, 'test_translate_word_starting_with_single_consonant', 'translate', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(translate, 'code_to_optimize_path', None, 'test_translate_word_starting_with_single_consonant', 'translate', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value"""
assert ast.unparse(new_module_node).strip("\n") == expected
@ -256,84 +256,84 @@ class SorterTestCase(unittest.TestCase):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_empty_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_empty_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_single_element_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([5])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_single_element_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_single_element_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_ascending_order_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([1, 2, 3, 4, 5])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_ascending_order_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_ascending_order_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_descending_order_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([5, 4, 3, 2, 1])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_descending_order_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_descending_order_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_random_order_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([3, 1, 4, 2, 5])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_random_order_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_random_order_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_duplicate_elements_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([3, 1, 4, 2, 2, 5, 1])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_duplicate_elements_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_duplicate_elements_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_negative_numbers_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([-5, -2, -8, -1, -3])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_negative_numbers_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_negative_numbers_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_mixed_data_types_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind(['apple', 2, 'banana', 1, 'cherry'])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_mixed_data_types_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_mixed_data_types_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_large_input_list(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind(list(range(1000, 0, -1)))
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_large_input_list', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_large_input_list', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_list_with_none_values(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([None, 2, None, 1, None])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_none_values', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_none_values', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_list_with_nan_values(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([float('nan'), 2, float('nan'), 1, float('nan')])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_nan_values', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_nan_values', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_list_with_complex_numbers(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
_call__bound__arguments = inspect.signature(sorter).bind([3 + 2j, 1 + 1j, 4 + 3j, 2 + 1j, 5 + 4j])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_complex_numbers', 'sorter', '0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_complex_numbers', 'sorter', '0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_list_with_custom_class_objects(self):
@ -351,7 +351,7 @@ class SorterTestCase(unittest.TestCase):
expected_output = [Person('Charlie', 20), Person('Alice', 25), Person('Bob', 30)]
_call__bound__arguments = inspect.signature(sorter).bind(input_list)
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_custom_class_objects', 'sorter', '3', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_custom_class_objects', 'sorter', '3', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
def test_list_with_uncomparable_elements(self):
@ -359,7 +359,7 @@ class SorterTestCase(unittest.TestCase):
with self.assertRaises(TypeError):
_call__bound__arguments = inspect.signature(sorter).bind([5, 'apple', 3, [1, 2, 3], 2])
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_uncomparable_elements', 'sorter', '0_0', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_uncomparable_elements', 'sorter', '0_0', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
def test_list_with_custom_comparison_function(self):
codeflash_loop_index = int(os.environ['CODEFLASH_LOOP_INDEX'])
@ -367,7 +367,7 @@ class SorterTestCase(unittest.TestCase):
expected_output = [5, 4, 3, 2, 1]
_call__bound__arguments = inspect.signature(sorter).bind(input_list, reverse=True)
_call__bound__arguments.apply_defaults()
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_custom_comparison_function', 'sorter', '2', codeflash_loop_index, **_call__bound__arguments.arguments)
codeflash_return_value = codeflash_wrap(sorter, 'code_to_optimize_path', 'SorterTestCase', 'test_list_with_custom_comparison_function', 'sorter', '2', codeflash_loop_index, *_call__bound__arguments.args, **_call__bound__arguments.kwargs)
codeflash_output = codeflash_return_value
if __name__ == '__main__':
unittest.main()"""

View file

@ -6,12 +6,118 @@ resolution-markers = [
"python_full_version < '3.13'",
]
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
]
[[package]]
name = "aiohttp"
version = "3.13.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
{ name = "aiosignal" },
{ name = "attrs" },
{ name = "frozenlist" },
{ name = "multidict" },
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" },
{ url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" },
{ url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" },
{ url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" },
{ url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" },
{ url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" },
{ url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" },
{ url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" },
{ url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" },
{ url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" },
{ url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" },
{ url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" },
{ url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" },
{ url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" },
{ url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" },
{ url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" },
{ url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" },
{ url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" },
{ url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" },
{ url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" },
{ url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" },
{ url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" },
{ url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" },
{ url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" },
{ url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" },
{ url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" },
{ url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" },
{ url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" },
{ url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" },
{ url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" },
{ url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" },
{ url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" },
{ url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" },
{ url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" },
{ url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" },
{ url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" },
{ url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" },
{ url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" },
{ url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" },
{ url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" },
{ url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" },
{ url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" },
{ url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" },
{ url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" },
{ url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" },
{ url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" },
{ url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" },
{ url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" },
{ url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" },
{ url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" },
{ url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" },
{ url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" },
{ url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" },
{ url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" },
{ url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" },
{ url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" },
{ url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" },
{ url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" },
{ url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" },
]
[[package]]
name = "aiosignal"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
[[package]]
name = "aiservice"
version = "0.0.1"
source = { editable = "." }
dependencies = [
{ name = "black" },
{ name = "dj-database-url" },
{ name = "django" },
{ name = "django-ninja" },
@ -20,12 +126,10 @@ dependencies = [
{ name = "isort" },
{ name = "jedi" },
{ name = "libcst" },
{ name = "openai" },
{ name = "openai", extra = ["aiohttp"] },
{ name = "posthog" },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "python-dotenv" },
{ name = "ruff" },
{ name = "sentry-sdk", extra = ["django"] },
@ -37,7 +141,9 @@ dependencies = [
dev = [
{ name = "ipython" },
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-django" },
{ name = "types-colorama" },
{ name = "types-gevent" },
{ name = "types-pexpect" },
@ -47,21 +153,18 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "black", specifier = ">=24.4.2,<25" },
{ name = "dj-database-url", specifier = ">=2.2.0,<3" },
{ name = "django", specifier = ">=5.0.6,<6" },
{ name = "django-ninja", specifier = ">=1.3.0,<2" },
{ name = "gitpython", specifier = ">=3.1.43,<4" },
{ name = "gunicorn", specifier = ">=22.0.0,<23" },
{ name = "isort", specifier = ">=7.0.0" },
{ name = "jedi", specifier = ">=0.19.0,<0.20" },
{ name = "jedi", specifier = ">=0.19.2" },
{ name = "libcst", specifier = ">=1.5.0,<2" },
{ name = "openai", specifier = ">=1.52.2,<2" },
{ name = "openai", extras = ["aiohttp"], specifier = ">=1.52.2,<2" },
{ name = "posthog", specifier = ">=3.5.0,<4" },
{ name = "psycopg2-binary", specifier = ">=2.9.9,<3" },
{ name = "pydantic", specifier = ">=2.9.0" },
{ name = "pytest", specifier = ">=8.2.1,<9" },
{ name = "pytest-django", specifier = ">=4.8.0,<5" },
{ name = "python-dotenv", specifier = ">=1.0.1,<2" },
{ name = "ruff", specifier = ">=0.7.0" },
{ name = "sentry-sdk", extras = ["django"], specifier = ">=2.35.0" },
@ -73,7 +176,9 @@ requires-dist = [
dev = [
{ name = "ipython", specifier = ">=8.12.0,<9" },
{ name = "mypy", specifier = ">=1.13" },
{ name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.1.0" },
{ name = "pytest-django", specifier = ">=4.11.1" },
{ name = "types-colorama", specifier = ">=0.4.15" },
{ name = "types-gevent", specifier = ">=24.11.0.20241230,<25" },
{ name = "types-pexpect", specifier = ">=4.9.0.20241208,<5" },
@ -122,6 +227,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" },
]
[[package]]
name = "attrs"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "backoff"
version = "2.2.1"
@ -131,30 +245,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
]
[[package]]
name = "black"
version = "24.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256, upload-time = "2024-10-07T19:27:53.355Z" },
{ url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534, upload-time = "2024-10-07T19:26:44.953Z" },
{ url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892, upload-time = "2024-10-07T19:24:10.264Z" },
{ url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796, upload-time = "2024-10-07T19:25:06.239Z" },
{ url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986, upload-time = "2024-10-07T19:28:50.684Z" },
{ url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085, upload-time = "2024-10-07T19:28:12.093Z" },
{ url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928, upload-time = "2024-10-07T19:24:15.233Z" },
{ url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875, upload-time = "2024-10-07T19:24:42.762Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898, upload-time = "2024-10-07T19:20:48.317Z" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
@ -309,6 +399,95 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
]
[[package]]
name = "frozenlist"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
{ url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
{ url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
{ url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
{ url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
{ url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
{ url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
{ url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
{ url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
{ url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
{ url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
{ url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
{ url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
{ url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
{ url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
{ url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
{ url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
{ url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
{ url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
{ url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
{ url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
{ url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
{ url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
{ url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
{ url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
{ url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
{ url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
{ url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
{ url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
{ url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
{ url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
{ url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
{ url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
{ url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
{ url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
{ url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
{ url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
{ url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
{ url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
{ url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
{ url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
{ url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
{ url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
{ url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
{ url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
{ url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
{ url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
{ url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
{ url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
{ url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
{ url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
{ url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
{ url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
{ url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
{ url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
{ url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
{ url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
{ url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
{ url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
{ url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
{ url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
{ url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
{ url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
{ url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
{ url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
{ url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
]
[[package]]
name = "gitdb"
version = "4.0.12"
@ -382,6 +561,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "httpx-aiohttp"
version = "0.1.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "httpx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/f2/9a86ce9bc48cf57dabb3a3160dfed26d8bbe5a2478a51f9d1dbf89f2f1fc/httpx_aiohttp-0.1.9.tar.gz", hash = "sha256:4ee8b22e6f2e7c80cd03be29eff98bfe7d89bd77f021ce0b578ee76b73b4bfe6", size = 206023, upload-time = "2025-10-15T08:52:57.475Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/db/5cfa8254a86c34a1ab7fe0dbec9f81bb5ebd831cbdd65aa4be4f37027804/httpx_aiohttp-0.1.9-py3-none-any.whl", hash = "sha256:3dc2845568b07742588710fcf3d72db2cbcdf2acc93376edf85f789c4d8e5fda", size = 6180, upload-time = "2025-10-15T08:52:56.521Z" },
]
[[package]]
name = "idna"
version = "3.11"
@ -563,14 +755,14 @@ wheels = [
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" },
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
]
[[package]]
@ -582,6 +774,105 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154, upload-time = "2021-04-09T21:58:05.122Z" },
]
[[package]]
name = "multidict"
version = "6.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" },
{ url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" },
{ url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" },
{ url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" },
{ url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" },
{ url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" },
{ url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" },
{ url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" },
{ url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" },
{ url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" },
{ url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" },
{ url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" },
{ url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" },
{ url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" },
{ url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" },
{ url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" },
{ url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" },
{ url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" },
{ url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" },
{ url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" },
{ url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" },
{ url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" },
{ url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" },
{ url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" },
{ url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" },
{ url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" },
{ url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" },
{ url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" },
{ url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" },
{ url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" },
{ url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" },
{ url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" },
{ url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" },
{ url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" },
{ url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" },
{ url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" },
{ url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" },
{ url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" },
{ url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" },
{ url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" },
{ url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" },
{ url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" },
{ url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" },
{ url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" },
{ url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" },
{ url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" },
{ url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" },
{ url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" },
{ url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" },
{ url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" },
{ url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" },
{ url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" },
{ url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" },
{ url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" },
{ url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" },
{ url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" },
{ url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" },
{ url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" },
{ url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" },
{ url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" },
{ url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" },
{ url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" },
{ url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" },
{ url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" },
{ url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" },
{ url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" },
{ url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" },
{ url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" },
{ url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" },
{ url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" },
{ url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" },
{ url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" },
{ url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" },
{ url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" },
{ url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" },
{ url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" },
{ url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" },
{ url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" },
{ url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" },
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" },
]
[[package]]
name = "mypy"
version = "1.18.2"
@ -642,6 +933,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" },
]
[package.optional-dependencies]
aiohttp = [
{ name = "aiohttp" },
{ name = "httpx-aiohttp" },
]
[[package]]
name = "packaging"
version = "25.0"
@ -681,15 +978,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
@ -728,6 +1016,90 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]]
name = "propcache"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
{ url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
{ url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
{ url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
{ url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
{ url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
{ url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
{ url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
{ url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
{ url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
{ url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
{ url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
{ url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
{ url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
{ url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
{ url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
{ url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
{ url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
{ url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
{ url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
{ url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
{ url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
{ url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
{ url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
{ url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
{ url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
{ url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
{ url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
{ url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
{ url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
{ url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
{ url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
{ url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
{ url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
{ url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
{ url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
{ url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
{ url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
{ url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
{ url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
{ url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
{ url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
{ url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
{ url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
{ url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
{ url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
{ url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
{ url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
{ url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
{ url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
{ url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
{ url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
{ url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
{ url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
{ url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
{ url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
{ url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
{ url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
{ url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
{ url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
[[package]]
name = "psycopg2-binary"
version = "2.9.11"
@ -739,8 +1111,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
{ url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
{ url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
{ url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
{ url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
{ url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
{ url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
{ url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
{ url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
@ -748,8 +1122,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
@ -757,8 +1133,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
]
@ -1021,28 +1399,28 @@ wheels = [
[[package]]
name = "ruff"
version = "0.14.1"
version = "0.14.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" },
{ url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" },
{ url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" },
{ url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" },
{ url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" },
{ url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" },
{ url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" },
{ url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" },
{ url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" },
{ url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" },
{ url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" },
{ url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" },
{ url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" },
{ url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" },
{ url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" },
{ url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" },
{ url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" },
{ url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" },
{ url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" },
{ url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" },
{ url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" },
{ url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" },
{ url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" },
{ url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" },
{ url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" },
{ url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" },
{ url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" },
{ url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" },
{ url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" },
{ url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" },
]
[[package]]
@ -1294,3 +1672,97 @@ sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc7
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
]
[[package]]
name = "yarl"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
{ url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
{ url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
{ url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
{ url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
{ url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
{ url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
{ url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
{ url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
{ url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
{ url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
{ url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
{ url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
{ url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
{ url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
{ url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
{ url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
{ url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
{ url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
{ url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
{ url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
{ url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
{ url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
{ url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
{ url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
{ url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
{ url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
{ url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
{ url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
{ url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
{ url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
{ url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
{ url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
{ url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
{ url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
{ url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
{ url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
{ url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
{ url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
{ url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
{ url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
{ url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
{ url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
{ url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
{ url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
{ url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
]

View file

@ -1036,19 +1036,111 @@ if [ -n "${EXIT_FILE:-}" ]; then echo "$TEST_RC" > "$EXIT_FILE" 2>/dev/null || t
# If tests below threshold, run Claude Code CLI setup loop
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
echo "Evaluating test pass ratio for Claude Code CLI setup gate..." | tee -a "$TEST_LOG_FILE"
# Attempt to extract passed/failed/errors from log tail (robust to order)
PASSED=$(sed -n "s/.* \([0-9]\+\) passed.*/\1/p" "$TEST_LOG_FILE" | tail -n1)
FAILED=$(sed -n "s/.* \([0-9]\+\) failed.*/\1/p" "$TEST_LOG_FILE" | tail -n1)
ERRORS=$(sed -n "s/.* \([0-9]\+\) errors.*/\1/p" "$TEST_LOG_FILE" | tail -n1)
PASSED=${PASSED:-0}
FAILED=${FAILED:-0}
ERRORS=${ERRORS:-0}
TOTAL=$((PASSED + FAILED + ERRORS))
RATIO=0
if [ "$TOTAL" -gt 0 ]; then
RATIO=$(( 100 * PASSED / TOTAL ))
# Add timeout protection for test parsing to prevent hanging
echo "DEBUG: Starting test parsing with timeout protection..." | tee -a "$TEST_LOG_FILE"
# Use a subshell with timeout to prevent hanging
(
# Attempt to extract passed/failed/errors from log tail (robust to order)
# Debug: Show what we're looking for in the log
echo "DEBUG: Looking for test summary in log file..." | tee -a "$TEST_LOG_FILE"
echo "DEBUG: Last 10 lines of test log:" | tee -a "$TEST_LOG_FILE"
# Use a temporary file to avoid reading from the same file being written to
TEMP_LOG_TAIL=$(mktemp)
tail -10 "$TEST_LOG_FILE" > "$TEMP_LOG_TAIL" 2>/dev/null || echo "No log content available"
cat "$TEMP_LOG_TAIL" | tee -a "$TEST_LOG_FILE"
rm -f "$TEMP_LOG_TAIL"
# Extract test counts with corrected regex patterns
# Handle different pytest output formats: "X passed, Y failed, Z errors" or "X passed, Y errors"
# Use grep to extract the number immediately before the keyword (handles ANSI codes)
PASSED=$(grep -oE '[0-9]+ passed' "$TEST_LOG_FILE" | tail -n1 | grep -oE '[0-9]+')
FAILED=$(grep -oE '[0-9]+ failed' "$TEST_LOG_FILE" | tail -n1 | grep -oE '[0-9]+')
ERRORS=$(grep -oE '[0-9]+ errors' "$TEST_LOG_FILE" | tail -n1 | grep -oE '[0-9]+')
# Debug: Show what each regex extracted
echo "DEBUG: Raw extracted values - PASSED='$PASSED' FAILED='$FAILED' ERRORS='$ERRORS'" | tee -a "$TEST_LOG_FILE"
# If no summary line found (interrupted test run), count individual test results
if [ -z "$PASSED" ] && [ -z "$FAILED" ] && [ -z "$ERRORS" ]; then
echo "DEBUG: No test summary found, analyzing test execution..." | tee -a "$TEST_LOG_FILE"
# Check if tests actually ran by looking for common test execution indicators
if grep -q "No such file or directory\|command not found\|make: \*\*\*" "$TEST_LOG_FILE"; then
echo "DEBUG: Detected build/test command failure - tests never executed" | tee -a "$TEST_LOG_FILE"
PASSED=0
FAILED=0
ERRORS=1 # Treat command failure as an error
elif grep -q "PASSED\|FAILED\|SKIPPED" "$TEST_LOG_FILE"; then
echo "DEBUG: Found individual test results, counting them..." | tee -a "$TEST_LOG_FILE"
PASSED=$(grep -c "PASSED" "$TEST_LOG_FILE" 2>/dev/null || echo "0")
FAILED=$(grep -c "FAILED" "$TEST_LOG_FILE" 2>/dev/null || echo "0")
SKIPPED=$(grep -c "SKIPPED" "$TEST_LOG_FILE" 2>/dev/null || echo "0")
ERRORS=0 # Individual test results don't show "errors" - they show as "FAILED"
echo "DEBUG: Individual test counts - PASSED=$PASSED FAILED=$FAILED SKIPPED=$SKIPPED" | tee -a "$TEST_LOG_FILE"
else
echo "DEBUG: No test execution detected - treating as setup failure" | tee -a "$TEST_LOG_FILE"
PASSED=0
FAILED=0
ERRORS=1 # Treat as setup failure
fi
fi
# Set defaults if extraction failed
PASSED=${PASSED:-0}
FAILED=${FAILED:-0}
ERRORS=${ERRORS:-0}
echo "DEBUG: After extraction and defaults - PASSED=$PASSED FAILED=$FAILED ERRORS=$ERRORS" | tee -a "$TEST_LOG_FILE"
# Calculate total and ratio
TOTAL=$((PASSED + FAILED + ERRORS))
RATIO=0
if [ "$TOTAL" -gt 0 ]; then
RATIO=$(( 100 * PASSED / TOTAL ))
fi
echo "DEBUG: Calculated totals - TOTAL=$TOTAL RATIO=$RATIO%" | tee -a "$TEST_LOG_FILE"
echo "Parsed test summary: passed=$PASSED failed=$FAILED errors=$ERRORS total=$TOTAL ratio=${RATIO}%" | tee -a "$TEST_LOG_FILE"
# Export variables for use outside the subshell
echo "PASSED=$PASSED" > /tmp/test_parsing_result
echo "FAILED=$FAILED" >> /tmp/test_parsing_result
echo "ERRORS=$ERRORS" >> /tmp/test_parsing_result
echo "TOTAL=$TOTAL" >> /tmp/test_parsing_result
echo "RATIO=$RATIO" >> /tmp/test_parsing_result
) &
PARSE_PID=$!
# Wait for parsing with timeout
if timeout 30 wait $PARSE_PID 2>/dev/null; then
echo "DEBUG: Test parsing completed successfully" | tee -a "$TEST_LOG_FILE"
# Load results from temp file
if [ -f /tmp/test_parsing_result ]; then
source /tmp/test_parsing_result
rm -f /tmp/test_parsing_result
else
echo "DEBUG: No parsing results found, using fallback values" | tee -a "$TEST_LOG_FILE"
PASSED=0
FAILED=0
ERRORS=1
TOTAL=1
RATIO=0
fi
else
echo "DEBUG: Test parsing timed out, using fallback values" | tee -a "$TEST_LOG_FILE"
kill $PARSE_PID 2>/dev/null || true
PASSED=0
FAILED=0
ERRORS=1
TOTAL=1
RATIO=0
fi
echo "Parsed test summary: passed=$PASSED failed=$FAILED errors=$ERRORS ratio=${RATIO}%" | tee -a "$TEST_LOG_FILE"
echo "Final test parsing results: passed=$PASSED failed=$FAILED errors=$ERRORS total=$TOTAL ratio=${RATIO}%" | tee -a "$TEST_LOG_FILE"
if [ "$TOTAL" -eq 0 ] || [ "$RATIO" -lt 50 ]; then
echo "Tests below threshold; invoking Claude Code CLI setup..." | tee -a "$TEST_LOG_FILE"
echo "DEBUG: About to start Claude Code CLI setup..." | tee -a "$TEST_LOG_FILE"
@ -2023,17 +2115,92 @@ echo "DEBUG: Exit code persistence completed" | tee -a "$TEST_LOG_FILE"
echo "DEBUG: Starting additional rounds loop..." | tee -a "$TEST_LOG_FILE"
while : ; do
echo "DEBUG: Evaluating pass ratio for round $ROUND..." | tee -a "$TEST_LOG_FILE"
PASSED=$(sed -n "s/.* \([0-9]\+\) passed.*/\1/p" "$TEST_LOG_FILE" | tail -n1)
FAILED=$(sed -n "s/.* \([0-9]\+\) failed.*/\1/p" "$TEST_LOG_FILE" | tail -n1)
ERRORS=$(sed -n "s/.* \([0-9]\+\) errors.*/\1/p" "$TEST_LOG_FILE" | tail -n1)
PASSED=${PASSED:-0}
FAILED=${FAILED:-0}
ERRORS=${ERRORS:-0}
TOTAL=$((PASSED + FAILED + ERRORS))
RATIO=0
if [ "$TOTAL" -gt 0 ]; then
RATIO=$(( 100 * PASSED / TOTAL ))
# Add timeout protection for test parsing to prevent hanging
echo "DEBUG: Starting test parsing with timeout protection for round $ROUND..." | tee -a "$TEST_LOG_FILE"
# Use a subshell with timeout to prevent hanging
(
# Extract test counts with corrected regex patterns
# Use grep to extract the number immediately before the keyword (handles ANSI codes)
PASSED=$(grep -oE '[0-9]+ passed' "$TEST_LOG_FILE" | tail -n1 | grep -oE '[0-9]+')
FAILED=$(grep -oE '[0-9]+ failed' "$TEST_LOG_FILE" | tail -n1 | grep -oE '[0-9]+')
ERRORS=$(grep -oE '[0-9]+ errors' "$TEST_LOG_FILE" | tail -n1 | grep -oE '[0-9]+')
# Debug: Show what each regex extracted
echo "DEBUG: Raw extracted values for round $ROUND - PASSED='$PASSED' FAILED='$FAILED' ERRORS='$ERRORS'" | tee -a "$TEST_LOG_FILE"
# If no summary line found (interrupted test run), count individual test results
if [ -z "$PASSED" ] && [ -z "$FAILED" ] && [ -z "$ERRORS" ]; then
echo "DEBUG: No test summary found for round $ROUND, analyzing test execution..." | tee -a "$TEST_LOG_FILE"
# Check if tests actually ran by looking for common test execution indicators
if grep -q "No such file or directory\|command not found\|make: \*\*\*\|ERROR collecting\|Interrupted:" "$TEST_LOG_FILE"; then
echo "DEBUG: Detected build/test command failure for round $ROUND - tests never executed properly" | tee -a "$TEST_LOG_FILE"
PASSED=0
FAILED=0
ERRORS=1 # Treat command failure as an error
elif grep -q "PASSED\|FAILED\|SKIPPED" "$TEST_LOG_FILE"; then
echo "DEBUG: Found individual test results for round $ROUND, counting them..." | tee -a "$TEST_LOG_FILE"
PASSED=$(grep -c "PASSED" "$TEST_LOG_FILE" 2>/dev/null || echo "0")
FAILED=$(grep -c "FAILED" "$TEST_LOG_FILE" 2>/dev/null || echo "0")
SKIPPED=$(grep -c "SKIPPED" "$TEST_LOG_FILE" 2>/dev/null || echo "0")
ERRORS=0 # Individual test results don't show "errors" - they show as "FAILED"
echo "DEBUG: Individual test counts for round $ROUND - PASSED=$PASSED FAILED=$FAILED SKIPPED=$SKIPPED" | tee -a "$TEST_LOG_FILE"
else
echo "DEBUG: No test execution detected for round $ROUND - treating as setup failure" | tee -a "$TEST_LOG_FILE"
PASSED=0
FAILED=0
ERRORS=1 # Treat as setup failure
fi
fi
PASSED=${PASSED:-0}
FAILED=${FAILED:-0}
ERRORS=${ERRORS:-0}
TOTAL=$((PASSED + FAILED + ERRORS))
RATIO=0
if [ "$TOTAL" -gt 0 ]; then
RATIO=$(( 100 * PASSED / TOTAL ))
fi
echo "DEBUG: Calculated totals for round $ROUND - TOTAL=$TOTAL RATIO=$RATIO%" | tee -a "$TEST_LOG_FILE"
# Export variables for use outside the subshell
echo "PASSED=$PASSED" > /tmp/test_parsing_result_round_$ROUND
echo "FAILED=$FAILED" >> /tmp/test_parsing_result_round_$ROUND
echo "ERRORS=$ERRORS" >> /tmp/test_parsing_result_round_$ROUND
echo "TOTAL=$TOTAL" >> /tmp/test_parsing_result_round_$ROUND
echo "RATIO=$RATIO" >> /tmp/test_parsing_result_round_$ROUND
) &
PARSE_PID=$!
# Wait for parsing with timeout
if timeout 30 wait $PARSE_PID 2>/dev/null; then
echo "DEBUG: Test parsing completed successfully for round $ROUND" | tee -a "$TEST_LOG_FILE"
# Load results from temp file
if [ -f /tmp/test_parsing_result_round_$ROUND ]; then
source /tmp/test_parsing_result_round_$ROUND
rm -f /tmp/test_parsing_result_round_$ROUND
else
echo "DEBUG: No parsing results found for round $ROUND, using fallback values" | tee -a "$TEST_LOG_FILE"
PASSED=0
FAILED=0
ERRORS=1
TOTAL=1
RATIO=0
fi
else
echo "DEBUG: Test parsing timed out for round $ROUND, using fallback values" | tee -a "$TEST_LOG_FILE"
kill $PARSE_PID 2>/dev/null || true
PASSED=0
FAILED=0
ERRORS=1
TOTAL=1
RATIO=0
fi
echo "DEBUG: Pass ratio evaluation - passed=$PASSED failed=$FAILED errors=$ERRORS total=$TOTAL ratio=${RATIO}%" | tee -a "$TEST_LOG_FILE"
echo "Post-LLM summary: passed=$PASSED failed=$FAILED errors=$ERRORS ratio=${RATIO}%" | tee -a "$TEST_LOG_FILE"
if [ "$TOTAL" -gt 0 ] && [ "$RATIO" -ge 50 ]; then

View file

@ -35,7 +35,6 @@ async function main() {
sourcesContent: true,
platform: "node",
outfile: "dist/extension.js",
// outfile: "/home/mohammed/.vscode-oss/extensions/codeflash.codeflash-0.0.2/dist/extension.js",
external: ["vscode"],
logLevel: "silent",
plugins: [

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "codeflash",
"displayName": "Codeflash",
"description": "Optimize Your Python Code - Automatically",
"version": "0.0.12",
"version": "0.0.14",
"icon": "media/Codeflash_black_background.jpg",
"publisher": "codeflash",
"repository": {
@ -61,9 +61,29 @@
"command": "codeflash.clearAllTasks",
"title": "Delete all non-running tasks",
"category": "Codeflash"
},
{
"command": "codeflash.viewPatch",
"title": "View Diff"
},
{
"command": "codeflash.acceptInlineRefactor",
"title": "Accept"
}
],
"menus": {
"comments/commentThread/title": [
{
"command": "codeflash.viewPatch",
"when": "commentController == codeflash-comments",
"group": "inline@1"
},
{
"command": "codeflash.acceptInlineRefactor",
"when": "commentController == codeflash-comments",
"group": "inline@2"
}
],
"commandPalette": [
{
"command": "codeflash.optimizeFunction",
@ -88,6 +108,14 @@
{
"command": "codeflash.showRefactorDiff",
"when": "editorLangId == python"
},
{
"command": "codeflash.viewPatch",
"when": "false"
},
{
"command": "codeflash.acceptInlineRefactor",
"when": "false"
}
]
}
@ -126,6 +154,7 @@
"@shikijs/themes": "^3.12.2",
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/semver": "^7.7.1",
"@types/split2": "^4.2.3",
"@types/vscode": "^1.94.0",
"@typescript-eslint/eslint-plugin": "^8.25.0",
@ -148,10 +177,13 @@
"dependencies": {
"@codeflash/shared": "*",
"@codeflash/types": "*",
"@sentry/node": "^10.20.0",
"@vscode/python-extension": "^1.0.5",
"diff": "^8.0.2",
"marked": "^15.0.12",
"p-queue": "^8.1.0",
"posthog-node": "^5.10.1",
"semver": "^7.7.3",
"vscode-languageclient": "^9.0.1"
},
"private": true,

View file

@ -1,6 +1,5 @@
import { useEffect } from "react";
import { useStore } from "./store/root";
import ApiKeyForm from "./components/apiKeyError";
import { messageHandler } from "./utils/webviewMessageHandler";
// import OptimizeCurrentDiff from "./components/optimizeCurrentDiff";
// import CurrentFileFunctions from "./components/currentFileFunctions";
@ -8,6 +7,7 @@ import { vscode } from "./utils/vscode";
import ChatView from "./components/chatView";
import Tabs from "./components/tabs";
import OptimizationQueue from "./components/optimizationQueue";
import SignInForm from "./components/signInForm";
function App() {
const store = useStore();
@ -25,7 +25,7 @@ function App() {
}, []);
if (store.status == "apiKeyError") {
return <ApiKeyForm />;
return <SignInForm />;
}
return (

View file

@ -1,74 +0,0 @@
import { useEffect, useRef } from "react";
import { vscode } from "../utils/vscode";
import type { WebviewMessage } from "@codeflash/types";
import { useStore } from "../store/root";
const ApiKeyForm = () => {
const inputRef = useRef<HTMLInputElement>(null);
const loading = useStore((state) => state.isValidatingApiKey);
useEffect(() => {
const onApiKeyInvalid = (message: MessageEvent) => {
const sidebarMessage = message.data as WebviewMessage;
if (sidebarMessage.type == "apiKeyEnterInvalid") {
const current = inputRef.current;
if (!current) {
return;
}
current.value = "";
current.focus();
}
};
window.addEventListener("message", onApiKeyInvalid);
return () => {
window.removeEventListener("message", onApiKeyInvalid);
};
}, []);
useEffect(() => {
const current = inputRef.current;
if (!current) {
return;
}
current.focus();
}, []);
return (
<div className="api-key-container">
<h2>Codeflash API Key</h2>
<div className="input-group">
<input
type="text"
id="api-key"
placeholder="cf-******"
ref={inputRef}
onKeyDown={(e) => {
if (e.key == "Enter") {
const apiKey = inputRef.current?.value || "";
if (!apiKey?.trim()) {
return;
}
vscode.postMessage({
type: "apiKeyEnter",
payload: {
apiKey: apiKey.trim(),
},
});
}
}}
disabled={loading}
/>
{loading && <span className="codicon codicon-loading spin"></span>}
</div>
<p className="hint">
You can generate an API key from your{" "}
<a href="https://app.codeflash.ai/apikeys" target="_blank">
Codeflash account
</a>
.
</p>
</div>
);
};
export default ApiKeyForm;

View file

@ -35,10 +35,19 @@
box-shadow: 0 0 0 2px var(--vscode-input-background);
} */
.inputWrapper {
.inputContainer {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: space-between;
align-items: flex-end;
height: 100%;
}
.inputWrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.input {
@ -174,3 +183,22 @@
color: var(--vscode-list-hoverForeground);
}
.outsideModuleWarning {
display: flex;
width: fit-content;
align-items: center;
gap: 0.4rem;
margin-top: 0.3rem;
font-size: 0.75rem;
background-color: color-mix(
in srgb,
var(--vscode-editor-foreground) 10%,
transparent
); /* soft background tint */
padding: 0.35rem 0.6rem;
border-radius: 6px;
color: var(--vscode-editorWarning-foreground, #f5c542);
opacity: 0.9;
}

View file

@ -68,6 +68,9 @@ const ChatInput: React.FC<ChatInputProps> = ({
functionSuggestions = { file: "", functions: [] },
currentOptimizationTask,
}) => {
const [outsideModuleRoot, setOutsideModuleRoot] = useState<boolean>(false);
const moduleRoot = useStore((state) => state.moduleRoot);
const currentFocusedFile = useStore((state) => state.currentFilePath);
const queueTasks = useStore((state) => state.queueTasks);
@ -84,9 +87,18 @@ const ChatInput: React.FC<ChatInputProps> = ({
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (selectedFile) {
checkOutsideModuleRoot(selectedFile);
} else {
setOutsideModuleRoot(false);
}
}, [selectedFile, moduleRoot]);
// Initialize with current file + request workspace files if not provided
useEffect(() => {
if (currentOptimizationTask) {
// if there is a current optimization task, don't change the selected file
return;
}
if (currentFocusedFile) {
@ -150,6 +162,22 @@ const ChatInput: React.FC<ChatInputProps> = ({
inputRef.current?.focus();
};
const checkOutsideModuleRoot = (filePath: string) => {
// check if current focused file is outside module root
const trimmedModuleRoot = moduleRoot?.trim() || "";
if (!trimmedModuleRoot) {
setOutsideModuleRoot(false);
return;
}
const trimmedFilePath = filePath.trim();
if (moduleRoot && !trimmedFilePath.startsWith(trimmedModuleRoot)) {
setOutsideModuleRoot(true);
} else {
setOutsideModuleRoot(false);
}
};
/** Handle input change and update dropdown */
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVal = e.target.value;
@ -338,20 +366,35 @@ const ChatInput: React.FC<ChatInputProps> = ({
)}
</div>
)}
<div className={styles.inputWrapper}>
<div className={styles.inputContainer}>
{!isRunning && (
<input
ref={inputRef}
className={styles.input}
type="text"
placeholder={placeholderText}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
readOnly={isRunning}
/>
<div className={styles.inputWrapper}>
<input
ref={inputRef}
className={styles.input}
type="text"
placeholder={placeholderText}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
readOnly={isRunning}
/>
{selectedFile && outsideModuleRoot && (
<div className={styles.outsideModuleWarning}>
<span className="codicon codicon-warning"></span>
<span>
{getFileNameFromPath(selectedFile)} is outside the
configured module root
</span>
</div>
)}
</div>
)}
<Tooltip position="left" content={isRunning ? "Stop" : "Optimize"}>
<Tooltip
position="left"
content={isRunning ? "Stop" : "Optimize"}
className={styles.tooltip}
>
<button
type="submit"
className={styles.button}

View file

@ -209,8 +209,10 @@ const InitForm = ({ suggestions, errorDetails, pyprojectPath }: Props) => {
/>
<button
type="button"
role="button"
className={styles.backButton}
onClick={() => {
onClick={(e) => {
e.preventDefault();
setCustomPaths((prev) => ({ ...prev, [field]: false }));
setFormData((prev) => ({
...prev,
@ -303,7 +305,7 @@ style="color: var(--vscode-editorWarning-foreground);"></span>` + errorDetails,
{isLoading ? (
<span className="codicon codicon-loading spin"></span>
) : (
"Save Configuration"
"Save Configurations"
)}
</button>
</form>

View file

@ -0,0 +1,477 @@
import { useEffect, useRef, useState } from "react";
import { vscode } from "../utils/vscode";
import type { WebviewMessage } from "@codeflash/types";
import { useStore } from "../store/root";
import CodeflashLogo from "./logo";
const SignInForm = () => {
const inputRef = useRef<HTMLInputElement>(null);
const loading = useStore((state) => state.isValidatingApiKey);
const [activeTab, setActiveTab] = useState<"apiKey" | "signIn">("signIn");
const [isAuthInProgress, setIsAuthInProgress] = useState(false);
useEffect(() => {
const handleMessage = (message: MessageEvent) => {
const sidebarMessage = message.data as WebviewMessage;
switch (sidebarMessage.type) {
case "apiKeyEnterInvalid":
const current = inputRef.current;
if (current) {
current.value = "";
current.focus();
}
break;
case "authStarted":
setIsAuthInProgress(true);
break;
case "authCompleted":
case "authFailed":
case "authCancelled":
setIsAuthInProgress(false);
break;
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, []);
useEffect(() => {
if (activeTab === "apiKey") {
const current = inputRef.current;
if (current) {
current.focus();
}
}
}, [activeTab]);
const handleApiKeySubmit = () => {
const apiKey = inputRef.current?.value || "";
if (!apiKey?.trim()) {
return;
}
vscode.postMessage({
type: "apiKeyEnter",
payload: {
apiKey: apiKey.trim(),
},
});
};
const handleSignIn = () => {
vscode.postMessage({
type: "signIn",
});
};
const cancelAuth = () => {
vscode.postMessage({
type: "cancelAuth",
});
};
const isDisabled = loading || isAuthInProgress;
const getApikeyInput = () => {
return (
<>
<div style={{ position: "relative", marginBottom: "8px" }}>
<input
type="text"
id="api-key"
placeholder="Enter your API key (cf-******)"
ref={inputRef}
style={{
width: "100%",
padding: "12px 36px 12px 14px",
fontSize: "14px",
background: "var(--vscode-input-background)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "6px",
color: "var(--vscode-input-foreground)",
outline: "none",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.5 : 1,
cursor: isDisabled ? "not-allowed" : "text",
transition: "border-color 0.2s",
textAlign: "left",
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleApiKeySubmit();
}
}}
onFocus={(e) => {
e.target.style.border = "1px solid var(--vscode-focusBorder)";
}}
onBlur={(e) => {
e.target.style.border = "1px solid var(--vscode-input-border)";
}}
disabled={isDisabled}
/>
{loading && (
<span
className="codicon codicon-loading spin"
style={{
position: "absolute",
right: "14px",
top: "50%",
transform: "translateY(-50%)",
color: "var(--vscode-foreground)",
fontSize: "16px",
}}
></span>
)}
</div>
<button
onClick={handleApiKeySubmit}
disabled={isDisabled}
style={{
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
background: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
border: "none",
borderRadius: "6px",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "background 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.7 : 1,
}}
onMouseEnter={(e) => {
if (!isDisabled) {
e.currentTarget.style.background =
"var(--vscode-button-hoverBackground)";
}
}}
onMouseLeave={(e) => {
if (!isDisabled) {
e.currentTarget.style.background =
"var(--vscode-button-background)";
}
}}
>
{loading ? "Saving..." : "Save"}
</button>
</>
);
};
return (
<div
style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
padding: "20px",
overflow: "hidden",
background: "var(--vscode-editor-background)",
}}
>
{/* Gradient glow effect from top to bottom */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "100%",
background: "linear-gradient(to bottom, #FFC043, transparent 60%)",
opacity: 0.2,
pointerEvents: "none",
filter: "blur(60px)",
}}
/>
{/* Grid Background */}
<div
style={{
position: "absolute",
inset: 0,
backgroundImage:
"linear-gradient(to right, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px), linear-gradient(to bottom, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px)",
backgroundSize: "24px 24px",
opacity: 0.03,
}}
/>
{/* Main Container */}
<div
style={{
position: "relative",
zIndex: 10,
width: "100%",
maxWidth: "380px",
textAlign: "center",
}}
>
{/* Logo */}
<div
style={{
fontSize: "32px",
fontWeight: "600",
marginBottom: "60px",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
}}
>
<CodeflashLogo width={"200"} height={"200"} />
</div>
{/* Auth in Progress State */}
{isAuthInProgress ? (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "16px",
padding: "24px",
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "8px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "12px",
}}
>
<span
className="codicon codicon-loading spin"
style={{
fontSize: "20px",
color: "var(--vscode-foreground)",
}}
/>
<span
style={{
fontSize: "14px",
color: "var(--vscode-foreground)",
}}
>
Waiting for authentication...
</span>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "8px 0",
lineHeight: "1.5",
}}
>
Complete the authentication in your browser.
</p>
<p
style={{
fontSize: "12px",
color: "var(--vscode-foreground)",
margin: "16px 0 8px 0",
lineHeight: "1.5",
}}
>
Having trouble? You can enter your API key
</p>
<button
onClick={() => {
cancelAuth();
setActiveTab("apiKey");
}}
style={{
width: "100%",
padding: "8px 16px",
fontSize: "12px",
fontWeight: "500",
background: "var(--vscode-button-secondaryBackground)",
color: "var(--vscode-button-secondaryForeground)",
border: "none",
borderRadius: "4px",
cursor: "pointer",
transition: "background 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: 1,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background =
"var(--vscode-button-secondaryHoverBackground)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background =
"var(--vscode-button-secondaryBackground)";
}}
>
Try API Key
</button>
</div>
) : (
/* Buttons Container */
<div
style={{
display: "flex",
flexDirection: "column",
gap: "12px",
marginBottom: "24px",
}}
>
{activeTab === "signIn" ? (
<>
<button
onClick={handleSignIn}
disabled={isDisabled}
style={{
position: "relative",
overflow: "hidden",
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
border: "none",
borderRadius: "6px",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "background 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.7 : 1,
background: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
}}
onMouseEnter={(e) => {
if (!isDisabled) {
e.currentTarget.style.background =
"var(--vscode-button-hoverBackground)";
}
}}
onMouseLeave={(e) => {
if (!isDisabled) {
e.currentTarget.style.background =
"var(--vscode-button-background)";
}
}}
>
<div className="glowEffect modePulse"></div>
<span style={{ position: "relative", zIndex: 1 }}>
{loading ? "Signing in..." : "Sign in with CodeFlash"}
</span>
</button>
<button
onClick={() => setActiveTab("apiKey")}
disabled={isDisabled}
style={{
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
background: "var(--vscode-button-secondaryBackground)",
color: "var(--vscode-button-secondaryForeground)",
border: "none",
borderRadius: "6px",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "background 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.7 : 1,
}}
onMouseEnter={(e) => {
if (!isDisabled) {
e.currentTarget.style.background =
"var(--vscode-button-secondaryHoverBackground)";
}
}}
onMouseLeave={(e) => {
if (!isDisabled) {
e.currentTarget.style.background =
"var(--vscode-button-secondaryBackground)";
}
}}
>
Use API Key
</button>
</>
) : (
<>
{getApikeyInput()}
<button
onClick={() => setActiveTab("signIn")}
disabled={isDisabled}
style={{
width: "100%",
padding: "12px 24px",
fontSize: "14px",
fontWeight: "500",
background: "transparent",
color: "var(--vscode-button-secondaryForeground)",
border: "none",
cursor: isDisabled ? "not-allowed" : "pointer",
transition: "opacity 0.2s",
fontFamily: "var(--vscode-font-family)",
opacity: isDisabled ? 0.5 : 0.7,
}}
onMouseEnter={(e) => {
if (!isDisabled) {
e.currentTarget.style.opacity = "1";
}
}}
onMouseLeave={(e) => {
if (!isDisabled) {
e.currentTarget.style.opacity = "0.7";
}
}}
>
Back
</button>
</>
)}
</div>
)}
{/* Footer Text */}
{activeTab === "apiKey" && !isAuthInProgress && (
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
marginTop: "16px",
}}
>
<a
href="https://app.codeflash.ai/apikeys"
target="_blank"
rel="noopener noreferrer"
style={{
color: "var(--vscode-textLink-foreground)",
textDecoration: "none",
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecoration = "underline";
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecoration = "none";
}}
>
Get your API key
</a>
</p>
)}
</div>
</div>
);
};
export default SignInForm;

View file

@ -7,7 +7,7 @@ export interface TooltipProps {
position?: "top" | "bottom" | "left" | "right";
delay?: number;
disabled?: boolean;
className?: string;
className: string | undefined;
maxWidth?: number;
}
@ -27,7 +27,9 @@ export const Tooltip: React.FC<TooltipProps> = ({
const tooltipRef = useRef<HTMLDivElement>(null);
const showTooltip = () => {
if (disabled || !content) {return;}
if (disabled || !content) {
return;
}
timeoutRef.current = setTimeout(() => {
setIsVisible(true);
@ -42,7 +44,9 @@ export const Tooltip: React.FC<TooltipProps> = ({
};
const calculatePosition = () => {
if (!triggerRef.current || !tooltipRef.current) {return;}
if (!triggerRef.current || !tooltipRef.current) {
return;
}
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();

View file

@ -8,16 +8,18 @@ export const handleRestoreStateFromCache = (
const payload = message.state;
const {
running,
// currentStatus,
// currentStatusMessage,
// currentFunctionName,
moduleRoot,
running,
focusedTaskId,
queueTasks,
} = payload as {
running?: boolean;
queueTasks?: QueueTaskItem[];
focusedTaskId: string;
moduleRoot?: string;
};
// console.log(
// `received restore state from cache message: ${message.type}, ${JSON.stringify(payload)}`,
@ -27,5 +29,6 @@ export const handleRestoreStateFromCache = (
optimizationRunning: running ?? false,
queueTasks: queueTasks ?? [],
focusedTaskId,
moduleRoot: moduleRoot ?? "",
};
};

View file

@ -15,6 +15,7 @@ import { handleRestoreStateFromCache } from "./actions/restoreStateFromMemory";
import { handleAddLog } from "./actions/addLog";
export type State = {
moduleRoot: string;
status: SidebarStatus;
activeTab: SidebarTab;
optimizationRunning: boolean;
@ -45,6 +46,7 @@ export type Action = {
};
const initialState: State = {
moduleRoot: "",
status: "idle",
activeTab: "optimization",
optimizationRunning: false,

View file

@ -124,8 +124,8 @@
}
.submitButton {
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
padding: 10px 16px;

View file

@ -26,7 +26,13 @@ export type MessageType =
| "setActiveSidebarTab"
| "setApiKeyLoadingState"
| "cancelTask"
| "submitInitForm";
| "signIn"
| "authStarted"
| "authCompleted"
| "authFailed"
| "authCancelled"
| "submitInitForm"
| "cancelAuth";
export type QueueTaskItemStatus =
| "queued"
@ -49,6 +55,7 @@ export type QueueTaskItem = {
patchFile?: string;
explanation?: string;
speedupStr?: string;
bestCandidateCode?: string;
logs: LogEntry[];
};
@ -75,7 +82,24 @@ export interface ShowMessageMessage extends WebviewMessage {
message: string;
};
}
export interface AuthStartedMessage {
type: "authStarted";
}
export interface AuthCompletedMessage {
type: "authCompleted";
}
export interface AuthCancelledMessage {
type: "authCancelled";
}
export interface AuthFailedMessage {
type: "authFailed";
payload: {
error: string;
};
}
export interface SubmitInitFormMessage extends WebviewMessage {
type: "submitInitForm";
payload: {
@ -209,6 +233,13 @@ export interface ApiKeyEnterMessage extends WebviewMessage {
apiKey: string;
};
}
export interface SignInMessage extends WebviewMessage {
type: "signIn";
}
export interface CancelAuthMessage extends WebviewMessage {
type: "cancelAuth";
}
export interface ViewDiffMessage extends WebviewMessage {
type: "viewDiff";
@ -263,10 +294,13 @@ export type ExecutionStatus =
/** Type of content in a log entry */
export type LogContentType = "text" | "code" | "markdown";
export type MessageID = "best_candidate" | "candidate";
interface LspMessage {
type: LogContentType;
id: string;
takes_time?: boolean;
task_id: string;
message_id?: MessageID;
}
export interface LSPTextMessage extends LspMessage {
text: string;
@ -315,8 +349,10 @@ export type IncomingWebviewMessage =
| OptimizeCurrentDiffMessage
| FilesInWorkspace
| RequestFunctionsMessage
| SignInMessage
| ChangeTaskFocusMessage
| SubmitInitFormMessage;
| SubmitInitFormMessage
| CancelAuthMessage;
export type OutgoingWebviewMessage =
| UpdateStateMessage
@ -329,6 +365,10 @@ export type OutgoingWebviewMessage =
| RecievedFunctionsMessage
| ChangeTaskFocusFromBackendMessage
| SetActiveSidebarTabMessage
| AuthStartedMessage
| AuthCompletedMessage
| AuthCancelledMessage
| AuthFailedMessage
| SetApiKeyLoadingStateMessage;
export type Suggestion = {

View file

@ -1,4 +1,5 @@
import type { CodeflashCodeLensProvider } from "../providers/CodeLensProvider";
import type { CommentThreadProvider } from "../providers/commentThreadProvider";
import type { SidebarProvider } from "../providers/SidebarProvider";
import type {
AnalysisService,
@ -16,10 +17,12 @@ export type BootCodeflashServerStepResult = {
navigationService: NavigationService;
sidebarProvider: SidebarProvider;
codeLensProvider: CodeflashCodeLensProvider;
commentThreadProvider: CommentThreadProvider;
};
export type CheckEnvironmentStepResult = {
pythonPath: string;
codeflashVersion: string;
};
export interface CodeflashInitResult {
@ -32,23 +35,24 @@ export type StepResult =
| CheckEnvironmentStepResult
| CodeflashInitResult;
export class StepError extends Error {
category?: string;
details?: string;
helperCmdText?: string;
helperCmd?: string;
icon?: string;
iconFill?: string;
vscodeActionCommand?: {
export class InitStepError extends Error {
public category?: string;
public details?: string;
public helperCmdText?: string;
public helperCmd?: string;
public icon?: string;
public iconFill?: string;
public vscodeActionCommand?: {
title: string;
command: string;
btnText: string;
};
override message: string;
constructor(message: string) {
super(message);
this.name = "StepError";
this.message = message;
this.name = "InitStepError";
Object.setPrototypeOf(this, new.target.prototype);
Error.captureStackTrace?.(this, InitStepError);
}
}
@ -64,7 +68,7 @@ abstract class BaseStep extends Disposable {
this.title = title;
}
abstract run(...args: unknown[]): Promise<StepResult | StepError>;
abstract run(...args: unknown[]): Promise<StepResult | InitStepError>;
}
export default BaseStep;

View file

@ -10,7 +10,7 @@ import {
} from "../services";
import { Logger } from "../utils";
import type { BootCodeflashServerStepResult } from "./baseStep";
import { StepError } from "./baseStep";
import { InitStepError } from "./baseStep";
import BaseStep from "./baseStep";
import * as vscode from "vscode";
import { SidebarProvider } from "../providers/SidebarProvider";
@ -18,6 +18,9 @@ import { CodeflashCodeLensProvider } from "../providers/CodeLensProvider";
import { GlobalStateKey, type GlobalState } from "../globalState";
import type { LanguageClient } from "vscode-languageclient/node";
import type { LogsEventEmitter } from "../utils/logsEventEmitter";
import { getLspServerFailedToStartError } from "./stepErrors";
import { CommentThreadProvider } from "../providers/commentThreadProvider";
import { GitWatcherService } from "../services/gitWatcherService";
export class BootCodeflashServerStep extends BaseStep {
logger = new Logger("BootCodeflashServerStep");
@ -33,14 +36,14 @@ export class BootCodeflashServerStep extends BaseStep {
override async run(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
..._args: unknown[]
): Promise<BootCodeflashServerStepResult | StepError> {
): Promise<BootCodeflashServerStepResult | InitStepError> {
try {
const { lspService, optimizationLspService } =
await this.provideLspServices();
const lspMainClient = lspService.getClient();
const optimizationClient = optimizationLspService.getClient();
if (!lspMainClient || !optimizationClient) {
return new StepError("LSP client is not initialized");
return new InitStepError("LSP client is not initialized");
}
const services = this.provideOtherServices(
this.context,
@ -56,8 +59,10 @@ export class BootCodeflashServerStep extends BaseStep {
...services,
};
} catch (error) {
this.logger.error(`Failed to start Codeflash server: ${error}`);
return new StepError(`Failed to start Codeflash server: ${error}`);
if (error instanceof InitStepError) {
return error;
}
return getLspServerFailedToStartError((error as Error).message);
}
}
@ -92,7 +97,7 @@ export class BootCodeflashServerStep extends BaseStep {
return { lspService, optimizationLspService };
} catch (error) {
throw new StepError(`Failed to start LSP client: ${error}`);
throw getLspServerFailedToStartError((error as Error).message);
}
};
@ -108,11 +113,19 @@ export class BootCodeflashServerStep extends BaseStep {
navigationService: NavigationService;
sidebarProvider: SidebarProvider;
codeLensProvider: CodeflashCodeLensProvider;
commentThreadProvider: CommentThreadProvider;
gitWatcherService: GitWatcherService;
} => {
const analysisService = new AnalysisService(lspClient);
const optimizationService = new OptimizationService(optimizationClient);
const navigationService = new NavigationService();
const commentThreadProvider = new CommentThreadProvider(
context,
globalState,
navigationService,
);
const sidebarProvider = new SidebarProvider(
context.extensionUri,
lspClient,
@ -121,7 +134,8 @@ export class BootCodeflashServerStep extends BaseStep {
navigationService,
optimizationEventEmitter,
globalState,
() => codeLensProvider.refresh(),
commentThreadProvider,
() => codeLensProvider.scheduleRefresh(),
);
const codeLensProvider = new CodeflashCodeLensProvider(
lspClient,
@ -136,7 +150,13 @@ export class BootCodeflashServerStep extends BaseStep {
codeLensProvider,
);
this.provideCommands(sidebarProvider);
const gitWatcherService = new GitWatcherService(
sidebarProvider,
lspClient,
);
this.logger.info("GitWatcherService initialized");
this.provideCommands(sidebarProvider, commentThreadProvider);
this._disposables.push(
codeLensDisposable,
@ -145,6 +165,8 @@ export class BootCodeflashServerStep extends BaseStep {
navigationService,
sidebarProvider,
codeLensProvider,
commentThreadProvider,
gitWatcherService,
);
return {
analysisService,
@ -152,10 +174,15 @@ export class BootCodeflashServerStep extends BaseStep {
navigationService,
sidebarProvider,
codeLensProvider,
commentThreadProvider,
gitWatcherService,
};
};
private provideCommands = (sidebarProvider: SidebarProvider) => {
private provideCommands = (
sidebarProvider: SidebarProvider,
commentThreadProvider: CommentThreadProvider,
) => {
const optimizeFunctionCommand = vscode.commands.registerCommand(
"codeflash.optimizeFunction",
(uri: vscode.Uri, functionName: string) => {
@ -171,7 +198,7 @@ export class BootCodeflashServerStep extends BaseStep {
for (const id of allTasksIds) {
sidebarProvider.handleDeleteTask(id);
}
this.globalState.set(GlobalStateKey.QueueTasks, []);
commentThreadProvider.scheduleRefresh();
},
);
const optimizeAllCommand = vscode.commands.registerCommand(
@ -184,23 +211,52 @@ export class BootCodeflashServerStep extends BaseStep {
const viewOptimization = vscode.commands.registerCommand(
"codeflash.viewPatch",
async (payload: ViewPatchMessage["payload"]) => {
async (
threadOrPayload:
| ViewPatchMessage["payload"]
| (vscode.CommentThread & {
viewPatchPayload: ViewPatchMessage["payload"];
}),
) => {
let payload: ViewPatchMessage["payload"];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((threadOrPayload as any).viewPatchPayload) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload = (threadOrPayload as any).viewPatchPayload;
} else {
payload = threadOrPayload as ViewPatchMessage["payload"];
}
await sidebarProvider.handleViewPatch(payload);
},
);
const acceptInlineCommentOptimizationCmd = vscode.commands.registerCommand(
"codeflash.acceptInlineRefactor",
async (
thread: vscode.CommentThread & {
viewPatchPayload: ViewPatchMessage["payload"];
},
) => {
await sidebarProvider.applyPatchDirectlyForInlineThread(
thread.viewPatchPayload,
);
},
);
const sendMessage = vscode.commands.registerCommand(
"codeflash.sendMessage",
(message: OutgoingWebviewMessage) => {
sidebarProvider.sendMessage(message);
},
);
this._disposables.push(
optimizeFunctionCommand,
optimizeAllCommand,
viewOptimization,
sendMessage,
clearAllTasksCommand,
acceptInlineCommentOptimizationCmd,
);
};

View file

@ -1,4 +1,5 @@
import type { CheckEnvironmentStepResult, StepError } from "./baseStep";
import type { CheckEnvironmentStepResult } from "./baseStep";
import { InitStepError } from "./baseStep";
import BaseStep from "./baseStep";
import * as vscode from "vscode";
import { type GitExtension } from "../types/git";
@ -10,11 +11,15 @@ import { Logger } from "../utils";
import {
getBareRepoError,
getCodeflashNotInstalledError,
getCodeflashVersionIncompatibleError,
getCodeflashVersionNotValidError,
getNoGitRepoError,
getNoInitCommitError,
getPythonNotSelectedError,
GIT_ERRORS,
} from "./stepErrors";
import semver from "semver";
import { MIN_CODEFLASH_VERSION } from "../constants/cf_min_version";
const execPromise = promisify(exec);
@ -36,10 +41,11 @@ export class CheckEnvironmentStep extends BaseStep {
public override async run(
...args: unknown[]
): Promise<StepError | CheckEnvironmentStepResult> {
): Promise<InitStepError | CheckEnvironmentStepResult> {
const [] = args;
const result: CheckEnvironmentStepResult = {
pythonPath: "",
codeflashVersion: "unknown",
};
const { pythonPath } = await this.validatePythonEnvironment();
@ -49,11 +55,17 @@ export class CheckEnvironmentStep extends BaseStep {
result.pythonPath = pythonPath;
const [gitError, codeflashInstalled] = await Promise.all([
const [gitError, installationCheckResult] = await Promise.all([
this.isGitUsedInRepo(),
this.isCodeflashInstalled(pythonPath),
this.checkCodeflashInstallation(pythonPath),
]);
if (installationCheckResult instanceof InitStepError) {
return installationCheckResult;
} else {
result.codeflashVersion = installationCheckResult.version;
}
if (gitError) {
const message = gitError.message as keyof typeof GIT_ERRORS;
if (message === GIT_ERRORS.NO_REPO) {
@ -64,11 +76,6 @@ export class CheckEnvironmentStep extends BaseStep {
return getNoInitCommitError();
}
}
if (!codeflashInstalled) {
// const isUvProject = await this.isUvProject();
const installCmd = `${pythonPath} -m pip install codeflash`;
return getCodeflashNotInstalledError(pythonPath, installCmd);
}
return result;
}
@ -139,22 +146,30 @@ export class CheckEnvironmentStep extends BaseStep {
};
};
private isCodeflashInstalled = async (
private checkCodeflashInstallation = async (
pythonPath: string,
): Promise<boolean> => {
const cmd = `${pythonPath} -c "import importlib.metadata as m; print(m.version('codeflash'))"`;
): Promise<InitStepError | { version: string }> => {
const cmd = `${pythonPath} -c "from codeflash.version import __version__ as v; print(v)"`;
try {
// TODO: from the stdout get the codeflash version and check if it's compatible
await execPromise(cmd);
return true;
const { stdout } = await execPromise(cmd);
const version = stdout.trim();
const semVer = semver.valid(version);
if (!semVer) {
return getCodeflashVersionNotValidError(pythonPath);
}
if (semver.lt(semVer, MIN_CODEFLASH_VERSION)) {
return getCodeflashVersionIncompatibleError(version, pythonPath);
}
return { version };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.debug("Error checking for codeflash installation " + message);
if (message.includes("PackageNotFoundError")) {
return false;
const installCmd = `${pythonPath} -m pip install codeflash`;
return getCodeflashNotInstalledError(pythonPath, installCmd);
}
// for other unexpected errors, we don't block the user
return true;
return { version: "unknown" };
}
};
}

View file

@ -5,7 +5,7 @@ import { GlobalStateKey } from "../globalState";
import type { OptimizationResponse } from "../types";
import { Logger } from "../utils";
import { getRootWorkspaceFolder } from "../utils/rootWorkspace";
import type { CodeflashInitResult, StepError } from "./baseStep";
import type { CodeflashInitResult, InitStepError } from "./baseStep";
import BaseStep from "./baseStep";
import {
getCodeflashPyprojectConfigNotDefined,
@ -26,7 +26,7 @@ export class CodeflashInitStep extends BaseStep {
super("Almost there!");
}
public async run(): Promise<CodeflashInitResult | StepError> {
public async run(): Promise<CodeflashInitResult | InitStepError> {
const workspaceFolder = getRootWorkspaceFolder();
const { status, moduleRoot, message, pyprojectPath, root, existingConfig } =
await this.lspClient.sendRequest<{

View file

@ -1,5 +1,5 @@
import { dirname, resolve } from "path";
import { StepError } from "./baseStep";
import { InitStepError } from "./baseStep";
export const GIT_ERRORS = {
NO_REPO: "NO_REPO",
@ -11,8 +11,8 @@ export const ERROR_CATEGORIES = {
CODEFLASH_INIT: "CODEFLASH_INIT",
};
export const getPythonNotSelectedError = (): StepError => {
const err = new StepError("No Python interpreter selected");
export const getPythonNotSelectedError = (): InitStepError => {
const err = new InitStepError("No Python interpreter selected");
err.details =
"No Python interpreter selected in the current workspace, are you sure you have python installed?";
err.vscodeActionCommand = {
@ -23,11 +23,40 @@ export const getPythonNotSelectedError = (): StepError => {
return err;
};
export const getCodeflashVersionNotValidError = (
interpreterPath: string,
): InitStepError => {
const err = new InitStepError("Codeflash version check failed");
err.details = `Failed to determine the installed Codeflash version in the current active Python environment (<code>${interpreterPath}</code>).`;
err.vscodeActionCommand = {
title: "Or you can select another interpreter:",
btnText: "Select interpreter",
command: "python.setInterpreter",
};
err.helperCmdText =
"You can try reinstalling Codeflash in your current Python environment using:";
err.helperCmd = "pip install --upgrade codeflash";
return err;
};
export const getCodeflashVersionIncompatibleError = (
installedVersion: string,
interpreterPath: string,
withMinVersion: boolean = true,
): InitStepError => {
const err = new InitStepError("Incompatible Codeflash version installed");
err.details = `The installed Codeflash version (<code>${installedVersion}</code>) is incompatible with this extension.${withMinVersion ? " Please upgrade to at least version <code>${MIN_CODEFLASH_VERSION}</code>." : ""}<br /><br />Your active Python interpreter is located at: <code>${interpreterPath}</code>.`;
err.helperCmdText =
"You can upgrade Codeflash in your current Python environment using:";
err.helperCmd = "pip install --upgrade codeflash";
return err;
};
export const getCodeflashNotInstalledError = (
interpreterPath: string,
installCmd: string,
): StepError => {
const err = new StepError("Codeflash is not installed");
): InitStepError => {
const err = new InitStepError("Codeflash is not installed");
err.details = `Codeflash is not installed in the current active Python environment (<code>${interpreterPath}</code>).`;
err.vscodeActionCommand = {
title: "Or you can select another interpreter:",
@ -39,8 +68,24 @@ export const getCodeflashNotInstalledError = (
return err;
};
export const getNoGitRepoError = (): StepError => {
const err = new StepError("No git repository found");
export const getLspServerFailedToStartError = (
lspError: string,
): InitStepError => {
const err = new InitStepError("Codeflash server failed to start");
err.details = `The Codeflash language server failed to start: <code>${lspError}</code>.<br />This may be caused by an incompatibility between your Codeflash server and the installed extension version.`;
err.helperCmdText =
"You can try reinstalling Codeflash in your current Python environment using:";
err.helperCmd = "pip install --upgrade codeflash";
err.vscodeActionCommand = {
title: "Or you can select another interpreter:",
btnText: "Select interpreter",
command: "python.setInterpreter",
};
return err;
};
export const getNoGitRepoError = (): InitStepError => {
const err = new InitStepError("No git repository found");
err.details =
"Codeflash requires a Git repository to be initialized. It leverages Gits worktree feature to safely test generated code in an isolated environment, allowing you to continue working seamlessly in your original code.";
// TODO: create a command to initialize and create initial commit in the current workspace
@ -50,8 +95,8 @@ export const getNoGitRepoError = (): StepError => {
return err;
};
export const getBareRepoError = (): StepError => {
const err = new StepError("Bare Git repository detected");
export const getBareRepoError = (): InitStepError => {
const err = new InitStepError("Bare Git repository detected");
err.details =
"Codeflash cannot operate inside a bare Git repository. Bare repositories do not have a working directory, which is required to safely test and modify generated code.";
err.helperCmdText =
@ -60,8 +105,8 @@ export const getBareRepoError = (): StepError => {
return err;
};
export const getNoInitCommitError = (): StepError => {
const err = new StepError("Repository has no initial commit");
export const getNoInitCommitError = (): InitStepError => {
const err = new InitStepError("Repository has no initial commit");
err.details =
"Codeflash requires at least one commit in your Git repository to create a worktree for testing generated code. Without an initial commit, Git worktree cannot function.";
err.helperCmdText =
@ -70,8 +115,8 @@ export const getNoInitCommitError = (): StepError => {
return err;
};
export const getCodeflashPyprojectNotFound = (): StepError => {
const err = new StepError("No pyproject.toml file");
export const getCodeflashPyprojectNotFound = (): InitStepError => {
const err = new InitStepError("No pyproject.toml file");
err.category = ERROR_CATEGORIES.CODEFLASH_INIT;
err.details =
"Codeflash is not initialized yet, no <code>pyproject.toml</code> file found, codeflash will create one for you.";
@ -85,9 +130,9 @@ export const getCodeflashPyprojectNotFound = (): StepError => {
export const getCodeflashPyprojectConfigNotDefined = (
pyprojectPath: string,
): StepError => {
): InitStepError => {
const selectedFolder = dirname(resolve(pyprojectPath));
const err = new StepError("Codeflash is not configured");
const err = new InitStepError("Codeflash is not configured");
err.category = ERROR_CATEGORIES.CODEFLASH_INIT;
err.details = `Found a <code>pyproject.toml</code> file at <code>${selectedFolder}</code>, but it does not have codeflash settings configured`;
err.icon = "warning";
@ -101,8 +146,8 @@ export const getCodeflashPyprojectConfigNotDefined = (
export const getCodeflashPyprojectConfigNotValid = (
pyprojectPath: string,
errorMessage: string,
): StepError => {
const err = new StepError("Codeflash config is not valid");
): InitStepError => {
const err = new InitStepError("Codeflash config is not valid");
err.category = ERROR_CATEGORIES.CODEFLASH_INIT;
err.details = `Codeflash configuration found at <code>${pyprojectPath}</code>, but it's not valid.<br />${errorMessage}`;
err.icon = "warning";

View file

@ -0,0 +1 @@
export const MIN_CODEFLASH_VERSION = "0.18.1";

View file

@ -10,6 +10,7 @@ export const OUTPUT_CHANNEL_PREFIX_NAMES = {
LSP: "CF-LSP",
} as const;
// always keep these in sync with the server-side LSP command names
export const LSP_COMMANDS = {
GET_CONFIG_SUGGESTIONS: "getConfigSuggestions",
WRITE_CONFIG: "writeConfig",
@ -21,10 +22,10 @@ export const LSP_COMMANDS = {
GET_OPTIMIZABLE_FUNCTIONS_IN_COMMIT: "getOptimizableFunctionsInCommit",
INITIALIZE_FUNCTION_OPTIMIZATION: "initializeFunctionOptimization",
PERFORM_FUNCTION_OPTIMIZATION: "performFunctionOptimization",
CLEANUP_CURRENT_OPTIMIZER_SESSION: "cleanupCurrentOptimizerSession",
INIT_PROJECT: "initProject",
ON_PATCH_APPLIED: "onPatchApplied",
} as const;
export const PYTHON_EXTENSION_ID = "ms-python.python";
export const CODEFLASH_EXTENSION_ID = "codeflash.codeflash";
export const SIDEBAR_BUNDLE_DIR = "packages/sidebar-webview/build/";

View file

@ -4,10 +4,15 @@ import { Logger } from "./utils";
import type { PythonExtension } from "@vscode/python-extension";
import { WebviewSwitcherProvider } from "./providers/webviewSwitcherProvider";
import { GlobalState } from "./globalState";
import { GlobalState, GlobalStateKey } from "./globalState";
import { InitWebviewProvider } from "./providers/InitWebviewProvider";
import { InitService } from "./services/initService";
import { LogsEventEmitter } from "./utils/logsEventEmitter";
import { CodeflashAuthProvider } from "./providers/CodeflashAuthProvider";
import { SentryLogger } from "./telemetry/sentry";
import { Telemetry } from "./telemetry/posthog";
import { getExtensionVersion } from "./utils/vscode";
import type { IdeInfo } from "./telemetry/types";
let logger: Logger = new Logger("Codeflash Extension");
@ -62,14 +67,40 @@ const webviewSwitcherProvider = new WebviewSwitcherProvider({
resolveWebviewView(_webviewView, _context, _token) {},
});
// eslint-disable-next-line @typescript-eslint/require-await
export async function activate(
context: vscode.ExtensionContext,
): Promise<void> {
logger.info("Activating Codeflash extension...");
addDisposable(context, provideReloadExtensionCommand(context));
const ideInfo: IdeInfo = {
name: vscode.env.appName,
extensionVersion: getExtensionVersion(),
};
SentryLogger.setup(true, vscode.env.machineId, ideInfo);
await Telemetry.setup(true, vscode.env.machineId, ideInfo);
addDisposable(context, {
dispose: () => {
SentryLogger.shutdownSentryClient();
Telemetry.shutdownPosthogClient();
},
});
const globalState = new GlobalState(context);
if (!globalState.get(GlobalStateKey.IsExtensionInstalled, false)) {
globalState.set(GlobalStateKey.IsExtensionInstalled, true);
await Telemetry.capture("extension_installed", {});
// show the sidebar view on first install
vscode.commands.executeCommand(
"workbench.view.extension." + SIDEBAR_VIEW_ID,
);
// maybe here we can open a welcome page, with some getting started guide
}
const optimizationEventEmitter = new LogsEventEmitter(globalState); // for emitting events from the lsp server logs to the sidebar, for tracking the running optimization progress
const codeflashAuth = new CodeflashAuthProvider(context);
addDisposable(context, codeflashAuth);
const showGlobalStateCommand = vscode.commands.registerCommand(
"codeflash.dev.showGlobalState",
async () => {

View file

@ -3,6 +3,7 @@ import type { ExtensionContext } from "vscode";
import { window, workspace } from "vscode";
export const enum GlobalStateKey {
IsExtensionInstalled = "isExtensionInstalled",
Running = "running",
UserID = "user_id",
QueueTasks = "queueTasks",
@ -16,6 +17,7 @@ export type ContextFiles = Record<string, string>;
interface GlobalStateKeyMapping {
[GlobalStateKey.Running]: boolean;
[GlobalStateKey.IsExtensionInstalled]: boolean;
[GlobalStateKey.UserID]: string | undefined;
[GlobalStateKey.QueueTasks]: QueueTaskItem[];
[GlobalStateKey.FocusedTaskId]: string | undefined;
@ -26,6 +28,7 @@ interface GlobalStateKeyMapping {
const workspaceSpecificKeys = [
GlobalStateKey.RootDir,
GlobalStateKey.Running,
GlobalStateKey.ModuleRoot,
GlobalStateKey.ContextFiles,
GlobalStateKey.QueueTasks,
@ -85,6 +88,7 @@ export class GlobalState {
running: this.get(GlobalStateKey.Running, false)!,
focusedTaskId: this.get(GlobalStateKey.FocusedTaskId)!,
queueTasks: this.get(GlobalStateKey.QueueTasks, [])!,
moduleRoot: this.get(GlobalStateKey.ModuleRoot)!,
};
}

View file

@ -0,0 +1,12 @@
export const AUTH_TYPE = `auth0`;
export const AUTH_NAME = `Codeflash`;
export const BASE_URL = "https://app.codeflash.ai/";
export const AUTH_TIMEOUT = 5 * 60 * 1000; // 5 minutes
export const ERRORS = {
TIMED_OUT: "Authentication request timed out",
USER_CANCELLED: "User cancelled authentication",
AUTH_FAILED: "Authentication failed",
NETWORK_ERROR: "Network error during authentication",
} as const;

View file

@ -2,7 +2,6 @@ import * as vscode from "vscode";
import type { LanguageClient } from "vscode-languageclient/node";
import { State as LanguageClientState } from "vscode-languageclient/node";
import { Logger } from "../utils";
import debounce from "debounce";
import type { AnalysisService, NavigationService } from "../services";
import { Disposable } from "../utils/dispose";
import { GlobalStateKey, type GlobalState } from "../globalState";
@ -20,7 +19,7 @@ interface OptimizationSuggestion {
potentialImprovements: string[];
severity: "info" | "warning" | "suggestion";
}
const CODELENS_PREFIX = "codeflash: ";
export class CodeflashCodeLensProvider
extends Disposable
implements vscode.CodeLensProvider
@ -28,7 +27,7 @@ export class CodeflashCodeLensProvider
private readonly logger = new Logger("Codeflash CodeLens");
private _onDidChangeCodeLenses = new vscode.EventEmitter<void>();
private _cachedSuggestions = new Map<string, OptimizationSuggestion[]>();
private _debouncedRefresh: () => void;
private refreshTimeout?: NodeJS.Timeout;
readonly onDidChangeCodeLenses: vscode.Event<void> =
this._onDidChangeCodeLenses.event;
@ -45,19 +44,26 @@ export class CodeflashCodeLensProvider
this._disposables.push(
this.lspClient.onDidChangeState((e) => {
if (e.newState === LanguageClientState.Running) {
this._onDidChangeCodeLenses.fire();
this.scheduleRefresh(500);
}
}),
vscode.workspace.onDidChangeTextDocument((e) => {
if (e.document.languageId === "python") {
this._debouncedRefresh();
this.scheduleRefresh(500);
}
}),
vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor && editor.document.languageId === "python") {
this.scheduleRefresh(500);
}
}),
vscode.window.onDidChangeTextEditorVisibleRanges((e) => {
if (e.textEditor.document.languageId === "python") {
this.scheduleRefresh(500);
}
}),
);
this._debouncedRefresh = debounce(() => {
this._onDidChangeCodeLenses.fire();
}, 500);
}
async provideCodeLenses(
@ -77,21 +83,12 @@ export class CodeflashCodeLensProvider
this.logger.warn(
`CodeLens requested but LSP client not running. State: ${this.lspClient.state}`,
);
return [];
}
// Show user-friendly message for LSP issues
if (this.lspClient.state === LanguageClientState.Stopped) {
vscode.window
.showWarningMessage(
"Codeflash Language Server is not running. CodeLens suggestions unavailable.",
"Restart Extension",
)
.then((action) => {
if (action === "Restart Extension") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
}
// check if it's inside the module root or not
const moduleRootAbs = this.globalState.get(GlobalStateKey.ModuleRoot, "")!;
if (!document.uri.fsPath.startsWith(moduleRootAbs)) {
return [];
}
@ -176,7 +173,7 @@ export class CodeflashCodeLensProvider
)
.then((action) => {
if (action === "Refresh Analysis") {
this.refresh();
this.scheduleRefresh();
}
});
} else if (
@ -206,17 +203,13 @@ export class CodeflashCodeLensProvider
return this._cachedSuggestions.get(uri.toString()) || [];
}
public refresh(delay: number = 0): void {
public scheduleRefresh(delay: number = 500): void {
this._cachedSuggestions.clear();
debounce(
() => {
this._onDidChangeCodeLenses.fire();
},
delay,
{
immediate: delay === 0,
},
)();
clearTimeout(this.refreshTimeout);
this.refreshTimeout = setTimeout(() => {
this._onDidChangeCodeLenses.fire();
}, delay);
}
public override dispose(): void {
@ -281,6 +274,8 @@ export class CodeflashCodeLensProvider
};
}
command.title = `${CODELENS_PREFIX}${command.title}`;
return command;
}
}

View file

@ -0,0 +1,470 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Logger } from "../utils";
import { Disposable } from "../utils/dispose";
import * as vscode from "vscode";
import crypto from "crypto";
import {
AUTH_NAME,
AUTH_TIMEOUT,
AUTH_TYPE,
BASE_URL,
ERRORS,
} from "./AuthConfig";
// interface SessionData {
// id: string;
// accessToken: string;
// account: {
// id: string;
// label: string;
// };
// scopes: readonly string[];
// }
const createCancellationPromise = (
cancellationToken: vscode.CancellationToken,
): Promise<never> => {
return new Promise<never>((_, reject) => {
cancellationToken.onCancellationRequested(() => {
reject(new Error(ERRORS.USER_CANCELLED));
});
});
};
/**
* Dedicated URI handler for OAuth callbacks
*/
export class AuthUriHandler
extends vscode.EventEmitter<vscode.Uri>
implements vscode.UriHandler
{
private readonly _pendingStates = new Set<string>();
private _codeExchangePromise:
| Promise<{ code: string; state: string }>
| undefined;
public handleUri(uri: vscode.Uri): void {
this.fire(uri);
}
public async waitForCallback(
expectedState: string,
): Promise<{ code: string; state: string }> {
this._pendingStates.add(expectedState);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(ERRORS.TIMED_OUT)), AUTH_TIMEOUT),
);
try {
const callbackPromise =
this._codeExchangePromise ||
new Promise<{ code: string; state: string }>((resolve, reject) => {
const disposable = this.event((uri) => {
try {
const result = this.handleCallback(uri);
if (result) {
disposable.dispose();
resolve(result);
}
} catch (error) {
disposable.dispose();
reject(error);
}
});
});
this._codeExchangePromise = callbackPromise;
const incommingCancellationPromise =
CodeflashAuthProvider.incommingCancellationTokenSource
? createCancellationPromise(
CodeflashAuthProvider.incommingCancellationTokenSource.token,
)
: new Promise<never>(() => {});
return await Promise.race([
callbackPromise,
timeoutPromise,
incommingCancellationPromise,
]);
} finally {
this._pendingStates.delete(expectedState);
this._codeExchangePromise = undefined;
}
}
private handleCallback(
uri: vscode.Uri,
): { code: string; state: string } | null {
if (uri.path !== "/callback") {
return null;
}
const params = new URLSearchParams(uri.query);
const code = params.get("code");
const state = params.get("state");
if (!code || !state) {
throw new Error(ERRORS.AUTH_FAILED);
}
if (!this._pendingStates.has(state)) {
return null;
}
return { code, state };
}
}
export class CodeflashAuthProvider
extends Disposable
implements vscode.AuthenticationProvider
{
private readonly logger = new Logger("CodeflashAuthProvider");
private readonly _sessionChangeEmitter =
new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
private readonly _uriHandler: AuthUriHandler;
public static incommingCancellationTokenSource: vscode.CancellationTokenSource | null =
null;
// private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
constructor(private readonly context: vscode.ExtensionContext) {
super();
this._uriHandler = new AuthUriHandler();
//this._sessionsPromise = this.readSessions();
this._disposables.push(
vscode.authentication.registerAuthenticationProvider(
AUTH_TYPE,
AUTH_NAME,
this,
{ supportsMultipleAccounts: false },
),
vscode.window.registerUriHandler(this._uriHandler),
this._sessionChangeEmitter,
// this.context.secrets.onDidChange(() => this.checkForUpdates()),
);
}
get onDidChangeSessions() {
return this._sessionChangeEmitter.event;
}
/**
* Get the existing sessions
*/
public async getSessions(
_scopes?: readonly string[],
_options?: vscode.AuthenticationProviderSessionOptions,
): Promise<vscode.AuthenticationSession[]> {
// this.logger.info(
// `Getting sessions for ${scopes?.length ? scopes.join(",") : "all scopes"}...`,
// );
// const sessions = await this._sessionsPromise;
// const filteredSessions = scopes?.length
// ? sessions.filter((session) => this.scopesMatch(session.scopes, scopes))
// : sessions;
// this.logger.info(`Got ${filteredSessions.length} session(s)`);
// return filteredSessions;
return Promise.resolve([]);
}
/**
* Create a new auth session
*/
public async createSession(
scopes: readonly string[],
): Promise<vscode.AuthenticationSession> {
try {
this.logger.info("Starting authentication flow");
const { code, codeVerifier, redirectUri } =
await this.initiateLoginWithProgress();
// Exchange authorization code for access token
const apiKey = await this.exchangeCodeForToken(
code,
codeVerifier,
redirectUri,
);
// Create session
const session: vscode.AuthenticationSession = {
id: this.generateSessionId(),
accessToken: apiKey,
account: { id: apiKey, label: apiKey },
scopes,
};
// // Update stored sessions
// await this.addSession(session);
this.logger.info("Authentication successful");
return session;
} catch (error) {
this.handleAuthError(error);
throw error;
}
}
/**
* Remove an existing session
*/
public async removeSession(_sessionId: string): Promise<void> {}
// ============================================
// Private Helper Methods
// ============================================
private async initiateLoginWithProgress(): Promise<{
code: string;
state: string;
codeVerifier: string;
redirectUri: string;
}> {
return await this.initiateLogin();
}
private async initiateLogin(): Promise<{
code: string;
state: string;
codeVerifier: string;
redirectUri: string;
}> {
const publisher = this.context.extension.packageJSON.publisher;
const name = this.context.extension.packageJSON.name;
const redirectUri = `${vscode.env.uriScheme}://${publisher}.${name}/callback`;
//TODO: FIX ENCODE ISSUE
// const externalUri = await vscode.env.asExternalUri(
// vscode.Uri.parse(redirectUri),
// );
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
const state = crypto.randomUUID();
await this.context.secrets.store("auth-code-verifier", codeVerifier);
await this.context.secrets.store("auth-state", state);
const clientId = "cf_vscode_app";
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
code_challenge: codeChallenge,
code_challenge_method: "sha256",
state,
originator: `codeflash_${vscode.env.appName}`,
});
const codeflashAuthUrl = `${BASE_URL}codeflash/auth?${params.toString()}`;
const opened = await vscode.env.openExternal(
vscode.Uri.parse(codeflashAuthUrl),
);
if (!opened) {
throw new Error("Failed to open browser for authentication");
}
this.logger.info(
"Browser opened for authentication, waiting for callback...",
);
try {
// new cancellation token source for incomming auth cancellation
CodeflashAuthProvider.incommingCancellationTokenSource?.dispose();
CodeflashAuthProvider.incommingCancellationTokenSource =
new vscode.CancellationTokenSource();
const result = await this._uriHandler.waitForCallback(state);
const savedState = await this.context.secrets.get("auth-state");
if (result.state !== savedState) {
throw new Error("State validation failed");
}
return {
code: result.code,
state: result.state,
codeVerifier,
redirectUri,
};
} catch (error) {
// Clean up stored secrets on error
await this.context.secrets.delete("auth-code-verifier");
await this.context.secrets.delete("auth-state");
throw error;
}
}
private async exchangeCodeForToken(
code: string,
codeVerifier: string,
redirectUri: string,
): Promise<string> {
try {
const response = await fetch(`${BASE_URL}/codeflash/auth/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
client_id: "cf_vscode_app",
}),
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
const data = await response.json();
if (!data.access_token) {
throw new Error("No access token in response");
}
return data.access_token;
} catch (error) {
if (error instanceof TypeError && error.message.includes("fetch")) {
throw new Error(ERRORS.NETWORK_ERROR);
}
throw error;
}
}
// private async readSessions(): Promise<vscode.AuthenticationSession[]> {
// try {
// this.logger.info("Reading sessions from storage...");
// const stored = await this.context.secrets.get("codeflash-sessions");
// if (!stored) {
// this.logger.info("No stored sessions found");
// return [];
// }
// const sessions = JSON.parse(stored) as SessionData[];
// this.logger.info(`Loaded ${sessions.length} session(s)`);
// return sessions.map(this.sessionDataToSession);
// } catch (error) {
// this.logger.error("Failed to read sessions: " + String(error));
// return [];
// }
// }
// private async storeSessions(
// sessions: vscode.AuthenticationSession[],
// ): Promise<void> {
// this.logger.info(`Storing ${sessions.length} session(s)...`);
// this._sessionsPromise = Promise.resolve(sessions);
// const sessionData: SessionData[] = sessions.map((s) => ({
// id: s.id,
// accessToken: s.accessToken,
// account: s.account,
// scopes: s.scopes,
// }));
// await this.context.secrets.store(
// "codeflash-sessions",
// JSON.stringify(sessionData),
// );
// this.logger.info("Sessions stored successfully");
// }
// private async addSession(
// session: vscode.AuthenticationSession,
// ): Promise<void> {
// const sessions = await this._sessionsPromise;
// // Remove any existing session (single account mode)
// const removed = sessions.splice(0, sessions.length);
// sessions.push(session);
// await this.storeSessions(sessions);
// this._sessionChangeEmitter.fire({
// added: [session],
// removed,
// changed: [],
// });
// }
// private async checkForUpdates(): Promise<void> {
// const previousSessions = await this._sessionsPromise;
// this._sessionsPromise = this.readSessions();
// const storedSessions = await this._sessionsPromise;
// const added: vscode.AuthenticationSession[] = [];
// const removed: vscode.AuthenticationSession[] = [];
// storedSessions.forEach((session) => {
// if (!previousSessions.some((s) => s.id === session.id)) {
// this.logger.info("Adding session found in storage");
// added.push(session);
// }
// });
// previousSessions.forEach((session) => {
// if (!storedSessions.some((s) => s.id === session.id)) {
// this.logger.info("Removing session no longer in storage");
// removed.push(session);
// }
// });
// if (added.length || removed.length) {
// this._sessionChangeEmitter.fire({ added, removed, changed: [] });
// }
// }
// private sessionDataToSession(
// data: SessionData,
// ): vscode.AuthenticationSession {
// return {
// id: data.id,
// accessToken: data.accessToken,
// account: data.account,
// scopes: data.scopes,
// };
// }
private generateSessionId(): string {
return crypto
.getRandomValues(new Uint32Array(2))
.reduce((prev, curr) => prev + curr.toString(16), "");
}
private handleAuthError(error: unknown): void {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage === ERRORS.USER_CANCELLED) {
this.logger.info("User cancelled authentication");
return;
}
if (errorMessage === ERRORS.TIMED_OUT) {
this.logger.debug("Authentication timed out");
return;
}
if (errorMessage === ERRORS.NETWORK_ERROR) {
vscode.window.showErrorMessage(
"Unable to connect to CodeFlash. Please check your internet connection.",
);
this.logger.error("Network error during authentication");
return;
}
this.logger.error("Authentication failed: " + errorMessage);
}
}

View file

@ -9,6 +9,7 @@ import type { Logger } from "../utils";
import type { GlobalState } from "../globalState";
import { GlobalStateKey } from "../globalState";
import { diff3Merge } from "../utils/diff.mjs";
import { captureException } from "../telemetry/sentry";
const LINEBREAKS = /^.*(\r?\n|$)/gm;
const markerSize = 7;
@ -207,7 +208,8 @@ export class GitPatchProvider {
private removeFilesPrefixes = (path: string): string => {
return path.replace("a/", "").replace("b/", "");
};
private async applyPatch() {
async applyPatch() {
const patchPath = this.patchFile;
const patchContent = await fs.readFile(patchPath, { encoding: "utf-8" });
@ -242,6 +244,7 @@ export class GitPatchProvider {
this.applyWithConflicts(index).then((err) => {
// Merge operation completed
if (err) {
captureException(err);
vscode.window.showErrorMessage(
`Failed to apply patch: ${err.message}`,
);

View file

@ -13,8 +13,8 @@ import {
import { Disposable } from "../utils/dispose";
import type { InitService } from "../services/initService";
import { Logger } from "../utils";
import type { InitStepError } from "../boot/baseStep";
import { LSP_COMMANDS, SIDEBAR_BUNDLE_DIR } from "../constants";
import type { StepError } from "../boot/baseStep";
import type { SubmitInitFormMessage, Suggestion } from "@codeflash/types";
import { getRootWorkspaceFolder } from "../utils/rootWorkspace";
import { join } from "path";
@ -24,7 +24,7 @@ export type StepInfo = {
icon?: string;
title: string;
description: string;
error?: StepError;
error?: InitStepError;
};
export class InitWebviewProvider

View file

@ -36,8 +36,12 @@ import {
import type { ContextFiles, GlobalState } from "../globalState";
import { GlobalStateKey } from "../globalState";
import { Disposable } from "../utils/dispose";
import { readFileSync } from "fs";
import { readFileSync, unlinkSync } from "fs";
import type { LogsEventEmitter } from "../utils/logsEventEmitter";
import { AUTH_TYPE, ERRORS } from "./AuthConfig";
import { Telemetry } from "../telemetry/posthog";
import type { CommentThreadProvider } from "./commentThreadProvider";
import { CodeflashAuthProvider } from "./CodeflashAuthProvider";
export class SidebarProvider
extends Disposable
@ -58,6 +62,7 @@ export class SidebarProvider
private readonly _navigationService: NavigationService,
private readonly optimizationEventEmitter: LogsEventEmitter,
private readonly globalState: GlobalState,
private readonly commentThreadProvider: CommentThreadProvider,
private readonly refreshCodelens: () => void,
) {
super();
@ -70,7 +75,6 @@ export class SidebarProvider
public resolveWebviewView(
webviewView: vscode.WebviewView,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_context: vscode.WebviewViewResolveContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -94,7 +98,11 @@ export class SidebarProvider
this.handleWebviewMessage(m),
);
const optimizationEventListener = this.optimizationEventEmitter.event(
(log) => this.addLogEntryToUI(log),
(log) => {
if (log.task_id === this.optimizationEventEmitter.getCurrentTaskId()) {
this.addLogEntryToUI(log);
}
},
);
this._disposables.push(
@ -185,7 +193,9 @@ export class SidebarProvider
): Promise<void> => {
if (
!this.globalState.get(GlobalStateKey.UserID) &&
!["apiKeyEnter", "webviewReady"].includes(data.type) &&
!["apiKeyEnter", "webviewReady", "signIn", "cancelAuth"].includes(
data.type,
) &&
this.initializedOnce
) {
this._logger.info(
@ -256,38 +266,54 @@ export class SidebarProvider
vscode.window.showWarningMessage(message);
}
break;
case "signIn":
await this.handleSignIn();
break;
case "cancelAuth":
CodeflashAuthProvider.incommingCancellationTokenSource?.cancel();
}
};
async handleViewPatch(payload: ViewPatchMessage["payload"]): Promise<void> {
const { id, patchFile } = payload;
if (this.openedPathches.has(patchFile)) {
this.openedPathches.get(patchFile)?.showPatch();
private async handleAfterPatchApplied(
id: string,
patchFile: string,
gotoFunction: boolean = true,
): Promise<void> {
const task = this.globalState
.get(GlobalStateKey.QueueTasks, [])
?.find((t) => t.id === id);
if (!task) {
return;
}
// eslint-disable-next-line @typescript-eslint/require-await
const onPatchApplied = async () => {
const task = this.globalState
.get(GlobalStateKey.QueueTasks, [])
?.find((t) => t.id === id);
if (!task) {
return;
}
if (task.filepath) {
this._navigationService.navigateToFunction(
task.functionName,
vscode.Uri.file(task.filepath),
);
}
const newContextFiles = this.globalState.get(
GlobalStateKey.ContextFiles,
{},
)!;
delete newContextFiles[id];
this.globalState.set(GlobalStateKey.ContextFiles, newContextFiles);
this.updateUIQueueTasks("remove", { id });
};
// TODO: get the trace id from the lsp
await Telemetry.capture("ext_optimization_applied", {});
if (task.filepath && gotoFunction) {
this._navigationService.navigateToFunction(
task.functionName,
vscode.Uri.file(task.filepath),
);
}
const newContextFiles = this.globalState.get(
GlobalStateKey.ContextFiles,
{},
)!;
delete newContextFiles[id];
unlinkSync(patchFile);
this.globalState.set(GlobalStateKey.ContextFiles, newContextFiles);
this.updateUIQueueTasks("remove", { id });
this.commentThreadProvider.scheduleRefresh();
}
async applyPatchDirectlyForInlineThread(
payload: ViewPatchMessage["payload"],
): Promise<void> {
const { id, patchFile } = payload;
const onPatchApplied = () =>
this.handleAfterPatchApplied(id, patchFile, false);
const newPatchProvider = new GitPatchProvider(
this._extensionUri,
@ -296,10 +322,91 @@ export class SidebarProvider
this._logger,
onPatchApplied,
);
await newPatchProvider.applyPatch();
}
private deleteContextFilesForTask(taskId: string): void {
const newContextFiles = this.globalState.get(
GlobalStateKey.ContextFiles,
{},
)!;
delete newContextFiles[taskId];
this.globalState.set(GlobalStateKey.ContextFiles, newContextFiles);
}
private async handleSignIn(): Promise<void> {
this.sendMessage({
type: "authStarted",
});
this._logger.info("Starting OAuth authentication flow");
try {
const session = await vscode.authentication.getSession(AUTH_TYPE, [], {
createIfNone: true,
});
if (session?.accessToken) {
await this.handleApiKeyEnter(session.accessToken);
this.sendMessage({
type: "authCompleted",
});
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (
errorMessage.includes("User cancelled") ||
errorMessage.includes("Cancelled") ||
errorMessage === ERRORS.USER_CANCELLED
) {
this._logger.info("OAuth authentication cancelled by user");
this.sendMessage({
type: "authCancelled",
});
} else if (
errorMessage.includes("timed out") ||
errorMessage === ERRORS.TIMED_OUT
) {
this._logger.error("OAuth authentication timed out");
this.sendMessage({
type: "authFailed",
payload: { error: "Authentication timed out" },
});
} else {
this._logger.error(`OAuth authentication failed: ${errorMessage}`);
this.sendMessage({
type: "authFailed",
payload: { error: errorMessage },
});
}
}
}
async handleViewPatch(payload: ViewPatchMessage["payload"]): Promise<void> {
const { id, patchFile } = payload;
if (this.openedPathches.has(patchFile)) {
this.openedPathches.get(patchFile)?.showPatch();
return;
}
const onPatchApplied = () => this.handleAfterPatchApplied(id, patchFile);
const newPatchProvider = new GitPatchProvider(
this._extensionUri,
this.globalState,
payload,
this._logger,
onPatchApplied,
);
this.openedPathches.set(patchFile, newPatchProvider);
await newPatchProvider.showPatch();
}
private async requestFunctionsInFile(
filePath: string,
functions?: string[],
@ -380,7 +487,7 @@ export class SidebarProvider
const msg =
added > 0
? `added ${added} functions to the optimization queue`
? `Detected ${added} functions to optimize.`
: "No optimizable functions in your current modified code";
vscode.window.showInformationMessage(msg);
@ -489,12 +596,13 @@ export class SidebarProvider
if (!isTaskRunning(task)) {
return;
}
this._optimizationService.cancelTask(taskId, task.status);
this._optimizationService.cancelTask(task);
this.updateUIQueueTasks("update", {
id: taskId,
status: "canceled",
description: "Optimization was canceled",
});
this.deleteContextFilesForTask(taskId);
this.changeTaskFocus("");
}
@ -517,6 +625,7 @@ export class SidebarProvider
// Remove from queue
this._optimizationService.abortTask(taskId);
this.updateUIQueueTasks("remove", { id: taskId });
this.deleteContextFilesForTask(taskId);
}
private handleViewLogs(): void {
@ -536,6 +645,7 @@ export class SidebarProvider
},
});
}
addFunctionToQueue(
functionName: string,
uri?: vscode.Uri | string,
@ -549,7 +659,6 @@ export class SidebarProvider
filePathStr = _uri?.fsPath || "";
}
if (!filePathStr) {
vscode.window.showErrorMessage("no final uri found");
return false;
}
@ -608,6 +717,7 @@ export class SidebarProvider
abortController,
cancelationToken,
onTaskStart: async (id): Promise<boolean> => {
this.optimizationEventEmitter.setCurrrentTaskId(id);
this.updateUIQueueTasks("update", {
id,
status: "initializing",
@ -620,15 +730,14 @@ export class SidebarProvider
}
this.changeTaskFocus(id);
const initLog: LogEntry = {
task_id: id,
id: randomUUID(),
type: "text",
text: "initializing codeflash optimization process",
takes_time: true,
};
this.optimizationEventEmitter.setCurrrentTaskId(taskId);
this.optimizationEventEmitter.addSingleBufferedLogEntry(initLog);
this.optimizationEventEmitter.flush();
this.addLogEntryToUI(initLog);
this._logger.debug(
@ -637,6 +746,7 @@ export class SidebarProvider
this.refreshCodelens();
const initResult =
await this._optimizationService.initializeFunctionOptimization(
id,
functionName,
filePathStr,
);
@ -647,7 +757,7 @@ export class SidebarProvider
`Failed to initialize optimization ${errorMsg ? " :" + errorMsg : ""}`,
);
// remove the task from the queue since it failed to initialize
this.updateUIQueueTasks("remove", { id: taskId });
this.updateUIQueueTasks("remove", { id });
if (errorMsg.toLowerCase().includes("not found")) {
vscode.window.showErrorMessage(
`Function '${functionName}' not found in file '${filePathStr}, re-analyzing the file...`,
@ -689,12 +799,13 @@ export class SidebarProvider
if (!task) {
return;
}
this.commentThreadProvider.scheduleRefresh();
// show the patch immediately after the task is completed
this.handleViewPatch({
id: task.id,
functionName: task.functionName,
...resultRes,
});
// this.handleViewPatch({
// id: task.id,
// functionName: task.functionName,
// ...resultRes,
// });
this.refreshCodelens();
},
onTaskError: (error, id) => {
@ -751,7 +862,7 @@ export class SidebarProvider
task: Partial<QueueTaskItem>,
): QueueTaskItem | null {
// flush to keep the logs in sync
this.optimizationEventEmitter.flush();
this.optimizationEventEmitter.flush(false); // don't reset current task id yet, logs may still come in for this task
const existingTasks =
this.globalState.get(GlobalStateKey.QueueTasks, []) || [];
@ -988,33 +1099,6 @@ export class SidebarProvider
this.sendMessage(updateMessage);
}
// Commenting out badge update for now, since we dont show the optimizable functions list
// private updateViewBadge(): void {
// if (!this._view) {
// return;
// }
// try {
// // Set badge on the individual view (this shows on the activity bar icon)
// if (this._currentFunctionCount > 0) {
// this._view.badge = {
// tooltip: `${this._currentFunctionCount} optimizable function${this._currentFunctionCount === 1 ? "" : "s"} found`,
// value: this._currentFunctionCount,
// };
// } else {
// this._view.badge = undefined;
// }
// this._logger.debug(
// `Updated activity bar badge: ${this._currentFunctionCount} functions`,
// );
// } catch (error) {
// this._logger.warn(
// `Failed to update activity bar badge ${error instanceof Error ? error.message : String(error)}`,
// );
// }
// }
public async refreshAnalysis(): Promise<void> {
this._logger.debug("Manual analysis refresh requested");
if (this._view?.visible) {

View file

@ -0,0 +1,293 @@
import * as vscode from "vscode";
import { Disposable } from "../utils/dispose";
import { GlobalStateKey, type GlobalState } from "../globalState";
import type { NavigationService } from "../services";
import { Logger } from "../utils";
import type { QueueTaskItem, ViewPatchMessage } from "@codeflash/types";
export class CommentThreadProvider extends Disposable {
private controller: vscode.CommentController;
private refreshTimer?: NodeJS.Timeout;
// a map between task id and comment thread
private threads: Map<
string,
{
thread: vscode.CommentThread;
line: number;
}
> = new Map();
private decorationTypes: Map<string, vscode.TextEditorDecorationType> =
new Map();
private readonly logger = new Logger("CommentThreadProvider");
private refreshing: boolean = false;
constructor(
private readonly context: vscode.ExtensionContext,
private readonly globalState: GlobalState,
private readonly navigationService: NavigationService,
) {
super();
const controller = vscode.comments.createCommentController(
"codeflash-comments",
"Codeflash suggestions",
);
controller.commentingRangeProvider = {
provideCommentingRanges: async (document) => {
if (document.languageId !== "python") {
return [];
}
// vscode has a limitation where in order to expand the comment thread, the line should be in the commenting ranges,
// this is for vscode only, other editors like cursor do not have this.
const successfulOptimizations =
this.getSuccessfulOptimizations().filter(
(t) => t.filepath === document.uri.fsPath,
);
if (successfulOptimizations.length === 0) {
return [];
}
const ranges: vscode.Range[] = [];
for (const task of successfulOptimizations) {
const filePath = task.filepath!;
if (filePath !== document.uri.fsPath) {
continue;
}
const pos =
await this.navigationService.manuallyFindTheFunctionInFile(
task.functionName,
document.uri,
);
if (pos) {
ranges.push(new vscode.Range(pos, pos));
}
}
return ranges;
},
};
this._disposables.push(controller);
this._disposables.push(
vscode.workspace.onDidChangeTextDocument((e) => {
if (e.document.languageId === "python") {
this.scheduleRefresh();
}
}),
vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor && editor.document.languageId === "python") {
this.scheduleRefresh();
}
}),
vscode.window.onDidChangeTextEditorVisibleRanges((e) => {
if (e.textEditor.document.languageId === "python") {
this.scheduleRefresh();
}
}),
);
this.controller = controller;
}
private getEditorForFileUri(
fileUri: vscode.Uri | string,
): vscode.TextEditor | undefined {
const visibleEditors = vscode.window.visibleTextEditors;
return visibleEditors.find(
(e) =>
e.document.uri.fsPath ===
(fileUri instanceof vscode.Uri
? fileUri.fsPath
: vscode.Uri.file(fileUri).fsPath),
);
}
public scheduleRefresh(delay: number = 500): void {
clearTimeout(this.refreshTimer);
this.refreshTimer = setTimeout(async () => {
await this.refresh();
}, delay);
}
private async refresh(): Promise<void> {
if (this.refreshing) {
return;
}
if (!this.controller) {
throw new Error("Comment controller is not initialized");
}
this.logger.debug("Refreshing comment threads... ");
this.refreshing = true;
const removedTaskIds = this.detectRemovedTasks();
for (const taskId of removedTaskIds) {
this.cleanupThread(taskId);
this.cleanUpDecoration(taskId);
}
const successfulOptimizations = this.getSuccessfulOptimizations();
if (successfulOptimizations.length === 0) {
this.logger.debug("No new tasks to add comment threads for.");
this.refreshing = false;
return;
}
for (const task of successfulOptimizations) {
const fileUri = vscode.Uri.file(task.filepath!);
const editor = this.getEditorForFileUri(fileUri);
if (!editor) {
this.logger.debug(`No visible editor for file ${task.filepath}`);
continue;
}
const pos = await this.navigationService.manuallyFindTheFunctionInFile(
task.functionName,
fileUri,
);
if (!pos) {
this.logger.debug(
`Could not find position for function ${task.functionName} in file ${task.filepath}`,
);
if (this.threads.has(task.id)) {
// cleanup if we can't find the function anymore
this.cleanupThread(task.id);
this.cleanUpDecoration(task.id);
}
continue;
}
this.drawDecorationForThread(editor, task.id, pos.line);
const existingThread = this.threads.get(task.id);
if (existingThread && existingThread.line === pos.line) {
// no change in position, keep the existing thread
continue;
}
this.cleanupThread(task.id);
const thread = this.controller.createCommentThread(
fileUri,
new vscode.Range(pos, pos),
[],
);
const viewPatchPayload: ViewPatchMessage["payload"] = {
id: task.id,
functionName: task.functionName,
patchFile: task.patchFile!,
explanation: task.explanation || "",
speedupStr: task.speedupStr || "",
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(thread as any).viewPatchPayload = viewPatchPayload;
// thread.state = vscode.CommentThreadState.Unresolved;
thread.contextValue = "codeflash";
thread.collapsibleState = vscode.CommentThreadCollapsibleState.Collapsed;
thread.label = task.speedupStr
? `Optimization found - ${task.speedupStr}`
: "Optimization found";
thread.canReply = false;
const botComment: vscode.Comment = {
author: {
name: "Codeflash",
iconPath: vscode.Uri.file(
this.context.asAbsolutePath("media/codeflash.svg"),
),
},
body: this.getMarkdownStringForTask(task),
mode: vscode.CommentMode.Preview,
};
thread.comments = [botComment];
this.threads.set(task.id, { thread, line: pos.line });
this._disposables.push(thread);
}
this.refreshing = false;
}
private drawDecorationForThread(
editor: vscode.TextEditor,
taskId: string,
line: number,
) {
this.cleanUpDecoration(taskId);
const pos = new vscode.Position(line, 0);
const range = new vscode.Range(pos, pos);
const decorationType = vscode.window.createTextEditorDecorationType({
gutterIconPath: vscode.Uri.file(
this.context.asAbsolutePath("media/codeflash.svg"),
),
gutterIconSize: "contain",
});
editor.setDecorations(decorationType, [
{
range: range,
},
]);
this.decorationTypes.set(taskId, decorationType);
this._disposables.push(decorationType);
}
private getSuccessfulOptimizations(): QueueTaskItem[] {
return this.globalState
.get(GlobalStateKey.QueueTasks, [])!
.filter((t) => t.status === "completed");
}
private getMarkdownStringForTask(task: QueueTaskItem): vscode.MarkdownString {
const md = new vscode.MarkdownString();
md.supportHtml = true;
md.isTrusted = true;
if (task.speedupStr) {
md.appendMarkdown(`**Estimated Speedup:** ${task.speedupStr}<br/><br/>`);
}
if (task.bestCandidateCode) {
md.appendCodeblock(task.bestCandidateCode, "python");
}
if (task.explanation) {
md.appendMarkdown(
`<br/><details><br/><summary>**Explanation:**</summary><br/>${task.explanation}<br/></details><br/>`,
);
}
return md;
}
private detectRemovedTasks(): string[] {
const currentTaskIds = new Set(
this.getSuccessfulOptimizations().map((t) => t.id),
);
const removedTaskIds: string[] = [];
for (const taskId of this.threads.keys()) {
if (!currentTaskIds.has(taskId)) {
removedTaskIds.push(taskId);
}
}
return removedTaskIds;
}
private cleanupThread(taskId: string) {
this.logger.debug(`Cleaning up comment thread for task ${taskId}...`);
const thread = this.threads.get(taskId);
if (thread) {
thread.thread.dispose();
this.threads.delete(taskId);
}
}
private cleanUpDecoration(taskId: string) {
const decoration = this.decorationTypes.get(taskId);
if (decoration) {
decoration.dispose();
this.decorationTypes.delete(taskId);
}
}
}

View file

@ -79,15 +79,15 @@ export class GitWatcherService {
.then((funcs) => this.addCommitFunctionsToQueue(funcs));
}
private async addCommitFunctionsToQueue(
private addCommitFunctionsToQueue(
functions: Record<string, string[]>,
): Promise<void> {
): void {
const totalFunctionsCount = Object.values(functions).reduce(
(acc, val) => acc + val.length,
0,
);
vscode.window.showInformationMessage(
`Found ${totalFunctionsCount} optimizable functions in the commit, adding to queue...`,
`Found ${totalFunctionsCount} optimizable functions in the commit, adding to optimization queue...`,
);
for (const [path, qualifiedNames] of Object.entries(functions)) {
for (const name of qualifiedNames) {

View file

@ -10,13 +10,20 @@ import type {
StepResult,
} from "../boot/baseStep";
import type BaseStep from "../boot/baseStep";
import { StepError } from "../boot/baseStep";
import { InitStepError } from "../boot/baseStep";
import { BootCodeflashServerStep } from "../boot/bootCodeflashServer";
import { type GlobalState } from "../globalState";
import { CodeflashInitStep } from "../boot/codeflashInitStep";
import type { LogsEventEmitter } from "../utils/logsEventEmitter";
import { ERROR_CATEGORIES } from "../boot/stepErrors";
import { captureException } from "../telemetry/sentry";
import {
ERROR_CATEGORIES,
getCodeflashVersionIncompatibleError,
} from "../boot/stepErrors";
import type { Suggestion } from "@codeflash/types";
import { Telemetry } from "../telemetry/posthog";
import type { LanguageClient } from "vscode-languageclient/node";
import { LSP_COMMANDS } from "../constants";
type CompleteInitResult = {
environmentResult?: CheckEnvironmentStepResult;
@ -136,12 +143,15 @@ export class InitService extends Disposable {
private async runWithoutPooling(
step: BaseStep,
stepNum: number,
): Promise<StepResult | StepError | null> {
): Promise<StepResult | InitStepError | null> {
if (this.isDisposed) {
return null;
}
const result = await step.run();
if (result instanceof StepError) {
if (result instanceof InitStepError) {
captureException(result, {
...result,
});
this.updateStep!(stepNum, {
status: "failed",
error: result,
@ -163,16 +173,18 @@ export class InitService extends Disposable {
}
if (this.pausePooling) {
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 600));
return this.runWithPooling(step, stepsSnapshot, stepNum, failed);
}
const result = await step.run();
if (result instanceof StepError) {
if (result instanceof InitStepError) {
// failed
this.updateStep!(stepNum, {
status: "failed",
error: result,
});
const isInitError = result.category === ERROR_CATEGORIES.CODEFLASH_INIT;
const stepErrorIdentifier = `${stepNum}-${result.message}-${result.details}`;
const isDifferentError =
@ -195,6 +207,14 @@ export class InitService extends Disposable {
// this is to avoid error flickering in the UI during pooling retries.
this.renderSteps!();
}
if (isDifferentError) {
await Telemetry.capture("ext_init_step_error", {
stepNumber: stepNum,
...result,
});
}
this.currentRenderedError = stepErrorIdentifier;
step.dispose();
@ -213,6 +233,38 @@ export class InitService extends Disposable {
return result;
}
private async checkFeaturesCompatibility(
codeflashVersion: string,
lspClient: LanguageClient,
interpreterPath: string,
): Promise<InitStepError | undefined> {
try {
const lspFeatures = await lspClient.sendRequest<string[]>(
"server/listFeatures",
{},
);
this.logger.info(`LSP features: ${lspFeatures.join(", ")}`);
const expectedFeatures = Object.values(LSP_COMMANDS);
const missingFeatures = expectedFeatures.filter(
(feature) => !lspFeatures.includes(feature),
);
if (missingFeatures.length > 0) {
return getCodeflashVersionIncompatibleError(
codeflashVersion,
interpreterPath,
false,
);
}
return undefined;
} catch {
return getCodeflashVersionIncompatibleError(
codeflashVersion,
interpreterPath,
false,
);
}
}
private async init(): Promise<CompleteInitResult | null> {
if (!this.uiMethodsIntialized()) {
throw new Error("InitService not initialized yet");
@ -223,7 +275,7 @@ export class InitService extends Disposable {
{
status: "idle",
// icon: "vm",
title: "Checking prerequisites",
title: "Verifying your environment",
description: "",
},
{
@ -269,10 +321,24 @@ export class InitService extends Disposable {
this.updateInitMessage!(bootServerStep.stepTitle);
const bootResult = (await this.runWithoutPooling(bootServerStep, 2)) as
| BootCodeflashServerStepResult
| StepError;
if (bootResult instanceof StepError) {
| InitStepError;
if (bootResult instanceof InitStepError) {
return null;
}
const featureCompatibilityError = await this.checkFeaturesCompatibility(
environmentResult.codeflashVersion,
bootResult.lspService.getClient(),
environmentResult.pythonPath,
);
if (featureCompatibilityError) {
this.updateStep!(2, {
status: "failed",
error: featureCompatibilityError,
});
this.renderSteps!();
return null;
}
this.updateStep!(2, { status: "completed" });
this.completeInitResult.bootResult = bootResult;
@ -297,9 +363,13 @@ export class InitService extends Disposable {
}
this.completeInitResult.initResult = finalizeResult;
bootResult.codeLensProvider.refresh(500);
bootResult.codeLensProvider.scheduleRefresh();
bootResult.commentThreadProvider.scheduleRefresh();
// DONE
return this.completeInitResult;
}
override dispose(): void {
super.dispose();
}
}

View file

@ -1,9 +1,14 @@
import type {
LanguageClientOptions,
ServerOptions,
State as LanguageClientState,
} from "vscode-languageclient/node";
import { LanguageClient, TransportKind } from "vscode-languageclient/node";
import { State as LanguageClientState } from "vscode-languageclient/node";
import {
CloseAction,
ErrorAction,
LanguageClient,
TransportKind,
} from "vscode-languageclient/node";
import {
LSP_CLIENT_ID,
LSP_CLIENT_NAME,
@ -20,10 +25,11 @@ export class LspService {
private client: LanguageClient | undefined;
private logger: Logger;
private name: string;
private startupError: Error | null = null;
// the optional event emitter is for emitting events from the lsp server (via the output channel) to the sidebar provider for tracking the progress of the optimization
constructor(
name: string,
// the optional event emitter is for emitting events from the lsp server (via the output channel) to the sidebar provider for tracking the progress of the optimization
private optimizationEventEmitter?: LogsEventEmitter,
) {
this.name = name;
@ -71,17 +77,27 @@ export class LspService {
);
}
const channel = createInterceptingOutputChannel(
this.logger._outputChannel,
this.optimizationEventEmitter,
);
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "python" }],
synchronize: {},
outputChannel: createInterceptingOutputChannel(
this.logger._outputChannel,
this.optimizationEventEmitter,
),
traceOutputChannel: createInterceptingOutputChannel(
this.logger._outputChannel,
this.optimizationEventEmitter,
),
outputChannel: channel,
traceOutputChannel: channel,
errorHandler: {
error: (error, message, count) => {
this.startupError = error;
this.logger.error(`[LSP ErrorHandler] Error #${count}: ${message}`);
return { action: ErrorAction.Shutdown };
},
closed: () => {
this.logger.error(`[LSP ErrorHandler] Connection closed`);
return { action: CloseAction.DoNotRestart };
},
},
};
this.client = new LanguageClient(
@ -122,7 +138,34 @@ export class LspService {
this.logger.debug("Starting CF-LSP client...");
try {
await this.client.start();
const closedPromise = new Promise<never>((_, reject) => {
this.client?.onDidChangeState((event) => {
// 1 = Starting, 2 = Running, 3 = Stopped (disposed)
if (event.newState === LanguageClientState.Stopped) {
reject(
this.startupError ??
new Error(
channel.lastTracebackMessage ||
"LSP connection closed during startup",
),
);
}
});
});
const startPromise = this.client.start();
// whichever happens first: either it starts or it closes
await Promise.race([startPromise, closedPromise]);
if (this.client.state !== LanguageClientState.Running) {
throw (
this.startupError ??
new Error(
channel.lastTracebackMessage || "LSP failed to reach running state",
)
);
}
var endTime = performance.now();
this.logger.debug(
@ -133,9 +176,7 @@ export class LspService {
const errorMessage =
error instanceof Error ? error.message : String(error);
this.logger.error("Failed to start CF-LSP client : " + errorMessage);
throw new Error(
`**${LSP_FAILED_MSG_TITLE}**\n\nPython path: \`${pythonPath}\``,
);
throw error;
}
}

View file

@ -8,7 +8,7 @@ import type { OptimizationResponse } from "../types";
import { LSP_COMMANDS } from "../constants";
import { Logger } from "../utils";
import PQueue from "p-queue";
import type { QueueTaskItem, QueueTaskItemStatus } from "@codeflash/types";
import type { QueueTaskItem } from "@codeflash/types";
export interface OptimizationResult {
success: boolean;
@ -93,17 +93,22 @@ export class OptimizationService {
this.abortControllerMap.delete(taskId);
}
cancelTask(taskId: string, status: QueueTaskItemStatus): void {
this.abortTask(taskId);
if (status === "optimizing") {
const cancelationToken = this.cancelationTokenMap.get(taskId);
if (!cancelationToken) {
return;
}
cancelationToken.cancel();
cancelationToken.dispose();
this.cancelationTokenMap.delete(taskId);
cancelTask(task: QueueTaskItem): void {
const { id: taskId } = task;
if (task.status === "initializing") {
// task is cancelled quickly before it starts optimization, clear the session
this.optimizationClient.sendRequest<Boolean>(
LSP_COMMANDS.CLEANUP_CURRENT_OPTIMIZER_SESSION,
{},
);
}
const cancelationToken = this.cancelationTokenMap.get(taskId);
if (!cancelationToken) {
return;
}
cancelationToken.cancel();
cancelationToken.dispose();
this.cancelationTokenMap.delete(taskId);
}
private async optimizeFunction(
@ -203,6 +208,7 @@ export class OptimizationService {
}
async initializeFunctionOptimization(
taskId: string,
functionName: string,
uriString: string,
): Promise<OptimizationResponse> {
@ -214,6 +220,7 @@ export class OptimizationService {
{
textDocument: { uri: uriString },
functionName: functionName,
task_id: taskId,
},
);
@ -241,23 +248,6 @@ export class OptimizationService {
return result;
}
async sendPatchCleanupMessage(taskId: string): Promise<Boolean> {
try {
const result =
await this.optimizationClient.sendRequest<OptimizationResponse>(
LSP_COMMANDS.ON_PATCH_APPLIED,
{ task_id: taskId },
);
if (result.status === "success") {
return true;
}
return false;
} catch (err) {
this.logger.error("error while sending on patch applied", err as Error);
return false;
}
}
dispose(): void {
this.logger.dispose();
}

View file

@ -0,0 +1,142 @@
import os from "node:os";
import type { PostHog as PostHogType } from "posthog-node";
import type { IdeInfo } from "./types";
import { Logger } from "../utils";
const logger = new Logger("Posthog Telemetry");
/**
* It's helpful to know what functions errors occur in, but absolute
* file paths are sensitive information, so we want to remove them.
* @param stack The stack trace to extract minimal information from.
* @returns A string containing the minimal stack trace information.
*/
export function extractMinimalStackTraceInfo(stack: unknown): string {
if (typeof stack !== "string") {
return "";
}
const lines = stack
.trim()
.split("\n")
.map((line) => line.trim());
const minimalLines = lines.filter((line) => {
return (
line.startsWith("at ") &&
!line.includes("node_modules") &&
!line.includes("node:internal")
);
});
return minimalLines
.map((line) => line.replace("at ", "").split(" (").slice(0, 1))
.flatMap((parts) =>
parts.map(
// to be safe, remove any lingering paths - anonymous function case
(part) =>
part.replace(/(?:[A-Za-z]:[\\/]|[\\/])[^\n]*?:\d+:\d+/g, "").trim(),
),
)
.filter((part) => !!part) // remove empty string parts (anonymous functions case)
.join(", ");
}
export class Telemetry {
// Set to undefined whenever telemetry is disabled
static client: PostHogType | undefined = undefined;
static uniqueId = "NOT_UNIQUE";
static os: string | undefined = undefined;
static ideInfo: IdeInfo | undefined = undefined;
/**
* Convenience method for capturing errors in a single event
*/
static async captureError(errorName: string, error: unknown) {
if (!(error instanceof Error)) {
return;
}
await Telemetry.capture(
"extension_error_caught",
{
errorName,
message: error.message,
stack: extractMinimalStackTraceInfo(error.stack),
},
false,
);
}
static async capture(
event: string,
properties: { [key: string]: unknown },
isExtensionActivationError: boolean = false,
) {
const extEvent = event.startsWith("ext_") ? event : `ext_${event}`;
try {
const augmentedProperties = {
...properties,
os: Telemetry.os,
extensionVersion: Telemetry.ideInfo?.extensionVersion,
ideName: Telemetry.ideInfo?.name,
};
const payload = {
distinctId: Telemetry.uniqueId,
event: extEvent,
properties: augmentedProperties,
sendFeatureFlags: false,
};
// In cases where an extremely early fatal error occurs, we may not have initialized yet
if (isExtensionActivationError && !Telemetry.client) {
const client = await Telemetry.getTelemetryClient();
client?.capture(payload);
return;
}
if (process.env.NODE_ENV === "test") {
return;
}
logger.info(
`Capturing telemetry event: ${extEvent} with properties: ${JSON.stringify(extEvent)}`,
);
const client = Telemetry.client;
if (!client) {
logger.info(
"Telemetry client not initialized, skipping event capture.",
);
return;
}
client.capture(payload);
} catch (e) {
console.error(`Failed to capture event: ${e}`);
}
}
static shutdownPosthogClient() {
Telemetry.client?.shutdown();
}
static async getTelemetryClient(): Promise<PostHogType | undefined> {
try {
const { PostHog } = await import("posthog-node");
return new PostHog("phc_aUO790jHd7z1SXwsYCz8dRApxueplZlZWeDSpKc5hol", {
host: "https://us.posthog.com",
});
} catch (e) {
console.error(`Failed to setup telemetry: ${e}`);
return undefined;
}
}
static async setup(allow: boolean, uniqueId: string, ideInfo: IdeInfo) {
Telemetry.uniqueId = uniqueId;
Telemetry.os = os.platform();
Telemetry.ideInfo = ideInfo;
if (!allow || process.env.NODE_ENV === "test") {
Telemetry.client = undefined;
} else if (!Telemetry.client) {
Telemetry.client = await Telemetry.getTelemetryClient();
}
}
}

View file

@ -0,0 +1,261 @@
import type { Extras } from "@sentry/core";
import { type Integration, type Event } from "@sentry/core";
import * as Sentry from "@sentry/node";
import os from "node:os";
import { anonymizeSentryEvent } from "./sentryAnonymization";
import type { IdeInfo } from "./types";
import { Logger } from "../utils";
import { InitStepError } from "../boot/baseStep";
const logger = new Logger("SentryLogger");
export class SentryLogger {
static client: Sentry.NodeClient | undefined = undefined;
static scope: Sentry.Scope | undefined = undefined;
static uniqueId = "NOT_UNIQUE";
static os: string | undefined = undefined;
static ideInfo: IdeInfo | undefined = undefined;
static allowTelemetry: boolean = false;
private static initializeSentryClient(release: string): {
client: Sentry.NodeClient | undefined;
scope: Sentry.Scope | undefined;
} {
try {
// For shared environments like VSCode extensions, we need to avoid global state pollution
// Filter out integrations that use global state
// See https://docs.sentry.io/platforms/javascript/best-practices/shared-environments/
// Filter integrations that use the global variable
const integrations = Sentry.getDefaultIntegrations({}).filter(
(defaultIntegration: Integration) => {
// Remove integrations that might interfere with shared environments
return ![
"OnUncaughtException",
"OnUnhandledRejection",
"ContextLines",
"LocalVariables",
].includes(defaultIntegration.name);
},
);
// Create client manually without polluting global state
const client = new Sentry.NodeClient({
dsn: "https://e464e3269d6cfcdb7242e40100f57585@o4506833230561280.ingest.us.sentry.io/4510228893466624",
debug: true,
release,
environment: process.env.NODE_ENV || "production",
transport: Sentry.makeNodeTransport,
stackParser: Sentry.defaultStackParser,
// FIXME: for now, we set sample rate to 1 to pick up more errors during early stages
sampleRate: 1.0,
tracesSampleRate: 1.0,
// Privacy-conscious default
sendDefaultPii: false,
// Strip sensitive data and add basic properties before sending events
beforeSend(event: Event) {
// First apply anonymization
const anonymizedEvent = anonymizeSentryEvent(event);
if (!anonymizedEvent) {
return null;
}
// Add basic properties similar to PostHog telemetry
if (!anonymizedEvent.tags) {
anonymizedEvent.tags = {};
}
if (!anonymizedEvent.extra) {
anonymizedEvent.extra = {};
}
// Add OS information
if (SentryLogger.os) {
anonymizedEvent.tags.os = SentryLogger.os;
}
// Add ideInfo properties spread out as top-level properties
if (SentryLogger.ideInfo) {
anonymizedEvent.tags.ideName = SentryLogger.ideInfo.name;
anonymizedEvent.tags.extensionVersion =
SentryLogger.ideInfo.extensionVersion;
}
return anonymizedEvent;
},
// Use filtered integrations for Node.js/VSCode shared environment
integrations,
// Enable structured logging
_experiments: {
enableLogs: true,
},
});
// Create a new scope and set the client
const scope = new Sentry.Scope();
scope.setClient(client);
// Initialize the client after setting it on the scope
client.init();
return { client, scope };
} catch (error) {
logger.error(
"Failed to initialize Sentry client: " + (error as Error).message,
);
return { client: undefined, scope: undefined };
}
}
static setup(
allowAnonymousTelemetry: boolean,
uniqueId: string,
ideInfo: IdeInfo,
) {
SentryLogger.allowTelemetry = allowAnonymousTelemetry;
SentryLogger.uniqueId = uniqueId;
SentryLogger.ideInfo = ideInfo;
SentryLogger.os = os.platform();
if (!SentryLogger.allowTelemetry) {
SentryLogger.client = undefined;
SentryLogger.scope = undefined;
} else if (!SentryLogger.client) {
const { client, scope } = SentryLogger.initializeSentryClient(
SentryLogger.ideInfo.extensionVersion,
);
SentryLogger.client = client;
SentryLogger.scope = scope;
}
}
private static ensureInitialized(): void {
if (!SentryLogger.allowTelemetry || SentryLogger.client) {
return;
}
if (SentryLogger.ideInfo) {
const { client, scope } = SentryLogger.initializeSentryClient(
SentryLogger.ideInfo.extensionVersion,
);
SentryLogger.client = client;
SentryLogger.scope = scope;
}
}
static get lazyClient(): Sentry.NodeClient | undefined {
SentryLogger.ensureInitialized();
return SentryLogger.client;
}
static get lazyScope(): Sentry.Scope | undefined {
SentryLogger.ensureInitialized();
return SentryLogger.scope;
}
static shutdownSentryClient() {
if (SentryLogger.client) {
void SentryLogger.client.close();
SentryLogger.client = undefined;
SentryLogger.scope = undefined;
}
}
}
// Export utility functions for using Sentry throughout the application
/**
* Create a custom span for performance monitoring
*
* @param operation The operation category (e.g., "http.client", "ui.click", "db.query")
* @param name A descriptive name for the span
* @param callback The function to execute within the span
* @returns The result of the callback function
*/
export function createSpan<T>(
operation: string,
name: string,
callback: () => T | Promise<T>,
): T | Promise<T> {
const client = SentryLogger.lazyClient;
if (!client) {
return callback();
}
// Use withScope from Sentry to isolate the span context
return Sentry.withScope((isolatedScope: Sentry.Scope) => {
isolatedScope.setClient(client);
return Sentry.startSpan(
{
op: operation,
name,
},
() => callback(),
);
});
}
/**
* Capture an exception and send it to Sentry
*
* @param error The error to capture
* @param context Additional context information
*/
export function captureException(
error: Error,
context?: Record<string, unknown>,
) {
const scope = SentryLogger.lazyScope;
if (!scope) {
return;
}
try {
// Add context to scope if provided
if (context) {
scope.setExtras(context);
}
if (error instanceof InitStepError) {
error.message = error.message + ` (${error.details})`;
}
logger.debug(`Capturing exception to Sentry: ${error.message}`);
// Use scope's captureException to avoid global state
scope.captureException(error);
} catch (e) {
logger.error(`Failed to capture exception to Sentry: ${e}`);
}
}
/**
* Capture a structured log message and send it to Sentry
*
* @param message The log message
* @param level The severity level (default: 'info')
* @param context Additional context information
*/
export function captureLog(
message: string,
level: Sentry.SeverityLevel = "info",
context?: Extras,
) {
const scope = SentryLogger.lazyScope;
if (!scope) {
return;
}
try {
// Add context to scope if provided
if (context) {
scope.setExtras(context);
}
// Use scope's captureMessage to avoid global state
scope.captureMessage(message, level);
} catch (e) {
logger.error(`Failed to capture log to Sentry: ${e}`);
}
}

View file

@ -0,0 +1,131 @@
/**
* Minimalist Sentry anonymization utilities
*/
/**
* Anonymize file paths - keep package names, remove user paths
*/
export function anonymizeFilePath(filePath: string): string {
if (!filePath) {
return filePath;
}
const normalized = filePath.replace(/\\/g, "/");
// Keep node_modules package names for debugging
if (normalized.includes("node_modules")) {
const match = normalized.match(/node_modules\/([^\/]+)/);
if (match) {
return `node_modules/${match[1]}/<file>`;
}
}
// Replace absolute paths with generic identifier
if (normalized.startsWith("/") || normalized.match(/^[A-Za-z]:/)) {
return "<file>";
}
return normalized;
}
/**
* Clean stack trace frames - remove sensitive data but keep the event
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function anonymizeStackTrace(frames: any[]): any[] {
if (!Array.isArray(frames)) {
return frames;
}
return frames.map((frame) => ({
...frame,
filename: frame.filename
? anonymizeFilePath(frame.filename)
: frame.filename,
abs_path: "",
// Remove local variables and source code context
vars: undefined,
pre_context: undefined,
post_context: undefined,
context_line: frame.context_line ? "<code>" : frame.context_line,
}));
}
/**
* Anonymize user information - hash ID, remove PII
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function anonymizeUserInfo(user: any): any {
if (!user) {
return user;
}
return {
id: user.id,
username: undefined,
email: undefined,
ip_address: undefined,
};
}
/**
* Main anonymization function - minimalist approach like Rasa
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function anonymizeSentryEvent(event: any): any | null {
try {
// Deep copy to avoid mutating the original event
const anonymized = structuredClone(event);
// Clean exception stack traces
if (anonymized.exception?.values) {
anonymized.exception.values = anonymized.exception.values.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(exception: any) => ({
...exception,
stacktrace: exception.stacktrace
? {
...exception.stacktrace,
frames: exception.stacktrace.frames
? anonymizeStackTrace(exception.stacktrace.frames)
: exception.stacktrace.frames,
}
: exception.stacktrace,
}),
);
}
// Clean thread stack traces
if (anonymized.threads?.values) {
anonymized.threads.values = anonymized.threads.values.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(thread: any) => ({
...thread,
stacktrace: thread.stacktrace
? {
...thread.stacktrace,
frames: thread.stacktrace.frames
? anonymizeStackTrace(thread.stacktrace.frames)
: thread.stacktrace.frames,
}
: thread.stacktrace,
}),
);
}
// Anonymize user info
if (anonymized.user) {
anonymized.user = anonymizeUserInfo(anonymized.user);
}
// Remove OS environment variables
if (anonymized.contexts?.os?.environment) {
anonymized.contexts.os.environment = undefined;
}
return anonymized;
} catch (error) {
console.error("Error anonymizing Sentry event:", error);
return null; // Drop event if anonymization fails
}
}

View file

@ -0,0 +1,4 @@
export type IdeInfo = {
name: string;
extensionVersion: string;
};

View file

@ -1,4 +1,4 @@
import type { LogEntry } from "@codeflash/types";
import type { LogEntry, LSPCodeMessage } from "@codeflash/types";
import * as vscode from "vscode";
import { GlobalStateKey, type GlobalState } from "../globalState";
import { isTaskRunning } from "@codeflash/shared";
@ -8,6 +8,7 @@ export class LogsEventEmitter extends vscode.EventEmitter<LogEntry> {
logger = new Logger("LogsEventEmitter");
private currentTaskId: string | null = null;
private currentBestCandidateCode: string | null = null;
private logsBuffer: Map<string, LogEntry[]> = new Map();
constructor(private readonly globalState: GlobalState) {
@ -15,18 +16,30 @@ export class LogsEventEmitter extends vscode.EventEmitter<LogEntry> {
}
setCurrrentTaskId(taskId: string) {
if (!!this.currentTaskId && this.currentTaskId !== taskId) {
// current task changed, flush previous task logs
this.flush();
}
this.logger.info(`Setting current task id to ${taskId}`);
this.currentTaskId = taskId;
}
public addSingleBufferedLogEntry(logEntry: LogEntry) {
if (!this.currentTaskId) {
return;
}
const currentTaskLogs = this.logsBuffer.get(this.currentTaskId) || [];
this.logsBuffer.set(this.currentTaskId, [...currentTaskLogs, logEntry]);
getCurrentTaskId() {
return this.currentTaskId;
}
public flush() {
public addSingleBufferedLogEntry(logEntry: LogEntry): LogEntry[] | undefined {
if (!this.currentTaskId) {
return undefined;
}
const currentTaskLogs = this.logsBuffer.get(this.currentTaskId) || [];
const newLogs = [...currentTaskLogs, logEntry];
this.logsBuffer.set(this.currentTaskId, newLogs);
return newLogs;
}
public flush(resetTaskId: boolean = true) {
this.logger.info(`Flushing logs for task ${this.currentTaskId}`);
if (!this.currentTaskId) {
return;
}
@ -46,39 +59,62 @@ export class LogsEventEmitter extends vscode.EventEmitter<LogEntry> {
`No task found with the id ${this.currentTaskId}, skipping flushing ${currentLogs.length} buffered log entries.`,
);
this.logsBuffer.delete(this.currentTaskId);
this.logger.info(
`Cleared buffered log entries for unknown task ${this.currentTaskId}. setting to null.`,
);
this.currentTaskId = null;
return;
}
const targetTask = currentTasks[targetTaskIdx];
targetTask.logs.push(...currentLogs);
if (this.currentBestCandidateCode) {
targetTask.bestCandidateCode = this.currentBestCandidateCode;
this.currentBestCandidateCode = null;
}
currentTasks[targetTaskIdx] = targetTask;
this.globalState.set(GlobalStateKey.QueueTasks, currentTasks);
this.logger.debug(
`Flushed ${currentLogs.length} buffered log entries to task ${targetTask.id}.`,
);
// reset
this.logsBuffer.delete(this.currentTaskId);
this.currentTaskId = null;
// reset
if (resetTaskId) {
this.currentTaskId = null;
}
}
private handleMessageId = (logEntry: LogEntry) => {
const messageId = logEntry.message_id;
if (!messageId) {
return;
}
if (messageId === "best_candidate") {
this.currentBestCandidateCode = (logEntry as LSPCodeMessage).code;
}
};
private logMssageHandler = (message: LogEntry) => {
const currentRunningTask = this.globalState
.get(GlobalStateKey.QueueTasks, [])!
.find((t) => t.id === this.currentTaskId);
if (!currentRunningTask || !isTaskRunning(currentRunningTask)) {
return;
}
if (message.task_id !== this.currentTaskId) {
this.logger.warn(
`Received log entry for task ${message.task_id} while current task is ${this.currentTaskId}, ignoring log entry.`,
);
return;
}
this.handleMessageId(message);
this.addSingleBufferedLogEntry(message);
};
public registerLogsListener(): vscode.Disposable {
const optimizationEventListener = this.event((message) => {
const currentRunningTask = this.globalState
.get(GlobalStateKey.QueueTasks, [])!
.find(isTaskRunning);
if (!currentRunningTask) {
return;
}
if (!isTaskRunning(currentRunningTask)) {
this.logger.debug(
`Task ${currentRunningTask.id} is not running, skipping log entry with message: ${message}.`,
);
return;
}
this.currentTaskId = currentRunningTask.id;
this.addSingleBufferedLogEntry(message);
});
const optimizationEventListener = this.event(
this.logMssageHandler.bind(this),
);
return optimizationEventListener;
}
}

View file

@ -1,6 +1,7 @@
import { Readable } from "stream";
import split2 from "split2";
import type * as vscode from "vscode";
import type { LogEntry } from "@codeflash/types";
import { LSPMessageType } from "@codeflash/types";
import { Logger } from "./logger";
import { randomUUID } from "crypto";
@ -11,7 +12,7 @@ const delimiter = "\u241F";
export function createInterceptingOutputChannel(
innerChannel: vscode.OutputChannel,
optimizationEventEmitter?: LogsEventEmitter,
): vscode.OutputChannel {
): vscode.OutputChannel & { lastTracebackMessage: string | null } {
class WrappedChannel implements vscode.OutputChannel {
constructor(
private readonly inner: vscode.OutputChannel,
@ -33,15 +34,14 @@ export function createInterceptingOutputChannel(
const parsed = JSON.parse(chunk.trim());
if (Object.values(LSPMessageType).includes(parsed?.type)) {
const uuid = randomUUID();
const message = parsed as LogEntry;
this.optimizationEventEmitter?.fire({
...parsed,
...message,
id: uuid,
});
}
} catch (err) {
this.logger.info(
"Failed to parse LSP message " + chunk + " with error " + err,
);
} catch {
// ignore JSON parse errors
}
});
}
@ -50,16 +50,37 @@ export function createInterceptingOutputChannel(
private logger: Logger;
private input: Readable;
public lastTracebackMessage: string | null = null;
append(value: string): void {
this.storeLastTracebackMessage(value);
this.logger.debug("LSP Message: " + value);
this.input.push(value);
}
appendLine(value: string): void {
this.storeLastTracebackMessage(value);
this.logger.debug("LSP Message: " + value);
this.input.push(value);
}
// private storeLastTracebackMessage = (message: string) => {
// if (message.trim().startsWith("Traceback (most recent call last):")) {
// this.lastTracebackMessage = message;
// }
// };
private storeLastTracebackMessage(message: string) {
const lines = message.trim().split("\n");
// If it looks like the final exception line, stop collecting
for (const line of lines) {
if (/^[A-Za-z]+Error:/.test(line)) {
this.lastTracebackMessage = line;
break;
}
}
}
clear() {}
show() {}
hide() {}

View file

@ -0,0 +1,7 @@
import * as vscode from "vscode";
import { CODEFLASH_EXTENSION_ID } from "../constants";
export const getExtensionVersion = (): string => {
const extension = vscode.extensions.getExtension(CODEFLASH_EXTENSION_ID);
return extension?.packageJSON.version || "0.1.0";
};

View file

@ -14,6 +14,7 @@ export const APPROVAL_CONFIG = {
Dashverse: true,
pydantic: true,
"Skyvern-AI": true,
// "aseembits93/my-best-repo": true,
stackai: true,
"Unstructured-IO": true,
// Example: "organization/specific-repo": true,
@ -26,6 +27,7 @@ export const APPROVAL_CONFIG = {
// Add repositories here that need quality monitoring
// Example: "organization": true,
// Example: "organization/specific-repo": true,
// "aseembits93/my-best-repo": true,
} as Record<string, boolean>,
} as const

View file

@ -15,6 +15,7 @@ import {
buildResultFooter,
buildResultHeader,
buildResultTestReport,
generateOptimizationReviewTemplate,
} from "../github/pr-changes-utils.js"
import { githubApp } from "../github/github-app.js"
import {
@ -62,6 +63,7 @@ export function createStandalonePRTitleAndBody(
builder: PrContentBuilder,
replayTests: string = "",
concolicTests: string = "",
optimizationReview: string = "",
): { title: string; body: string } {
const prCommentHeader = builder.buildResultHeader(prCommentFields)
@ -87,9 +89,13 @@ export function createStandalonePRTitleAndBody(
)
const metadata = buildOptimizationMetadata(prCommentFields)
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
if (optReviewBadge) {
optReviewBadge = ` ${optReviewBadge}\n`
}
const body: string = benchmarkInfo
? `${metadata}\n${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
: `${metadata}\n${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
? `${metadata}\n${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}${optReviewBadge}`
: `${metadata}\n${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}${optReviewBadge}`
return { title, body }
}
@ -512,6 +518,7 @@ export async function triggerCreatePr(
triggerCreatePrDeps.prContentBuilder,
replayTests,
concolicTests,
optimizationReview,
)
console.log(`[triggerCreatePr] Creating PR with title: ${title}`)

View file

@ -98,7 +98,7 @@ export default async function createStagingReview(req: AuthorizedUserReq, res: R
try {
// Fetch existing metadata for the given traceId
const existingEvent = await dependencies.prisma.optimization_events.findFirst({
where: { trace_id: traceId },
where: { trace_id: traceId, user_id: req.userId },
select: { metadata: true },
})

View file

@ -3,7 +3,11 @@ import { determineValidHunks, fileDiffsToMap, isDiffContentsWellFormed } from ".
import { userNickname } from "../auth0-mgmt.js"
import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js"
import { githubApp } from "../github/github-app.js"
import { buildDependentPrTitle, buildPrCommentBody } from "../github/pr-changes-utils.js"
import {
buildDependentPrTitle,
buildPrCommentBody,
generateOptimizationReviewTemplate,
} from "../github/pr-changes-utils.js"
import {
createDependentPullRequest,
createNewBranchFromDiffContents,
@ -228,6 +232,7 @@ export async function suggestPrChanges(
generatedTests,
coverage_message,
userId,
optimizationReview,
}
await dependencies.requestApproval(
@ -286,6 +291,7 @@ export async function suggestPrChanges(
userId,
replayTests,
concolicTests,
optimizationReview,
}
// Send quality monitoring notification (non-blocking)
@ -497,10 +503,12 @@ export async function triggerSuggestPrChanges(
newBranchName,
replayTests,
concolicTests,
optimizationReview,
{ isUnifiedReview: true, includeHeader: false, isCollapsed: true },
)
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
if (optReviewBadge) {
optReviewBadge = `\n\n${optReviewBadge}\n`
}
let reviewComments = []
let foundInvalidHunk = false
@ -520,9 +528,18 @@ export async function triggerSuggestPrChanges(
"```suggestion\n" +
newContent +
"\n```\n" +
"</details>"
"</details>" +
"\n" +
optReviewBadge
} else {
commentBody = prCommentBody + "\n\n" + "```suggestion\n" + newContent + "\n```"
commentBody =
prCommentBody +
"\n\n" +
"```suggestion\n" +
newContent +
"\n```" +
"\n" +
optReviewBadge
}
reviewComments.push({

View file

@ -45,7 +45,7 @@ describe("createPr", () => {
traceId: "test-trace-id",
replayTests: "replay tests",
concolicTests: "concolic tests",
optimizationReview: "optimization_review",
optimizationReview: "medium",
},
userId: "test-user-id",
}
@ -154,7 +154,7 @@ describe("createPr", () => {
"replay tests",
"concolic tests",
"", // traceId should be empty string
"optimization_review",
"medium",
)
})
})
@ -401,7 +401,7 @@ describe("createPr", () => {
"replay tests",
"concolic tests",
"test-trace-id",
"optimization_review",
"medium",
)
expect(mockRes.json).toHaveBeenCalledWith(456)
})
@ -601,6 +601,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"trace123",
"medium",
)
expect(result).toBe(456)
@ -624,6 +625,7 @@ describe("triggerCreatePr", () => {
mockDeps.prContentBuilder,
"replay tests",
"concolic tests",
"medium",
)
expect(mockDeps.createStandalonePullRequest).toHaveBeenCalledWith(
mockInstallationOctokit,
@ -694,6 +696,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"", // empty traceId
"medium",
)
expect(result).toBe(456)
@ -784,6 +787,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"trace 123",
"medium",
)
expect(result).toBe(456)
@ -836,6 +840,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"trace123",
"medium",
)
expect(result).toBe(456)
@ -881,6 +886,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"trace123",
"medium",
)
expect(result).toBe(456)
@ -921,6 +927,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"trace123",
"medium",
)
expect(result).toBe(456)
@ -959,6 +966,7 @@ describe("triggerCreatePr", () => {
"replay tests",
"concolic tests",
"trace123",
"medium",
)
expect(result).toBe(456)
@ -988,6 +996,8 @@ describe("triggerCreatePr", () => {
mockInstallationOctokit,
"replay tests",
"concolic tests",
"",
"medium",
)
expect(result).toBe(-1)
@ -1027,6 +1037,8 @@ describe("triggerCreatePr", () => {
mockInstallationOctokit,
"replay tests",
"concolic tests",
"",
"medium",
)
expect(result).toBe(-1)
@ -1067,6 +1079,8 @@ describe("triggerCreatePr", () => {
mockInstallationOctokit,
"replay tests",
"concolic tests",
"",
"medium",
)
expect(result).toBe(-1)
@ -1120,11 +1134,12 @@ describe("createStandalonePRTitleAndBody", () => {
mockBuilder,
"replay tests",
"concolic tests",
"medium",
)
expect(result.title).toBe("Test PR Title")
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter',
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
expect(mockBuilder.buildPrTitle).toHaveBeenCalledWith("testFunc", 50, 2)
expect(mockBuilder.buildResultHeader).toHaveBeenCalledWith(prCommentFields)
@ -1170,11 +1185,12 @@ describe("createStandalonePRTitleAndBody", () => {
mockBuilder,
"replay tests",
"concolic tests",
"medium",
)
expect(result.title).toBe("Test PR Title")
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nBenchmark data\nDetails\nTest report\nFooter',
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nBenchmark data\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
expect(mockBuilder.buildBenchmarkInfo).toHaveBeenCalledWith(prCommentFields)
})
@ -1197,10 +1213,11 @@ describe("createStandalonePRTitleAndBody", () => {
mockBuilder,
"replay tests",
"concolic tests",
"medium",
)
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter',
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
// buildBenchmarkInfo should NOT be called when benchmark_details is empty array
expect(mockBuilder.buildBenchmarkInfo).not.toHaveBeenCalled()
@ -1224,10 +1241,11 @@ describe("createStandalonePRTitleAndBody", () => {
mockBuilder,
"replay tests",
"concolic tests",
"medium",
)
expect(result.body).toBe(
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter',
'<!-- CODEFLASH_OPTIMIZATION: {"function":"testFunc","speedup_pct":50,"speedup_x":2,"optimization_type":"general","timestamp":"2025-01-01T00:00:00.000Z","version":"1.0"} -->\n## Header\nDetails\nTest report\nFooter ![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-medium-blue)\n\n',
)
// buildBenchmarkInfo should NOT be called when benchmark_details is null
expect(mockBuilder.buildBenchmarkInfo).not.toHaveBeenCalled()

View file

@ -605,7 +605,7 @@ describe("Suggest PR Changes", () => {
line: 5,
start_line: 1,
side: "RIGHT",
body: "Test PR comment body\n\n```suggestion\nnew code line 1\nnew code line 2\n```",
body: "Test PR comment body\n\n```suggestion\nnew code line 1\nnew code line 2\n```\n\n\n![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-low-yellow)\n\n",
},
],
})

View file

@ -20,6 +20,7 @@ export async function getInstallationId(
return installation.data.id
}
//TODO:Remove Org creation after create script
export async function registerRepositoryAndMember(
owner: string,
repo: string,
@ -32,13 +33,14 @@ export async function registerRepositoryAndMember(
where: { full_name: fullName },
})
let validInstallationId = 0
if (!existingRepo) {
const installation = await installationOctokit.rest.apps.getRepoInstallation({
owner,
repo,
})
validInstallationId = installation.data.id
validInstallationId = installation.data.id
// Check if installation exists in DB
const installationExists = await getAppInstallationByInstalltionId(validInstallationId)
if (!installationExists) {
@ -58,6 +60,10 @@ export async function registerRepositoryAndMember(
repo,
})
const { data: githubRepo } = githubRepoResponse
const accountType = githubRepo.owner.type
const isOrg = accountType === "Organization"
const repository = await upsertRepository({
github_repo_id: String(githubRepo.id),
installation_id: validInstallationId,
@ -65,14 +71,16 @@ export async function registerRepositoryAndMember(
full_name: githubRepo.full_name,
is_private: githubRepo.private,
})
const userRole = await getUserRole({
octokit: installationOctokit,
owner: githubRepo.owner.login,
repo: githubRepo.name,
username: nickname,
isOrg: githubRepo.owner.type === "Organization",
isOrg,
})
console.log("User role fetched")
await createRepositoryMember({
repository_id: repository.id,
user_id: userId,

View file

@ -6,6 +6,7 @@ import {
buildDependentPrTitle,
buildPrTitle,
buildResultFooter,
generateOptimizationReviewTemplate,
originalPRComment,
} from "./pr-changes-utils.js"
import type { RestEndpointMethodTypes } from "@octokit/rest"
@ -305,6 +306,7 @@ export async function createDependentPullRequest(
baseBranch,
replayTests,
concolicTests,
optimizationReview,
)
const newPrData = await createNewPullRequest(
@ -334,6 +336,7 @@ export async function createDependentPullRequest(
prCommentFields,
newPrData.data.number,
baseBranch,
optimizationReview,
)
await installationOctokit.rest.issues.createComment({
owner,
@ -355,6 +358,7 @@ function createDependentPRTitleAndBody(
baseBranch: string,
replayTests: string = "",
concolicTests: string = "",
optimizationReview: string = "",
): { title: string; body: string } {
const prCommentHeader = dependencies.buildResultHeader(prCommentFields)
// Build benchmark info if available
@ -384,11 +388,14 @@ function createDependentPRTitleAndBody(
`## ⚡️ This pull request contains optimizations for PR #${origPrNumber}
If you approve this dependent PR, these changes will be merged into the original PR branch \`${baseBranch}\`.
>This PR will be automatically closed if the original PR is merged.\n` + `----\n`
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
if (optReviewBadge) {
optReviewBadge = ` ${optReviewBadge}\n`
}
// Conditionally build the body based on whether benchmark info exists
const body = benchmarkInfo
? `${introSection}${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
: `${introSection}${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}`
? `${introSection}${prCommentHeader}\n${benchmarkInfo}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}${optReviewBadge}`
: `${introSection}${prCommentHeader}\n${prCommentBody}\n${prCommentTestReport}\n${prCommentFooter}${optReviewBadge}`
return { title, body }
}

View file

@ -11,10 +11,12 @@ import * as Sentry from "@sentry/node"
import {
createAppInstallation,
createOrUpdateUser,
createRepositoryMember,
getAppInstallationByInstalltionId,
organizationMemberRepository,
organizationRepository,
prisma,
upsertRepository,
upsertRepositoryMember,
} from "@codeflash-ai/common"
import { getUserRole } from "./github-utils.js"
@ -263,6 +265,7 @@ export const githubApp = await (async () => {
})
app.webhooks.on("installation.created", async ({ octokit, payload }) => {
// TODO: check if it's organization
console.log(`Received a installation.created event: ${JSON.stringify(payload)}`)
})
@ -416,14 +419,6 @@ export const githubApp = await (async () => {
// Process each repository in the list of added repositories
for (const repo of repositories_added) {
try {
// Upsert logic for repository creation or update
const savedRepo = await upsertRepository({
github_repo_id: String(repo.id),
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
})
// Get the GitHub user ID of the sender
const githubUserId = sender?.id
@ -446,12 +441,48 @@ export const githubApp = await (async () => {
sender.email ?? null,
sender.name ?? null,
)
await createRepositoryMember({
let orgId: string = undefined
if (accountType === "Organization") {
const ghOrgId = String(account.id)
const existingOrg = await prisma.organizations.findUnique({
where: { github_org_id: ghOrgId },
})
orgId = existingOrg?.id
if (!existingOrg) {
const organization = await organizationRepository.upsertOrganization({
github_org_id: ghOrgId,
name: account.name || accountLogin,
added_by: user.user_id,
})
await organizationMemberRepository.addMember({
organizationId: organization.id,
userId: user.user_id,
role: "admin",
addedBy: user.user_id, // Indicates that this user was the first to be added . If user_id equals addedBy, it means this user installed GitHub App for this repository.
})
console.log(`Organization upserted: ${accountLogin}`)
orgId = organization.id
}
}
const savedRepo = await upsertRepository({
github_repo_id: String(repo.id),
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
organization_id: orgId,
})
console.log(`Repository upserted: ${savedRepo.full_name}`)
await upsertRepositoryMember({
repository_id: savedRepo.id,
user_id: user.user_id,
role: userRole,
})
console.log(`Repository upserted: ${repo.full_name}`)
} else {
console.error("GitHub User ID not found in sender.")
}

View file

@ -1,6 +1,14 @@
import { type App, type Octokit } from "octokit"
import * as Sentry from "@sentry/node"
import {
createAppInstallation,
createOrUpdateUser,
getAppInstallationByInstalltionId,
organizationMemberRepository,
organizationRepository,
prisma,
upsertRepository,
} from "@codeflash-ai/common"
// Dependencies interface for easier testing
export interface GithubUtilsDependencies {
console: {
@ -136,7 +144,6 @@ async function fetchOrgRole(octokit: Octokit, owner: string, username: string):
org: owner,
username,
})
console.log(`Org role for ${username} in ${owner}: ${membership.role}`)
return membership.role || ""
} catch (error) {
console.error(`Error fetching org role for ${username} in ${owner}:`, error)
@ -197,3 +204,164 @@ export async function getUserRole({
return await fetchRepoRole(octokit, owner, repo, username)
}
}
export async function getInstalledOrgsWithMembers(app: App, orgNames?: string[]) {
const result: {
organization: { id: number; name: string; github_org_id: string }
repos: any[]
members: { id: number; username: string; role: string }[]
}[] = []
try {
// Fetch all app installations (orgs)
let installations: any[] = []
let page = 1
console.log("fetcting installion....")
while (true) {
const response = await app.octokit.rest.apps.listInstallations({
per_page: 90, // max is 100
page,
})
if (!response.data.length) break
installations.push(...response.data)
page++
}
console.log("Done")
let orgInstallations: typeof installations = []
for (const installation of installations) {
const login = installation.account?.login
if (installation.account?.type === "Organization" && login) {
orgInstallations.push(installation)
}
}
// If orgNames provided, filter to only those
if (orgNames && orgNames.length) {
orgInstallations = orgInstallations.filter(install =>
orgNames.includes(install.account!.login),
)
}
for (const installation of orgInstallations) {
const login = installation.account!.login
let repos: any[] = []
let members: { id: number; username: string; role: string }[] = []
console.log("fetch repos for " + login)
try {
const installationOctokit = await app.getInstallationOctokit(installation.id)
const accountLogin = installation.account!.login
const accountType = installation.account!.type
// Check if the installation exists, if not, create it
const installationExists = await getAppInstallationByInstalltionId(installation.id)
if (!installationExists) {
await createAppInstallation({
installation_id: installation.id,
account_id: installation.account!.id,
account_login: accountLogin,
account_type: accountType,
})
console.log(`Installation created for ID: ${installation.id}`)
}
// --- Fetch all repos with pagination ---
page = 1
while (true) {
const reposResponse =
await installationOctokit.rest.apps.listReposAccessibleToInstallation({
per_page: 100,
page,
})
if (!reposResponse.data.repositories.length) break
repos.push(...reposResponse.data.repositories)
page++
}
console.log("Done... ")
console.log("fetch members for " + login)
// --- Fetch all members with pagination ---
page = 1
const memberData: { id: number; login: string }[] = []
while (true) {
const membersResponse = await installationOctokit.rest.orgs.listMembers({
org: login,
per_page: 100,
page,
})
if (!membersResponse.data.length) break
memberData.push(
...membersResponse.data.map(member => ({ id: member.id, login: member.login })),
)
page++
}
// Get roles for all members
for (const member of memberData) {
const role = await fetchOrgRole(installationOctokit, login, member.login)
members.push({ id: member.id, username: member.login, role })
}
console.log("Done... ")
// Upsert organization
const organization = await organizationRepository.upsertOrganization({
github_org_id: String(installation.account!.id),
name: login,
added_by: null,
})
// Process each member: create or update user and add to organization
for (const member of members) {
await createOrUpdateUser(`github|${member.id}`, member.username, null, null)
try {
// Check if member already exists in organization_members
const existingMember = await prisma.organization_members.findUnique({
where: {
organization_id_user_id: {
organization_id: organization.id,
user_id: `github|${member.id}`,
},
},
})
if (!existingMember) {
await organizationMemberRepository.addMember({
organizationId: organization.id,
userId: `github|${member.id}`,
role: member.role,
})
}
} catch (error) {
// If error is due to unique constraint, skip
if (error instanceof Error && error.message.includes("Unique constraint failed")) {
// skip
} else {
throw error
}
}
}
// Process each repo
for (const repo of repos) {
// Upsert repository
await upsertRepository({
github_repo_id: String(repo.id),
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
organization_id: organization.id,
})
}
} catch (error) {
dependencies.console.error(`Error fetching data for org ${login}:`, error)
Sentry.captureException(error)
continue
}
}
} catch (error) {
dependencies.console.error("Error fetching app installations:", error)
Sentry.captureException(error)
}
}

View file

@ -111,6 +111,14 @@ export async function sendQualityMonitoringNotification(
fields: [
{ type: "mrkdwn", text: `*User ID:*\n👤 ${userId}` },
{ type: "mrkdwn", text: `*Trace ID:*\n🏷 \`${traceId}\`` },
...(requestData.optimizationReview
? [
{
type: "mrkdwn",
text: `*Optimization Review Rating:*\n🎯 \`${requestData.optimizationReview}\``,
},
]
: []),
],
},
]
@ -261,6 +269,14 @@ export async function requestApproval(
fields: [
{ type: "mrkdwn", text: `*User ID:*\n👤 ${userId}` },
{ type: "mrkdwn", text: `*Trace ID:*\n🏷 \`${traceId}\`` }, // Changed emoji
...(requestData.optimizationReview
? [
{
type: "mrkdwn",
text: `*Optimization Review Rating:*\n🎯 \`${requestData.optimizationReview}\``,
},
]
: []),
],
},
]
@ -506,6 +522,7 @@ export async function processReaction(event: any): Promise<boolean> {
requestData.replayTests,
requestData.concolicTests,
optimization.trace_id,
requestData.optimizationReview,
)
} else if (requestData.type === "suggest-pr-changes") {
const { triggerSuggestPrChanges } = await import("../endpoints/suggest-pr-changes.js")
@ -545,6 +562,7 @@ export async function processReaction(event: any): Promise<boolean> {
requestData.replayTests,
requestData.concolicTests,
optimization.trace_id,
requestData.optimizationReview,
)
}
} catch (err: any) {

View file

@ -62,7 +62,6 @@ describe("buildPrCommentBody (Integration)", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
OPTIMIZATION_IMPACT,
)
// Test that all key sections are present
@ -74,9 +73,6 @@ describe("buildPrCommentBody (Integration)", () => {
expect(result).toContain("This is a test explanation")
expect(result).toContain("Correctness verification report")
expect(result).toContain("git merge codeflash/optimize-pr144-2025-02-15T00.30.35")
expect(result).toContain(
"![Static Badge](https://img.shields.io/badge/🎯_Optimization_Quality-low-yellow)",
)
// Test that template substitution worked
expect(result).not.toContain("{function_name}")
@ -93,7 +89,6 @@ describe("buildPrCommentBody (Integration)", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
OPTIMIZATION_IMPACT,
{ isCollapsed: true },
)
@ -152,7 +147,6 @@ describe("parseAndCreateOptimizationsDict", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
OPTIMIZATION_IMPACT,
)
const result = parseAndCreateOptimizationsDict(PRBODY, [])
expect(result).toEqual({
@ -326,7 +320,6 @@ describe("buildPrCommentBody - Extended Coverage", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
OPTIMIZATION_IMPACT,
{ includeHeader: false },
)
expect(result).not.toContain("Codeflash found optimizations for this PR")
@ -341,7 +334,6 @@ describe("buildPrCommentBody - Extended Coverage", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
OPTIMIZATION_IMPACT,
{ isUnifiedReview: true },
)
// Should still contain the core content
@ -369,7 +361,6 @@ describe("buildPrCommentBody - Extended Coverage", () => {
NEW_BRANCH_NAME,
REPLAY_TESTS,
CONCOLIC_TESTS,
OPTIMIZATION_IMPACT,
)
expect(result).toContain("This change will improve the performance")
})

View file

@ -154,7 +154,6 @@ export function buildPrCommentBody(
newBranchName: string,
replayTests: any,
concolicTests: any,
optimizationReview: any,
options: {
isUnifiedReview?: boolean
includeHeader?: boolean
@ -171,7 +170,6 @@ export function buildPrCommentBody(
`${buildOptimizationMetadata(prCommentFields)}\n` +
(includeHeader ? `#### ⚡️ Codeflash found optimizations for this PR\n` : "") +
`${buildResultHeader(prCommentFields, isUnifiedReview)}\n` +
generateOptimizationReviewTemplate(optimizationReview) +
(benchmarkInfo ? `${benchmarkInfo}\n` : "") +
`${buildResultDetails(prCommentFields, isCollapsed)}\n` +
`${buildResultTestReport(prCommentFields, existingTests, generatedTests, coverage_message, replayTests, concolicTests)}\n` +
@ -354,9 +352,8 @@ export function buildResultTestReport(
reportTableMd += concolicTests.trim()
reportTableMd += "\n"
} else if (testType.includes("Generated")) {
reportTableMd += "```python\n"
reportTableMd += generatedTests.trim()
reportTableMd += "\n```\n"
reportTableMd += "\n"
} else {
reportTableMd += "_No additional details available._\n"
}
@ -481,13 +478,22 @@ export function originalPRComment(
prCommentFields: PrCommentFields,
newPrNumber: string,
baseBranch: string,
optimizationReview: string,
): string {
const prCommentHeader = buildResultHeader(prCommentFields)
return `#### ⚡️ Codeflash found optimizations for this PR
let optReviewBadge = generateOptimizationReviewTemplate(optimizationReview)
if (optReviewBadge) {
optReviewBadge = `\n\n${optReviewBadge}\n`
}
return (
`
#### Codeflash found optimizations for this PR
${prCommentHeader}
#### A dependent PR with the suggested changes has been created. Please review:
- ### #${newPrNumber}
If you approve, it will be merged into this PR (branch \`${baseBranch}\`).`
If you approve, it will be merged into this PR (branch \`${baseBranch}\`).
` + optReviewBadge
)
}

View file

@ -381,7 +381,7 @@ describe("Create PR from Diff Contents", () => {
"main",
mockPrCommentFields,
"existing tests",
"generated tests",
'```python:myfile.py\nprint("Hello, World!")\n```\n\n```python:myfile.py\nprint("Hello, World!")\n```',
"coverage message",
"replay tests",
"concolic tests",
@ -392,7 +392,7 @@ describe("Create PR from Diff Contents", () => {
expect(mockDependencies.buildResultTestReport).toHaveBeenCalledWith(
mockPrCommentFields,
"existing tests",
"generated tests",
'```python:myfile.py\nprint("Hello, World!")\n```\n\n```python:myfile.py\nprint("Hello, World!")\n```',
"coverage message",
"replay tests",
"concolic tests",
@ -493,6 +493,7 @@ describe("Create PR from Diff Contents", () => {
"coverage message",
"replay tests",
"concolic tests",
"medium",
)
expect(mockDependencies.buildDependentPrTitle).toHaveBeenCalledWith(
@ -536,6 +537,7 @@ describe("Create PR from Diff Contents", () => {
mockPrCommentFields,
456,
"pr-789-branch",
"medium",
)
expect(result.data.id).toBe(123)

View file

@ -13,7 +13,7 @@
"@azure/keyvault-keys": "^4.7.2",
"@azure/keyvault-secrets": "^4.7.0",
"@codeflash-ai/code-suggester": "^5.0.3",
"@codeflash-ai/common": "^1.0.19",
"@codeflash-ai/common": "^1.0.22",
"@octokit/app": "^16.0.1",
"@octokit/auth-app": "^8.0.1",
"@octokit/core": "^7.0.2",
@ -1077,9 +1077,9 @@
"license": "ISC"
},
"node_modules/@codeflash-ai/common": {
"version": "1.0.19",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.19/96401c69690a9bb07e6410523a097c487967f103",
"integrity": "sha512-U06CU+Ds7c1IT18EzCl1VK0DFzOxuye5fidLqgW2oVyE9syhEOoCaW0i4MY93ycIelYAX/TWQk6neqct/Jjmkg==",
"version": "1.0.22",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.22/26f248d8c6ced1d2b0bfbc9382db9313e2f1dc7d",
"integrity": "sha512-DgKkA+T1Uu/bnK+r2x7xMnJQE2HoyL/uDROuNSRIjnzdj0mZoAA0t5CRUYESwPlu+UBNLM+5St6spy5v/cqriA==",
"dependencies": {
"@azure/identity": "^4.2.0",
"@azure/keyvault-secrets": "^4.8.0",

View file

@ -28,7 +28,7 @@
"@azure/keyvault-keys": "^4.7.2",
"@azure/keyvault-secrets": "^4.7.0",
"@codeflash-ai/code-suggester": "^5.0.3",
"@codeflash-ai/common": "^1.0.19",
"@codeflash-ai/common": "^1.0.22",
"@octokit/app": "^16.0.1",
"@octokit/auth-app": "^8.0.1",
"@octokit/core": "^7.0.2",

View file

@ -0,0 +1,17 @@
import { getInstalledOrgsWithMembers } from "../github/github-utils.js"
import { githubApp } from "../github/github-app.js"
;(async () => {
try {
const orgs = process.argv.slice(2)
if (orgs.length === 0) {
console.error(
"Error: Please provide at least one organization name as a command line argument.",
)
process.exit(1)
}
await getInstalledOrgsWithMembers(githubApp, orgs)
console.log("✅ DONE")
} catch (error) {
console.error(error)
}
})()

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@
"dependencies": {
"@auth0/nextjs-auth0": "^3.3.0",
"@azure/msal-node": "^3.7.3",
"@codeflash-ai/common": "^1.0.19",
"@codeflash-ai/common": "^1.0.22",
"@hookform/resolvers": "^3.3.2",
"@monaco-editor/react": "^4.7.0",
"@prisma/client": "^6.7.0",
@ -40,6 +40,7 @@
"@types/pg": "^8.10.9",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-syntax-highlighter": "^15.5.13",
"chart.js": "^4.4.9",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
@ -47,10 +48,13 @@
"diff": "^8.0.2",
"framer-motion": "^12.12.1",
"github-markdown-css": "^5.4.0",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.381.0",
"marked": "^16.1.1",
"next": "^14.2.32",
"next-themes": "^0.3.0",
"node-ts-cache": "^4.4.0",
"node-ts-cache-storage-memory": "^4.4.0",
"pg": "^8.11.3",
"postcss": "^8",
"posthog-js": "1.108.3",
@ -61,6 +65,7 @@
"react-hook-form": "^7.48.2",
"react-markdown": "^9.0.1",
"react-papaparse": "^4.4.0",
"react-syntax-highlighter": "^16.1.0",
"remark-gfm": "^4.0.0",
"sharp": "^0.34.2",
"sonner": "^2.0.6",
@ -71,6 +76,7 @@
},
"devDependencies": {
"@testing-library/react": "^16.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-react": "^4.3.1",

View file

@ -0,0 +1,266 @@
"use server"
import { generateTokenForVsCode } from "@/app/(dashboard)/apikeys/tokenfuncs"
import { getUserId } from "@/app/utils/auth"
import crypto from "crypto"
import jwt from "jsonwebtoken"
import { CacheContainer } from "node-ts-cache"
import { MemoryStorage } from "node-ts-cache-storage-memory"
const RATE_LIMIT = 5
const RATE_LIMIT_WINDOW_MS = 60 * 1000
const rateLimitCache = new CacheContainer(new MemoryStorage())
// TODO:: Find a way to save it in Session
const JWT_SECRET = process.env.JWT_SECRET || "abrakadabra-codeflash-jwt-secret"
if (!JWT_SECRET) {
throw new Error("JWT_SECRET is not defined in environment variables")
}
interface OAuthStatePayload {
userId: string
redirectUri: string
codeChallenge: string
codeChallengeMethod: string
clientId: string
type: "oauth_state"
}
interface AuthCodePayload {
userId: string
codeChallenge: string
codeChallengeMethod: string
redirectUri: string
clientId: string
type: "auth_code"
}
export async function isRateLimited(userId: string): Promise<boolean> {
const cacheKey = `rate_limit_vsc_signin_${userId}`
const record = await rateLimitCache.getItem<{ count: number; startTime: number }>(cacheKey)
const now = Date.now()
if (!record || now - record.startTime > RATE_LIMIT_WINDOW_MS) {
await rateLimitCache.setItem(
cacheKey,
{ count: 1, startTime: now },
{ ttl: RATE_LIMIT_WINDOW_MS / 1000 },
)
console.log(`Rate limit initialized for user: ${userId}`)
return false
}
if (record.count >= RATE_LIMIT) {
console.warn(`Rate limit exceeded for user: ${userId}, count: ${record.count}`)
return true
}
record.count++
await rateLimitCache.setItem(cacheKey, record, {
ttl: (RATE_LIMIT_WINDOW_MS - (now - record.startTime)) / 1000,
})
console.log(`Rate limit check passed for user: ${userId}, count: ${record.count}`)
return false
}
export async function createOAuthState(params: {
redirectUri: string
codeChallenge: string
codeChallengeMethod: string
clientId: string
}): Promise<{ state: string; error?: string }> {
console.log("=== Creating OAuth State (JWT) ===")
console.log("Params:", {
redirectUri: params.redirectUri,
codeChallenge: params.codeChallenge.substring(0, 10) + "...",
codeChallengeMethod: params.codeChallengeMethod,
clientId: params.clientId,
})
try {
const userId = await getUserId()
if (!userId) {
console.error("No user ID found - unauthorized")
return { state: "", error: "Unauthorized" }
}
console.log("User ID:", userId)
const limited = await isRateLimited(userId)
if (limited) {
console.error("Rate limit exceeded for user:", userId)
return { state: "", error: "Rate limit exceeded" }
}
const statePayload: OAuthStatePayload = {
userId,
redirectUri: params.redirectUri,
codeChallenge: params.codeChallenge,
codeChallengeMethod: params.codeChallengeMethod,
clientId: params.clientId,
type: "oauth_state",
}
const state = jwt.sign(statePayload, JWT_SECRET, {
expiresIn: "2m",
jwtid: crypto.randomBytes(16).toString("hex"),
})
console.log("OAuth state JWT created successfully")
return { state }
} catch (error) {
console.error("Error creating OAuth state:", error)
return { state: "", error: "Internal server error" }
}
}
export async function authorizeOAuth(state: string): Promise<{
code?: string
redirectUri?: string
error?: string
}> {
console.log("=== Authorizing OAuth (JWT) ===")
try {
const userId = await getUserId()
if (!userId) {
console.error("No user ID found - unauthorized")
return { error: "Unauthorized" }
}
console.log("User ID:", userId)
let oauthState: OAuthStatePayload
try {
oauthState = jwt.verify(state, JWT_SECRET) as OAuthStatePayload
} catch (error) {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
return { error: "Invalid or expired state" }
}
if (oauthState.type !== "oauth_state") {
console.error("Invalid token type:", oauthState.type)
return { error: "Invalid state token" }
}
console.log("OAuth state JWT verified successfully")
if (oauthState.userId !== userId) {
console.error("User mismatch:", { expected: oauthState.userId, actual: userId })
return { error: "User mismatch" }
}
const authCodePayload: AuthCodePayload = {
userId,
codeChallenge: oauthState.codeChallenge,
codeChallengeMethod: oauthState.codeChallengeMethod,
redirectUri: oauthState.redirectUri,
clientId: oauthState.clientId,
type: "auth_code",
}
const code = jwt.sign(authCodePayload, JWT_SECRET, {
expiresIn: "2m",
jwtid: crypto.randomBytes(16).toString("hex"),
})
console.log("Authorization code JWT created successfully")
return {
code,
redirectUri: oauthState.redirectUri,
}
} catch (error) {
console.error("Error authorizing OAuth:", error)
return { error: "Internal server error" }
}
}
interface TokenExchangeParams {
code: string
codeVerifier: string
redirectUri: string
clientId: string
}
export async function exchangeCodeForToken(
params: TokenExchangeParams,
): Promise<{ accessToken?: string; error?: string }> {
console.log("=== Exchanging Code for Token (JWT) ===")
console.log("Params:", {
codeVerifier: params.codeVerifier.substring(0, 10) + "...",
redirectUri: params.redirectUri,
clientId: params.clientId,
})
try {
let codeData: AuthCodePayload
try {
codeData = jwt.verify(params.code, JWT_SECRET) as AuthCodePayload
} catch (error) {
console.error("JWT verification failed:", error instanceof Error ? error.message : error)
return { error: "Invalid or expired authorization code" }
}
if (codeData.type !== "auth_code") {
console.error("Invalid token type:", codeData.type)
return { error: "Invalid authorization code" }
}
console.log("✓ Authorization code JWT verified successfully!")
console.log("Code data:", {
userId: codeData.userId,
redirectUri: codeData.redirectUri,
clientId: codeData.clientId,
})
if (codeData.clientId !== params.clientId) {
console.error("Client ID mismatch:", { expected: codeData.clientId, actual: params.clientId })
return { error: "Client ID mismatch" }
}
if (codeData.redirectUri !== params.redirectUri) {
console.error("Redirect URI mismatch:", {
expected: codeData.redirectUri,
actual: params.redirectUri,
})
return { error: "Redirect URI mismatch" }
}
console.log("Computing code challenge...")
const computedChallenge = crypto
.createHash(codeData.codeChallengeMethod)
.update(params.codeVerifier)
.digest("base64url")
if (computedChallenge !== codeData.codeChallenge) {
console.error("Code verifier validation failed")
return { error: "Code verifier validation failed" }
}
console.log("✓ PKCE validation successful")
console.log("Generating API token for userId:", codeData.userId)
try {
const apiKey = await generateTokenForVsCode(codeData.userId)
console.log("API token generated successfully")
console.log("=== Token Exchange Completed Successfully ===")
return { accessToken: apiKey.token }
} catch (tokenError: unknown) {
if (tokenError instanceof Error && tokenError.message === "NEXT_REDIRECT") {
console.error("Caught redirect error during token generation")
return { error: "Authentication required" }
}
console.error("Error generating token:", tokenError)
return { error: "Failed to generate API token" }
}
} catch (error) {
console.error("=== Token Exchange Failed ===")
console.error("Error:", error)
return { error: "Internal server error" }
}
}

View file

@ -0,0 +1,360 @@
"use client"
import LogoBox from "@/components/dashboard/logo-box"
import { useState, useEffect } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Loading } from "@/components/ui/loading"
import { authorizeOAuth, createOAuthState } from "./action"
export default function CodeFlashAuthContent() {
const [isLoading, setIsLoading] = useState(false)
const [isCheckingAuth, setIsCheckingAuth] = useState(true)
const [error, setError] = useState<string | null>(null)
const [step, setStep] = useState<"checking" | "ready" | "authorizing" | "waiting">("checking")
const [hasAuthenticated, setHasAuthenticated] = useState(false)
const searchParams = useSearchParams()
const router = useRouter()
useEffect(() => {
// Check if user already authenticated in this session
const authenticated = sessionStorage.getItem("oauth_authenticated")
if (authenticated === "true") {
setHasAuthenticated(true)
setStep("waiting")
setIsCheckingAuth(false)
return
}
const checkAuth = async () => {
setStep("checking")
try {
// Validate OAuth parameters
const responseType = searchParams.get("response_type")
const clientId = searchParams.get("client_id")
const redirectUri = searchParams.get("redirect_uri")
const codeChallenge = searchParams.get("code_challenge")
const codeChallengeMethod = searchParams.get("code_challenge_method")
const state = searchParams.get("state")
if (responseType !== "code") {
setError("Invalid request parameters")
return
}
if (!clientId || clientId !== "cf_vscode_app") {
setError("Invalid client application")
return
}
if (!redirectUri) {
setError("Invalid redirect destination")
return
}
if (!codeChallenge || !codeChallengeMethod) {
setError("Missing security parameters")
return
}
if (!state) {
setError("Missing request identifier")
return
}
// Create OAuth state
const result = await createOAuthState({
redirectUri,
codeChallenge,
codeChallengeMethod,
clientId,
})
if (result.error) {
if (result.error === "Unauthorized") {
const currentPath = window.location.pathname + window.location.search
router.replace(`/login?returnTo=${encodeURIComponent(currentPath)}`)
return
}
setError(
result.error === "Rate limit exceeded"
? "Too many authentication attempts. Please try again later."
: "An error occurred. Please try again.",
)
return
}
// Store the internal state and original VS Code state
sessionStorage.setItem("oauth_internal_state", result.state)
sessionStorage.setItem("oauth_vscode_state", state)
setStep("ready")
} catch (err) {
console.error("Error checking authentication:", err)
setError("An unexpected error occurred. Please try again.")
} finally {
setIsCheckingAuth(false)
}
}
checkAuth()
}, [router, searchParams])
const handleAuthenticate = async () => {
// Prevent multiple authentications
if (hasAuthenticated) {
return
}
setIsLoading(true)
setError(null)
setStep("authorizing")
try {
const internalState = sessionStorage.getItem("oauth_internal_state")
const vscodeState = sessionStorage.getItem("oauth_vscode_state")
if (!internalState || !vscodeState) {
setError("Session expired. Please refresh the page and try again.")
setIsLoading(false)
setStep("ready")
return
}
const result = await authorizeOAuth(internalState)
if (result.error) {
setError(
result.error === "Rate limit exceeded"
? "Too many authentication attempts. Please try again later."
: "Authentication failed. Please try again.",
)
setIsLoading(false)
setStep("ready")
return
}
if (!result.code || !result.redirectUri) {
setError("Authentication failed. Please try again.")
setIsLoading(false)
setStep("ready")
return
}
// Mark as authenticated
sessionStorage.setItem("oauth_authenticated", "true")
setHasAuthenticated(true)
// Clean up OAuth state
sessionStorage.removeItem("oauth_internal_state")
sessionStorage.removeItem("oauth_vscode_state")
// Redirect back to VS Code with code and state
const redirectUrl = new URL(result.redirectUri)
redirectUrl.searchParams.set("code", result.code)
redirectUrl.searchParams.set("state", vscodeState)
setStep("waiting")
setIsLoading(false)
// Redirect immediately
window.location.href = redirectUrl.toString()
} catch (err) {
console.error("Error authorizing:", err)
setError("An error occurred. Please try again.")
setIsLoading(false)
setStep("ready")
}
}
if (isCheckingAuth || step === "checking") {
return <Loading />
}
return (
<div className="min-h-screen bg-gradient-to-b from-primary/10 via-primary/5 to-background relative">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:24px_24px]" />
<div className="min-h-screen flex flex-col items-center justify-center px-6 py-12 relative z-10">
<div className="mb-16">
<LogoBox />
</div>
<div className="max-w-md w-full">
<div className="bg-card border border-border rounded-2xl shadow-xl overflow-hidden">
<div className="bg-gradient-to-r from-[#007ACC]/5 to-[#007ACC]/10 px-6 py-4 border-b border-border">
<div className="flex items-center justify-center gap-3">
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 128 128"
>
<defs>
<linearGradient
id="vscodeGradient"
x1="63.922"
x2="63.922"
y1=".33"
y2="127.67"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" />
<stop offset="1" stopColor="#fff" stopOpacity="0" />
</linearGradient>
</defs>
<mask
id="vscodeMask"
width="128"
height="128"
x="0"
y="0"
maskUnits="userSpaceOnUse"
style={{ maskType: "alpha" }}
>
<path
fill="#fff"
fillRule="evenodd"
d="M90.767 127.126a7.968 7.968 0 0 0 6.35-.244l26.353-12.681a8 8 0 0 0 4.53-7.209V21.009a8 8 0 0 0-4.53-7.21L97.117 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026L15.6 32.013a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.335 5.335 0 0 0-.006 7.888L20.796 64L1.74 81.387a5.336 5.336 0 0 0 .006 7.887l7.048 6.411a5.327 5.327 0 0 0 6.807.303l21.974-16.68l50.45 46.025a7.96 7.96 0 0 0 2.743 1.793Zm5.252-92.183L57.74 64l38.28 29.058V34.943Z"
clipRule="evenodd"
/>
</mask>
<g mask="url(#vscodeMask)">
<path
fill="#0065A9"
d="M123.471 13.82L97.097 1.12A7.973 7.973 0 0 0 88 2.668L1.662 81.387a5.333 5.333 0 0 0 .006 7.887l7.052 6.411a5.333 5.333 0 0 0 6.811.303l103.971-78.875c3.488-2.646 8.498-.158 8.498 4.22v-.306a8.001 8.001 0 0 0-4.529-7.208Z"
/>
<path
fill="#007ACC"
d="m123.471 114.181l-26.374 12.698A7.973 7.973 0 0 1 88 125.333L1.662 46.613a5.333 5.333 0 0 1 .006-7.887l7.052-6.411a5.333 5.333 0 0 1 6.811-.303l103.971 78.874c3.488 2.647 8.498.159 8.498-4.219v.306a8.001 8.001 0 0 1-4.529 7.208Z"
/>
<path
fill="#1F9CF0"
d="M97.098 126.882A7.977 7.977 0 0 1 88 125.333c2.952 2.952 8 .861 8-3.314V5.98c0-4.175-5.048-6.266-8-3.313a7.977 7.977 0 0 1 9.098-1.549L123.467 13.8A8 8 0 0 1 128 21.01v85.982a8 8 0 0 1-4.533 7.21l-26.369 12.681Z"
/>
<path
fill="url(#vscodeGradient)"
fillRule="evenodd"
d="M90.69 127.126a7.968 7.968 0 0 0 6.349-.244l26.353-12.681a8 8 0 0 0 4.53-7.21V21.009a8 8 0 0 0-4.53-7.21L97.039 1.12a7.97 7.97 0 0 0-9.093 1.548l-50.45 46.026l-21.974-16.68a5.328 5.328 0 0 0-6.807.302l-7.048 6.411a5.336 5.336 0 0 0-.006 7.888L20.718 64L1.662 81.386a5.335 5.335 0 0 0 .006 7.888l7.048 6.411a5.328 5.328 0 0 0 6.807.303l21.975-16.681l50.45 46.026a7.959 7.959 0 0 0 2.742 1.793Zm5.252-92.184L57.662 64l38.28 29.057V34.943Z"
clipRule="evenodd"
opacity=".25"
/>
</g>
</svg>
</div>
<div className="text-left">
<p className="text-sm font-semibold text-foreground">
Visual Studio Code Extension
</p>
</div>
</div>
</div>
{error ? (
<div className="p-8 space-y-6">
<div className="w-20 h-20 bg-amber-500/10 rounded-2xl flex items-center justify-center mx-auto relative">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-amber-600 dark:text-amber-500"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<div className="space-y-3 text-center">
<h2 className="text-2xl font-bold text-foreground">Authentication Error</h2>
<p className="text-sm text-muted-foreground leading-relaxed">{error}</p>
</div>
</div>
) : step === "waiting" || hasAuthenticated ? (
<div className="p-8 space-y-6">
<div className="w-16 h-16 bg-primary/10 rounded-xl flex items-center justify-center mx-auto">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-primary"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</div>
<div className="space-y-2 text-center">
<h2 className="text-xl font-bold text-foreground">Go to VS Code</h2>
</div>
</div>
) : (
<div className="p-8 space-y-6">
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mx-auto">
<svg
className="w-6 h-6 text-primary"
fill="none"
strokeWidth="2"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
/>
</svg>
</div>
<div className="space-y-2 text-center">
<h1 className="text-2xl font-bold text-foreground">
Authenticate for CodeFlash Extension
</h1>
<p className="text-sm text-muted-foreground leading-relaxed">
CodeFlash requires authentication to associate with the Visual Studio Code
extension on your machine.
</p>
</div>
<button
onClick={handleAuthenticate}
disabled={isLoading}
className="w-full px-6 py-3.5 bg-primary hover:bg-primary/90 active:scale-[0.99] text-primary-foreground font-semibold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100 shadow-sm"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Authenticating...
</span>
) : (
"Authenticate with CodeFlash"
)}
</button>
</div>
)}
</div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from "next/server"
import { exchangeCodeForToken } from "../../action"
export async function POST(request: NextRequest) {
console.log("=== Token Exchange Request Started ===")
try {
const body = await request.json()
console.log("Request body:", {
grant_type: body.grant_type,
client_id: body.client_id,
redirect_uri: body.redirect_uri,
has_code: !!body.code,
has_code_verifier: !!body.code_verifier,
code_length: body.code?.length,
code_verifier_length: body.code_verifier?.length,
})
const { grant_type, code, redirect_uri, code_verifier, client_id } = body
// Validate grant type
if (grant_type !== "authorization_code") {
console.error("Invalid grant type:", grant_type)
return NextResponse.json({ error: "unsupported_grant_type" }, { status: 400 })
}
// Validate required parameters
if (!code || !redirect_uri || !code_verifier || !client_id) {
console.error("Missing required parameters:", {
has_code: !!code,
has_redirect_uri: !!redirect_uri,
has_code_verifier: !!code_verifier,
has_client_id: !!client_id,
})
return NextResponse.json(
{ error: "invalid_request", error_description: "Missing required parameters" },
{ status: 400 },
)
}
console.log("Exchanging code for token...")
const result = await exchangeCodeForToken({
code,
codeVerifier: code_verifier,
redirectUri: redirect_uri,
clientId: client_id,
})
if (result.error) {
console.error("Token exchange failed:", result.error)
return NextResponse.json(
{ error: "invalid_grant", error_description: result.error },
{ status: 400 },
)
}
console.log("Token exchange successful, access_token length:", result.accessToken?.length)
console.log("=== Token Exchange Request Completed Successfully ===")
return NextResponse.json({
access_token: result.accessToken,
token_type: "Bearer",
})
} catch (error) {
console.error("=== Token Exchange Request Failed ===")
console.error("Error type:", error instanceof Error ? error.constructor.name : typeof error)
console.error("Error message:", error instanceof Error ? error.message : String(error))
console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace")
console.error("Full error object:", error)
return NextResponse.json(
{ error: "server_error", error_description: "Internal server error" },
{ status: 500 },
)
}
}

View file

@ -0,0 +1,11 @@
import { Suspense } from "react"
import { Loading } from "@/components/ui/loading"
import CodeFlashAuthContent from "./content"
export default function CodeFlashAuthPage() {
return (
<Suspense fallback={<Loading />}>
<CodeFlashAuthContent />
</Suspense>
)
}

Some files were not shown because too many files have changed in this diff Show more