diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f3c0ced --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [abi] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..386e7e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Screenshots of backend AND frontend terminal logs** +If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..48d5f81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.vscode/settings.json b/.vscode/settings.json index d6e2638..bfbce46 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,5 @@ { - "python.analysis.typeCheckingMode": "strict" + "python.analysis.typeCheckingMode": "strict", + "python.analysis.extraPaths": ["./backend"], + "python.autoComplete.extraPaths": ["./backend"] } diff --git a/Evaluation.md b/Evaluation.md new file mode 100644 index 0000000..e937b78 --- /dev/null +++ b/Evaluation.md @@ -0,0 +1,19 @@ +## Evaluating models and prompts + +Evaluation dataset consists of 16 screenshots. A Python script for running screenshot-to-code on the dataset and a UI for rating outputs is included. With this set up, we can compare and evaluate various models and prompts. + +### Running evals + +- Input screenshots should be located at `backend/evals_data/inputs` and the outputs will be `backend/evals_data/outputs`. If you want to modify this, modify `EVALS_DIR` in `backend/evals/config.py`. You can download the input screenshot dataset here: TODO. +- Set a stack and model (`STACK` var, `MODEL` var) in `backend/run_evals.py` +- Run `OPENAI_API_KEY=sk-... python run_evals.py` - this runs the screenshot-to-code on the input dataset in parallel but it will still take a few minutes to complete. +- Once the script is done, you can find the outputs in `backend/evals_data/outputs`. + +### Rating evals + +In order to view and rate the outputs, visit your front-end at `/evals`. + +- Rate each output on a scale of 1-4 +- You can also print the page as PDF to share your results with others. + +Generally, I run three tests for each model/prompt + stack combo and take the average score out of those tests to evaluate. diff --git a/README.md b/README.md index bc82497..d744478 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,53 @@ # screenshot-to-code -This simple app converts a screenshot to code (HTML/Tailwind CSS, or React or Bootstrap or Vue). It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. You can now also enter a URL to clone a live website! +A simple tool to convert screenshots, mockups and Figma designs into clean, functional code using AI. **Now supporting Claude Sonnet 3.5 and GPT-4O!** https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045 +Supported stacks: + +- HTML + Tailwind +- React + Tailwind +- Vue + Tailwind +- Bootstrap +- Ionic + Tailwind +- SVG + +Supported AI models: + +- Claude Sonnet 3.5 - Best model! +- GPT-4O - also recommended! +- GPT-4 Turbo (Apr 2024) +- GPT-4 Vision (Nov 2023) +- Claude 3 Sonnet +- DALL-E 3 for image generation + See the [Examples](#-examples) section below for more demos. -## ๐Ÿš€ Try It Out! +We also just added experimental support for taking a video/screen recording of a website in action and turning that into a functional prototype. -๐Ÿ†• [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions. +![google in app quick 3](https://github.com/abi/screenshot-to-code/assets/23818/8758ffa4-9483-4b9b-bb66-abd6d1594c33) -## ๐ŸŒŸ Recent Updates +[Learn more about video here](https://github.com/abi/screenshot-to-code/wiki/Screen-Recording-to-Code). -- Dec 11 - Start a new project from existing code (allows you to come back to an older project) -- Dec 7 - ๐Ÿ”ฅ ๐Ÿ”ฅ ๐Ÿ”ฅ View a history of your edits, and branch off them -- Nov 30 - Dark mode, output code in Ionic (thanks [@dialmedu](https://github.com/dialmedu)), set OpenAI base URL -- Nov 28 - ๐Ÿ”ฅ ๐Ÿ”ฅ ๐Ÿ”ฅ Customize your stack: React or Bootstrap or TailwindCSS -- Nov 23 - Send in a screenshot of the current replicated version (sometimes improves quality of subsequent generations) -- Nov 21 - Edit code in the code editor and preview changes live thanks to [@clean99](https://github.com/clean99) -- Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code)) -- Nov 19 - Support for dark/light code editor theme - thanks [@kachbit](https://github.com/kachbit) -- Nov 16 - Added a setting to disable DALL-E image generation if you don't need that -- Nov 16 - View code directly within the app -- Nov 15 - You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section. +[Follow me on Twitter for updates](https://twitter.com/_abi_). + +## Sponsors + + + +## ๐Ÿš€ Hosted Version + +[Try it live on the hosted version (paid)](https://screenshottocode.com). ## ๐Ÿ›  Getting Started -The app has a React/Vite frontend and a FastAPI backend. You will need an OpenAI API key with access to the GPT-4 Vision API. +The app has a React/Vite frontend and a FastAPI backend. + +Keys needed: + +* [OpenAI API key with access to GPT-4](https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md) +* Anthropic key (optional) - only if you want to use Claude Sonnet, or for experimental video support. Run the backend (I use Poetry for package management - `pip install poetry` if you don't have it): @@ -38,11 +59,7 @@ poetry shell poetry run uvicorn main:app --reload --port 7001 ``` -You can also run the backend (when you're in `backend`): - -```bash -poetry run pyright -``` +If you want to use Anthropic, add `ANTHROPIC_API_KEY` to `backend/.env`. You can also set up the keys using the settings dialog on the front-end (click the gear icon after loading the frontend). Run the frontend: @@ -62,10 +79,6 @@ For debugging purposes, if you don't want to waste GPT4-Vision credits, you can MOCK=true poetry run uvicorn main:app --reload --port 7001 ``` -## Configuration - -- You can configure the OpenAI base URL if you need to use a proxy: Set OPENAI_BASE_URL in the `backend/.env` or directly in the UI in the settings dialog - ## Docker If you have Docker installed on your system, in the root directory, run: @@ -81,6 +94,9 @@ The app will be up and running at http://localhost:5173. Note that you can't dev - **I'm running into an error when setting up the backend. How can I fix it?** [Try this](https://github.com/abi/screenshot-to-code/issues/3#issuecomment-1814777959). If that still doesn't work, open an issue. - **How do I get an OpenAI API key?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md +- **How can I configure an OpenAI proxy?** - If you're not able to access the OpenAI API directly (due to e.g. country restrictions), you can try a VPN or you can configure the OpenAI base URL to use a proxy: Set OPENAI_BASE_URL in the `backend/.env` or directly in the UI in the settings dialog. Make sure the URL has "v1" in the path so it should look like this: `https://xxx.xxxxx.xxx/v1` +- **How can I update the backend host that my front-end connects to?** - Configure VITE_HTTP_BACKEND_URL and VITE_WS_BACKEND_URL in front/.env.local For example, set VITE_HTTP_BACKEND_URL=http://124.10.20.1:7001 +- **Seeing UTF-8 errors when running the backend?** - On windows, open the .env file with notepad++, then go to Encoding and select UTF-8. - **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_). ## ๐Ÿ“š Examples @@ -101,6 +117,4 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a ## ๐ŸŒ Hosted Version -๐Ÿ†• [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions. - -[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/abiraja) +๐Ÿ†• [Try it here (paid)](https://screenshottocode.com). Or see [Getting Started](#-getting-started) for local install instructions to use with your own API keys. diff --git a/Troubleshooting.md b/Troubleshooting.md index 20fa815..dbb1186 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -1,17 +1,22 @@ -### Getting an OpenAI API key with GPT4-Vision model access +### Getting an OpenAI API key with GPT-4 model access You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions: 1. Open [OpenAI Dashboard](https://platform.openai.com/) 1. Go to Settings > Billing 1. Click at the Add payment details -285636868-c80deb92-ab47-45cd-988f-deee67fbd44d +285636868-c80deb92-ab47-45cd-988f-deee67fbd44d + 4. You have to buy some credits. The minimum is $5. - 5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access -285636973-da38bd4d-8a78-4904-8027-ca67d729b933 -6. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers. +285636973-da38bd4d-8a78-4904-8027-ca67d729b933 -Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated. +6. Navigate to OpenAI [api keys](https://platform.openai.com/api-keys) page and create and copy a new secret key. +7. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers. -If you've followed these steps, and it still doesn't work, feel free to open a Github issue. +## Still not working? + +- Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated. +- You need to add credits to your account AND set it to renew when credits run out in order to be upgraded to Tier 1. Make sure your "Settings > Limits" page shows that you are at Tier 1. + +If you've followed these steps, and it still doesn't work, feel free to open a Github issue. We only provide support for the open source version since we don't have debugging logs on the hosted version. If you're looking to use the hosted version, we recommend getting a paid subscription on screenshottocode.com diff --git a/backend/.gitignore b/backend/.gitignore index a42aad3..5d03006 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -154,3 +154,7 @@ cython_debug/ # Temporary eval output evals_data + + +# Temporary video evals (Remove before merge) +video_evals diff --git a/backend/.pre-commit-config.yaml b/backend/.pre-commit-config.yaml new file mode 100644 index 0000000..b54da93 --- /dev/null +++ b/backend/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: local + hooks: + - id: poetry-pytest + name: Run pytest with Poetry + entry: poetry run --directory backend pytest + language: system + pass_filenames: false + always_run: true + files: ^backend/ + # - id: poetry-pyright + # name: Run pyright with Poetry + # entry: poetry run --directory backend pyright + # language: system + # pass_filenames: false + # always_run: true + # files: ^backend/ diff --git a/backend/Dockerfile b/backend/Dockerfile index c520517..5eaf95d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim-bullseye +FROM python:3.12.3-slim-bullseye ENV POETRY_VERSION 1.4.1 diff --git a/backend/access_token.py b/backend/access_token.py deleted file mode 100644 index 35fa657..0000000 --- a/backend/access_token.py +++ /dev/null @@ -1,21 +0,0 @@ -import json -import os -import httpx - - -async def validate_access_token(access_code: str): - async with httpx.AsyncClient() as client: - url = ( - "https://backend.buildpicoapps.com/screenshot_to_code/validate_access_token" - ) - data = json.dumps( - { - "access_code": access_code, - "secret": os.environ.get("PICO_BACKEND_SECRET"), - } - ) - headers = {"Content-Type": "application/json"} - - response = await client.post(url, content=data, headers=headers) - response_data = response.json() - return response_data diff --git a/backend/codegen/__init__.py b/backend/codegen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/codegen/test_utils.py b/backend/codegen/test_utils.py new file mode 100644 index 0000000..cd663f8 --- /dev/null +++ b/backend/codegen/test_utils.py @@ -0,0 +1,57 @@ +import unittest +from codegen.utils import extract_html_content + + +class TestUtils(unittest.TestCase): + + def test_extract_html_content_with_html_tags(self): + text = "

Hello, World!

" + expected = "

Hello, World!

" + result = extract_html_content(text) + self.assertEqual(result, expected) + + def test_extract_html_content_without_html_tags(self): + text = "No HTML content here." + expected = "No HTML content here." + result = extract_html_content(text) + self.assertEqual(result, expected) + + def test_extract_html_content_with_partial_html_tags(self): + text = "

Hello, World!

" + expected = "

Hello, World!

" + result = extract_html_content(text) + self.assertEqual(result, expected) + + def test_extract_html_content_with_multiple_html_tags(self): + text = "

First

Some text

Second

" + expected = "

First

" + result = extract_html_content(text) + self.assertEqual(result, expected) + + ## The following are tests based on actual LLM outputs + + def test_extract_html_content_some_explanation_before(self): + text = """Got it! You want the song list to be displayed horizontally. I'll update the code to ensure that the song list is displayed in a horizontal layout. + + Here's the updated code: + + """ + expected = '' + result = extract_html_content(text) + self.assertEqual(result, expected) + + def test_markdown_tags(self): + text = "```html```" + expected = "```html```" + result = extract_html_content(text) + self.assertEqual(result, expected) + + def test_doctype_text(self): + text = '' + expected = '' + result = extract_html_content(text) + self.assertEqual(result, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/codegen/utils.py b/backend/codegen/utils.py new file mode 100644 index 0000000..107579b --- /dev/null +++ b/backend/codegen/utils.py @@ -0,0 +1,14 @@ +import re + + +def extract_html_content(text: str): + # Use regex to find content within tags and include the tags themselves + match = re.search(r"(.*?)", text, re.DOTALL) + if match: + return match.group(1) + else: + # Otherwise, we just send the previous HTML over + print( + "[HTML Extraction] No tags found in the generated content: " + text + ) + return text diff --git a/backend/config.py b/backend/config.py index 7dd21f9..05592b0 100644 --- a/backend/config.py +++ b/backend/config.py @@ -3,8 +3,13 @@ # TODO: Should only be set to true when value is 'True', not any abitrary truthy value import os +ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", None) + +# Debugging-related SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) +IS_DEBUG_ENABLED = bool(os.environ.get("IS_DEBUG_ENABLED", False)) +DEBUG_DIR = os.environ.get("DEBUG_DIR", "") # Set to True when running in production (on the hosted version) # Used as a feature flag to enable or disable certain features diff --git a/backend/custom_types.py b/backend/custom_types.py new file mode 100644 index 0000000..b6c9fee --- /dev/null +++ b/backend/custom_types.py @@ -0,0 +1,7 @@ +from typing import Literal + + +InputMode = Literal[ + "image", + "video", +] diff --git a/backend/debug/DebugFileWriter.py b/backend/debug/DebugFileWriter.py new file mode 100644 index 0000000..bbcd77b --- /dev/null +++ b/backend/debug/DebugFileWriter.py @@ -0,0 +1,30 @@ +import os +import logging +import uuid + +from config import DEBUG_DIR, IS_DEBUG_ENABLED + + +class DebugFileWriter: + def __init__(self): + if not IS_DEBUG_ENABLED: + return + + try: + self.debug_artifacts_path = os.path.expanduser( + f"{DEBUG_DIR}/{str(uuid.uuid4())}" + ) + os.makedirs(self.debug_artifacts_path, exist_ok=True) + print(f"Debugging artifacts will be stored in: {self.debug_artifacts_path}") + except: + logging.error("Failed to create debug directory") + + def write_to_file(self, filename: str, content: str) -> None: + try: + with open(os.path.join(self.debug_artifacts_path, filename), "w") as file: + file.write(content) + except Exception as e: + logging.error(f"Failed to write to file: {e}") + + def extract_html_content(self, text: str) -> str: + return str(text.split("")[-1].rsplit("", 1)[0] + "") diff --git a/backend/debug/__init__.py b/backend/debug/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/evals/core.py b/backend/evals/core.py index 61db1a3..2fc0352 100644 --- a/backend/evals/core.py +++ b/backend/evals/core.py @@ -1,29 +1,40 @@ import os +from config import ANTHROPIC_API_KEY -from llm import stream_openai_response +from llm import Llm, stream_claude_response, stream_openai_response from prompts import assemble_prompt from prompts.types import Stack -from utils import pprint_prompt -async def generate_code_core(image_url: str, stack: Stack) -> str: +async def generate_code_core(image_url: str, stack: Stack, model: Llm) -> str: prompt_messages = assemble_prompt(image_url, stack) openai_api_key = os.environ.get("OPENAI_API_KEY") + anthropic_api_key = ANTHROPIC_API_KEY openai_base_url = None - pprint_prompt(prompt_messages) - async def process_chunk(content: str): pass - if not openai_api_key: - raise Exception("OpenAI API key not found") + if model == Llm.CLAUDE_3_SONNET or model == Llm.CLAUDE_3_5_SONNET_2024_06_20: + if not anthropic_api_key: + raise Exception("Anthropic API key not found") - completion = await stream_openai_response( - prompt_messages, - api_key=openai_api_key, - base_url=openai_base_url, - callback=lambda x: process_chunk(x), - ) + completion = await stream_claude_response( + prompt_messages, + api_key=anthropic_api_key, + callback=lambda x: process_chunk(x), + model=model, + ) + else: + if not openai_api_key: + raise Exception("OpenAI API key not found") + + completion = await stream_openai_response( + prompt_messages, + api_key=openai_api_key, + base_url=openai_base_url, + callback=lambda x: process_chunk(x), + model=model, + ) return completion diff --git a/backend/image_generation.py b/backend/image_generation.py index d3e71b1..e3f609f 100644 --- a/backend/image_generation.py +++ b/backend/image_generation.py @@ -5,7 +5,7 @@ from openai import AsyncOpenAI from bs4 import BeautifulSoup -async def process_tasks(prompts: List[str], api_key: str, base_url: str): +async def process_tasks(prompts: List[str], api_key: str, base_url: str | None): tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts] results = await asyncio.gather(*tasks, return_exceptions=True) @@ -20,17 +20,18 @@ async def process_tasks(prompts: List[str], api_key: str, base_url: str): return processed_results -async def generate_image(prompt: str, api_key: str, base_url: str): +async def generate_image( + prompt: str, api_key: str, base_url: str | None +) -> Union[str, None]: client = AsyncOpenAI(api_key=api_key, base_url=base_url) - image_params: Dict[str, Union[str, int]] = { - "model": "dall-e-3", - "quality": "standard", - "style": "natural", - "n": 1, - "size": "1024x1024", - "prompt": prompt, - } - res = await client.images.generate(**image_params) + res = await client.images.generate( + model="dall-e-3", + quality="standard", + style="natural", + n=1, + size="1024x1024", + prompt=prompt, + ) await client.close() return res.data[0].url @@ -63,13 +64,13 @@ def create_alt_url_mapping(code: str) -> Dict[str, str]: async def generate_images( code: str, api_key: str, base_url: Union[str, None], image_cache: Dict[str, str] -): +) -> str: # Find all images soup = BeautifulSoup(code, "html.parser") images = soup.find_all("img") # Extract alt texts as image prompts - alts = [] + alts: List[str | None] = [] for img in images: # Only include URL if the image starts with https://placehold.co # and it's not already in the image_cache @@ -80,10 +81,10 @@ async def generate_images( alts.append(img.get("alt", None)) # Exclude images with no alt text - alts = [alt for alt in alts if alt is not None] + filtered_alts: List[str] = [alt for alt in alts if alt is not None] # Remove duplicates - prompts = list(set(alts)) + prompts = list(set(filtered_alts)) # Return early if there are no images to replace if len(prompts) == 0: diff --git a/backend/image_processing/__init__.py b/backend/image_processing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/image_processing/utils.py b/backend/image_processing/utils.py new file mode 100644 index 0000000..0bac644 --- /dev/null +++ b/backend/image_processing/utils.py @@ -0,0 +1,52 @@ +import base64 +import io +import time +from PIL import Image + +CLAUDE_IMAGE_MAX_SIZE = 5 * 1024 * 1024 + + +# Process image so it meets Claude requirements +def process_image(image_data_url: str) -> tuple[str, str]: + + media_type = image_data_url.split(";")[0].split(":")[1] + base64_data = image_data_url.split(",")[1] + + # If image is already under max size, return as is + if len(base64_data) <= CLAUDE_IMAGE_MAX_SIZE: + print("[CLAUDE IMAGE PROCESSING] no processing needed") + return (media_type, base64_data) + + # Time image processing + start_time = time.time() + + image_bytes = base64.b64decode(base64_data) + img = Image.open(io.BytesIO(image_bytes)) + + # Convert and compress as JPEG + quality = 95 + output = io.BytesIO() + img = img.convert("RGB") # Ensure image is in RGB mode for JPEG conversion + img.save(output, format="JPEG", quality=quality) + + # Reduce quality until image is under max size + while ( + len(base64.b64encode(output.getvalue())) > CLAUDE_IMAGE_MAX_SIZE + and quality > 10 + ): + output = io.BytesIO() + img.save(output, format="JPEG", quality=quality) + quality -= 5 + + # Log so we know it was modified + old_size = len(base64_data) + new_size = len(base64.b64encode(output.getvalue())) + print( + f"[CLAUDE IMAGE PROCESSING] image size updated: old size = {old_size} bytes, new size = {new_size} bytes" + ) + + end_time = time.time() + processing_time = end_time - start_time + print(f"[CLAUDE IMAGE PROCESSING] processing time: {processing_time:.2f} seconds") + + return ("image/jpeg", base64.b64encode(output.getvalue()).decode("utf-8")) diff --git a/backend/llm.py b/backend/llm.py index 66e3a47..0637416 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -1,8 +1,35 @@ -from typing import Awaitable, Callable, List +import base64 +from enum import Enum +from typing import Any, Awaitable, Callable, List, cast +from anthropic import AsyncAnthropic from openai import AsyncOpenAI from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk +from config import IS_DEBUG_ENABLED +from debug.DebugFileWriter import DebugFileWriter +from image_processing.utils import process_image -MODEL_GPT_4_VISION = "gpt-4-vision-preview" +from utils import pprint_prompt + + +# Actual model versions that are passed to the LLMs and stored in our logs +class Llm(Enum): + GPT_4_VISION = "gpt-4-vision-preview" + GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09" + GPT_4O_2024_05_13 = "gpt-4o-2024-05-13" + CLAUDE_3_SONNET = "claude-3-sonnet-20240229" + CLAUDE_3_OPUS = "claude-3-opus-20240229" + CLAUDE_3_HAIKU = "claude-3-haiku-20240307" + CLAUDE_3_5_SONNET_2024_06_20 = "claude-3-5-sonnet-20240620" + + +# Will throw errors if you send a garbage string +def convert_frontend_str_to_llm(frontend_str: str) -> Llm: + if frontend_str == "gpt_4_vision": + return Llm.GPT_4_VISION + elif frontend_str == "claude_3_sonnet": + return Llm.CLAUDE_3_SONNET + else: + return Llm(frontend_str) async def stream_openai_response( @@ -10,27 +37,192 @@ async def stream_openai_response( api_key: str, base_url: str | None, callback: Callable[[str], Awaitable[None]], + model: Llm, ) -> str: client = AsyncOpenAI(api_key=api_key, base_url=base_url) - model = MODEL_GPT_4_VISION - # Base parameters - params = {"model": model, "messages": messages, "stream": True, "timeout": 600} + params = { + "model": model.value, + "messages": messages, + "stream": True, + "timeout": 600, + "temperature": 0.0, + } - # Add 'max_tokens' only if the model is a GPT4 vision model - if model == MODEL_GPT_4_VISION: + # Add 'max_tokens' only if the model is a GPT4 vision or Turbo model + if ( + model == Llm.GPT_4_VISION + or model == Llm.GPT_4_TURBO_2024_04_09 + or model == Llm.GPT_4O_2024_05_13 + ): params["max_tokens"] = 4096 - params["temperature"] = 0 stream = await client.chat.completions.create(**params) # type: ignore full_response = "" async for chunk in stream: # type: ignore assert isinstance(chunk, ChatCompletionChunk) - content = chunk.choices[0].delta.content or "" - full_response += content - await callback(content) + if ( + chunk.choices + and len(chunk.choices) > 0 + and chunk.choices[0].delta + and chunk.choices[0].delta.content + ): + content = chunk.choices[0].delta.content or "" + full_response += content + await callback(content) await client.close() return full_response + + +# TODO: Have a seperate function that translates OpenAI messages to Claude messages +async def stream_claude_response( + messages: List[ChatCompletionMessageParam], + api_key: str, + callback: Callable[[str], Awaitable[None]], + model: Llm, +) -> str: + + client = AsyncAnthropic(api_key=api_key) + + # Base parameters + max_tokens = 4096 + temperature = 0.0 + + # Translate OpenAI messages to Claude messages + system_prompt = cast(str, messages[0].get("content")) + claude_messages = [dict(message) for message in messages[1:]] + for message in claude_messages: + if not isinstance(message["content"], list): + continue + + for content in message["content"]: # type: ignore + if content["type"] == "image_url": + content["type"] = "image" + + # Extract base64 data and media type from data URL + # Example base64 data URL: data:image/png;base64,iVBOR... + image_data_url = cast(str, content["image_url"]["url"]) + + # Process image and split media type and data + # so it works with Claude (under 5mb in base64 encoding) + (media_type, base64_data) = process_image(image_data_url) + + # Remove OpenAI parameter + del content["image_url"] + + content["source"] = { + "type": "base64", + "media_type": media_type, + "data": base64_data, + } + + # Stream Claude response + async with client.messages.stream( + model=model.value, + max_tokens=max_tokens, + temperature=temperature, + system=system_prompt, + messages=claude_messages, # type: ignore + ) as stream: + async for text in stream.text_stream: + await callback(text) + + # Return final message + response = await stream.get_final_message() + + # Close the Anthropic client + await client.close() + + return response.content[0].text + + +async def stream_claude_response_native( + system_prompt: str, + messages: list[Any], + api_key: str, + callback: Callable[[str], Awaitable[None]], + include_thinking: bool = False, + model: Llm = Llm.CLAUDE_3_OPUS, +) -> str: + + client = AsyncAnthropic(api_key=api_key) + + # Base model parameters + max_tokens = 4096 + temperature = 0.0 + + # Multi-pass flow + current_pass_num = 1 + max_passes = 2 + + prefix = "" + response = None + + # For debugging + full_stream = "" + debug_file_writer = DebugFileWriter() + + while current_pass_num <= max_passes: + current_pass_num += 1 + + # Set up message depending on whether we have a prefix + messages_to_send = ( + messages + [{"role": "assistant", "content": prefix}] + if include_thinking + else messages + ) + + pprint_prompt(messages_to_send) + + async with client.messages.stream( + model=model.value, + max_tokens=max_tokens, + temperature=temperature, + system=system_prompt, + messages=messages_to_send, # type: ignore + ) as stream: + async for text in stream.text_stream: + print(text, end="", flush=True) + full_stream += text + await callback(text) + + response = await stream.get_final_message() + response_text = response.content[0].text + + # Write each pass's code to .html file and thinking to .txt file + if IS_DEBUG_ENABLED: + debug_file_writer.write_to_file( + f"pass_{current_pass_num - 1}.html", + debug_file_writer.extract_html_content(response_text), + ) + debug_file_writer.write_to_file( + f"thinking_pass_{current_pass_num - 1}.txt", + response_text.split("")[0], + ) + + # Set up messages array for next pass + messages += [ + {"role": "assistant", "content": str(prefix) + response.content[0].text}, + { + "role": "user", + "content": "You've done a good job with a first draft. Improve this further based on the original instructions so that the app is fully functional and looks like the original video of the app we're trying to replicate.", + }, + ] + + print( + f"Token usage: Input Tokens: {response.usage.input_tokens}, Output Tokens: {response.usage.output_tokens}" + ) + + # Close the Anthropic client + await client.close() + + if IS_DEBUG_ENABLED: + debug_file_writer.write_to_file("full_stream.txt", full_stream) + + if not response: + raise Exception("No HTML response found in AI response") + else: + return response.content[0].text diff --git a/backend/mock_llm.py b/backend/mock_llm.py index 0102bad..b85b1b1 100644 --- a/backend/mock_llm.py +++ b/backend/mock_llm.py @@ -1,14 +1,35 @@ import asyncio from typing import Awaitable, Callable +from custom_types import InputMode -async def mock_completion(process_chunk: Callable[[str], Awaitable[None]]) -> str: - code_to_return = NO_IMAGES_NYTIMES_MOCK_CODE - for i in range(0, len(code_to_return), 10): - await process_chunk(code_to_return[i : i + 10]) +STREAM_CHUNK_SIZE = 20 + + +async def mock_completion( + process_chunk: Callable[[str], Awaitable[None]], input_mode: InputMode +) -> str: + code_to_return = ( + TALLY_FORM_VIDEO_PROMPT_MOCK + if input_mode == "video" + else NO_IMAGES_NYTIMES_MOCK_CODE + ) + + for i in range(0, len(code_to_return), STREAM_CHUNK_SIZE): + await process_chunk(code_to_return[i : i + STREAM_CHUNK_SIZE]) await asyncio.sleep(0.01) + if input_mode == "video": + # Extract the last block from code_to_return + # because we can have multiple passes + start = code_to_return.rfind("") + len("") + if start != -1 and end != -1: + code_to_return = code_to_return[start:end] + else: + code_to_return = "Error: HTML block not found." + return code_to_return @@ -206,3 +227,1357 @@ NO_IMAGES_NYTIMES_MOCK_CODE = """ """ + +MORTGAGE_CALCULATOR_VIDEO_PROMPT_MOCK = """ + +The user flow in the video seems to be: +1. The calculator starts with some default values for loan amount, loan term, interest rate, etc. +2. The user toggles the "Include taxes & fees" checkbox which shows an explanation tooltip. +3. The user selects different loan terms from the dropdown, which updates the monthly payment amount. +4. The user enters a custom loan amount. +5. The user selects a different loan term (30-yr fixed FHA). +6. The user enters additional details like home price, down payment, state, credit score, property tax, home insurance, and HOA fees. +7. The calculator updates the total monthly payment breakdown. + +To build this: +- Use a container div for the whole calculator +- Have sections for Monthly Payment, Purchase Budget, loan details, additional costs +- Use input fields, dropdowns, and checkboxes for user input +- Update values dynamically using JavaScript when inputs change +- Show/hide explanation tooltip when checkbox is toggled +- Update monthly payment whenever loan amount, interest rate or term is changed +- Allow selecting loan term from a dropdown +- Update total monthly payment breakdown as additional costs are entered +- Style everything to match the screenshots using Tailwind utility classes + + + + + + + + + +
+
Mortgage Calculator
+ +
+
Monthly payment
+
Purchase budget
+
+ +
+ + + + +
+ +
$1,696
+ +
+
Loan amount
+ +
+ +
+
Loan term
+ +
+ +
+
+
Interest
+
7.61 %
+
+ +
+ +
+
+
Home price
+ +
+
+
Down payment
+
+ +
20 %
+
+
+
+ +
+
+
State
+ +
+
+
Credit score
+ +
+
+ +
+
+
Property tax (yearly)
+ +
+
+
Home insurance (yearly)
+ +
+
+
Private mortgage insurance (monthly)
+ +
+
+
Homeowners association (monthly)
+ +
+
+ +
+
+
Total monthly payment
+
$2,036
+
+
+
Loan
+
$1,635
+
Taxes & fees
+
$401
+
+
+ +
+
Disclaimer โ€ข Feedback
+
+
+ + + + + + +More things to consider: +- Add validation to input fields to prevent non-numeric values +- Add a reset button to clear all inputs and reset to default values + + + + + + + + + +
+
Mortgage Calculator
+ +
+
Monthly payment
+
Purchase budget
+
+ +
+ + + + +
+ +
$1,696
+ +
+
Loan amount
+ +
+ +
+
Loan term
+ +
+ +
+
+
Interest
+
7.61 %
+
+ +
+ +
+
+
Home price
+ +
+
+
Down payment
+
+ +
20 %
+
+
+
+ +
+
+
State
+ +
+
+
Credit score
+ +
+
+ +
+
+
Property tax (yearly)
+ +
+
+
Home insurance (yearly)
+ +
+
+
Private mortgage insurance (monthly)
+ +
+
+
Homeowners association (monthly)
+ +
+
+ +
+
+
Total monthly payment
+
$2,036
+
+
+
Loan
+
$1,635
+
Taxes & fees
+
$401
+
+
+ +
+
Disclaimer โ€ข Feedback
+
+
+ + + + + +""" + +GOOGLE_FORM_VIDEO_PROMPT_MOCK = """ + +User flow: +1. User starts on the Google search page and types in "times" in the search bar +2. As the user types, Google provides autocomplete suggestions related to "times" +3. User selects the "times" suggestion from the autocomplete dropdown +4. The search results page for "times" loads, showing various results related to The New York Times newspaper +5. User clicks the "Generate" button under "Get an AI-powered overview for this search?" +6. An AI-generated overview about The New York Times loads on the right side of the search results + +Code structure: +- HTML structure with header, search bar, autocomplete dropdown, search button +- Search results area to display search results +- Sidebar area to display the AI-generated overview +- Use Tailwind CSS utility classes for styling +- Use jQuery to handle user interactions: + - Typing in search bar to show/filter autocomplete suggestions + - Selecting autocomplete suggestion to populate search bar + - Clicking search button to display search results + - Clicking "Generate" button to display AI overview +- Hardcode search results and AI overview content for demo purposes + + + + + + + + + + +
+
+ Google logo +
+ + +
+ + Profile picture of the user +
+ +
+ + + +
+
+ + + + + + + +To improve the app further and make it fully functional like the original video: + +1. Add more visual styling to match the original, including: + - Google logo and favicon + - User profile icon + - Search bar styling and hover effects + - Search button icon and styling + - Fonts and text sizes + - Spacing and layout adjustments + +2. Implement the search functionality: + - Perform a search when the user presses Enter or clicks the search button + - Show a loading state while searching + - Display the search results dynamically based on the query + - Handle cases when no results are found + +3. Enhance the autocomplete functionality: + - Fetch autocomplete suggestions from a data source based on the user's input + - Highlight the text match in the suggestions + - Allow navigating the suggestions with arrow keys + - Update the search input when a suggestion is selected + +4. Improve the AI-powered overview: + - Fetch the overview content dynamically based on the search query + - Display a loading state while generating the overview + - Enhance the styling of the overview to match the original + +5. Add additional UI elements: + - Header navigation tabs (About, Store, Gmail, Images) + - Footer links (Advertising, Business, How Search works, Privacy, Terms) + - Pagination for search results + - Feedback link + +6. Optimize performance: + - Lazy-load images and content + - Implement caching mechanisms + - Minify and compress assets + +7. Ensure responsiveness: + - Make the layout responsive and mobile-friendly + - Adjust font sizes, padding, and margins for different screen sizes + - Hide/collapse certain elements on smaller screens + +8. Enhance accessibility: + - Add appropriate ARIA attributes + - Ensure keyboard navigation works properly + - Provide alternative text for images + +9. Implement error handling: + - Handle network errors gracefully + - Display user-friendly error messages + - Provide options to retry or refresh + +10. Add smooth animations and transitions: + - Fade in/out effects for loading states + - Smooth sliding animations for autocomplete suggestions + - Subtle hover and click animations for interactive elements + + + + + Google Search + + + + + + + + +
+
+ About + Store + Gmail + Images + Profile picture of the user +
+ +
+
+ Google logo +
+ + +
+
+ + +
+
+
+ + + + + + +
+ + + + + +""" + +TALLY_FORM_VIDEO_PROMPT_MOCK = """ + +The user flow in the video seems to be: +1. The quiz starts by asking "What is the capital of Bulgaria?" with three multiple choice options. The user selects "Sofia". +2. The next question asks "What is the most famous dish in Belgium?" with two image-based options. The user selects "Steak with fries". +3. The next question asks "What is the name of this actor?" with an image of an older man with gray hair and beard. The user types in "Clooney" and submits the answer. +4. The quiz ends showing the user's score of 10/30 and a "Bummer, try again!" message with an arrow to restart. + +To build this as a functional web app: +1. Create the overall page structure with a title, question area, answer options area, and next/submit buttons. +2. Use jQuery to show/hide different questions and update the answer options based on the current question. +3. Store the questions, answer options, and correct answers in a Javascript array. +4. Keep track of the user's selections and compare them to the correct answers to calculate the score. +5. After the last question, show the final score and restart option. +6. Style everything to match the screenshots using Tailwind utility classes. + + + + + + + Online Trivia Quiz + + + + +

Online Trivia Quiz

+ +
+

+
+
+ + + + + + + + + + + + + + +This implements the online trivia quiz app as shown in the video. It uses jQuery to handle the interactions and Tailwind CSS for styling. The questions, options, and answers are defined in the quizData array. The app keeps track of the user's current question and score, allows selecting or entering answers, and displays the final score at the end with the option to restart. + + +To further improve the app to match the original video: + +1. Adjust the styling to more closely match the font, colors, spacing, and layout in the screenshots. +2. Add a cursor pointer when hovering over answer options. +3. Improve the styling of the selected answer option. +4. Make the "Next" button change to an arrow icon on hover. +5. Add a "Back" button to go to the previous question. +6. Show the current question number and total (e.g. "Question 1 of 3"). +7. Animate the transitions between questions. +8. Improve the responsive layout for different screen sizes. +9. Preload the images to avoid flickering. +10. Refactor the code to be more modular and maintainable. + + + + + + + Online Trivia Quiz + + + + + + +

Online Trivia Quiz

+ +
+
+
+ + +
+

+
+
+ + + + + + + + +
+ + + + + + +This updated version makes several improvements to match the original app more closely: + +1. Styling is refined to better match the font, colors, spacing, and layout of the screenshots. +2. Cursor changes to a pointer when hovering over answer options. +3. Selected answer option is highlighted. +4. "Next" button shows an animated arrow on hover. +5. "Back" button added to go to the previous question. +6. Current question number and total are displayed. +7. Fade transitions are added between questions. +8. Layout is made responsive for different screen sizes. +9. Images are preloaded to avoid flickering. +10. Code is refactored to be more modular and maintainable. + +The app should now be fully functional and closely resemble the look and behavior of the original video. Let me know if you would like any further adjustments! +""" diff --git a/backend/poetry.lock b/backend/poetry.lock index 42a44ee..79bcd8a 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,63 +1,197 @@ -# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] -name = "anyio" -version = "3.7.1" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" +name = "anthropic" +version = "0.18.1" +description = "The official Python library for the anthropic API" optional = false python-versions = ">=3.7" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anthropic-0.18.1-py3-none-any.whl", hash = "sha256:b85aee64f619ce1b1964ba733a09adc4053e7bc4e6d4186001229ec191099dcf"}, + {file = "anthropic-0.18.1.tar.gz", hash = "sha256:f5d1caafd43f6cc933a79753a93531605095f040a384f6a900c3de9c3fb6694e"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tokenizers = ">=0.13.0" +typing-extensions = ">=4.7,<5" [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +vertex = ["google-auth (>=2,<3)"] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "beautifulsoup4" -version = "4.12.2" +version = "4.12.3" description = "Screen-scraping library" -category = "main" optional = false python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [package.dependencies] soupsieve = ">1.2" [package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -72,7 +206,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -80,23 +213,43 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "decorator" +version = "4.4.2" +description = "Decorators for Humans" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +files = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "distro" -version = "1.8.0" +version = "1.9.0" description = "Distro - an OS platform information API" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, - {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] [[package]] name = "exceptiongroup" version = "1.2.0" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -111,7 +264,6 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.95.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -129,11 +281,61 @@ dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (> doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fsspec" +version = "2024.2.0" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.2.0-py3-none-any.whl", hash = "sha256:817f969556fa5916bc682e02ca2045f96ff7f586d45110fcb76022063ad2c7d8"}, + {file = "fsspec-2024.2.0.tar.gz", hash = "sha256:b6ad1a679f760dda52b1168c859d01b7b80648ea6f7f7c7f5a8a91dc3f3ecb84"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -143,14 +345,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.4" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, ] [package.dependencies] @@ -160,14 +361,13 @@ h11 = ">=0.13,<0.15" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] -trio = ["trio (>=0.22.0,<0.23.0)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.25.0)"] [[package]] name = "httpx" version = "0.25.2" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -178,21 +378,67 @@ files = [ [package.dependencies] anyio = "*" certifi = "*" -httpcore = ">=1.0.0,<2.0.0" +httpcore = "==1.*" idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "huggingface-hub" +version = "0.21.4" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "huggingface_hub-0.21.4-py3-none-any.whl", hash = "sha256:df37c2c37fc6c82163cdd8a67ede261687d80d1e262526d6c0ce73b6b3630a7b"}, + {file = "huggingface_hub-0.21.4.tar.gz", hash = "sha256:e1f4968c93726565a80edf6dc309763c7b546d0cfe79aa221206034d50155531"}, +] + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] +inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"] +quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["safetensors", "torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + +[[package]] +name = "identify" +version = "2.5.35" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, +] + +[package.extras] +license = ["ukkonen"] [[package]] name = "idna" version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -200,11 +446,60 @@ files = [ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] +[[package]] +name = "imageio" +version = "2.34.0" +description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." +optional = false +python-versions = ">=3.8" +files = [ + {file = "imageio-2.34.0-py3-none-any.whl", hash = "sha256:08082bf47ccb54843d9c73fe9fc8f3a88c72452ab676b58aca74f36167e8ccba"}, + {file = "imageio-2.34.0.tar.gz", hash = "sha256:ae9732e10acf807a22c389aef193f42215718e16bd06eed0c5bb57e1034a4d53"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=8.3.2" + +[package.extras] +all-plugins = ["astropy", "av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] +all-plugins-pypy = ["av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] +build = ["wheel"] +dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"] +docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"] +ffmpeg = ["imageio-ffmpeg", "psutil"] +fits = ["astropy"] +full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "itk", "numpydoc", "pillow-heif", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"] +gdal = ["gdal"] +itk = ["itk"] +linting = ["black", "flake8"] +pillow-heif = ["pillow-heif"] +pyav = ["av"] +test = ["fsspec[github]", "pytest", "pytest-cov"] +tifffile = ["tifffile"] + +[[package]] +name = "imageio-ffmpeg" +version = "0.4.9" +description = "FFMPEG wrapper for Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "imageio-ffmpeg-0.4.9.tar.gz", hash = "sha256:39bcd1660118ef360fa4047456501071364661aa9d9021d3d26c58f1ee2081f5"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24095e882a126a0d217197b86265f821b4bb3cf9004104f67c1384a2b4b49168"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-manylinux2010_x86_64.whl", hash = "sha256:2996c64af3e5489227096580269317719ea1a8121d207f2e28d6c24ebc4a253e"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7eead662d2f46d748c0ab446b68f423eb63d2b54d0a8ef96f80607245540866d"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-win32.whl", hash = "sha256:b6de1e18911687c538d5585d8287ab1a23624ca9dc2044fcc4607de667bcf11e"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-win_amd64.whl", hash = "sha256:7e900c695c6541b1cb17feb1baacd4009b30a53a45b81c23d53a67ab13ffb766"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -212,11 +507,34 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "moviepy" +version = "1.0.3" +description = "Video editing with Python" +optional = false +python-versions = "*" +files = [ + {file = "moviepy-1.0.3.tar.gz", hash = "sha256:2884e35d1788077db3ff89e763c5ba7bfddbd7ae9108c9bc809e7ba58fa433f5"}, +] + +[package.dependencies] +decorator = ">=4.0.2,<5.0" +imageio = {version = ">=2.5,<3.0", markers = "python_version >= \"3.4\""} +imageio_ffmpeg = {version = ">=0.2.0", markers = "python_version >= \"3.4\""} +numpy = {version = ">=1.17.3", markers = "python_version > \"2.7\""} +proglog = "<=1.0.0" +requests = ">=2.8.1,<3.0" +tqdm = ">=4.11.2,<5.0" + +[package.extras] +doc = ["Sphinx (>=1.5.2,<2.0)", "numpydoc (>=0.6.0,<1.0)", "pygame (>=1.9.3,<2.0)", "sphinx_rtd_theme (>=0.1.10b0,<1.0)"] +optional = ["matplotlib (>=2.0.0,<3.0)", "opencv-python (>=3.0,<4.0)", "scikit-image (>=0.13.0,<1.0)", "scikit-learn", "scipy (>=0.19.0,<1.5)", "youtube_dl"] +test = ["coverage (<5.0)", "coveralls (>=1.1,<2.0)", "pytest (>=3.0.0,<4.0)", "pytest-cov (>=2.5.1,<3.0)", "requests (>=2.8.1,<3.0)"] + [[package]] name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -227,102 +545,276 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + [[package]] name = "openai" -version = "1.3.7" +version = "1.14.0" description = "The official Python library for the openai API" -category = "main" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.3.7-py3-none-any.whl", hash = "sha256:e5c51367a910297e4d1cd33d2298fb87d7edf681edbe012873925ac16f95bee0"}, - {file = "openai-1.3.7.tar.gz", hash = "sha256:18074a0f51f9b49d1ae268c7abc36f7f33212a0c0d08ce11b7053ab2d17798de"}, + {file = "openai-1.14.0-py3-none-any.whl", hash = "sha256:5c9fd3a59f5cbdb4020733ddf79a22f6b7a36d561968cb3f3dd255cdd263d9fe"}, + {file = "openai-1.14.0.tar.gz", hash = "sha256:e287057adf0ec3315abc32ddcc968d095879abd9b68bf51c0402dab13ab5ae9b"}, ] [package.dependencies] -anyio = ">=3.5.0,<4" +anyio = ">=3.5.0,<5" distro = ">=1.7.0,<2" httpx = ">=0.23.0,<1" pydantic = ">=1.9.0,<3" sniffio = "*" tqdm = ">4" -typing-extensions = ">=4.5,<5" +typing-extensions = ">=4.7,<5" [package.extras] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" +name = "pillow" +version = "10.3.0" +description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "3.6.2" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, + {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "proglog" +version = "0.1.10" +description = "Log and progress bar manager for console, notebooks, web..." +optional = false +python-versions = "*" +files = [ + {file = "proglog-0.1.10-py3-none-any.whl", hash = "sha256:19d5da037e8c813da480b741e3fa71fb1ac0a5b02bf21c41577c7f327485ec50"}, + {file = "proglog-0.1.10.tar.gz", hash = "sha256:658c28c9c82e4caeb2f25f488fff9ceace22f8d69b15d0c1c86d64275e4ddab4"}, +] + +[package.dependencies] +tqdm = "*" + [[package]] name = "pydantic" -version = "1.10.13" +version = "1.10.14" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, - {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, - {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, - {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, - {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, - {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, - {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, - {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, - {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, - {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, + {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, + {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, + {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, + {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, + {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, + {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, + {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, + {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, + {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, + {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, ] [package.dependencies] @@ -334,14 +826,13 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyright" -version = "1.1.345" +version = "1.1.354" description = "Command line wrapper for pyright" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.345-py3-none-any.whl", hash = "sha256:00891361baf58698aa660d9374823d65782823ceb4a65515ff5dd159b0d4d2b1"}, - {file = "pyright-1.1.345.tar.gz", hash = "sha256:bb8c80671cdaeb913142b49642a741959f3fcd728c99814631c2bde3a7864938"}, + {file = "pyright-1.1.354-py3-none-any.whl", hash = "sha256:f28d61ae8ae035fc52ded1070e8d9e786051a26a4127bbd7a4ba0399b81b37b5"}, + {file = "pyright-1.1.354.tar.gz", hash = "sha256:b1070dc774ff2e79eb0523fe87f4ba9a90550de7e4b030a2bc9e031864029a1f"}, ] [package.dependencies] @@ -353,14 +844,13 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -376,53 +866,130 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "setuptools" -version = "69.0.3" +version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -434,7 +1001,6 @@ files = [ name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -448,11 +1014,137 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "tokenizers" +version = "0.15.2" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tokenizers-0.15.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:52f6130c9cbf70544287575a985bf44ae1bda2da7e8c24e97716080593638012"}, + {file = "tokenizers-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:054c1cc9c6d68f7ffa4e810b3d5131e0ba511b6e4be34157aa08ee54c2f8d9ee"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9b9b070fdad06e347563b88c278995735292ded1132f8657084989a4c84a6d5"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea621a7eef4b70e1f7a4e84dd989ae3f0eeb50fc8690254eacc08acb623e82f1"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7fd9a5141634fa3aa8d6b7be362e6ae1b4cda60da81388fa533e0b552c98fd"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44f2a832cd0825295f7179eaf173381dc45230f9227ec4b44378322d900447c9"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b9ec69247a23747669ec4b0ca10f8e3dfb3545d550258129bd62291aabe8605"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b6a4c78da863ff26dbd5ad9a8ecc33d8a8d97b535172601cf00aee9d7ce9ce"}, + {file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5ab2a4d21dcf76af60e05af8063138849eb1d6553a0d059f6534357bce8ba364"}, + {file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a47acfac7e511f6bbfcf2d3fb8c26979c780a91e06fb5b9a43831b2c0153d024"}, + {file = "tokenizers-0.15.2-cp310-none-win32.whl", hash = "sha256:064ff87bb6acdbd693666de9a4b692add41308a2c0ec0770d6385737117215f2"}, + {file = "tokenizers-0.15.2-cp310-none-win_amd64.whl", hash = "sha256:3b919afe4df7eb6ac7cafd2bd14fb507d3f408db7a68c43117f579c984a73843"}, + {file = "tokenizers-0.15.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:89cd1cb93e4b12ff39bb2d626ad77e35209de9309a71e4d3d4672667b4b256e7"}, + {file = "tokenizers-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfed5c64e5be23d7ee0f0e98081a25c2a46b0b77ce99a4f0605b1ec43dd481fa"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a907d76dcfda37023ba203ab4ceeb21bc5683436ebefbd895a0841fd52f6f6f2"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ea60479de6fc7b8ae756b4b097572372d7e4032e2521c1bbf3d90c90a99ff0"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48e2b9335be2bc0171df9281385c2ed06a15f5cf121c44094338306ab7b33f2c"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:112a1dd436d2cc06e6ffdc0b06d55ac019a35a63afd26475205cb4b1bf0bfbff"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4620cca5c2817177ee8706f860364cc3a8845bc1e291aaf661fb899e5d1c45b0"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccd73a82751c523b3fc31ff8194702e4af4db21dc20e55b30ecc2079c5d43cb7"}, + {file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:107089f135b4ae7817affe6264f8c7a5c5b4fd9a90f9439ed495f54fcea56fb4"}, + {file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0ff110ecc57b7aa4a594396525a3451ad70988e517237fe91c540997c4e50e29"}, + {file = "tokenizers-0.15.2-cp311-none-win32.whl", hash = "sha256:6d76f00f5c32da36c61f41c58346a4fa7f0a61be02f4301fd30ad59834977cc3"}, + {file = "tokenizers-0.15.2-cp311-none-win_amd64.whl", hash = "sha256:cc90102ed17271cf0a1262babe5939e0134b3890345d11a19c3145184b706055"}, + {file = "tokenizers-0.15.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f86593c18d2e6248e72fb91c77d413a815153b8ea4e31f7cd443bdf28e467670"}, + {file = "tokenizers-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0774bccc6608eca23eb9d620196687c8b2360624619623cf4ba9dc9bd53e8b51"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0222c5b7c9b26c0b4822a82f6a7011de0a9d3060e1da176f66274b70f846b98"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3835738be1de66624fff2f4f6f6684775da4e9c00bde053be7564cbf3545cc66"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0143e7d9dcd811855c1ce1ab9bf5d96d29bf5e528fd6c7824d0465741e8c10fd"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db35825f6d54215f6b6009a7ff3eedee0848c99a6271c870d2826fbbedf31a38"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f5e64b0389a2be47091d8cc53c87859783b837ea1a06edd9d8e04004df55a5c"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e0480c452217edd35eca56fafe2029fb4d368b7c0475f8dfa3c5c9c400a7456"}, + {file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a33ab881c8fe70474980577e033d0bc9a27b7ab8272896e500708b212995d834"}, + {file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a308a607ca9de2c64c1b9ba79ec9a403969715a1b8ba5f998a676826f1a7039d"}, + {file = "tokenizers-0.15.2-cp312-none-win32.whl", hash = "sha256:b8fcfa81bcb9447df582c5bc96a031e6df4da2a774b8080d4f02c0c16b42be0b"}, + {file = "tokenizers-0.15.2-cp312-none-win_amd64.whl", hash = "sha256:38d7ab43c6825abfc0b661d95f39c7f8af2449364f01d331f3b51c94dcff7221"}, + {file = "tokenizers-0.15.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:38bfb0204ff3246ca4d5e726e8cc8403bfc931090151e6eede54d0e0cf162ef0"}, + {file = "tokenizers-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c861d35e8286a53e06e9e28d030b5a05bcbf5ac9d7229e561e53c352a85b1fc"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:936bf3842db5b2048eaa53dade907b1160f318e7c90c74bfab86f1e47720bdd6"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:620beacc3373277700d0e27718aa8b25f7b383eb8001fba94ee00aeea1459d89"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2735ecbbf37e52db4ea970e539fd2d450d213517b77745114f92867f3fc246eb"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:473c83c5e2359bb81b0b6fde870b41b2764fcdd36d997485e07e72cc3a62264a"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968fa1fb3c27398b28a4eca1cbd1e19355c4d3a6007f7398d48826bbe3a0f728"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:865c60ae6eaebdde7da66191ee9b7db52e542ed8ee9d2c653b6d190a9351b980"}, + {file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7c0d8b52664ab2d4a8d6686eb5effc68b78608a9008f086a122a7b2996befbab"}, + {file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f33dfbdec3784093a9aebb3680d1f91336c56d86cc70ddf88708251da1fe9064"}, + {file = "tokenizers-0.15.2-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d44ba80988ff9424e33e0a49445072ac7029d8c0e1601ad25a0ca5f41ed0c1d6"}, + {file = "tokenizers-0.15.2-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:dce74266919b892f82b1b86025a613956ea0ea62a4843d4c4237be2c5498ed3a"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0ef06b9707baeb98b316577acb04f4852239d856b93e9ec3a299622f6084e4be"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73e2e74bbb07910da0d37c326869f34113137b23eadad3fc00856e6b3d9930c"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eeb12daf02a59e29f578a865f55d87cd103ce62bd8a3a5874f8fdeaa82e336b"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ba9f6895af58487ca4f54e8a664a322f16c26bbb442effd01087eba391a719e"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccec77aa7150e38eec6878a493bf8c263ff1fa8a62404e16c6203c64c1f16a26"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f40604f5042ff210ba82743dda2b6aa3e55aa12df4e9f2378ee01a17e2855e"}, + {file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5645938a42d78c4885086767c70923abad047163d809c16da75d6b290cb30bbe"}, + {file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:05a77cbfebe28a61ab5c3891f9939cc24798b63fa236d84e5f29f3a85a200c00"}, + {file = "tokenizers-0.15.2-cp37-none-win32.whl", hash = "sha256:361abdc068e8afe9c5b818769a48624687fb6aaed49636ee39bec4e95e1a215b"}, + {file = "tokenizers-0.15.2-cp37-none-win_amd64.whl", hash = "sha256:7ef789f83eb0f9baeb4d09a86cd639c0a5518528f9992f38b28e819df397eb06"}, + {file = "tokenizers-0.15.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4fe1f74a902bee74a3b25aff180fbfbf4f8b444ab37c4d496af7afd13a784ed2"}, + {file = "tokenizers-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c4b89038a684f40a6b15d6b09f49650ac64d951ad0f2a3ea9169687bbf2a8ba"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d05a1b06f986d41aed5f2de464c003004b2df8aaf66f2b7628254bcbfb72a438"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508711a108684111ec8af89d3a9e9e08755247eda27d0ba5e3c50e9da1600f6d"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:daa348f02d15160cb35439098ac96e3a53bacf35885072611cd9e5be7d333daa"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494fdbe5932d3416de2a85fc2470b797e6f3226c12845cadf054dd906afd0442"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2d60f5246f4da9373f75ff18d64c69cbf60c3bca597290cea01059c336d2470"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93268e788825f52de4c7bdcb6ebc1fcd4a5442c02e730faa9b6b08f23ead0e24"}, + {file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6fc7083ab404019fc9acafe78662c192673c1e696bd598d16dc005bd663a5cf9"}, + {file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e39b41e5531d6b2122a77532dbea60e171ef87a3820b5a3888daa847df4153"}, + {file = "tokenizers-0.15.2-cp38-none-win32.whl", hash = "sha256:06cd0487b1cbfabefb2cc52fbd6b1f8d4c37799bd6c6e1641281adaa6b2504a7"}, + {file = "tokenizers-0.15.2-cp38-none-win_amd64.whl", hash = "sha256:5179c271aa5de9c71712e31cb5a79e436ecd0d7532a408fa42a8dbfa4bc23fd9"}, + {file = "tokenizers-0.15.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82f8652a74cc107052328b87ea8b34291c0f55b96d8fb261b3880216a9f9e48e"}, + {file = "tokenizers-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02458bee6f5f3139f1ebbb6d042b283af712c0981f5bc50edf771d6b762d5e4f"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c9a09cd26cca2e1c349f91aa665309ddb48d71636370749414fbf67bc83c5343"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:158be8ea8554e5ed69acc1ce3fbb23a06060bd4bbb09029431ad6b9a466a7121"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ddba9a2b0c8c81633eca0bb2e1aa5b3a15362b1277f1ae64176d0f6eba78ab1"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ef5dd1d39797044642dbe53eb2bc56435308432e9c7907728da74c69ee2adca"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:454c203164e07a860dbeb3b1f4a733be52b0edbb4dd2e5bd75023ffa8b49403a"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cf6b7f1d4dc59af960e6ffdc4faffe6460bbfa8dce27a58bf75755ffdb2526d"}, + {file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2ef09bbc16519f6c25d0c7fc0c6a33a6f62923e263c9d7cca4e58b8c61572afb"}, + {file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c9a2ebdd2ad4ec7a68e7615086e633857c85e2f18025bd05d2a4399e6c5f7169"}, + {file = "tokenizers-0.15.2-cp39-none-win32.whl", hash = "sha256:918fbb0eab96fe08e72a8c2b5461e9cce95585d82a58688e7f01c2bd546c79d0"}, + {file = "tokenizers-0.15.2-cp39-none-win_amd64.whl", hash = "sha256:524e60da0135e106b254bd71f0659be9f89d83f006ea9093ce4d1fab498c6d0d"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9b648a58281c4672212fab04e60648fde574877d0139cd4b4f93fe28ca8944"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7c7d18b733be6bbca8a55084027f7be428c947ddf871c500ee603e375013ffba"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:13ca3611de8d9ddfbc4dc39ef54ab1d2d4aaa114ac8727dfdc6a6ec4be017378"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:237d1bf3361cf2e6463e6c140628e6406766e8b27274f5fcc62c747ae3c6f094"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67a0fe1e49e60c664915e9fb6b0cb19bac082ab1f309188230e4b2920230edb3"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4e022fe65e99230b8fd89ebdfea138c24421f91c1a4f4781a8f5016fd5cdfb4d"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d857be2df69763362ac699f8b251a8cd3fac9d21893de129bc788f8baaef2693"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:708bb3e4283177236309e698da5fcd0879ce8fd37457d7c266d16b550bcbbd18"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c35e09e9899b72a76e762f9854e8750213f67567787d45f37ce06daf57ca78"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1257f4394be0d3b00de8c9e840ca5601d0a4a8438361ce9c2b05c7d25f6057b"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02272fe48280e0293a04245ca5d919b2c94a48b408b55e858feae9618138aeda"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dc3ad9ebc76eabe8b1d7c04d38be884b8f9d60c0cdc09b0aa4e3bcf746de0388"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:32e16bdeffa7c4f46bf2152172ca511808b952701d13e7c18833c0b73cb5c23f"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fb16ba563d59003028b678d2361a27f7e4ae0ab29c7a80690efa20d829c81fdb"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:2277c36d2d6cdb7876c274547921a42425b6810d38354327dd65a8009acf870c"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cf75d32e8d250781940d07f7eece253f2fe9ecdb1dc7ba6e3833fa17b82fcbc"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b3b31884dc8e9b21508bb76da80ebf7308fdb947a17affce815665d5c4d028"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10122d8d8e30afb43bb1fe21a3619f62c3e2574bff2699cf8af8b0b6c5dc4a3"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d88b96ff0fe8e91f6ef01ba50b0d71db5017fa4e3b1d99681cec89a85faf7bf7"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:37aaec5a52e959892870a7c47cef80c53797c0db9149d458460f4f31e2fb250e"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2ea752f2b0fe96eb6e2f3adbbf4d72aaa1272079b0dfa1145507bd6a5d537e6"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b19a808d8799fda23504a5cd31d2f58e6f52f140380082b352f877017d6342b"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c86e5e068ac8b19204419ed8ca90f9d25db20578f5881e337d203b314f4104"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de19c4dc503c612847edf833c82e9f73cd79926a384af9d801dcf93f110cea4e"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea09acd2fe3324174063d61ad620dec3bcf042b495515f27f638270a7d466e8b"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cf27fd43472e07b57cf420eee1e814549203d56de00b5af8659cb99885472f1f"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7ca22bd897537a0080521445d91a58886c8c04084a6a19e6c78c586e0cfa92a5"}, + {file = "tokenizers-0.15.2.tar.gz", hash = "sha256:e6e9c6e019dd5484be5beafc775ae6c925f4c69a3487040ed09b45e13df2cb91"}, +] + +[package.dependencies] +huggingface_hub = ">=0.16.4,<1.0" + +[package.extras] +dev = ["tokenizers[testing]"] +docs = ["setuptools_rust", "sphinx", "sphinx_rtd_theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"] + [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -462,14 +1154,13 @@ files = [ [[package]] name = "tqdm" -version = "4.66.1" +version = "4.66.2" description = "Fast, Extensible Progress Meter" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, - {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, ] [package.dependencies] @@ -482,22 +1173,48 @@ slack = ["slack-sdk"] telegram = ["requests"] [[package]] -name = "typing-extensions" -version = "4.8.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" +name = "types-pillow" +version = "10.2.0.20240520" +description = "Typing stubs for Pillow" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "types-Pillow-10.2.0.20240520.tar.gz", hash = "sha256:130b979195465fa1e1676d8e81c9c7c30319e8e95b12fae945e8f0d525213107"}, + {file = "types_Pillow-10.2.0.20240520-py3-none-any.whl", hash = "sha256:33c36494b380e2a269bb742181bea5d9b00820367822dbd3760f07210a1da23d"}, ] +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.25.0" description = "The lightning-fast ASGI server." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -513,11 +1230,30 @@ typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "virtualenv" +version = "20.25.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "websockets" version = "12.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -598,4 +1334,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "00950515c8d7f2061811ce196e4aa47204eb98ea078bbeb875548072dbfa38b1" +content-hash = "5c21a836f7db5b25b6d0aa971bc04c9a935444be57b3fbf83b8316c46f261e24" diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py index 4f2e329..dc96ab9 100644 --- a/backend/prompts/__init__.py +++ b/backend/prompts/__init__.py @@ -1,6 +1,7 @@ from typing import List, NoReturn, Union from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam +from llm import Llm from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS from prompts.screenshot_system_prompts import SYSTEM_PROMPTS @@ -17,7 +18,7 @@ Generate code for a SVG that looks exactly like this. def assemble_imported_code_prompt( - code: str, stack: Stack, result_image_data_url: Union[str, None] = None + code: str, stack: Stack, model: Llm ) -> List[ChatCompletionMessageParam]: system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack] @@ -26,16 +27,25 @@ def assemble_imported_code_prompt( if stack != "svg" else "Here is the code of the SVG: " + code ) - return [ - { - "role": "system", - "content": system_content, - }, - { - "role": "user", - "content": user_content, - }, - ] + + if model == Llm.CLAUDE_3_5_SONNET_2024_06_20: + return [ + { + "role": "system", + "content": system_content + "\n " + user_content, + } + ] + else: + return [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": user_content, + }, + ] # TODO: Use result_image_data_url diff --git a/backend/prompts/claude_prompts.py b/backend/prompts/claude_prompts.py new file mode 100644 index 0000000..3519e49 --- /dev/null +++ b/backend/prompts/claude_prompts.py @@ -0,0 +1,114 @@ +# Not used yet +# References: +# https://github.com/hundredblocks/transcription_demo +# https://docs.anthropic.com/claude/docs/prompt-engineering +# https://github.com/anthropics/anthropic-cookbook/blob/main/multimodal/best_practices_for_vision.ipynb + +VIDEO_PROMPT = """ +You are an expert at building single page, funtional apps using HTML, Jquery and Tailwind CSS. +You also have perfect vision and pay great attention to detail. + +You will be given screenshots in order at consistent intervals from a video of a user interacting with a web app. You need to re-create the same app exactly such that the same user interactions will produce the same results in the app you build. + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- If some fuctionality requires a backend call, just mock the data instead. +- MAKE THE APP FUNCTIONAL using Javascript. Allow the user to interact with the app and get the same behavior as the video. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: +- Use jQuery: + +Before generating the code for the app, think step-by-step: first, about the user flow depicated in the video and then about you how would you build it and how you would structure the code. Do the thinking within tags. Then, provide your code within tags. +""" + +VIDEO_PROMPT_ALPINE_JS = """ +You are an expert at building single page, funtional apps using HTML, Alpine.js and Tailwind CSS. +You also have perfect vision and pay great attention to detail. + +You will be given screenshots in order at consistent intervals from a video of a user interacting with a web app. You need to re-create the same app exactly such that the same user interactions will produce the same results in the app you build. + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- If some fuctionality requires a backend call, just mock the data instead. +- MAKE THE APP FUNCTIONAL using Javascript. Allow the user to interact with the app and get the same behavior as the video. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: +- Use Alpine.js: + +Before generating the code for the app, think step-by-step: first, about the user flow depicated in the video and then about you how would you build it and how you would structure the code. Do the thinking within tags. Then, provide your code within tags. +""" + + +HTML_TAILWIND_CLAUDE_SYSTEM_PROMPT = """ +You have perfect vision and pay great attention to detail which makes you an expert at building single page apps using Tailwind, HTML and JS. +You take screenshots of a reference web page from the user, and then build single page apps +using Tailwind, HTML and JS. +You might also be given a screenshot (The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Do not leave out smaller UI elements. Make sure to include every single thing in the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- In particular, pay attention to background color and overall color scheme. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Make sure to always get the layout right (if things are arranged in a row in the screenshot, they should be in a row in the app as well) +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +# + +REACT_TAILWIND_CLAUDE_SYSTEM_PROMPT = """ +You have perfect vision and pay great attention to detail which makes you an expert at building single page apps using React/Tailwind. +You take screenshots of a reference web page from the user, and then build single page apps +using React and Tailwind CSS. +You might also be given a screenshot (The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Do not leave out smaller UI elements. Make sure to include every single thing in the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- In particular, pay attention to background color and overall color scheme. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Make sure to always get the layout right (if things are arranged in a row in the screenshot, they should be in a row in the app as well) +- CREATE REUSABLE COMPONENTS FOR REPEATING ELEMENTS. For example, if there are 15 similar items in the screenshot, your code should include a reusable component that generates these items. and use loops to instantiate these components as needed. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" diff --git a/backend/prompts/test_prompts.py b/backend/prompts/test_prompts.py index 9e60410..42e89c3 100644 --- a/backend/prompts/test_prompts.py +++ b/backend/prompts/test_prompts.py @@ -311,35 +311,35 @@ def test_prompts(): tailwind_prompt = assemble_prompt( "image_data_url", "html_tailwind", "result_image_data_url" ) - assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT + assert tailwind_prompt[0].get("content") == TAILWIND_SYSTEM_PROMPT assert tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore react_tailwind_prompt = assemble_prompt( "image_data_url", "react_tailwind", "result_image_data_url" ) - assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT + assert react_tailwind_prompt[0].get("content") == REACT_TAILWIND_SYSTEM_PROMPT assert react_tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore bootstrap_prompt = assemble_prompt( "image_data_url", "bootstrap", "result_image_data_url" ) - assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT + assert bootstrap_prompt[0].get("content") == BOOTSTRAP_SYSTEM_PROMPT assert bootstrap_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore ionic_tailwind = assemble_prompt( "image_data_url", "ionic_tailwind", "result_image_data_url" ) - assert ionic_tailwind[0]["content"] == IONIC_TAILWIND_SYSTEM_PROMPT + assert ionic_tailwind[0].get("content") == IONIC_TAILWIND_SYSTEM_PROMPT assert ionic_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore vue_tailwind = assemble_prompt( "image_data_url", "vue_tailwind", "result_image_data_url" ) - assert vue_tailwind[0]["content"] == VUE_TAILWIND_SYSTEM_PROMPT + assert vue_tailwind[0].get("content") == VUE_TAILWIND_SYSTEM_PROMPT assert vue_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore svg_prompt = assemble_prompt("image_data_url", "svg", "result_image_data_url") - assert svg_prompt[0]["content"] == SVG_SYSTEM_PROMPT + assert svg_prompt[0].get("content") == SVG_SYSTEM_PROMPT assert svg_prompt[1]["content"][2]["text"] == SVG_USER_PROMPT # type: ignore diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bdaaed1..a60c22f 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -14,10 +14,15 @@ openai = "^1.2.4" python-dotenv = "^1.0.0" beautifulsoup4 = "^4.12.2" httpx = "^0.25.1" +pre-commit = "^3.6.2" +anthropic = "^0.18.0" +moviepy = "^1.0.3" +pillow = "^10.3.0" +types-pillow = "^10.2.0.20240520" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" -pyright = "^1.1.345" +pyright = "^1.1.352" [build-system] requires = ["poetry-core"] diff --git a/backend/routes/evals.py b/backend/routes/evals.py index 798a9d8..22262cd 100644 --- a/backend/routes/evals.py +++ b/backend/routes/evals.py @@ -7,10 +7,13 @@ from evals.config import EVALS_DIR router = APIRouter() +# Update this if the number of outputs generated per input changes +N = 1 + class Eval(BaseModel): input: str - output: str + outputs: list[str] @router.get("/evals") @@ -25,21 +28,27 @@ async def get_evals(): input_file_path = os.path.join(input_dir, file) input_file = await image_to_data_url(input_file_path) - # Construct the corresponding output file name - output_file_name = file.replace(".png", ".html") - output_file_path = os.path.join(output_dir, output_file_name) + # Construct the corresponding output file names + output_file_names = [ + file.replace(".png", f"_{i}.html") for i in range(0, N) + ] # Assuming 3 outputs for each input - # Check if the output file exists - if os.path.exists(output_file_path): - with open(output_file_path, "r") as f: - output_file_data = f.read() - else: - output_file_data = "Output file not found." + output_files_data: list[str] = [] + for output_file_name in output_file_names: + output_file_path = os.path.join(output_dir, output_file_name) + # Check if the output file exists + if os.path.exists(output_file_path): + with open(output_file_path, "r") as f: + output_files_data.append(f.read()) + else: + output_files_data.append( + "

Output file not found.

" + ) evals.append( Eval( input=input_file, - output=output_file_data, + outputs=output_files_data, ) ) diff --git a/backend/routes/generate_code.py b/backend/routes/generate_code.py index 092289c..6426f20 100644 --- a/backend/routes/generate_code.py +++ b/backend/routes/generate_code.py @@ -2,19 +2,30 @@ import os import traceback from fastapi import APIRouter, WebSocket import openai -from config import IS_PROD, SHOULD_MOCK_AI_RESPONSE -from llm import stream_openai_response +from codegen.utils import extract_html_content +from config import ANTHROPIC_API_KEY, IS_PROD, SHOULD_MOCK_AI_RESPONSE +from custom_types import InputMode +from llm import ( + Llm, + convert_frontend_str_to_llm, + stream_claude_response, + stream_claude_response_native, + stream_openai_response, +) from openai.types.chat import ChatCompletionMessageParam from mock_llm import mock_completion -from typing import Dict, List, cast, get_args +from typing import Dict, List, Union, cast, get_args from image_generation import create_alt_url_mapping, generate_images from prompts import assemble_imported_code_prompt, assemble_prompt -from access_token import validate_access_token from datetime import datetime import json +from prompts.claude_prompts import VIDEO_PROMPT from prompts.types import Stack +from utils import pprint_prompt -from utils import pprint_prompt # type: ignore +# from utils import pprint_prompt +from video.utils import extract_tag_content, assemble_claude_prompt_video +from ws.constants import APP_ERROR_WEB_SOCKET_CODE # type: ignore router = APIRouter() @@ -49,7 +60,7 @@ async def stream_code(websocket: WebSocket): message: str, ): await websocket.send_json({"type": "error", "value": message}) - await websocket.close() + await websocket.close(APP_ERROR_WEB_SOCKET_CODE) # TODO: Are the values always strings? params: Dict[str, str] = await websocket.receive_json() @@ -60,52 +71,70 @@ async def stream_code(websocket: WebSocket): generated_code_config = "" if "generatedCodeConfig" in params and params["generatedCodeConfig"]: generated_code_config = params["generatedCodeConfig"] - print(f"Generating {generated_code_config} code") - - # Get the OpenAI API key from the request. Fall back to environment variable if not provided. - # If neither is provided, we throw an error. - openai_api_key = None - if "accessCode" in params and params["accessCode"]: - print("Access code - using platform API key") - res = await validate_access_token(params["accessCode"]) - if res["success"]: - openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY") - else: - await websocket.send_json( - { - "type": "error", - "value": res["failure_reason"], - } - ) - return - else: - if params["openAiApiKey"]: - openai_api_key = params["openAiApiKey"] - print("Using OpenAI API key from client-side settings dialog") - else: - openai_api_key = os.environ.get("OPENAI_API_KEY") - if openai_api_key: - print("Using OpenAI API key from environment variable") - - if not openai_api_key: - print("OpenAI API key not found") - await websocket.send_json( - { - "type": "error", - "value": "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file. If you add it to .env, make sure to restart the backend server.", - } - ) - return - - # Validate the generated code config if not generated_code_config in get_args(Stack): await throw_error(f"Invalid generated code config: {generated_code_config}") return # Cast the variable to the Stack type valid_stack = cast(Stack, generated_code_config) + # Validate the input mode + input_mode = params.get("inputMode") + if not input_mode in get_args(InputMode): + await throw_error(f"Invalid input mode: {input_mode}") + raise Exception(f"Invalid input mode: {input_mode}") + # Cast the variable to the right type + validated_input_mode = cast(InputMode, input_mode) + + # Read the model from the request. Fall back to default if not provided. + code_generation_model_str = params.get( + "codeGenerationModel", Llm.GPT_4O_2024_05_13.value + ) + try: + code_generation_model = convert_frontend_str_to_llm(code_generation_model_str) + except: + await throw_error(f"Invalid model: {code_generation_model_str}") + raise Exception(f"Invalid model: {code_generation_model_str}") + exact_llm_version = None + + print( + f"Generating {generated_code_config} code for uploaded {input_mode} using {code_generation_model} model..." + ) + + # Get the OpenAI API key from the request. Fall back to environment variable if not provided. + # If neither is provided, we throw an error. + openai_api_key = None + if params["openAiApiKey"]: + openai_api_key = params["openAiApiKey"] + print("Using OpenAI API key from client-side settings dialog") + else: + openai_api_key = os.environ.get("OPENAI_API_KEY") + if openai_api_key: + print("Using OpenAI API key from environment variable") + + if not openai_api_key and ( + code_generation_model == Llm.GPT_4_VISION + or code_generation_model == Llm.GPT_4_TURBO_2024_04_09 + or code_generation_model == Llm.GPT_4O_2024_05_13 + ): + print("OpenAI API key not found") + await throw_error( + "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file. If you add it to .env, make sure to restart the backend server." + ) + return + + # Get the Anthropic API key from the request. Fall back to environment variable if not provided. + # If neither is provided, we throw an error later only if Claude is used. + anthropic_api_key = None + if "anthropicApiKey" in params and params["anthropicApiKey"]: + anthropic_api_key = params["anthropicApiKey"] + print("Using Anthropic API key from client-side settings dialog") + else: + anthropic_api_key = ANTHROPIC_API_KEY + if anthropic_api_key: + print("Using Anthropic API key from environment variable") + # Get the OpenAI Base URL from the request. Fall back to environment variable if not provided. - openai_base_url = None + openai_base_url: Union[str, None] = None # Disable user-specified OpenAI Base URL in prod if not os.environ.get("IS_PROD"): if "openAiBaseURL" in params and params["openAiBaseURL"]: @@ -139,7 +168,7 @@ async def stream_code(websocket: WebSocket): if params.get("isImportedFromCode") and params["isImportedFromCode"]: original_imported_code = params["history"][0] prompt_messages = assemble_imported_code_prompt( - original_imported_code, valid_stack + original_imported_code, valid_stack, code_generation_model ) for index, text in enumerate(params["history"][1:]): if index % 2 == 0: @@ -190,18 +219,60 @@ async def stream_code(websocket: WebSocket): image_cache = create_alt_url_mapping(params["history"][-2]) - pprint_prompt(prompt_messages) + if validated_input_mode == "video": + video_data_url = params["image"] + prompt_messages = await assemble_claude_prompt_video(video_data_url) + + # pprint_prompt(prompt_messages) # type: ignore if SHOULD_MOCK_AI_RESPONSE: - completion = await mock_completion(process_chunk) + completion = await mock_completion( + process_chunk, input_mode=validated_input_mode + ) else: try: - completion = await stream_openai_response( - prompt_messages, - api_key=openai_api_key, - base_url=openai_base_url, - callback=lambda x: process_chunk(x), - ) + if validated_input_mode == "video": + if not anthropic_api_key: + await throw_error( + "Video only works with Anthropic models. No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog" + ) + raise Exception("No Anthropic key") + + completion = await stream_claude_response_native( + system_prompt=VIDEO_PROMPT, + messages=prompt_messages, # type: ignore + api_key=anthropic_api_key, + callback=lambda x: process_chunk(x), + model=Llm.CLAUDE_3_OPUS, + include_thinking=True, + ) + exact_llm_version = Llm.CLAUDE_3_OPUS + elif ( + code_generation_model == Llm.CLAUDE_3_SONNET + or code_generation_model == Llm.CLAUDE_3_5_SONNET_2024_06_20 + ): + if not anthropic_api_key: + await throw_error( + "No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog" + ) + raise Exception("No Anthropic key") + + completion = await stream_claude_response( + prompt_messages, # type: ignore + api_key=anthropic_api_key, + callback=lambda x: process_chunk(x), + model=code_generation_model, + ) + exact_llm_version = code_generation_model + else: + completion = await stream_openai_response( + prompt_messages, # type: ignore + api_key=openai_api_key, + base_url=openai_base_url, + callback=lambda x: process_chunk(x), + model=code_generation_model, + ) + exact_llm_version = code_generation_model except openai.AuthenticationError as e: print("[GENERATE_CODE] Authentication failed", e) error_message = ( @@ -237,8 +308,16 @@ async def stream_code(websocket: WebSocket): ) return await throw_error(error_message) + if validated_input_mode == "video": + completion = extract_tag_content("html", completion) + + print("Exact used model for generation: ", exact_llm_version) + + # Strip the completion of everything except the HTML content + completion = extract_html_content(completion) + # Write the messages dict into a log so that we can debug later - write_logs(prompt_messages, completion) + write_logs(prompt_messages, completion) # type: ignore try: if should_generate_images: diff --git a/backend/run_evals.py b/backend/run_evals.py index ddb7eaa..bbf355a 100644 --- a/backend/run_evals.py +++ b/backend/run_evals.py @@ -1,6 +1,8 @@ # Load environment variables first from dotenv import load_dotenv +from llm import Llm + load_dotenv() import os @@ -11,6 +13,10 @@ from evals.config import EVALS_DIR from evals.core import generate_code_core from evals.utils import image_to_data_url +STACK = "ionic_tailwind" +MODEL = Llm.GPT_4O_2024_05_13 +N = 1 # Number of outputs to generate + async def main(): INPUT_DIR = EVALS_DIR + "/inputs" @@ -23,16 +29,21 @@ async def main(): for filename in evals: filepath = os.path.join(INPUT_DIR, filename) data_url = await image_to_data_url(filepath) - task = generate_code_core(data_url, "vue_tailwind") - tasks.append(task) + for _ in range(N): # Generate N tasks for each input + task = generate_code_core(image_url=data_url, stack=STACK, model=MODEL) + tasks.append(task) results = await asyncio.gather(*tasks) os.makedirs(OUTPUT_DIR, exist_ok=True) - for filename, content in zip(evals, results): - # File name is derived from the original filename in evals - output_filename = f"{os.path.splitext(filename)[0]}.html" + for i, content in enumerate(results): + # Calculate index for filename and output number + eval_index = i // N + output_number = i % N + filename = evals[eval_index] + # File name is derived from the original filename in evals with an added output number + output_filename = f"{os.path.splitext(filename)[0]}_{output_number}.html" output_filepath = os.path.join(OUTPUT_DIR, output_filename) with open(output_filepath, "w") as file: file.write(content) diff --git a/backend/test_llm.py b/backend/test_llm.py new file mode 100644 index 0000000..aeb02ab --- /dev/null +++ b/backend/test_llm.py @@ -0,0 +1,41 @@ +import unittest +from llm import convert_frontend_str_to_llm, Llm + + +class TestConvertFrontendStrToLlm(unittest.TestCase): + def test_convert_valid_strings(self): + self.assertEqual( + convert_frontend_str_to_llm("gpt_4_vision"), + Llm.GPT_4_VISION, + "Should convert 'gpt_4_vision' to Llm.GPT_4_VISION", + ) + self.assertEqual( + convert_frontend_str_to_llm("claude_3_sonnet"), + Llm.CLAUDE_3_SONNET, + "Should convert 'claude_3_sonnet' to Llm.CLAUDE_3_SONNET", + ) + self.assertEqual( + convert_frontend_str_to_llm("claude-3-opus-20240229"), + Llm.CLAUDE_3_OPUS, + "Should convert 'claude-3-opus-20240229' to Llm.CLAUDE_3_OPUS", + ) + self.assertEqual( + convert_frontend_str_to_llm("gpt-4-turbo-2024-04-09"), + Llm.GPT_4_TURBO_2024_04_09, + "Should convert 'gpt-4-turbo-2024-04-09' to Llm.GPT_4_TURBO_2024_04_09", + ) + self.assertEqual( + convert_frontend_str_to_llm("gpt-4o-2024-05-13"), + Llm.GPT_4O_2024_05_13, + "Should convert 'gpt-4o-2024-05-13' to Llm.GPT_4O_2024_05_13", + ) + + def test_convert_invalid_string_raises_exception(self): + with self.assertRaises(ValueError): + convert_frontend_str_to_llm("invalid_string") + with self.assertRaises(ValueError): + convert_frontend_str_to_llm("another_invalid_string") + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/video/utils.py b/backend/video/utils.py new file mode 100644 index 0000000..dc772fc --- /dev/null +++ b/backend/video/utils.py @@ -0,0 +1,134 @@ +# Extract HTML content from the completion string +import base64 +import io +import mimetypes +import os +import tempfile +import uuid +from typing import Any, Union, cast +from moviepy.editor import VideoFileClip # type: ignore +from PIL import Image +import math + + +DEBUG = True +TARGET_NUM_SCREENSHOTS = ( + 20 # Should be max that Claude supports (20) - reduce to save tokens on testing +) + + +async def assemble_claude_prompt_video(video_data_url: str) -> list[Any]: + images = split_video_into_screenshots(video_data_url) + + # Save images to tmp if we're debugging + if DEBUG: + save_images_to_tmp(images) + + # Validate number of images + print(f"Number of frames extracted from video: {len(images)}") + if len(images) > 20: + print(f"Too many screenshots: {len(images)}") + raise ValueError("Too many screenshots extracted from video") + + # Convert images to the message format for Claude + content_messages: list[dict[str, Union[dict[str, str], str]]] = [] + for image in images: + + # Convert Image to buffer + buffered = io.BytesIO() + image.save(buffered, format="JPEG") + + # Encode bytes as base64 + base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8") + media_type = "image/jpeg" + + content_messages.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": base64_data, + }, + } + ) + + return [ + { + "role": "user", + "content": content_messages, + }, + ] + + +# Returns a list of images/frame (RGB format) +def split_video_into_screenshots(video_data_url: str) -> list[Image.Image]: + target_num_screenshots = TARGET_NUM_SCREENSHOTS + + # Decode the base64 URL to get the video bytes + video_encoded_data = video_data_url.split(",")[1] + video_bytes = base64.b64decode(video_encoded_data) + + mime_type = video_data_url.split(";")[0].split(":")[1] + suffix = mimetypes.guess_extension(mime_type) + + with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as temp_video_file: + print(temp_video_file.name) + temp_video_file.write(video_bytes) + temp_video_file.flush() + clip = VideoFileClip(temp_video_file.name) + images: list[Image.Image] = [] + total_frames = cast(int, clip.reader.nframes) # type: ignore + + # Calculate frame skip interval by dividing total frames by the target number of screenshots + # Ensuring a minimum skip of 1 frame + frame_skip = max(1, math.ceil(total_frames / target_num_screenshots)) + + # Iterate over each frame in the clip + for i, frame in enumerate(clip.iter_frames()): + # Save every nth frame + if i % frame_skip == 0: + frame_image = Image.fromarray(frame) # type: ignore + images.append(frame_image) + # Ensure that we don't capture more than the desired number of frames + if len(images) >= target_num_screenshots: + break + + # Close the video file to release resources + clip.close() + + return images + + +# Save a list of PIL images to a random temporary directory +def save_images_to_tmp(images: list[Image.Image]): + + # Create a unique temporary directory + unique_dir_name = f"screenshots_{uuid.uuid4()}" + tmp_screenshots_dir = os.path.join(tempfile.gettempdir(), unique_dir_name) + os.makedirs(tmp_screenshots_dir, exist_ok=True) + + for idx, image in enumerate(images): + # Generate a unique image filename using index + image_filename = f"screenshot_{idx}.jpg" + tmp_filepath = os.path.join(tmp_screenshots_dir, image_filename) + image.save(tmp_filepath, format="JPEG") + + print("Saved to " + tmp_screenshots_dir) + + +def extract_tag_content(tag: str, text: str) -> str: + """ + Extracts content for a given tag from the provided text. + + :param tag: The tag to search for. + :param text: The text to search within. + :return: The content found within the tag, if any. + """ + tag_start = f"<{tag}>" + tag_end = f"" + start_idx = text.find(tag_start) + end_idx = text.find(tag_end, start_idx) + if start_idx != -1 and end_idx != -1: + return text[start_idx : end_idx + len(tag_end)] + return "" diff --git a/backend/video_to_app.py b/backend/video_to_app.py new file mode 100644 index 0000000..c876804 --- /dev/null +++ b/backend/video_to_app.py @@ -0,0 +1,122 @@ +# Load environment variables first + +from dotenv import load_dotenv + +load_dotenv() + + +import base64 +import mimetypes +import time +import subprocess +import os +import asyncio +from datetime import datetime +from prompts.claude_prompts import VIDEO_PROMPT +from utils import pprint_prompt +from config import ANTHROPIC_API_KEY +from video.utils import extract_tag_content, assemble_claude_prompt_video +from llm import ( + Llm, + stream_claude_response_native, +) + +STACK = "html_tailwind" + +VIDEO_DIR = "./video_evals/videos" +SCREENSHOTS_DIR = "./video_evals/screenshots" +OUTPUTS_DIR = "./video_evals/outputs" + + +async def main(): + video_filename = "shortest.mov" + is_followup = False + + if not ANTHROPIC_API_KEY: + raise ValueError("ANTHROPIC_API_KEY is not set") + + # Get previous HTML + previous_html = "" + if is_followup: + previous_html_file = max( + [ + os.path.join(OUTPUTS_DIR, f) + for f in os.listdir(OUTPUTS_DIR) + if f.endswith(".html") + ], + key=os.path.getctime, + ) + with open(previous_html_file, "r") as file: + previous_html = file.read() + + video_file = os.path.join(VIDEO_DIR, video_filename) + mime_type = mimetypes.guess_type(video_file)[0] + with open(video_file, "rb") as file: + video_content = file.read() + video_data_url = ( + f"data:{mime_type};base64,{base64.b64encode(video_content).decode('utf-8')}" + ) + + prompt_messages = await assemble_claude_prompt_video(video_data_url) + + # Tell the model to continue + # {"role": "assistant", "content": SECOND_MESSAGE}, + # {"role": "user", "content": "continue"}, + + if is_followup: + prompt_messages += [ + {"role": "assistant", "content": previous_html}, + { + "role": "user", + "content": "You've done a good job with a first draft. Improve this further based on the original instructions so that the app is fully functional like in the original video.", + }, + ] # type: ignore + + async def process_chunk(content: str): + print(content, end="", flush=True) + + response_prefix = "" + + pprint_prompt(prompt_messages) # type: ignore + + start_time = time.time() + + completion = await stream_claude_response_native( + system_prompt=VIDEO_PROMPT, + messages=prompt_messages, + api_key=ANTHROPIC_API_KEY, + callback=lambda x: process_chunk(x), + model=Llm.CLAUDE_3_OPUS, + include_thinking=True, + ) + + end_time = time.time() + + # Prepend the response prefix to the completion + completion = response_prefix + completion + + # Extract the outputs + html_content = extract_tag_content("html", completion) + thinking = extract_tag_content("thinking", completion) + + print(thinking) + print(f"Operation took {end_time - start_time} seconds") + + os.makedirs(OUTPUTS_DIR, exist_ok=True) + + # Generate a unique filename based on the current time + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + filename = f"video_test_output_{timestamp}.html" + output_path = os.path.join(OUTPUTS_DIR, filename) + + # Write the HTML content to the file + with open(output_path, "w") as file: + file.write(html_content) + + print(f"Output file path: {output_path}") + + # Show a notification + subprocess.run(["osascript", "-e", 'display notification "Coding Complete"']) + + +asyncio.run(main()) diff --git a/backend/ws/__init__.py b/backend/ws/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/ws/constants.py b/backend/ws/constants.py new file mode 100644 index 0000000..e992ecb --- /dev/null +++ b/backend/ws/constants.py @@ -0,0 +1,2 @@ +# WebSocket protocol (RFC 6455) allows for the use of custom close codes in the range 4000-4999 +APP_ERROR_WEB_SOCKET_CODE = 4332 diff --git a/blog/evaluating-claude.md b/blog/evaluating-claude.md new file mode 100644 index 0000000..373a343 --- /dev/null +++ b/blog/evaluating-claude.md @@ -0,0 +1,59 @@ +# Claude 3 for converting screenshots to code + +Claude 3 dropped yesterday, claiming to rival GPT-4 on a wide variety of tasks. I maintain a very popular open source project called โ€œscreenshot-to-codeโ€ (this one!) that uses GPT-4 vision to convert screenshots/designs into clean code. Naturally, I was excited to see how good Claude 3 was at this task. + +**TLDR:** Claude 3 is on par with GPT-4 vision for screenshot to code, better in some ways but worse in others. + +## Evaluation Setup + +I donโ€™t know of a public benchmark for โ€œscreenshot to codeโ€ so I created simple evaluation setup for the purposes of testing: + +- **Evaluation Dataset**: 16 screenshots with a mix of UI elements, landing pages, dashboards and popular websites. +Screenshot 2024-03-05 at 3 05 52โ€ฏPM + +- **Evaluation Metric**: Replication accuracy, as in โ€œHow close does the generated code look to the screenshot?โ€ While there are other metrics that are important like code quality, speed and so on, this is by far the #1 thing most users of the repo care about. +- **Evaluation Mechanism**: Each output is subjectively rated by a human on a rating scale from 0 to 4. 4 = very close to an exact replica while 0 = nothing like the screenshot. With 16 screenshots, the maximum any model can score is 64. + + +To make the evaluation process easy, I created [a Python script](https://github.com/abi/screenshot-to-code/blob/main/backend/run_evals.py) that runs code for all the inputs in parallel. I also made a simple UI to do a side-by-side comparison of the input and output. + +![Google Chrome](https://github.com/abi/screenshot-to-code/assets/23818/38126f8f-205d-4ed1-b8cf-039e81dcc3d0) + + +## Results + +Quick note about what kind of code weโ€™ll be generating: currently, screenshot-to-code supports generating code in HTML + Tailwind, React, Vue, and several other frameworks. Stacks can impact the replication accuracy quite a bit. For example, because Bootstrap uses a relatively restrictive set of user elements, generations using Bootstrap tend to have a distinct "Bootstrap" style. + +I only ran the evals on HTML/Tailwind here which is the stack where GPT-4 vision tends to perform the best. + +Here are the results (average of 3 runs for each model): + +- GPT-4 Vision obtains a score of **65.10%** - this is what weโ€™re trying to beat +- Claude 3 Sonnet receives a score of **70.31%**, which is a bit better. +- Surprisingly, Claude 3 Opus which is supposed to be the smarter and slower model scores worse than both GPT-4 vision and Claude 3 Sonnet, comes in at **61.46%**. + +Overall, a very strong showing for Claude 3. Obviously, there's a lot of subjectivity involved in this evaluation but Claude 3 is definitely on par with GPT-4 Vision, if not better. + +You can see the [side-by-side comparison for a run of Claude 3 Sonnet here](https://github.com/abi/screenshot-to-code-files/blob/main/sonnet%20results.png). And for [a run of GPT-4 Vision here](https://github.com/abi/screenshot-to-code-files/blob/main/gpt%204%20vision%20results.png). + +Other notes: + +- The prompts used are optimized for GPT-4 vision. Adjusting the prompts a bit for Claude did yield a small improvement. But nothing game-changing and potentially not worth the trade-off of maintaining two sets of prompts. +- All the models excel at code quality - the quality is usually comparable to a human or better. +- Claude 3 is much less lazy than GPT-4 Vision. When asked to recreate Hacker News, GPT-4 Vision will only create two items in the list and leave comments in this code like `` and ``. +Screenshot 2024-03-05 at 9 25 04โ€ฏPM + +While Claude 3 Sonnet can sometimes be lazy too, most of the time, it does what you ask it to do. + +Screenshot 2024-03-05 at 9 30 23โ€ฏPM + +- For some reason, all the models struggle with side-by-side "flex" layouts +Screenshot 2024-03-05 at 9 20 58โ€ฏPM + +- Claude 3 Sonnet is a lot faster +- Claude 3 gets background and text colors wrong quite often! (like in the Hacker News image above) +- My suspicion is that Claude 3 Opus results can be improved to be on par with the other models through better prompting + +Overall, I'm very impressed with Claude 3 Sonnet for this use case. I've added it as an alternative to GPT-4 Vision in the open source repo (hosted version update coming soon). + +If youโ€™d like to contribute to this effort, I have some documentation on [running these evals yourself here](https://github.com/abi/screenshot-to-code/blob/main/Evaluation.md). I'm also working on a better evaluation mechanism with Elo ratings and would love some help on that. diff --git a/frontend/.gitignore b/frontend/.gitignore index 17ceca3..a0d3702 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -25,3 +25,6 @@ dist-ssr # Env files .env* + +# Test files +src/tests/results/ diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 938e2bf..8579e17 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.9-bullseye-slim +FROM node:22-bullseye-slim # Set the working directory in the container WORKDIR /app @@ -6,6 +6,9 @@ WORKDIR /app # Copy package.json and yarn.lock COPY package.json yarn.lock /app/ +# Set the environment variable to skip Puppeteer download +ENV PUPPETEER_SKIP_DOWNLOAD=true + # Install dependencies RUN yarn install @@ -16,4 +19,4 @@ COPY ./ /app/ EXPOSE 5173 # Command to run the application -CMD ["yarn", "dev", "--host", "0.0.0.0"] \ No newline at end of file +CMD ["yarn", "dev", "--host", "0.0.0.0"] diff --git a/frontend/components.json b/frontend/components.json index 88a9a6e..1cd599c 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -13,4 +13,4 @@ "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/frontend/index.html b/frontend/index.html index 2a7fa0e..39cfccc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,11 +2,7 @@ - + diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 0000000..310efb5 --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,9 @@ +export default { + preset: "ts-jest", + testEnvironment: "node", + setupFiles: ["/src/setupTests.ts"], + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + testTimeout: 30000, +}; diff --git a/frontend/package.json b/frontend/package.json index 8b4f0ee..4652dc7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "build-hosted": "tsc && vite build --mode prod", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", - "test": "vitest" + "test": "jest" }, "dependencies": { "@codemirror/lang-html": "^6.4.6", @@ -45,21 +45,29 @@ "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", "thememirror": "^2.0.1", - "vite-plugin-checker": "^0.6.2" + "vite-plugin-checker": "^0.6.2", + "webm-duration-fix": "^1.0.4", + "zustand": "^4.5.2" }, "devDependencies": { + "@types/jest": "^29.5.12", "@types/node": "^20.9.0", + "@types/puppeteer": "^7.0.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", "autoprefixer": "^10.4.16", + "dotenv": "^16.4.5", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "jest": "^29.7.0", "postcss": "^8.4.31", + "puppeteer": "^22.6.4", "tailwindcss": "^3.3.5", + "ts-jest": "^29.1.2", "typescript": "^5.0.2", "vite": "^4.4.5", "vite-plugin-html": "^3.2.0", diff --git a/frontend/public/favicon/coding.png b/frontend/public/favicon/coding.png new file mode 100644 index 0000000..71a6a0b Binary files /dev/null and b/frontend/public/favicon/coding.png differ diff --git a/frontend/public/favicon/main.png b/frontend/public/favicon/main.png new file mode 100644 index 0000000..4a32069 Binary files /dev/null and b/frontend/public/favicon/main.png differ diff --git a/frontend/src/.env.jest.example b/frontend/src/.env.jest.example new file mode 100644 index 0000000..59bc657 --- /dev/null +++ b/frontend/src/.env.jest.example @@ -0,0 +1,2 @@ +TEST_SCREENSHOTONE_API_KEY= +TEST_ROOT_PATH= diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3f7a025..bef14d0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,6 +35,13 @@ import { extractHistoryTree } from "./components/history/utils"; import toast from "react-hot-toast"; import ImportCodeSection from "./components/ImportCodeSection"; import { Stack } from "./lib/stacks"; +import { CodeGenerationModel } from "./lib/models"; +import ModelSettingsSection from "./components/ModelSettingsSection"; +import { extractHtml } from "./components/preview/extractHtml"; +import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; +import TipLink from "./components/core/TipLink"; +import SelectAndEditModeToggleButton from "./components/select-and-edit/SelectAndEditModeToggleButton"; +import { useAppStore } from "./store/app-store"; const IS_OPENAI_DOWN = false; @@ -42,27 +49,36 @@ function App() { const [appState, setAppState] = useState(AppState.INITIAL); const [generatedCode, setGeneratedCode] = useState(""); + const [inputMode, setInputMode] = useState<"image" | "video">("image"); + const [referenceImages, setReferenceImages] = useState([]); const [executionConsole, setExecutionConsole] = useState([]); const [updateInstruction, setUpdateInstruction] = useState(""); const [isImportedFromCode, setIsImportedFromCode] = useState(false); + const { disableInSelectAndEditMode } = useAppStore(); + // Settings const [settings, setSettings] = usePersistedState( { openAiApiKey: null, openAiBaseURL: null, + anthropicApiKey: null, screenshotOneApiKey: null, isImageGenerationEnabled: true, editorTheme: EditorTheme.COBALT, generatedCodeConfig: Stack.HTML_TAILWIND, + codeGenerationModel: CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20, // Only relevant for hosted version isTermOfServiceAccepted: false, - accessCode: null, }, "setting" ); + // Code generation model from local storage or the default value + const selectedCodeGenerationModel = + settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION; + // App history const [appHistory, setAppHistory] = useState([]); // Tracks the currently shown version from app history @@ -73,6 +89,26 @@ function App() { const wsRef = useRef(null); + const showReactWarning = + selectedCodeGenerationModel === + CodeGenerationModel.GPT_4_TURBO_2024_04_09 && + settings.generatedCodeConfig === Stack.REACT_TAILWIND; + + const showBetterModelMessage = + selectedCodeGenerationModel !== CodeGenerationModel.GPT_4O_2024_05_13 && + selectedCodeGenerationModel !== + CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20 && + appState === AppState.INITIAL; + + const showSelectAndEditFeature = + (selectedCodeGenerationModel === CodeGenerationModel.GPT_4O_2024_05_13 || + selectedCodeGenerationModel === + CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20) && + settings.generatedCodeConfig === Stack.HTML_TAILWIND; + + // Indicate coding state using the browser tab's favicon and title + useBrowserTabIndicator(appState === AppState.CODING); + // When the user already has the settings in local storage, newly added keys // do not get added to the settings so if it's falsy, we populate it with the default // value @@ -125,6 +161,26 @@ function App() { setAppHistory([]); setCurrentVersion(null); setShouldIncludeResultImage(false); + disableInSelectAndEditMode(); + }; + + const regenerate = () => { + if (currentVersion === null) { + toast.error( + "No current version set. Please open a Github issue as this shouldn't happen." + ); + return; + } + + // Retrieve the previous command + const previousCommand = appHistory[currentVersion]; + if (previousCommand.type !== "ai_create") { + toast.error("Only the first version can be regenerated."); + return; + } + + // Re-run the create + doCreate(referenceImages, inputMode); }; const cancelCodeGeneration = () => { @@ -133,6 +189,11 @@ function App() { cancelCodeGenerationAndReset(); }; + const previewCode = + inputMode === "video" && appState === AppState.CODING + ? extractHtml(generatedCode) + : generatedCode; + const cancelCodeGenerationAndReset = () => { // When this is the first version, reset the entire app state if (currentVersion === null) { @@ -189,7 +250,9 @@ function App() { parentIndex: parentVersion, code, inputs: { - prompt: updateInstruction, + prompt: params.history + ? params.history[params.history.length - 1] + : updateInstruction, }, }, ]; @@ -212,16 +275,18 @@ function App() { } // Initial version creation - function doCreate(referenceImages: string[]) { + function doCreate(referenceImages: string[], inputMode: "image" | "video") { // Reset any existing state reset(); setReferenceImages(referenceImages); + setInputMode(inputMode); if (referenceImages.length > 0) { doGenerateCode( { generationType: "create", image: referenceImages[0], + inputMode, }, currentVersion ); @@ -229,7 +294,15 @@ function App() { } // Subsequent updates - async function doUpdate() { + async function doUpdate( + updateInstruction: string, + selectedElement?: HTMLElement + ) { + if (updateInstruction.trim() === "") { + toast.error("Please include some instructions for AI on what to update."); + return; + } + if (currentVersion === null) { toast.error( "No current version set. Contact support or open a Github issue." @@ -247,13 +320,24 @@ function App() { return; } - const updatedHistory = [...historyTree, updateInstruction]; + let modifiedUpdateInstruction = updateInstruction; + + // Send in a reference to the selected element if it exists + if (selectedElement) { + modifiedUpdateInstruction = + updateInstruction + + " referring to this element specifically: " + + selectedElement.outerHTML; + } + + const updatedHistory = [...historyTree, modifiedUpdateInstruction]; if (shouldIncludeResultImage) { const resultImage = await takeScreenshot(); doGenerateCode( { generationType: "update", + inputMode, image: referenceImages[0], resultImage: resultImage, history: updatedHistory, @@ -265,6 +349,7 @@ function App() { doGenerateCode( { generationType: "update", + inputMode, image: referenceImages[0], history: updatedHistory, isImportedFromCode, @@ -291,6 +376,13 @@ function App() { })); } + function setCodeGenerationModel(codeGenerationModel: CodeGenerationModel) { + setSettings((prev) => ({ + ...prev, + codeGenerationModel, + })); + } + function importFromCode(code: string, stack: Stack) { setIsImportedFromCode(true); @@ -312,7 +404,7 @@ function App() { return (
- {IS_RUNNING_ON_CLOUD && } + {IS_RUNNING_ON_CLOUD && } {IS_RUNNING_ON_CLOUD && ( - {IS_RUNNING_ON_CLOUD && - !(settings.openAiApiKey || settings.accessCode) && ( - - )} + + + {showReactWarning && ( +
+ Sorry - React is not currently working with GPT-4 Turbo. Please + use GPT-4 Vision or Claude Sonnet. We are working on a fix. +
+ )} + + {showBetterModelMessage && ( +
+

+ Now supporting GPT-4o and Claude Sonnet 3.5. Higher quality and + 2x faster. Give it a try! +

+
+ )} + + {appState !== AppState.CODE_READY && } + + {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && } {IS_OPENAI_DOWN && (
@@ -352,11 +467,25 @@ function App() { {/* Show code preview only when coding */} {appState === AppState.CODING && (
+ {/* Speed disclaimer for video mode */} + {inputMode === "video" && ( +
+ Code generation from videos can take 3-4 minutes. We do + multiple passes to get the best result. Please be patient. +
+ )} +
{executionConsole.slice(-1)[0]}
-
+ + + +
-
)} @@ -387,26 +515,25 @@ function App() { />
-
+
- + {showSelectAndEditFeature && ( + + )} +
+
+
)} @@ -420,14 +547,27 @@ function App() { "scanning relative": appState === AppState.CODING, })} > - Reference + {inputMode === "image" && ( + Reference + )} + {inputMode === "video" && ( +
- Original Screenshot + {inputMode === "video" + ? "Original Video" + : "Original Screenshot"}
)} @@ -482,29 +622,59 @@ function App() { {(appState === AppState.CODING || appState === AppState.CODE_READY) && (
-
- - - Desktop - - - Mobile - - - - Code - - +
+
+ {appState === AppState.CODE_READY && ( + <> + + + + )} +
+
+ + + Desktop + + + Mobile + + + + Code + + +
- + - + diff --git a/frontend/src/components/ImageUpload.tsx b/frontend/src/components/ImageUpload.tsx index ff97466..bbecd92 100644 --- a/frontend/src/components/ImageUpload.tsx +++ b/frontend/src/components/ImageUpload.tsx @@ -3,6 +3,10 @@ import { useState, useEffect, useMemo } from "react"; import { useDropzone } from "react-dropzone"; // import { PromptImage } from "../../../types"; import { toast } from "react-hot-toast"; +import { URLS } from "../urls"; +import { Badge } from "./ui/badge"; +import ScreenRecorder from "./recording/ScreenRecorder"; +import { ScreenRecorderState } from "../types"; const baseStyle = { flex: 1, @@ -51,19 +55,31 @@ type FileWithPreview = { } & File; interface Props { - setReferenceImages: (referenceImages: string[]) => void; + setReferenceImages: ( + referenceImages: string[], + inputMode: "image" | "video" + ) => void; } function ImageUpload({ setReferenceImages }: Props) { const [files, setFiles] = useState([]); + // TODO: Switch to Zustand + const [screenRecorderState, setScreenRecorderState] = + useState(ScreenRecorderState.INITIAL); + const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } = useDropzone({ maxFiles: 1, - maxSize: 1024 * 1024 * 5, // 5 MB + maxSize: 1024 * 1024 * 20, // 20 MB accept: { + // Image formats "image/png": [".png"], "image/jpeg": [".jpeg"], "image/jpg": [".jpg"], + // Video formats + "video/quicktime": [".mov"], + "video/mp4": [".mp4"], + "video/webm": [".webm"], }, onDrop: (acceptedFiles) => { // Set up the preview thumbnail images @@ -78,7 +94,14 @@ function ImageUpload({ setReferenceImages }: Props) { // Convert images to data URLs and set the prompt images state Promise.all(acceptedFiles.map((file) => fileToDataURL(file))) .then((dataUrls) => { - setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); + if (dataUrls.length > 0) { + setReferenceImages( + dataUrls.map((dataUrl) => dataUrl as string), + (dataUrls[0] as string).startsWith("data:video") + ? "video" + : "image" + ); + } }) .catch((error) => { toast.error("Error reading files" + error); @@ -140,15 +163,34 @@ function ImageUpload({ setReferenceImages }: Props) { return (
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} -
- -

- Drag & drop a screenshot here,
- or paste from clipboard,
- or click to upload -

-
+ {screenRecorderState === ScreenRecorderState.INITIAL && ( + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +
+ +

+ Drag & drop a screenshot here,
+ or click to upload +

+
+ )} + {screenRecorderState === ScreenRecorderState.INITIAL && ( +
+ New! Upload a screen recording (.mp4, .mov) or record + your screen to clone a whole app (experimental).{" "} + + Learn more. + +
+ )} +
); } diff --git a/frontend/src/components/ImportCodeSection.tsx b/frontend/src/components/ImportCodeSection.tsx index b320a97..c31e753 100644 --- a/frontend/src/components/ImportCodeSection.tsx +++ b/frontend/src/components/ImportCodeSection.tsx @@ -38,7 +38,9 @@ function ImportCodeSection({ importFromCode }: Props) { return ( - + @@ -62,7 +64,7 @@ function ImportCodeSection({ importFromCode }: Props) { /> - diff --git a/frontend/src/components/ModelSettingsSection.tsx b/frontend/src/components/ModelSettingsSection.tsx new file mode 100644 index 0000000..4a4e7b9 --- /dev/null +++ b/frontend/src/components/ModelSettingsSection.tsx @@ -0,0 +1,65 @@ +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, +} from "./ui/select"; +import { + CODE_GENERATION_MODEL_DESCRIPTIONS, + CodeGenerationModel, +} from "../lib/models"; +import { Badge } from "./ui/badge"; + +interface Props { + codeGenerationModel: CodeGenerationModel; + setCodeGenerationModel: (codeGenerationModel: CodeGenerationModel) => void; + shouldDisableUpdates?: boolean; +} + +function ModelSettingsSection({ + codeGenerationModel, + setCodeGenerationModel, + shouldDisableUpdates = false, +}: Props) { + return ( +
+
+ AI Model: + +
+
+ ); +} + +export default ModelSettingsSection; diff --git a/frontend/src/components/PicoBadge.tsx b/frontend/src/components/PicoBadge.tsx index 70a4f20..bdd0083 100644 --- a/frontend/src/components/PicoBadge.tsx +++ b/frontend/src/components/PicoBadge.tsx @@ -1,6 +1,4 @@ -import { Settings } from "../types"; - -export function PicoBadge({ settings }: { settings: Settings }) { +export function PicoBadge() { return ( <> - {!settings.accessCode && ( - -
+
- an open source project by Pico -
-
- )} - {settings.accessCode && ( - -
- email support -
-
- )} + > + an open source project by Pico +
+ ); } diff --git a/frontend/src/components/Preview.tsx b/frontend/src/components/Preview.tsx index d2eff16..d601f78 100644 --- a/frontend/src/components/Preview.tsx +++ b/frontend/src/components/Preview.tsx @@ -1,24 +1,35 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import classNames from "classnames"; -// import useThrottle from "../hooks/useThrottle"; +import useThrottle from "../hooks/useThrottle"; +import EditPopup from "./select-and-edit/EditPopup"; interface Props { code: string; device: "mobile" | "desktop"; + doUpdate: (updateInstruction: string, selectedElement?: HTMLElement) => void; } -function Preview({ code, device }: Props) { - const throttledCode = code; - // Temporary disable throttling for the preview not updating when the code changes - // useThrottle(code, 200); +function Preview({ code, device, doUpdate }: Props) { const iframeRef = useRef(null); + // Don't update code more often than every 200ms. + const throttledCode = useThrottle(code, 200); + + // Select and edit functionality + const [clickEvent, setClickEvent] = useState(null); + useEffect(() => { const iframe = iframeRef.current; - if (iframe && iframe.contentDocument) { - iframe.contentDocument.open(); - iframe.contentDocument.write(throttledCode); - iframe.contentDocument.close(); + if (iframe) { + iframe.srcdoc = throttledCode; + + // Set up click handler for select and edit funtionality + iframe.addEventListener("load", function () { + iframe.contentWindow?.document.body.addEventListener( + "click", + setClickEvent + ); + }); } }, [throttledCode]); @@ -37,6 +48,7 @@ function Preview({ code, device }: Props) { } )} > +
); } diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index ea23bc8..97d8f38 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -22,7 +22,6 @@ import { AccordionItem, AccordionTrigger, } from "./ui/accordion"; -import AccessCodeSection from "./settings/AccessCodeSection"; interface Props { settings: Settings; @@ -47,15 +46,10 @@ function SettingsDialog({ settings, setSettings }: Props) { Settings - {/* Access code */} - {IS_RUNNING_ON_CLOUD && ( - - )} -