diff --git a/.gitignore b/.gitignore index 47a17f5..7b79637 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ # Ignore CSV files *.csv +# Ignore data folder +data/*.bak +data/*.json +data/*.jsonl + # Ignore SSH Keys *.pub group_key diff --git a/data/create_db.py b/archived/create_db.py similarity index 100% rename from data/create_db.py rename to archived/create_db.py diff --git a/data/populate_mongo_db.py b/archived/populate_mongo_db.py similarity index 100% rename from data/populate_mongo_db.py rename to archived/populate_mongo_db.py diff --git a/backend/configs/app_config.yml b/backend/configs/app_config.yml index 501d638..63b1d3b 100644 --- a/backend/configs/app_config.yml +++ b/backend/configs/app_config.yml @@ -4,4 +4,7 @@ default: max_session_cookie_age: 3600 same_site_protection: "lax" + mongo_anime_collection_name: "anime_enriched" + mongo_anime_db_name: "anizenith" + log_level: "info" \ No newline at end of file diff --git a/backend/configs/backend_config.py b/backend/configs/backend_config.py index fb661c2..9908734 100644 --- a/backend/configs/backend_config.py +++ b/backend/configs/backend_config.py @@ -27,7 +27,11 @@ class BackendAppConfig(Config): MAL_CLIENT_ID: str = os.getenv("MAL_CLIENT_ID", "") MAL_CLIENT_SECRET: str = os.getenv("MAL_CLIENT_SECRET", "") BACKEND_SECRET: str = os.getenv("BACKEND_SECRET", "") + + # MongoDB params ATLAS_URI: str = os.getenv("ATLAS_URI", "") + mongo_anime_collection_name: Optional[str] = None + mongo_anime_db_name: Optional[str] = None class ModelConfig(Config): """ @@ -38,6 +42,7 @@ class ModelConfig(Config): # Chatbot parameters local_model_id: Optional[str] = None external_model_id: Optional[str] = None + embedding_model_id: Optional[str] = None max_new_tokens: Optional[int] = None temperature: Optional[float] = None top_p: Optional[float] = None diff --git a/backend/configs/model_config.yml b/backend/configs/model_config.yml index 1e40c78..5100a3d 100644 --- a/backend/configs/model_config.yml +++ b/backend/configs/model_config.yml @@ -2,6 +2,7 @@ default: # Chatbot parameters local_model_id: "Qwen/Qwen3-0.6B" external_model_id: "openai/gpt-oss-20b" + embedding_model_id: "sentence-transformers/all-MiniLM-L6-v2" max_new_tokens: 2048 temperature: 0.7 top_p: 0.7 diff --git a/backend/mongo/AniZenithMongoClient.py b/backend/mongo/AniZenithMongoClient.py index ffee798..9ce9899 100644 --- a/backend/mongo/AniZenithMongoClient.py +++ b/backend/mongo/AniZenithMongoClient.py @@ -5,6 +5,7 @@ from backend.mongo.utils import create_text_metadata_and_embedding from backend.mongo.AniZenithVectorSearchResult import AniZenithVectorSearchResult +from backend.configs import backend_app_config, model_config # Class to model Anizenith MongoDB Client related utilities class AniZenithMongoClient: @@ -14,7 +15,7 @@ def __init__(self, conn_string): raise ValueError("ATLAS_URI must be set to a non-empty MongoDB connection string") self.conn_string = conn_string - self.embedding_model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') + self.embedding_model = SentenceTransformer(model_config.embedding_model_id) # Set internals to None for lazy init self._db_client = None @@ -38,8 +39,7 @@ def anime_collection(self): @property decorator defines this as a property of a class, rather than a class method """ if self._anime_collection is None: - # TODO: Move the hardcoded DB name and collection name into a central Config object - self._anime_collection = self.db_client["anizenith"]["anime"] + self._anime_collection = self.db_client[backend_app_config.mongo_anime_db_name][backend_app_config.mongo_anime_collection_name] return self._anime_collection @@ -55,10 +55,7 @@ def add_anime(self, anime_document: AnimeDocument) -> None: # Create a new document to be inserted into MongoDB anime_document_dict = { - "name": anime_document.name, - "score": anime_document.score, - "synopsis": anime_document.synopsis, - "genres": anime_document.genres, + **anime_document.model_dump(), "text_metadata": text_metadata, "text_metadata_embedding": text_metadata_embedding } diff --git a/backend/mongo/AnimeDocument.py b/backend/mongo/AnimeDocument.py index 240e2d1..7090a8a 100644 --- a/backend/mongo/AnimeDocument.py +++ b/backend/mongo/AnimeDocument.py @@ -1,9 +1,22 @@ from pydantic import BaseModel -from typing import List +from datetime import datetime +from typing import List, Dict # Class to Model a typical Anime Document class AnimeDocument(BaseModel): - name: str - score: float - synopsis: str - genres: List[str] \ No newline at end of file + mal_id: int # ID of anime on MyAnimeList + name: str # English title of anime + alt_titles: Dict[str, str] # { "en": ENGLISH_TITLE, "jp": JAPANESE_TITLE, ...} + score: float # MAL User Mean Score -- Typically 6-10 + synopsis: str # Short synopsis of shows, does not contain spoilers beyond first episode + genres: List[str] # Genres list not including demographic genre + demographic: str # Primary demographic + age_rating: str # g | pg | pg-13 | r | r+ | rx + cover_image_url: str # link to MAL image (not CDN) + date_aired: datetime # date aired as datetime (YYYY-MM-DD stored, not hr/mins/secs/ms) + status: str # finished_airing | currently_airing | not_aired + episode_count: int # number of episodes in the anime + avg_episode_len_mins: int # average duration per episode in mins + publishing_company: str # publishing company name | Unknown + recommendations: Dict[str, int] # {"TITLE1":#_OF_RECOMMENDATIONS, "TITLE2":#_OF_RECOMMENDATIONS, ...} + node_name: str # Title of anime (node) -- May not be English, but always matches recommendations title \ No newline at end of file diff --git a/backend/mongo/utils.py b/backend/mongo/utils.py index 68a67cc..71821a3 100644 --- a/backend/mongo/utils.py +++ b/backend/mongo/utils.py @@ -6,7 +6,8 @@ def create_text_metadata_and_embedding( anime_name: str, anime_genres: List[str], anime_synopsis: str - ) -> Tuple[str, str]: + ) -> Tuple[str, List]: + # TODO: Make this metadata better for retrieval # Create text_metadata field using synopsis, genres and name text_metadata = f"Synopsis: {anime_synopsis}\n\nGenres: {', '.join(anime_genres)}\n\nName: {anime_name}" diff --git a/data/anime.db b/data/anime.db deleted file mode 100644 index cdb0050..0000000 Binary files a/data/anime.db and /dev/null differ diff --git a/data/anime_scrape.py b/data/anime_scrape.py new file mode 100644 index 0000000..0668fcb --- /dev/null +++ b/data/anime_scrape.py @@ -0,0 +1,174 @@ +import json +import time +from datetime import datetime +from typing import List, Dict + +import pandas as pd +import requests + +from backend.configs import backend_app_config +from backend.mongo.AnimeDocument import AnimeDocument + +# Jikan is a web scraping REST API for anime data, but not all data can be available (specific anime search 100% uptime) +JIKAN_ENDPOINT = "https://api.jikan.moe/v4/anime" + +# MyAnimeList (MAL) is an anime database access REST API service with limited features +MAL_ENDPOINT = "https://api.myanimelist.net/v2/anime/ranking" +MAL_CLIENT_HEADER = {"X-MAL-CLIENT-ID": backend_app_config.MAL_CLIENT_ID} +MAL_FIELDS = "id,title,alternative_titles,mean,synopsis,genres,main_picture,start_date,status,studios,num_episodes,average_episode_duration,rating" + +# API limits +ANIME_PER_PAGE = 25 +JIKAN_RATE_LIMIT = 1.0 # Jikan: (~3 req/sec, 60 req/min) +MAL_RATE_LIMIT = 0.5 +MAL_MAX_PER_PAGE = 100 + +# Used for data cleaning +DEMOGRAPHIC_GENRES = {"Shounen", "Shoujo", "Seinen", "Josei", "Kids", "Demographic", "Shonen", "Shojo"} + +def _get_jikan_recommendations(mal_id: int, limit: int = 10) -> Dict[str, int]: + """Fetch anime user recommendations from Jikan.""" + url = f"{JIKAN_ENDPOINT}/{mal_id}/recommendations" + try: + resp = requests.get(url, timeout=10) + resp.raise_for_status() + data = resp.json().get("data", []) + return { + entry.get("entry", {}).get("title"): entry.get("votes", 0) + for entry in data[:limit] + } + except Exception: + return {} + +def _normalize_from_mal(item: Dict) -> Dict: + """Convert MAL API response (node) into a flat dict with required fields for an AnimeDocument.""" + node = item.get("node", item) + + # ----- Data Cleaning ----- + duration = node.get("average_episode_duration") + age_rating = node.get("rating", "") + studios = node.get("studios", []) + publishing_company = studios[0].get("name", "Unknown") if studios else "Unknown" + synopsis = node.get("synopsis").replace("[Written by MAL Rewrite]", "").strip() + date_aired = (datetime.strptime(node.get("start_date"), "%Y-%m-%d")) + + # Alt titles (in case main is not en) + alt_titles = node.get("alternative_titles") + alt_titles.pop("synonyms") + + # Extract genre + genre_list = [g["name"] for g in node.get("genres", [])] + genres = [g for g in genre_list if g not in DEMOGRAPHIC_GENRES] + demographic = next((g for g in genre_list if g in DEMOGRAPHIC_GENRES), "All") + + return { + "mal_id": node["id"], + "title": node.get("title"), + "alt_titles": alt_titles, + "score": node.get("mean"), + "synopsis": synopsis, + "genres": genres, + "demographic": demographic, + "cover_image_url": node.get("main_picture").get("medium", ""), + "date_aired": date_aired, + "status": node.get("status", "not_aired"), + "episode_count": node.get("num_episodes", 0), + "publishing_company": publishing_company, + "avg_episode_len_mins": int(duration // 60), + "age_rating": age_rating.split(" - ")[0] if age_rating else "Unknown", + } + +def _build_documents(mal_items: List[Dict], search_recommended: bool = False) -> List[AnimeDocument]: + """Convert MAL items into AnimeDocument, enriching with Jikan.""" + results = [] + + for raw in mal_items: + entry = _normalize_from_mal(raw) + + # Fetch recommendations if requested + recs = [] + if search_recommended: + time.sleep(JIKAN_RATE_LIMIT) + recs = _get_jikan_recommendations(entry["mal_id"]) + + try: + results.append( + AnimeDocument( + mal_id=entry["mal_id"], + node_name=entry["title"], + name=entry["title"], + alt_titles=entry["alt_titles"], + score=entry["score"], + synopsis=entry["synopsis"], + genres=entry["genres"], + demographic=entry["demographic"], + age_rating=entry["age_rating"], + cover_image_url=entry["cover_image_url"], + date_aired=entry["date_aired"], + status=entry["status"], + episode_count=entry["episode_count"], + avg_episode_len_mins=entry["avg_episode_len_mins"], + publishing_company=entry["publishing_company"], + recommendations=recs, + ) + ) + except Exception: + print(f"[SCRAPE ERR] Failed to append anime: {entry["title"]} ({entry['mal_id']})") + + return results + +# Fetch data from MAL +def _fetch_mal_page(ranking_type: str, page: int, limit: int) -> List[Dict]: + params = { + "ranking_type": ranking_type, + "limit": limit, + "offset": (page - 1) * limit, + "fields": MAL_FIELDS, + } + resp = requests.get(MAL_ENDPOINT, headers=MAL_CLIENT_HEADER, params=params, timeout=15) + resp.raise_for_status() + return resp.json().get("data", []) + +# Search API +def search_anime(sort_by: str = "score", n: int = 10, search_recommended: bool = False) -> List[AnimeDocument]: + """ + Search anime using MAL ranking, then enrich with Jikan. + + Args: + sort_by: 'score' (top rated) or 'popularity' (most members) + n: number of results + search_recommended: if True, fetch recommendations from Jikan per anime + Returns: + List[AnimeDocument] + """ + results = [] + page = 1 + + ranking_type = {"score": "all", "popularity": "bypopularity"}.get(sort_by, "all") + + with open("./data/anime_scrape.jsonl", "w", encoding="utf-8") as f: + while len(results) < n: + remaining = n - len(results) + limit = min(MAL_MAX_PER_PAGE, remaining) # Limit in case requesting more data than needed + print(f"[MAL INFO] Fetching page {page} sorted by {sort_by} (need {remaining} more items, requesting {limit} items)") + try: + data = _fetch_mal_page(ranking_type, page, limit) + documents = _build_documents(data, search_recommended) + [f.write(json.dumps(doc.model_dump(), ensure_ascii=False) + "\n") for doc in documents] + results.extend(documents) + page += 1 + time.sleep(MAL_RATE_LIMIT) + except Exception as e: + print(f"[MAL WARN] Page {page} failed: {e}") + page += 1 + continue + + return results[:n] + +if __name__ == "__main__": + results = search_anime(sort_by="popularity", n=5000, search_recommended=True) + + data = [r.model_dump() for r in results] + df = pd.DataFrame(data) + df.to_json("./data/anime.json", orient="records", indent=2, force_ascii=False) + print(f"Saved {len(df)} records to ./data/anime.json") \ No newline at end of file diff --git a/data/populate_mongo_db_scraped.py b/data/populate_mongo_db_scraped.py new file mode 100644 index 0000000..8970753 --- /dev/null +++ b/data/populate_mongo_db_scraped.py @@ -0,0 +1,121 @@ +from pymongo import MongoClient +from dotenv import load_dotenv +import os +import pandas as pd +from sentence_transformers import SentenceTransformer +from pymongo.operations import SearchIndexModel + +from backend.mongo.utils import create_text_metadata_and_embedding + +from backend.configs import model_config, backend_app_config + +# Load all Env variables +load_dotenv() + +# Load Sentence Transformer Model +embedding_model = SentenceTransformer(model_config.embedding_model_id) + +def clean_data(): + # Load the Anime Dataset as a Pandas DataFrame + anime_df = pd.read_json('./data/anime.json') + + # Exclude Adult Rated Content + anime_df = anime_df[anime_df["age_rating"] != "rx"] + + # Exclude specific genres and empty genre lists + # Using rating + genre exclusion since sometimes rating or genre alone do not capture all non-friendly show content + EXCLUDED_GENRES = {"Hentai", "Erotica", "Unknown"} # If there exist genres # Remove if any of the genres are in excluded + anime_df = anime_df[anime_df["genres"].apply(lambda g: bool(g) and not any(genre in EXCLUDED_GENRES for genre in g))] + + # Project current name as its own node_name column (since they may not be english, but useful data) + anime_df["node_name"] = anime_df["name"] + + # Convert date_aired to datetime + anime_df["date_aired"] = pd.to_datetime(anime_df["date_aired"], format="mixed") + + # Filter out animes without English Names (check alt_titles dict for "en" key, and ensure it is not empty value) + anime_df = anime_df[anime_df["alt_titles"].apply(lambda x: "en" in x and x["en"] != "")] + + # Project en titles as new name column + anime_df["name"] = anime_df["alt_titles"].apply(lambda x: x.pop("en")) # Pop en out of alt_titles since now it is the primary title + + # Filter out animes without scores + anime_df = anime_df[anime_df["score"].notna()] + + # Filter out animes without synopsis + anime_df = anime_df[anime_df["synopsis"].notna() & (anime_df["synopsis"] != "")] + + # Generate text metadata + print("Generating text metadata and embeddings... this might take a minute...") + anime_df[['text_metadata', 'text_metadata_embedding']] = anime_df.apply( + lambda row: create_text_metadata_and_embedding( + embedding_model=embedding_model, + anime_name=row['name'], + anime_genres=row['genres'], + anime_synopsis=row['synopsis'] + ), + axis=1, + result_type='expand' + ) + + return anime_df + +def create_mongodb_and_indexes(df): + # Convert DataFrame to a list of dictionaries (documents) suitable for MongoDB + documents = df.to_dict(orient='records') + print(f"Successfully prepared {len(documents)} anime records for DB inserts!") + + # Connect to MongoDB Atlas + client = MongoClient(os.environ["ATLAS_URI"]) + print(f"Successfully connected to MongoDB Atlas") + + # Create / Access the "anizenith" DB + db = client[backend_app_config.mongo_anime_db_name] + + # Create / Access the "anime_enriched" collection + collection = db[backend_app_config.mongo_anime_collection_name] + + # Clear existing data + collection.delete_many({}) + + # Insert the documents into the collection + if documents: + # Define the Vector Search index + search_index_model = SearchIndexModel( + definition={ + "fields": [ + { + "type": "vector", + "path": "text_metadata_embedding", + "numDimensions": embedding_model.get_embedding_dimension(), + "similarity": "cosine" # Options: "cosine", "dotProduct", "euclidean" + } + ] + }, + name="vector_index", + type="vectorSearch" + ) + + # Create the index on your collection + result = collection.create_search_index(model=search_index_model) + print(f"Index creation started: {result}") + + result = collection.insert_many(documents) + print(f"Successfully inserted {len(result.inserted_ids)} anime documents.") + else: + print("No valid documents found to insert.") + +if __name__ == "__main__": + # Clean data if not already existing + if not os.path.exists("./data/anime_cleaned.json"): + output_df = clean_data() + # Save temporarily in case of failure + output_df.to_json("./data/anime_cleaned.json", orient="records", force_ascii=False) + else: + output_df = pd.read_json('./data/anime_cleaned.json') + + print(f"Collected ({len(output_df)}) records:\n{output_df.head(3)}") + + # Store into MongoDB + create_mongodb_and_indexes(output_df) + diff --git a/pyproject.toml b/pyproject.toml index d1ee309..6764281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "pandas>=3.0.2", "pymongo>=4.16.0", "sentence-transformers>=5.4.0", + "requests>=2.33.1", ] [tool.uv.sources] diff --git a/tests/test_AniZenithMongoClient.py b/tests/test_AniZenithMongoClient.py index c1fdbe8..424aea8 100644 --- a/tests/test_AniZenithMongoClient.py +++ b/tests/test_AniZenithMongoClient.py @@ -58,7 +58,7 @@ def test_perform_vector_search_relevance(test_db_client: AniZenithMongoClient): top_doc = results[0] # Assert that one punch man is present in the name - assert "one punch man" in top_doc.name.lower() + assert "one-punch man" in top_doc.name.lower() # The similarity score should exist and be decently high assert top_doc.similarity_score > 0.5 @@ -83,11 +83,26 @@ def test_add_anime(test_db_client: AniZenithMongoClient): # Create a dummy AnimeDocument test_anime_name = "Test Anime: Super Adventure" + test_anime = AnimeDocument( + mal_id=999999, name=test_anime_name, + alt_titles={ + "en": test_anime_name, + }, score=9.9, - synopsis="A completely unique and fake anime created for testing purposes.", - genres=["Action", "Adventure", "Testing"] + synopsis="A completely unique and fake anime created for Testing purposes.", + genres=[], + demographic="Shounen", + age_rating="pg-13", + cover_image_url="", + date_aired="2025-01-01", + status="finished_airing", + episode_count=1, + avg_episode_len_mins=1, + publishing_company="Test Studio", + recommendations={}, + node_name=test_anime_name ) try: diff --git a/uv.lock b/uv.lock index e4d4eb5..558824c 100644 --- a/uv.lock +++ b/uv.lock @@ -49,11 +49,84 @@ wheels = [ [[package]] name = "certifi" -version = "2022.12.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/f7/2b1b0ec44fdc30a3d31dfebe52226be9ddc40cd6c0f34ffc8923ba423b69/certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", size = 156897, upload-time = "2022-12-07T20:13:22.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/4c/3db2b8021bd6f2f0ceb0e088d6b2d49147671f25832fb17970e9b583d742/certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18", size = 155255, upload-time = "2022-12-07T20:13:19.428Z" }, +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -89,6 +162,7 @@ dependencies = [ { name = "prometheus-client" }, { name = "pymongo" }, { name = "pytest" }, + { name = "requests" }, { name = "sentence-transformers" }, { name = "torch", version = "2.11.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, @@ -104,6 +178,7 @@ requires-dist = [ { name = "prometheus-client", specifier = ">=0.24.1" }, { name = "pymongo", specifier = ">=4.16.0" }, { name = "pytest", specifier = ">=9.0.2" }, + { name = "requests", specifier = ">=2.33.1" }, { name = "sentence-transformers", specifier = ">=5.4.0" }, { name = "torch", specifier = ">=2.10.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "transformers", specifier = ">=5.0.0" }, @@ -1048,6 +1123,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/45/affdf2d851b42adf3d13fc5b3b059372e9bd299371fd84cf5723c45871fa/regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", size = 274932, upload-time = "2026-02-19T19:03:45.488Z" }, ] +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + [[package]] name = "rich" version = "14.3.3" @@ -1409,11 +1499,11 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43b35116802c85fb88d99f4a396b8bd4472bfca1dd82e69499e5a4f9b8b4e252" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442ec9dc78592564fdad69cf0beaa9da2f82ab810ccb4f13903869a90bf3f15d" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cc3a195701bba2239c313ee311487f80f8aaebe9e89b9073dddbcf2f93b5a0ba" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a0d6e4865e8b0dc0dbfe6ebed68fae235124222835ef03e5814d414d8c012" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23ec7789017da9d95b6d543d790814785e6f30905c5443efa8257d1490d73f79" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43b35116802c85fb88d99f4a396b8bd4472bfca1dd82e69499e5a4f9b8b4e252", upload-time = "2026-03-23T15:16:58Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442ec9dc78592564fdad69cf0beaa9da2f82ab810ccb4f13903869a90bf3f15d", upload-time = "2026-03-23T15:17:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cc3a195701bba2239c313ee311487f80f8aaebe9e89b9073dddbcf2f93b5a0ba", upload-time = "2026-03-23T15:17:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a0d6e4865e8b0dc0dbfe6ebed68fae235124222835ef03e5814d414d8c012", upload-time = "2026-03-23T15:17:10Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23ec7789017da9d95b6d543d790814785e6f30905c5443efa8257d1490d73f79", upload-time = "2026-03-23T15:17:14Z" }, ] [[package]] @@ -1441,26 +1531,26 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-linux_s390x.whl", upload-time = "2026-03-23T14:59:01Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-win_amd64.whl", upload-time = "2026-03-23T14:59:04Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl", upload-time = "2026-03-23T14:59:04Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:04Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:05Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl", upload-time = "2026-03-23T14:59:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl", upload-time = "2026-03-23T14:59:07Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:07Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:07Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl", upload-time = "2026-03-23T14:59:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl", upload-time = "2026-03-23T14:59:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:10Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl", upload-time = "2026-03-23T14:59:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl", upload-time = "2026-03-23T14:59:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:13Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl", upload-time = "2026-03-23T14:59:15Z" }, ] [[package]]